stream-interruption-recovery · ZH · 2026-11-02

流在句子中间断了:识别被截断的 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 与实际接收字节数不符(如果存在)。

续接生成:把剩余部分当作第二次调用

一旦确认截断,可以保存已生成的部分文本,然后发起第二次请求,将已生成内容作为上下文,并指示模型继续生成。关键点:

  1. 保留原始用户输入与已生成文本:将第一次请求的 messages 加上已生成的 assistant 回复(标记为截断)作为第二次请求的上下文。
  2. 明确续接指令:在第二次请求中追加一条用户消息,例如“请从上面中断的地方继续生成,不要重复已生成的内容”。
  3. 使用相同的参数:保持 temperature、max_tokens 等参数一致,以减少风格差异。
  4. 只处理新增内容:第二次调用返回的文本应仅包含续接部分,避免拼接时重复。

示例消息构造(伪代码):

messages = [
    {"role": "user", "content": original_user_input},
    {"role": "assistant", "content": partial_output},
    {"role": "user", "content": "请从上面中断的地方继续生成,不要重复已生成的内容。"}
]

避免重复扣费:只对新增内容计费

按 token 计费的 API 会对每次请求的输入和输出 token 计费。续接时,第二次请求的输入包含了部分已生成内容,这部分会被再次计费。为了控制成本:

  • 精确拼接:确保最终输出不包含重复内容,避免为重复内容付费。
  • 合理使用续接:仅在截断时续接,正常结束时不发起额外请求。
  • 监控用量:记录每次请求的 token 消耗,便于分析异常。

注意,续接本身会产生额外调用费用,但相比重新生成整个响应,通常更经济。

工程实现建议

在客户端实现流式处理时,建议:

  • 使用可靠的 SSE 解析库,正确处理事件边界。
  • 设置合理的超时和重试机制,但重试前先判断是否截断。
  • 保存每次请求的原始消息和已生成文本,便于续接。
  • 在续接请求中,使用相同的模型和参数。
  • 对最终拼接结果进行去重检查(例如检查末尾与开头是否重复)。

小结

通过检查 [DONE] 和 finish_reason 可以准确识别截断。续接时,将已生成内容作为上下文并明确指示模型继续,同时注意避免重复计费。这些实践能提升流式应用的鲁棒性,并控制成本。