流式中途断流:已经吐出 token 后报错该怎么续
本文讲解流式(SSE)对话在已经输出部分 token 后中途报错的成因、检测方式与恢复策略:如何用 finish_reason 和重试标记区分正常结束与异常中断,如何在客户端缓存已生成文本做断点续写,以及通过幂等、超时与重试预算让聚合 API 调用更稳。
流式接口为什么“吐一半会断”
用 SSE 调用 LLM 时,响应是一个持续推送的事件流。理想情况下你会在结束时收到明确的结束事件(如 finish_reason 或一个 [DONE] 标记),代表模型正常完成。
但真实链路很长:客户端 → 聚合站网关 → 上游供应商 → 模型。任何一环出问题,都会让流在“已经吐出一些 token 之后”断掉。常见原因包括:
- 上游连接超时或空闲超时(长时间没有新 token)。
- 上游返回中途错误事件,或直接关闭连接。
- 网络抖动、代理重置、客户端读取超时。
- 模型侧限流或过载导致流被中断。
关键点:HTTP 层面连接关闭 ≠ 请求失败。你可能已经拿到了一半有效内容,直接丢弃很浪费,尤其是在长文生成场景。
第一步:区分“正常结束”和“异常断流”
不要用“连接关闭”判断成功与否,要看你是否收到了结束信号。
- 正常结束:收到
finish_reason(如stop)或收到的[DONE]标记,说明这一轮生成完整。 - 异常断流:连接关闭,但既没有
finish_reason,也没有完成标记。 - 中途错误事件:部分实现会在流里推一个 error 事件,此时应立刻记录并停止累积。
一个稳妥的做法是在客户端维护状态机:
状态:streaming -> finished
\-> interrupted(连接断/错误,且未 finished)
只有进入 finished 才认为这一轮成功。进入 interrupted 时,已累积的文本就是“部分结果”,可用于续写。
第二步:把已生成的文本当成可用资产
SSE 是增量推送,所以最自然的做法是边收边拼:
- 每收到一个 delta,就 append 到本地 buffer。
- buffer 建议持久化(内存 + 可选落盘),因为断流可能发生在任何时刻。
- 记录断流时的
finish_reason=null状态,并标记这条消息“未完成”。
这样即使请求中断,UI 上也能保留用户已经看到的内容,而不是整段消失。
第三步:断点续写的三种策略
续写的本质是:如何把“已生成的部分”告诉模型,让它接着写。 没有一种方法能保证与原轨迹完全一致,需要按场景取舍。
策略一:拼接式续写(最通用)
把“原始 prompt + 已生成的部分”作为上下文,再发一轮请求,并指示模型“从断开处继续,不要重复已有内容”。
- 优点:实现简单,不依赖任何特殊接口。
- 缺点:模型可能在衔接处轻微重复或风格微变;长内容会消耗更多上下文。
适合:长文、报告、代码等可接受轻微接缝的场景。
策略二:从安全断点重试
很多应用会遇到“结构化输出写一半断掉”的情况,比如 JSON。这时更稳的做法不是续写,而是从最近一个完整边界重新生成。
- 只保留到最后一个能解析的完整块(如一个完整对象/数组元素)。
- 丢弃不完整的尾部,重新请求补全。
- 因为聚合站按 token 计费,重试会带来额外消耗,所以要在“重试成本”和“输出正确性”之间权衡。
适合:JSON、表格、逐条列表等对结构完整性要求高的输出。
策略三:分段生成
与其让模型一次输出很长内容,不如在业务层就把任务拆成多轮:每轮生成一个较小的段落,每段单独处理成功/失败。
- 单段断流时,只重试该段,爆炸半径小。
- 每段都有明确的
finish_reason,状态更清晰。
适合:可控长度的长文档、批量抽取等。
第四步:重试要用“预算”管住
断流后自动重试很常见,但盲目重试会放大成本和延迟,尤其是在按量计费的聚合站上。
建议给每次请求设置重试预算:
- 限制最大重试次数(少量次数即可)。
- 只对可重试错误重试:网络断开、超时、上游 5xx;对参数错误、鉴权失败不要重试。
- 采用退避(backoff),避免瞬时打满上游。
- 超过预算仍失败时,明确告诉用户“内容未完成”,并保留已生成部分供手动继续。
另外注意:流式请求本身不一定幂等。同一个 prompt 重试可能得到不同输出,所以续写通常比整轮重放更省。
在聚合 API 场景下的额外注意点
用“一个 key 调多个模型”的聚合站时,断流的来源可能在你和上游之间多一跳。可以做这些事让链路更可观测:
- 记录每次请求用的是哪个模型、是否收到完成标记、断流发生在第几段。
- 对同一任务做多模型对比时,把“断流率”当作一个监控指标,而不是只看单次结果。
- 设置客户端读取超时(idle timeout),不要让一个卡住的流无限等待。
- 保留 request id 之类的追踪标识,便于排查是网络问题还是上游问题。
一个可落地的处理流程
把上面的要点串起来:
- 发起 SSE 请求,进入 streaming 状态,逐块 append 到 buffer。
- 收到
finish_reason/完成标记 → 标记 finished,正常结束。 - 连接关闭但未 finished → 标记 interrupted,保留已生成文本。
- 判断输出类型:
- 普通长文 → 拼接式续写;
- 结构化输出 → 从安全断点重试;
- 可拆分任务 → 按段生成并只重试失败段。
- 在重试预算内续写,超预算则提示用户并展示部分结果。
- 记录日志,便于观察断流分布与模型差异。
小结
流式断流不是罕见故障,而是需要被设计的正常路径。核心原则有两条:
- 用结束信号判断成功,而不是用连接是否关闭。
- 把已生成的 token 当作资产,先保留、再决定是续写、从断点重试还是分段重做。
把这两点做扎实,再配合重试预算和日志监控,就能在不稳定的网络与多跳链路下,给用户一个“断了也能接着用”的流式体验。