SSE API stream 消息格式怎么读与解析指南
简答:SSE(Server-Sent Events)流式消息以 <code>text/event-stream</code> 格式传输,每条消息由一个或多个键值对组成,并以双换行符(<code>\n\n</code>)作为单条事件的边界。读取时需注意网络传输的 HTTP 块(Chunk)不等于完整事件,必须通过缓冲区按行或按双换行符切分,提取 <code>data:</code> 后面的 JSON 字符串进行解析。
定义:SSE API stream 消息格式是一种基于 HTTP 的单向流式通信规范,通过文本事件流将 AI 模型的增量输出实时传输给客户端。
一、适用范围与核心结论
本文适用于所有采用 OpenAI 兼容接口、Anthropic Claude API 或自定义大模型 Agent 服务的开发者,在调试流式输出(stream: true)遇到消息粘连、解析中断或乱码时参考。
- 核心结论 1:网络传输的 HTTP 分块(Chunk)可能把一条 SSE 消息切成多半,不能直接拿单个 Chunk 当 JSON 解析。
- 核心结论 2:单条 SSE 事件的结束标志是连续两个换行符(
\n\n),而不是单个换行。 - 核心结论 3:绝大多数 AI API 的增量内容都藏在
data:字段的 JSON 结构中,末尾通常以data: [DONE]标记流结束。
二、SSE 消息的完整结构与字段定义
一条标准的 SSE 响应由多行文本组成,每行以可选的字段名开头,后跟冒号和具体数值。字段定义需以各大模型供应商的官方文档为准。
| 字段名 | 含义与用法 |
|---|---|
event | 事件类型(如 message、error 或自定义事件),未指定时默认为 message。 |
data | 消息负载。大模型接口的增量文本或 JSON 对象均在此字段,可多行出现。 |
id | 事件唯一标识符,可用于设置 Last-Event-ID。 |
retry | 客户端重连的时间间隔(毫秒)。 |
: (冒号开头) | 注释行或心跳保持行(Keep-alive),客户端应直接忽略。 |
以下是一个真实的 AI API SSE 响应片段示例:
event: message
data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"你好"}}]}
data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"世界"}}]}
data: [DONE]
三、为什么不能直接按行读?理解事件边界
初学者在处理流式 API 时最容易犯的错误是:直接按单行(\n)切割读取。由于底层 TCP/HTTP 分块传输的不确定性,一个 data: 的 JSON 可能会被拆在两个不同的 Chunk 中传输。
正确的分界逻辑:
- 将接收到的底层文本流追加到本地字符串缓冲区。
- 在缓冲区中搜索双换行符(
\n\n或\r\n\r\n)。 - 一旦找到,截取出完整的一个事件块进行解析。
- 将已解析的部分从缓冲区移除,剩余部分留作下一次拼接。
四、JavaScript 手动解析 SSE 流示例
由于大模型流式请求通常使用 POST 方法并需要携带 Authorization 请求头,浏览器原生的 EventSource(仅支持 GET)往往无法直接满足需求。推荐使用 fetch 配合 ReadableStream 进行手动解析:
async function readSSEStream(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split(/\r?\n\r?\n/);
buffer = parts.pop(); // 留下最后不完整的一块
for (const part of parts) {
const lines = part.split(/\r?\n/);
let dataStr = '';
for (const line of lines) {
if (line.startsWith('data:')) {
dataStr += line.slice(5).trim();
}
}
if (dataStr) {
if (dataStr === '[DONE]') {
console.log('流式传输结束');
return;
}
try {
const json = JSON.parse(dataStr);
console.log('收到增量内容:', json.choices?.[0]?.delta?.content || '');
} catch (e) {
console.error('JSON 解析失败:', dataStr);
}
}
}
}
}
五、常见错误与排查清单
在对接或读取 SSE 消息格式时,如遇到异常可对照以下清单排查:
- 现象 1:整段响应一次性返回,没有流式效果。
排查点:检查请求头是否漏掉了Accept: text/event-stream,或者上游/中间代理服务器(如 Nginx)启用了响应缓冲(Buffer),需关闭代理缓冲。 - 现象 2:控制台报错
SyntaxError: Unexpected token。
排查点:没有过滤data: [DONE]或未处理:开头的注释行,导致把非 JSON 文本传给了JSON.parse()。 - 现象 3:中文出现乱码。
排查点:解码器未指定utf-8,或者在处理TextDecoder时未开启{ stream: true }参数导致多字节字符被截断。
六、高级治理与统一网关建议
在生产环境中,直接处理不同大模型供应商(如 OpenAI、Anthropic、DeepSeek 等)略有差异的 SSE 事件格式和错误码会增加维护成本。通过引入标准化的 AI 网关,可以将多源模型的流式事件进行归一化处理,并提供统一的用量监控与限流保护。若您的项目正需要高效的 API 路由与网关支持,可以了解下犀速-AI的接入方案。
常见问题
SSE 流式响应必须以 [DONE] 结尾吗?
不一定。OpenAI 兼容接口广泛采用 <code>data: [DONE]</code> 作为结束标志,但部分其他厂商的模型 API 使用自定义的 <code>event</code>(如 <code>done</code> 或 <code>stop</code>)来标识结束。编写解析代码时应同时兼容 <code>[DONE]</code> 标记与连接正常关闭(<code>done: true</code>)。
为什么 EventSource 无法直接用来调用大模型 API?
浏览器自带的 <code>EventSource</code> 对象只支持发送 <code>GET</code> 请求,且无法在请求中携带自定义的 <code>Authorization</code>(API Key)请求头。因此调用 AI API 时,必须改用 <code>fetch</code> 配合 <code>ReadableStream</code> 自定义请求方法与头部。
如何避免代理服务器(如 Nginx)缓冲导致 SSE 变成一次性返回?
如果你的服务通过 Nginx 或其他反向代理转发,代理可能会等待整个响应生成完毕才一起下发。需在服务端和网关配置中关闭缓存(如设置响应头 <code>X-Accel-Buffering: no</code> 和 <code>Cache-Control: no-cache</code>)。
相关阅读
参考资料
- Using server-sent events - Web APIs | MDN
- Understanding the Event Stream Format — Server-Sent Events(server-sent-events.com)