13. Streaming 输出的工程实现有哪些注意事项(SSE、WebSocket、断流重连)?
整理 Streaming 输出、SSE / WebSocket 与断流重连实现要点。
简单回答
大模型的流式输出通常用 SSE(Server-Sent Events)或 WebSocket 实现。SSE 更简单(单向推送、基于 HTTP),适合大多数对话场景;WebSocket 是双向通信,适合需要实时交互的复杂场景。核心注意事项包括:断流重连、token 拼接、错误中途处理、以及中间代理(Nginx、CDN)的超时配置。
详细解释
SSE vs WebSocket
SSE(Server-Sent Events)是基于 HTTP 的单向推送协议。服务端推送数据,客户端接收,适合"服务端持续发 token,客户端只接收"的场景。优点是实现简单、基于标准 HTTP(不需要特殊协议升级)、浏览器原生支持(EventSource API)、和现有 HTTP 基础设施兼容性好。缺点是只能服务端推送,不支持客户端向服务端发消息(需要额外的 HTTP 请求)。
WebSocket 是全双工通信协议。客户端和服务端可以同时收发消息。适合需要实时双向交互的场景,比如用户在生成过程中可以随时发送"停止生成"或"追问"。缺点是实现更复杂、需要维护连接状态、和某些代理/CDN 的兼容性不如 HTTP。
大多数大模型应用(包括 ChatGPT、Claude)用的是 SSE。因为对话场景的主要需求就是服务端推 token,用户的"停止生成"可以通过一个单独的 HTTP 请求实现,不需要 WebSocket。
SSE 的实现要点
响应头设置:Content-Type: text/event-stream,Cache-Control: no-cache,Connection: keep-alive。
数据格式:每个事件以 data: 开头,以 \n\n 结束。模型每生成一个 token(或一个 chunk)就发一个 event。最后发一个 data: [DONE] 表示生成完毕。
断流重连
网络不稳定时 SSE 连接可能中断。浏览器的 EventSource API 有自动重连机制,但重连后需要从断点继续而不是重新开始。实现方式有几种:
方案一是用 Last-Event-ID。每个 SSE 事件带一个递增的 ID,客户端重连时发送 Last-Event-ID 头,服务端从该 ID 之后继续推送。这要求服务端缓存已生成的内容。
方案二是客户端缓存。客户端记录已收到的所有 token,重连后服务端重新生成(如果是调用外部 API 则无法精确恢复),客户端根据已有缓存跳过重复部分。
方案三是不做精确恢复。重连后直接告诉用户"生成中断,请重新发送"。对于低频中断的场景,这种简单方案反而最实用。
中间代理的超时问题
这是实际部署中最常踩的坑。Nginx 默认的 proxy_read_timeout 是 60 秒,如果模型生成一个完整回答超过 60 秒(长文本场景很常见),Nginx 会断开连接。SSE 的情况更微妙——即使在持续推 token,如果两个 token 之间的间隔超过了 proxy_read_timeout(Prefill 阶段可能有几秒间隔),也会被 Nginx 断开。
解决方式:加大 proxy_read_timeout(如设成 300 秒);在 Prefill 阶段发心跳 event(空 comment)保持连接活跃;CDN 层也要做相应的超时配置。
流式场景的错误处理
如果生成过程中出错了(模型 OOM、API 超时、Guardrails 拦截),已经推送出去的 token 无法撤回。处理方式通常是发一个 error event,前端收到后展示错误提示。比较好的体验是把已经生成的部分保留,在末尾加上错误标记。
流式场景的 Token 计费
流式和非流式的计费方式相同,但统计时间点不同。流式场景下 output tokens 在生成过程中逐步增加,需要在 [DONE] 事件时统计最终的 token 数。如果中途断开,需要决定是否对已生成的 token 计费。
面试时可以这样答
大模型流式输出主流用 SSE,因为对话场景就是服务端推 token、客户端接收,SSE 的单向推送正好匹配,而且基于 HTTP 实现简单,兼容性好。WebSocket 适合需要真正双向实时通信的复杂场景,大部分对话应用不需要。
工程上最容易踩的坑是中间代理的超时。Nginx 默认 proxy_read_timeout 60 秒,长文本生成很容易超时被断开。特别是 Prefill 阶段可能有几秒没有输出,也会被判定超时。需要加大超时配置,或者在等待阶段发心跳保持连接。
断流重连可以用 Last-Event-ID 机制做精确恢复,但实现成本不低。如果中断率不高,直接提示用户重新发送可能是最实用的方案。
流式场景的错误处理也要注意——已经推送出去的 token 撤不回来,只能在末尾发 error event 让前端处理。
常见追问
- SSE 和 HTTP/2 Server Push 有什么区别?
- 移动端网络不稳定时流式输出体验怎么优化?
- 如果前端需要对流式输出做 Markdown 实时渲染,有什么技术难点?