流在句子中间断了:识别被截断的 SSE 响应,续接而不重复扣费
本文讲解如何区分流式响应中的正常结束和意外截断,并提供续接生成的方法,避免重复扣费。内容涵盖 SSE 协议关键字段、常见截断原因、判断逻辑与工程实现。
流式响应让大模型输出像打字一样逐字返回,但网络波动或服务超时可能导致连接意外断开,留下半截句子。如果不加处理直接重试,可能重复计费并得到重复内容。本文介绍如何准确识别截断,并正确续接剩余生成。
SSE 协议关键字段:区分正常结束与异常断开
Server-Sent Events(SSE)是流式响应的常见格式。每个事件以 data: 开头,事件之间用空行分隔。对于 OpenAI 兼容的 API,流结束标志通常是 data: [DONE]。
- 正常结束:客户端会收到包含
[DONE]的最终事件,或收到finish_reason为stop、length等非空值的 chunk。 - 异常断开:TCP 连接被关闭或超时,没有收到
[DONE],且最后一个 chunk 的finish_reason为null。
因此,判断是否截断的核心逻辑是:检查流是否以 [DONE] 结束,以及最后一个数据块中 finish_reason 是否为空。
常见截断原因与识别信号
截断可能由以下原因引起:
- 网络不稳定导致连接中断。
- 服务端超时(如长时间无数据发送)。
- 客户端主动取消(如用户关闭页面)。
- 代理或网关限制(如缓冲或超时)。
识别信号包括:
- 连接关闭前未收到
[DONE]。 - 最后一个 chunk 的
finish_reason为null。 - 收到
error事件(某些实现会发送错误信息)。 - HTTP 响应头中的
Content-Length与实际接收字节数不符(如果存在)。
续接生成:把剩余部分当作第二次调用
一旦确认截断,可以保存已生成的部分文本,然后发起第二次请求,将已生成内容作为上下文,并指示模型继续生成。关键点:
- 保留原始用户输入与已生成文本:将第一次请求的 messages 加上已生成的 assistant 回复(标记为截断)作为第二次请求的上下文。
- 明确续接指令:在第二次请求中追加一条用户消息,例如“请从上面中断的地方继续生成,不要重复已生成的内容”。
- 使用相同的参数:保持 temperature、max_tokens 等参数一致,以减少风格差异。
- 只处理新增内容:第二次调用返回的文本应仅包含续接部分,避免拼接时重复。
示例消息构造(伪代码):
messages = [
{"role": "user", "content": original_user_input},
{"role": "assistant", "content": partial_output},
{"role": "user", "content": "请从上面中断的地方继续生成,不要重复已生成的内容。"}
]
避免重复扣费:只对新增内容计费
按 token 计费的 API 会对每次请求的输入和输出 token 计费。续接时,第二次请求的输入包含了部分已生成内容,这部分会被再次计费。为了控制成本:
- 精确拼接:确保最终输出不包含重复内容,避免为重复内容付费。
- 合理使用续接:仅在截断时续接,正常结束时不发起额外请求。
- 监控用量:记录每次请求的 token 消耗,便于分析异常。
注意,续接本身会产生额外调用费用,但相比重新生成整个响应,通常更经济。
工程实现建议
在客户端实现流式处理时,建议:
- 使用可靠的 SSE 解析库,正确处理事件边界。
- 设置合理的超时和重试机制,但重试前先判断是否截断。
- 保存每次请求的原始消息和已生成文本,便于续接。
- 在续接请求中,使用相同的模型和参数。
- 对最终拼接结果进行去重检查(例如检查末尾与开头是否重复)。
小结
通过检查 [DONE] 和 finish_reason 可以准确识别截断。续接时,将已生成内容作为上下文并明确指示模型继续,同时注意避免重复计费。这些实践能提升流式应用的鲁棒性,并控制成本。