AI 视频生成不是一次 HTTP 请求:先把长任务状态机补齐

把 AI 视频生成写成一次同步 HTTP 请求,Demo 往往能跑,生产环境却很容易在 60 秒网关超时、进程重启或用户重复点击时失控。真正困难的不是"怎么调用模型",而是如何在几分钟甚至更久的等待中,知道任务有没有被重复创建、哪个结果可信、回调能不能重放,以及超时后还能不能恢复。

2026 年 8 月,Vercel AI SDK 给实验性视频接口加入了异步 start/status、轮询和 Webhook 能力。这是一个值得注意的信号:视频生成需要的是可恢复的长任务控制面,而不是把连接一直挂着。本文不连接真实视频供应商,而是用一个通过 6 项测试的 TypeScript 最小实验,把状态机、幂等键、事件去重和终态保护拆开验证。

一、视频生成不是慢一点的普通请求

普通接口通常在一次请求里完成"接受、处理、返回"。视频生成则更像订单履约:先拿到供应商 operation ID,随后多次查询状态,最后得到产物或失败原因。调用方、供应商和回调服务都可能独立重试。

风险 表面现象 真正需要的控制
网关或函数超时 请求断开,但供应商仍在生成 start/status 分离与持久化任务
用户重复提交 同一视频被计费两次 业务幂等键和唯一约束
轮询与 Webhook 同时到达 同一成功事件处理两次 统一事件归并与事件 ID 去重
旧轮询晚于新回调 成功被旧失败覆盖 时间/版本门禁与终态保护
Worker 重启 内存中的计时器消失 可恢复调度,不依赖单进程等待

因此,"请求成功"最多说明供应商接受了任务,不等于视频已经生成,更不等于文件已下载、校验并进入后续剪辑流水线。

二、从 AI SDK 的异步接口看控制面

官方 ai@7.0.50 Release 说明,实验性 VideoModelV4 可以实现 doStartdoStatushandleWebhookOptionexperimental_generateVideo 也接受 pollwebhook 选项。轮询还允许传入自定义 delay,以适配 durable workflow。

这次能力最早出现在 @ai-sdk/xai@4.0.26 的补丁说明中;当前核验到的后续补丁 @ai-sdk/xai@4.0.27 只更新依赖。版本号会继续变化,可复用的不是某个函数签名,而是下面这层分工:

负责什么 不该负责什么
Provider 适配层 创建任务、查询状态、解析回调 决定业务幂等与最终交付
SDK 编排层 选择轮询或 Webhook、等待策略 代替业务数据库做唯一约束
业务任务层 状态机、幂等、审计、超时、产物验收 假设回调只来一次

接口是 experimental 也意味着生产代码要固定版本、包一层适配器,并为状态映射写回归测试,避免升级后供应商状态语义悄悄改变。

三、先定义显式状态机

不要只存一个 status: string。最小状态至少应包含 submittedrunningsucceededfailedcancelledtimed_out,并把成功产物和失败原因约束在对应分支里。

ts 复制代码
type JobState =
  | { status: "submitted"; operationId: string; updatedAt: number }
  | { status: "running"; operationId: string; updatedAt: number }
  | { status: "succeeded"; operationId: string; outputUrl: string; updatedAt: number }
  | { status: "failed"; operationId: string; errorCode: string; updatedAt: number }
  | { status: "cancelled"; operationId: string; updatedAt: number }
  | { status: "timed_out"; operationId: string; updatedAt: number }

timed_out 要成为显式终态,而不是只记一条日志。它表示业务等待预算耗尽,不一定表示供应商已经停止生成。是否继续低频补偿查询、是否尝试取消供应商任务,应由单独策略决定。

四、幂等键要挡在供应商调用之前

最危险的顺序是:先调用供应商,再把 operation ID 写入数据库。只要写库失败,重试就可能再次创建视频。更稳妥的入口是先用业务幂等键占位,再由唯一约束保证同一业务动作只映射到一个本地任务。

ts 复制代码
function startOrReuse(jobs: readonly JobAggregate[], request: StartRequest) {
  const existing = jobs.find(
    (job) => job.state.idempotencyKey === request.idempotencyKey,
  )
  if (existing !== undefined) return { kind: "reused", job: existing }

  return { kind: "created", job: createSubmittedJob(request) }
}

实验中的纯函数只证明"相同键复用同一任务"的语义。生产环境还需要数据库唯一索引、事务或原子 upsert。幂等键应来自稳定业务身份,例如 projectId + shotId + renderVersion,不能用每次重试都会变化的随机 UUID。

五、轮询和 Webhook 必须汇入同一个归并器

轮询与 Webhook 不是两套状态更新代码。它们只是事件来源不同,最终都要进入同一个 applyEvent。否则一边做了去重,另一边仍可能覆盖终态。

ts 复制代码
type TransitionResult =
  | { kind: "applied"; aggregate: JobAggregate }
  | {
      kind: "ignored"
      reason: "duplicate" | "stale" | "terminal"
      aggregate: JobAggregate
    }

