跳到正文
docai.cc

DOCAI · 概念 · L3

SSE 是什么,流式输出怎么传

一句话定义

服务器通过一条 HTTP 长连接持续推送文本事件的协议,LLM 的流式输出就建立在它之上。

专业更新 2026-11-13核验 2026-11-13 · docai 编辑部

它是什么

SSE(Server-Sent Events)是一种在单条 HTTP 连接上,由服务器持续向客户端推送文本事件的协议。响应类型是 text/event-stream,数据是纯文本,浏览器和命令行都能读。

和 WebSocket 不同,SSE 是单向的:只有服务器往客户端发,客户端不能在同一条连接上回话。LLM 的流式输出正好符合这个形状——请求发一次,回答分很多段回来。

怎么工作

一次流式请求,响应头里通常有:

Content-Type: text/event-stream
Cache-Control: no-cache

响应体按行组织。data: 后面是内容,事件之间用空行分隔:

data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: [DONE]

每一行 data: 是一个增量事件,客户端读到就追加文本。这是流式输出的传输层:模型还在生成,前端已经能显示。

以 OpenAI 形状的接口为例,最后一行是 data: [DONE]。它不是 SSE 协议的一部分,而是应用层约定,用来告诉客户端「正常结束」。换个厂商,结束标记可能不同,甚至没有;官方未说明时按实际响应解析,不要假定一定有。

为什么流会变成一次性返回

这是接流式时最常踩的坑:代码里明明开了 stream: true,用户却要等到最后才看到全部文字。

原因通常在中间层:

  • 反向代理缓冲:代理默认攒够一段再转发,流就被压成一次返回。需要对该路由关闭缓冲。
  • 压缩中间件:gzip 一类压缩会把小块数据攒起来,破坏实时性。
  • 客户端读法不对:用 requests.get() 一次性读完全文,而不是逐行读流。
  • CDN 或网关超时:连接被提前掐断,表现成「流到一半停了」。

排查时先用 curl -N 直接打接口,绕过所有中间层。如果 curl 能看到分段到达,问题就在你的代理或客户端;如果 curl 也是一次性返回,先查服务端。

心跳、断线与 [DONE]

长连接会空闲,代理和负载均衡器通常会掐掉长时间没有数据的连接。SSE 允许用注释行(以 : 开头)当心跳,保持连接活着。发不发、隔多久发,各厂商不一致,官方未说明的按实际行为处理。

断线重连方面,SSE 规范定义了重连间隔字段,但它假设事件可以重放。LLM 的生成过程不是这样:多数接口不支持从断点续传,断了只能重发请求,输入 token 会再算一次。

[DONE] 的作用就是区分「正常结束」和「连接被截断」。客户端没收到结束标记就应当按异常处理,而不是把半截文本当成完整答案。

常见误解

  • 「SSE 就是 WebSocket」:不是。SSE 单向、走普通 HTTP、实现简单;WebSocket 双向,复杂得多。
  • 「开了 stream 就一定流式」:中间任何一层缓冲都能把流吃掉。
  • 「流断了能接着生成」:多数 LLM 接口不支持续传,重连等于重发。
  • 「[DONE] 是协议规定的」:它是应用层约定,不同接口的结束方式可能不一样。

延伸阅读

什么时候用得上

当你要接流式输出、做打字机效果,或者排查「明明开了流式却一次性返回」的时候。

常见误解

很多人以为代码里开了 stream 就一定会流式到用户,其实中间任何一层代理开了缓冲,都可能把整段响应攒到最后一次返回。

来源

定义与事实以以下来源为准;表述与取舍是本站整理的。
  1. [1]MDN · Using server-sent events
  2. [2]OpenAI · Streaming responses
查看 Markdown

所属专题

相邻概念