流式输出断在半句:一把 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 等不同后端,好处是接入和计费统一;但流式重连属于后端协议的能力差异,统一接口不会抹平它。
现实的做法是:把"能否续接"当作每个后端的一项能力记录下来,在客户端做分支处理。这样断线时你的行为是可预期的,而不是把希望寄托在聚合层。