multi-backend-stream-reconnect · ZH · 2026-10-22

流式输出断在半句:一把 key 上哪些后端能从中断处续、哪些只能重来

从一把聚合 key 调用多个模型时,流式输出断线后能否从半句续接,取决于后端协议本身而非聚合层。文章讲清 SSE 增量返回、可重放请求与断点续传的区别,给出识别信号与代码写法,并说明哪些情况只能整段重发。

问题从哪里来

流式输出的本质是服务端把一次生成拆成很多个增量块(delta/event)陆续推给你。网络一断,你已经收到的是半句话。

此时有两种期望:

  • 续接:把已收到的部分交给后端,让它接着往下生成。
  • 重来:重新发起同一次请求,从头再生成一遍。

是否能续,很大程度上不取决于你的聚合层,而取决于后端 API 本身有没有暴露"从这里继续"的语义。

三个容易混淆的概念

先把它们分开,后面的判断才准确。

  • 增量传输:SSE / chunked 只负责把结果分片推过来,不承诺可恢复。
  • 请求幂等重放:同一个请求体再发一次,你能拿到完整结果,但通常会重新生成,不是续接。
  • 断点续传:协议层提供游标、序号或 continuation token,允许从某个位置继续。

绝大多数聊天补全类接口只做到第一条。少数场景(长任务、文件、批处理)才具备第三条。

判断一个后端能否续接,看这几个信号

不用猜,看响应里有没有可用的"位置"信息:

  • 每个数据块是否带有单调递增的序号或索引,且能作为下一次请求的入参。
  • 响应里是否有 continuation / cursor / resume token 一类字段。
  • 文档是否提供"在上一次输出之后继续"的显式参数,而不是让你把已生成内容拼回 prompt。
  • 中断后,服务端是否保留了这次生成的状态(会话、任务 ID)。

如果以上都没有,那么无论中间层怎么封装,都拿不到真正的续接能力。

常见后端的实际形态

按可续接性粗略分类(具体以各家当前文档为准):

  • 纯对话式流式接口(多数聊天补全类模型):通常只给增量文本,没有游标,断线后一般只能整段重发。把已收到的文本拼回 prompt 再请求,得到的是"重新生成后半段",而不是原轨迹的延续,措辞和内容可能明显不同。
  • 带会话/任务 ID 的接口:如果服务端保留了生成状态,可能支持重新连接后继续推送;这属于协议特性,需要逐家核实。
  • 长任务或异步作业类接口:常见做法是先提交任务拿到 ID,再轮询或重新订阅结果流,这类天然更适合"重连取剩余结果"。

结论:跨后端统一续接很难。聚合层能做的是让你用同一把 key、同一套代码去调用,但不会凭空给后端加上断点续传能力。

代码里怎么识别

1. 解析 SSE 时记录可恢复位置

如果数据块里有 index/seq 字段,就存下来:

last_index = None
buf = []
for event in sse_stream(resp):
    data = json.loads(event.data)
    if "index" in data:
        last_index = data["index"]
    delta = data.get("choices", [{}])[0].get("delta", {})
    if delta.get("content"):
        buf.append(delta["content"])

last_index 只有在能作为下次请求参数时才有意义;否则它只是一个计数器。

2. 中断时区分"网络错"和"服务端错"

try:
    stream_once()
except (ConnectionError, TimeoutError) as e:
    # 可考虑重连;但要先确认后端是否支持续接
    handle_reconnect()
except APIError as e:
    # 4xx/5xx 语义错误,重连通常无意义
    raise

注意:超时、连接重置属于传输层;限流、参数错误属于业务层,两者处理策略不同。

3. 没有续接能力时的重发策略

此时务必要有幂等与去重意识:

request_id = str(uuid.uuid4())
payload = {"messages": messages, "stream": True}
# 重发时带上同一个 request_id,便于服务端或本地去重

重发前应决定:是整段重来,还是把已输出内容作为 assistant 消息拼回对话再请求后半段。后者在多数对话接口上可行,但要接受"不是原轨迹"的事实,并且要防止重复开头。

工程上的实用做法

  • 先探测再决策:初始化时用一个小请求确认该后端响应字段里有没有可续接标识,把结果缓存成能力表。
  • 按后端配置重连策略:可续接的走重连续传;不可续接的走整段重发,并设置最大重试次数。
  • 前端兜底:把已收到的文本立刻渲染出来,重发时用占位或"重新生成中"提示,避免用户看到内容闪烁。
  • 日志里记录断点:记录模型名、请求 ID、已接收块数、最后事件类型,方便事后判断是网络抖动还是后端行为。

一把 key 的意义与边界

用同一把 key 调用 Claude、GPT、DeepSeek、Qwen、GLM、Kimi 等不同后端,好处是接入和计费统一;但流式重连属于后端协议的能力差异,统一接口不会抹平它。

现实的做法是:把"能否续接"当作每个后端的一项能力记录下来,在客户端做分支处理。这样断线时你的行为是可预期的,而不是把希望寄托在聚合层。