API 流式输出怎么接?SSE 分帧、文本组装与完成判断
接收 API 流式输出,应先把网络字节还原成完整 SSE 事件,再按实际接口组装内容,最后检查协议定义的结束状态。收到文字、读到连接末尾,都不能单独证明生成成功。本文面向写应用的开发者;官方资料核对日期为 2026 年 10 月 24 日,配套演练只回放人工编写的数据,没有调用真实 API。
三层边界,先别混在一起
| 你拿到的东西 | 它代表什么 | 不能直接推断什么 |
|---|---|---|
| 网络读取返回的一块字节 | 本次读取获得的数据 | 一个字符、一个 JSON 或一个事件已完整 |
| 按 SSE 规则分出的事件 | 一段带事件类型和 data 的消息 | 模型整个响应已经结束 |
| 接口的完成状态 | 当前响应的生命周期结果 | 答案事实正确、工具任务全部完成 |
同一条事件可以跨多个网络块;一次读取也可能装着多个事件。不要对每个块直接调用 JSON 解析,再把异常一律归为服务端返回坏数据。尚未跑通普通请求时,先核对地址、协议与模型;已能收到流、但应用显示错乱或提前结束时,再沿下面两层检查。
第一层:先分出完整 SSE 事件
SSE 使用 UTF-8,空行结束一个事件。处理顺序应是增量解码、按行读取、累计字段、遇到空行才交给下一层;同时处理 LF、CRLF 和 CR 换行。多条 data: 要用换行拼接,冒号开头的注释不属于回答正文。流末尾没有完成分隔的事件应丢弃,不能靠“连接已经关闭”把它补成完整事件。WHATWG SSE 解析规则
例如下面是一个人工编写的事件,末尾空行是分隔符:
: 心跳注释
event: demo
data: 第一行
data: 第二行
它的事件名是 demo,data 是“第一行\n第二行”。data 可以是普通文本,SSE 本身不要求 JSON;只有具体 API 约定它是 JSON 时,才在分帧后解析。
这里的关键是保留跨块状态。中文可能被切在一个 UTF-8 字符的中间,分隔符也可能跨块;每块单独解码或简单 split("\n\n") 容易漏掉这些情况。已使用官方 SDK 时,先确认拿到的是 SDK 已解析的事件对象,避免重复做字节层解析。
第二层:按接口识别增量和结束
Chat Completions 与 Responses 都可使用 SSE,但正文位置和事件含义不同。OpenAI 的流式指南说明:Chat 的生成片段在 delta,它有时只有角色信息或为空,不能假设每次都有正文。流式响应指南
| 场景 | 文本怎么组装 | 完成怎么确认 |
|---|---|---|
| Chat Completions 文本流 | 按 choice 分别追加 delta.content |
看对应 choice 的 finish_reason,并处理该接口的流结束标记 |
| Responses 文本流 | 按输出项/内容块归组,追加 response.output_text.delta |
看响应级终态,不把某个文本块结束当整体完成 |
Chat 的官方 HTTP 示例以 data: [DONE] 结束;它是协议标记,不是 JSON。finish_reason 还需要区分正常停止、长度上限、工具调用等情况。不要把所有非空结束原因都显示成“回答完整”。Chat 流式参考
Responses 的 response.output_text.done 表示一块文本完成,response.completed 才是响应级完成事件;response.incomplete、response.failed 要分别处理。完整文本快照用于核对或替换对应缓冲区,不要再追加一遍,否则界面会重复显示。Responses 事件定义
也别给所有接口套 [DONE]。例如 DeepSeek 当前 Responses 文档明确使用 completed、incomplete、failed 终态事件,没有该消息。原生 Messages、Gemini 等协议应查各自文档,不能因为都叫“流式”就复用同一终态判断。DeepSeek Responses 流式说明
显示可以渐进,完成状态要单独保存
建议把“已显示的文本”和“响应状态”分开记录。下面是应用层设计示例,并非厂商统一状态码:
| 观察结果 | 应用怎么处理 |
|---|---|
| 收到正文增量,尚无终态 | 可以预览,保持生成中 |
| 收到已验证的正常终态 | 标记本次响应完成,再验收业务结果 |
| 收到 incomplete、failed 或错误 | 保留已有内容和原因,进入相应分支 |
| 连接结束却没有有效终态 | 标记中断或未确认,不伪造成功 |
| 收到未实现的事件类型 | 记录并按能力边界处理,不把它当正文 |
拒绝信息也应与普通答案区分。即使响应正常结束,输出仍可能是拒绝或需要工具执行;完成事件不是业务验收章。收到工具请求时转到工具执行与回传流程,需要把答案入库时再做JSON 与字段验收。
中断后保留协议、最后事件、结束原因、请求标识及可用用量。不要自动无限重发:原请求可能已消耗资源,工具是否执行也可能尚未确认。Codex 客户端报 stream disconnected 时,使用专门的分场景排查,本文不替代客户端诊断。
用离线演练检查自己的接收逻辑
下载 SSE 流式接收演练,解压后在该目录运行:
python3 replay.py
python3 -m unittest -v test_stream_lab.py
资源只使用 Python 内置模块,不读 Key 或环境变量,不连接服务器。它把合成数据切成不同大小的字节块,演示 SSE 分帧,以及限定 Responses 文本事件的组装与终态检查;最终输出也是预先写好的内容,不是模型回答。
先对照正常完成、缺完成事件、截断和失败样例,再故意移除最后一个空行,观察接收结果。验收重点是中文没有乱码、分片方式改变不影响事件、完整快照没有重复追加、未完成没有显示为成功。包内另有空白记录表,可记录你以后真实接入时的条件与结果。
它不是完整 SDK:没有 HTTP、自动重连、持久化、多模态或工具参数组装。id、retry 的重连行为也不在本地演练范围。云岚API的模型、分组及协议支持需分别验证,离线测试通过不能代替真实兼容性检查。
常见问题
HTTP 200,为什么仍不能算完成?
HTTP 状态先说明请求层面的结果;后续流仍可能中断或报告失败。需要继续处理事件及终态,随后检查答案是否完成你的任务。
一次读取就是一个 token 吗?
不能这样计数。读取块、事件和模型 token 是不同单位;切块方式也不是计费用量。需要核账时使用实际返回的用量与账单,不用块数量推算。
看到 output_text.done 就能停止读取吗?
不能仅据此停止,它只对应文本块。继续检查响应级终态;如果把同一文本的 done 快照再次追加到 delta 后面,还会造成重复。
SSE 断开后能从最后一个字继续吗?
不能默认支持。是否可恢复取决于服务端与客户端协议,SSE 的事件 ID 不自动提供模型生成续传保证。先保存状态并核对服务说明,不把重新发起请求当成原请求无损续接。
返回的是语音时,先区分音频字节与事件
本文的文本增量组装不能直接用于音频文件。语音接口可能直接返回音频字节,也可能把音频数据放在事件中;先核对所选输出方式,再决定解码、保存和播放流程。参见 OpenAI Speech 音频保存与完整性验收,不要把任意网络块直接当成完整音频。
事件已经收完,回答仍被截断怎么办?
完成接收与完成用户任务需要分别核对。如果已取得响应状态或结束原因,却发现文本缺段、JSON 未闭合,按API 输出截断的分类与补齐流程区分输出预算、工具调用和其他停止原因;不要只凭流关闭就把内容标记为合格。
Vercel AI SDK 的聊天界面应该接收哪一种流?
应用框架可能在上游事件之外定义自己的消息协议。使用 useChat 等前端组件时,继续看 Vercel AI SDK 的消息转换与前端流检查,分别确认上游模型适配器、自有服务端路由和浏览器消息传输;不能把提供方原始 SSE 直接当成框架的 UI 消息流。
