任务流边界
生成任务不是一次请求
文本、图像、视频生成都有一个共同特点:结果不一定马上返回。请求可能排队,可能执行很久,可能中途失败,也可能先输出一部分内容。
如果把它设计成普通同步接口,客户端会一直等。网络断开时,任务状态也说不清。用户刷新页面后,更不知道任务到底还在不在。
更合适的模型是任务制。
创建任务 -> 返回 task_id |
这个结构看起来多了一层,但它把长任务从 HTTP 请求里解放出来。请求只是创建任务,任务自己完成生命周期。
轮询负责状态
轮询适合回答一个问题:任务现在怎么样了?
状态字段应该少而稳定。常见的四个就够用:pending、processing、succeeded、failed。如果有过期机制,再加 expired。状态越多,前端分支越多,用户也越难理解。
{ |
轮询接口不要承担内容流职责。它可以返回最终结果,也可以返回失败原因,但不应该每次都带上不断增长的中间文本。那会让接口变重,也会让缓存、重试和分页都变复杂。
状态查询要轻,内容传输要专。
前端每 1-3 秒查一次状态,足够支撑大多数生成任务。用户离开页面再回来,也可以通过 task_id 恢复进度。
SSE 负责内容
SSE 适合回答另一个问题:这次生成正在输出什么?
它不负责证明任务是否存在,也不负责维护完整任务状态。它只是一条内容流。服务端持续发送 text_delta,结束时发送 done,失败时发送 error。
event: text_delta |
关键点是:中间 text_delta 可以不落库,最终 done 必须完整。这样存储层只保存稳定结果,不保存大量临时碎片。客户端断线后,可以重新查任务状态和最终输出;需要实时体验时,再重新订阅流。
这也降低了后端负担。数据库存最终结果,SSE 管实时传输,两者不互相绑死。
边界清楚,系统才轻
轮询和 SSE 经常被拿来比较,好像只能二选一。实际项目里,它们更像两种工具。
轮询可靠、简单、容易恢复,适合任务状态。SSE 实时、轻量、单向推送,适合生成内容。把它们混在一起,接口会越来越重;把它们拆开,系统反而更容易维护。
异步任务的设计重点不是追求高级协议,而是让每条链路只做一件事。创建任务、查询状态、订阅内容、获取结果。边界越清楚,失败时越好排查。