function applyEvent(aggregate: JobAggregate, event: VideoJobEvent): TransitionResult

Webhook 的 eventId 应先通过签名校验,再进入去重表;如果供应商没有稳定事件 ID,可以用供应商任务 ID、事件类型和版本号组成可重放指纹。处理成功后再确认消息,失败则允许重试。

六、按"重复、乱序、终态"顺序拒绝坏更新

事件归并器的判断顺序应固定并可测试:先识别重复事件,再拒绝旧观察,最后保护终态。实验使用 observedAt 演示乱序门禁;生产环境更适合供应商单调递增版本号或数据库 compare-and-set。

ts 复制代码
if (aggregate.seenEventIds.includes(event.eventId)) {
  return { aggregate, kind: "ignored", reason: "duplicate" }
}
if (event.observedAt < aggregate.state.updatedAt) {
  return { aggregate, kind: "ignored", reason: "stale" }
}
if (isTerminal(aggregate.state)) {
  return { aggregate, kind: "ignored", reason: "terminal" }
}

终态保护很关键:任务一旦成功,迟到的失败轮询不能把它改回失败;失败或超时后若要"复活",也应创建新的尝试记录,而不是偷偷改写原任务历史。

七、最小实验验证了什么

实验使用 Bun 1.3.1、TypeScript 7.0.2 和 Biome 2.2.0。测试先失败于缺失实现,补齐最小代码后,幂等复用、合法状态迁移、Webhook 去重、乱序拒绝、终态保护、显式超时和 CLI 场景均通过。

text 复制代码
Biome: Checked 7 files. No fixes applied.
TypeScript --noEmit: passed
6 pass, 0 fail, 14 expect() calls
CLI: {"appliedEvents":2,"createdJobs":1,"finalStatus":"succeeded","ignoredEvents":2,"reusedJobs":1}
ts 复制代码
const success = applyEvent(job, webhookSucceeded)
const duplicate = applyEvent(success.aggregate, webhookSucceeded)
const lateFailure = applyEvent(success.aggregate, newerPollFailed)

// duplicate.reason === "duplicate"
// lateFailure.reason === "terminal"

这些结果只能证明本地状态机的示例行为符合测试,不能证明真实供应商稳定,也不能证明 Webhook 来源可信、文件可用或账单正确。

八、接入生产前的检查表

  • 业务幂等键有数据库唯一约束,重复请求返回同一任务;
  • 供应商 operation ID 在任务表中持久化且可追踪;
  • 轮询与 Webhook 进入同一个状态归并器;
  • Webhook 验签、防重放,并记录事件 ID;
  • 状态更新有版本或 compare-and-set,拒绝乱序覆盖;
  • 成功、失败、取消、业务超时都是显式终态;
  • Worker 重启后能从数据库恢复调度;
  • 轮询有退避、抖动、最大次数和全局并发上限;
  • 成功后还要下载、校验 MIME/大小/时长并转存自有对象存储;
  • 任务、尝试、事件和产物分表审计,费用能对到业务动作;
  • experimental API 固定版本并由适配层隔离;
  • 超时不被误报为供应商取消,补偿策略单独配置。

一条稳健的视频流水线,关注的不只是"生成成功率",还包括重复创建率、终态冲突数、回调延迟、轮询次数、超时后晚到成功数和产物验收失败率。只有这些指标可见,长任务才真正可运营。

九、来源与验证边界

验证边界 本文通过 Ego 核验了官方 Release 与提交说明,并完成脱敏 TypeScript 状态机实验;没有调用真实视频模型,没有验证任何供应商的成功率、成本、计费、Webhook 签名或产物质量。AI SDK 视频接口仍标记为 experimental,生产接入前应以当日官方文档和锁定版本为准。

相关推荐
a1117761 小时前
FDE(前沿部署工程师)从零入门指南
人工智能·开源
武子康1 小时前
MiniMax H3 的多模态参考为什么比 2K 更重要?
人工智能·llm·agent
IT爱学堂1 小时前
AI Agent大师之路:从认知架构到自主智能体的完整设计哲学
人工智能
想会飞的蒲公英1 小时前
PyTorch中SGD 与 Momentum 从零理解:给最朴素的优化器加上“惯性“
人工智能·pytorch·python·深度学习·机器学习
额恩661 小时前
自然语言处理 NLP 入门与语言学基础
人工智能·自然语言处理
paopaokaka_luck1 小时前
基于springboot3+vue3的乡村医生诊疗管理系统(AI助手、协同过滤算法、webSocket实时聊天、Echarts图形化分析)
前端·网络·人工智能·spring boot·websocket·网络协议·echarts
用户298698530141 小时前
PDF 转图片?三种方案,覆盖全平台与自动化场景
人工智能·后端
Coffeeee1 小时前
天天 AI Coding 的你,如果出去面试,你的竞争力是什么?
人工智能·程序员·ai编程
小飞猪。。1 小时前
笔记十八:大模型 RLHF 系统工程实战笔记
人工智能·笔记