---
title: "SSE 是什么，流式输出怎么传"
description: "SSE 是 LLM 流式输出的传输方式：一条 HTTP 长连接、text/event-stream、data 行分帧。这页讲清代理缓冲为什么会把流压成一次性返回，以及心跳、重连与结束标记。"
updated: 2026-11-13
canonical: https://docai.cc/concepts/sse/
markdown_mirror: https://docai.cc/concepts/sse.md
---
## 它是什么

SSE（Server-Sent Events）是一种在单条 HTTP 连接上，由服务器持续向客户端推送文本事件的协议。响应类型是 `text/event-stream`，数据是纯文本，浏览器和命令行都能读。

和 WebSocket 不同，SSE 是单向的：只有服务器往客户端发，客户端不能在同一条连接上回话。LLM 的流式输出正好符合这个形状——请求发一次，回答分很多段回来。

## 怎么工作

一次流式请求，响应头里通常有：

```http
Content-Type: text/event-stream
Cache-Control: no-cache
```

响应体按行组织。`data:` 后面是内容，事件之间用空行分隔：

```text
data: {"choices":[{"delta":{"content":"你"}}]}

data: {"choices":[{"delta":{"content":"好"}}]}

data: [DONE]
```

每一行 `data:` 是一个增量事件，客户端读到就追加文本。这是[流式输出](/concepts/streaming/)的传输层：模型还在生成，前端已经能显示。

以 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] 是协议规定的」**：它是应用层约定，不同接口的结束方式可能不一样。

## 延伸阅读

- 流式输出是什么：[什么是流式输出](/concepts/streaming/)
- 首字延迟：[什么是 TTFT](/concepts/ttft/)
- 少踩超时的坑：[流式输出与超时](/learn/streaming-and-timeouts/)
