模式

提交 + 轮询:我怎么停止和 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")

五个原语:

  1. Submit(提交)——同步,立刻返回 task_id
  2. Status(状态)——pending | working | done | failed | blocked。 A2A 协议的五个值。不要发明新的
  3. Progress(进度)——可选,0-100,给 UI 用
  4. Result(结果)——status === done 时填充。URL 或内联 JSON
  5. 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 是我能加到任何一种模式 上的;可恢复性是只有轮询模式给我的。

这个答案具体、有权衡、显示你用过这个模式。大多数候选人没用过。