任务流边界

生成任务不是一次请求

文本、图像、视频生成有个共同点:结果不一定马上返回。

  • 请求可能排队。
  • 可能执行很久。
  • 可能中途失败。
  • 可能先输出一部分内容。

如果设计成同步接口,客户端会一直等。网络断开,任务状态说不清。用户刷新页面,不知道任务还在不在。

更合适的模型是任务制。

创建任务 -> 返回 task_id
查询任务 -> pending / processing / succeeded / failed
订阅内容 -> 持续接收流式输出

多了一层,但把长任务从 HTTP 请求里解放出来。请求只负责创建任务,任务自己走完生命周期。

轮询负责状态

轮询回答一个问题:任务现在怎么样了?

状态字段要少而稳定。四个就够:pendingprocessingsucceededfailed。有过期机制再加 expired

状态越多,前端分支越多,用户越难懂。

{
"task_id": "task_123",
"status": "processing",
"created_at": "2026-05-20T10:00:00Z",
"updated_at": "2026-05-20T10:00:10Z"
}

轮询接口不要承担内容流职责。它可以返回最终结果或失败原因,但不要每次带上不断增长的中间文本。

那会让接口变重,缓存、重试、分页都变复杂。

状态查询要轻,内容传输要专。

前端每 1-3 秒查一次状态,足够支撑大多数生成任务。用户离开再回来,也能用 task_id 恢复进度。

SSE 负责内容

SSE 回答另一个问题:这次生成正在输出什么?

它不证明任务是否存在,也不维护完整状态。它只是一条内容流。

服务端持续发 text_delta,结束发 done,失败发 error

event: text_delta
data: {"content":"第一段"}

event: text_delta
data: {"content":"第二段"}

event: done
data: {"output":"第一段第二段","usage":{"total_tokens":120}}

关键点:中间 text_delta 可以不落库,最终 done 必须完整。

存储层只保存稳定结果,不保存大量临时碎片。客户端断线后,重新查状态和最终输出;要实时体验,再重新订阅流。

这样数据库存最终结果,SSE 管实时传输,两者不互相绑死。

flowchart LR
    A[创建任务] --> B[轮询状态]
    A --> C[订阅 SSE]
    B -->|pending/processing| B
    B -->|succeeded| D[获取最终结果]
    C -->|text_delta| E[实时输出]
    C -->|done| D

边界清楚,系统才轻

轮询和 SSE 常被拿来比较,好像只能二选一。实际项目里,它们是两种工具。

  • 轮询:可靠、简单、容易恢复,适合任务状态。
  • SSE:实时、轻量、单向推送,适合生成内容。

混在一起,接口越来越重。拆开,系统反而好维护。

异步任务的设计重点不是追求高级协议,而是让每条链路只做一件事:创建任务、查询状态、订阅内容、获取结果。

边界越清楚,失败时越好排查。