提交 + 轮询:我怎么停止和 WebSocket 战斗
WebSocket 看上去是 AI 长任务的正确答案。它几乎从不是。提交+轮询模式 看上去更丑、生产里更稳定、易于扩展、优雅降级。我停止和 WebSocket 战斗之后,AI 功能的失败率下降了一个数量级。
30 秒版
WebSocket 看上去是 AI 长任务(视频生成、深度研究、多步 agent)的 正确答案。它几乎从不是。 正确的默认是:提交一个任务、拿到 task ID、轮询 task ID、渲染结果。看上去更丑,生产里更稳定,扩展更简单, 优雅降级。我停止和 WebSocket 战斗之后,AI 功能的失败率下降了一个数量级。
我必须重新学的默认值
“实时 AI”的传统智慧是 WebSocket、Server-Sent Events、长连接 HTTP、 token 流式输出。
这对短 AI 交互对——一个 LLM 流式输出 5-30 秒的 chat。对这些, 你确实要流式连接:用户在那等着,延迟预算小,看到 token 一个个出现 体验更好。
但是,一旦你的 AI 任务超出用户的注意力跨度(比如 30 秒),让 WebSocket “正确”的所有假设都翻转了。
| 假设 | Chat 成立 | 长任务成立 |
|---|---|---|
| 用户停在页面 | 是 | 否 |
| 连接保持 | 是 | 否(移动、网络切换) |
| 结果必须实时到 | 是 | 否(用户晚点回来) |
| 有状态服务器 OK | 是(短命) | 否(资源成本累计) |
| 失败 = 重试 | 是(便宜) | 否(昂贵) |
我先试了,然后崩溃的:
- 用户提交一个 4 分钟的视频生成任务
- 连接在第 2 分钟掉了(移动网络、锁屏、后台 tab——任选其一)
- 服务器已经为 2 分钟的计算保了状态
- 用户回来:什么都没有。任务没了、连接没了,用户必须从头开始。
这事一周内发生了三次,我才换模式。
这个模式
把流式连接换成任务生命周期:
1. POST /tasks → 返回 { task_id, status: "pending" }
2. (服务端在后台跑任务)
3. GET /tasks/:id → 返回 { status, progress, result? }
4. (前端轮询直到 status === "done" 或 "failed")
五个原语:
- Submit(提交)——同步,立刻返回 task_id
- Status(状态)——
pending | working | done | failed | blocked。 A2A 协议的五个值。不要发明新的。 - Progress(进度)——可选,0-100,给 UI 用
- Result(结果)——
status === done时填充。URL 或内联 JSON - Error(错误)——
status === failed时填充。结构化
完了。没有 WebSocket。没有保连接。服务端把状态存在表里,后台处理工作。
为什么看上去更丑
我承认让它感觉更糟的部分:
- 轮询耗网络——是,但你可以指数轮询:前 10 秒每 1s 一次,30 秒 每 3s 一次,之后每 10s 一次。
- 延迟感觉更糟——最坏情况是把轮询间隔加在真实完成时间上。完成时 自适应轮询能缩这个。
- demo 看起来不那么炫——是。WebSocket 显得现代。轮询显得 2008。 在 pitch 里这是个真实代价。
- 放弃了流式——一个长任务你本来就不会流式。任务在它自己那忙 好几分钟。
为什么生产里更好
六件事抵消了”丑”:
1. 对断连健壮。 任务不在乎用户是否在线。用户可以关 tab、断网、 切设备、明天再回来。任务要么完成要么没。可恢复性免费。
2. 横向扩展便宜。 你的任务处理器和 API 服务器可以是不同的服务。 API 服务器没有长连接。你可以加 API 实例,不用想 sticky session。
3. 重试免费。 一个失败的任务可以通过重新提交(同输入幂等)或 显式 retry 端点重试。WebSocket 里,重试意味着用户重新发起连接、 重发请求。提交+轮询里,重试就一个按钮。
4. 可观测性免费。 每个任务在数据库里都有一行。你可以查失败率、 p99 延迟、重试次数、被放弃的任务。WebSocket 里你有的是连接指标, 不是任务指标。问题不一样。
5. 升级到 webhook 免费。 当你需要真正的推送(任务完成时通知 用户),在任务提交里加一个 webhook URL 字段。任务处理器在状态变化 时调这个 webhook。轮询模式对不要 webhook 的客户端仍然能工作。 两者共存。
6. 前端代码更简单。 一个 useTask(taskId) 轮询 hook 是 30 行。
一个 useWebSocketTask hook 处理断连、重连、消息顺序、状态恢复,
是 300 行——其中两行有 bug,你直到生产都不会发现。
流式仍然胜出的地方
我不是说永远不要用 WebSocket。它仍然对的情况:
- 带 token 流式的实时 chat。 用户在场,延迟重要,任务短。用 SSE (HTTP,简单于完整的 WebSocket)或 WebSocket。
- 多人协作。 两个用户编辑同一文档需要双向实时。WebSocket 值回 代价。
- Agent 透明度。 如果你要在一个长任务中展示”agent 现在在用 tool X,现在在思考,现在在回复”,流式那些事件。但不要流式 结果——让结果落到数据库,流式只用于 trace。
我现在用的规则:默认提交+轮询;只在用户可证在场且延迟预算 < 10s 时再考虑流式。
一个具体例子
我一个产品里的视频生成:60-120 秒墙钟时间,50-500 MB 结果,偶尔失败 需要重试。
V0(WebSocket):
- 14% 的生成因为掉连接丢失
- 移动用户的完成率 30%
- 重试痛苦(要重新上传源)
- 200 行前端 WebSocket 管理器,其中 80 行是错误恢复
- 用户切到后台 tab 时静默失败
V1(提交+轮询):
- 0.3% 的生成丢失(只有真正的失败)
- 移动和桌面的完成率没有区别
- 重试是一个按钮,幂等
- 35 行前端 hook
- Tab 切到后台:用户回来时看见结果
V0 → V1 迁移用了一周。两天生产时间就回本了。
我会怎么在面试里讲
“你怎么处理 AI 长任务?“——有时被问,候选人也常常主动提。
标准的候选人答案是 “WebSocket”。这个答案只对短任务正确,面试官 通常能看出候选人没想过长任务。
更好的答案:
用户在场、< 10 秒的短任务我用流式。更长的任务我默认提交+轮询: 一张任务表、五个状态值、前端轮询、可选的 webhook 推送。这对断连 更健壮、扩展更简单、可观测性免费。流式 UX 是我能加到任何一种模式 上的;可恢复性是只有轮询模式给我的。
这个答案具体、有权衡、显示你用过这个模式。大多数候选人没用过。