AI 流式输出断线后怎么处理:SSE 事件序号、重放与重复片段去重
来源:17golang原创
时间:2026-07-26 14:01:34 217浏览 收藏
页面上的回答刚显示到“订单已经进入……”就停住,浏览器控制台却只剩一条 SSE 连接断开的提示。AI 流式输出遇到这种情况,最危险的处理方式是立刻再发一次请求,然后把新收到的文字直接接在旧文本后面:网络抖动、代理重试或服务端重放都可能让半句话重复两遍。
稳定的做法是把流式响应当成有序事件,而不是一串无法定位的字符:先记录事件 ID 和序号,再按序追加;重连时只接收缺失事件,重复事件直接丢弃,最后还要核对完成态。
要点速览
- SSE 断线只说明传输链路中断,不代表模型任务一定失败。
sequence_number适合做顺序检查,事件 ID 适合做幂等去重。- 客户端不能凭已显示的字符猜断点,服务端要提供事件缓存或完整结果查询。
- 只有收到完成事件并核对最终文本,才能把消息标成成功。
为什么重试一次会得到重复答案
流式接口通常是一边生成、一边通过 Server-Sent Events(SSE)发送事件。客户端已经收到的内容可能只存在浏览器内存里,服务端却还在生成;也可能服务端已经完成,只是最后几个事件没有穿过反向代理。两种情况在客户端看起来都像“卡住了”,处理方式却不同。
Responses API 的流式事件带有事件类型、对象标识和序号。以文本增量为例,response.output_text.delta 表示一小段增量,response.output_text.done 表示文本部分已经收尾。把 delta 当作普通字符串拼接,既看不到中间缺口,也无法判断某一段是否已经处理过。
先把每个事件变成可核对的记录
先定义一条内部事件记录,不要让页面 DOM 成为唯一的状态来源。下面的结构足够支撑顺序检查、短时重放和刷新后恢复:
type StreamEvent struct {
RequestID string `json:"request_id"`
EventID string `json:"event_id"`
Seq int64 `json:"sequence_number"`
Kind string `json:"type"`
Delta string `json:"delta,omitempty"`
Done bool `json:"done,omitempty"`
}
收到事件时,先检查 RequestID 是否属于当前页面,再检查 Seq。如果序号比上次大 1,可以追加;如果等于上次,说明是重复投递;如果跳过了某个序号,就先不要把当前片段展示为最终文本,应进入补取流程。

序号和事件 ID 各自解决什么问题
序号回答“前后顺序是什么”,事件 ID 回答“这一条是否已经处理过”。两者不要混成一个字段:序号可能只在单次响应内有意义,事件 ID 则更适合写入去重表。服务端存储时可以使用 (request_id, event_id) 作为唯一键,客户端则保存当前最大序号和已完成的事件集合。
如果上游没有提供可靠序号,也不要从文本内容推测边界。可以让自己的流式网关为每个下游事件补一个单调递增序号,但要在网关重试时保持这组序号不变,否则同一片段会被当成新事件。
断线后应该重放缺口,而不是拼字符
最小的恢复流程是:先暂停页面追加;根据 request_id 查询服务端是否已经完成;未完成则带上“已收到的最大序号”请求事件重放;完成后再补一次完整结果核对。如果事件缓存已经过期,就只能重新发起一次请求,并使用新的请求编号,不能假装它从旧请求的位置继续。
func accept(e StreamEvent, state *State) bool {
if e.RequestID != state.RequestID || e.EventID == "" {
return false
}
if _, ok := state.Seen[e.EventID]; ok {
return false
}
if e.Seq != state.LastSeq+1 {
state.Missing = true
return false
}
state.Seen[e.EventID] = struct{}{}
state.LastSeq = e.Seq
state.Text += e.Delta
state.Done = e.Done
return true
}
这个函数故意把“缺序号”当成异常状态,而不是把当前 delta 硬塞进文本。生产代码还要限制 Seen 的大小,并在完成后清理短时缓存;长时间保留每个字符级事件,成本往往比保存最终文本更高。
重放、去重和完成态要一起验收
我更建议把恢复状态分成四种:receiving 表示正常接收,waiting_replay 表示缺口待补,completed 表示收到完成事件,failed 表示明确失败。页面刷新时先读取状态,再决定是继续收流、查询最终结果还是显示失败,不要只根据文本框里有没有字来判断。
一次可复现的核对可以这样做:模拟页面显示半句话后断开,让测试代理在序号 4 后断开,再把序号 4、5、6 重复发一次,随后补发序号 5、6。正确结果应只追加一次 5 和 6,最终文本与未断线的基准结果一致,状态从 waiting_replay 变为 completed。日志至少保留 request_id、最后序号、缺口范围、重放次数和最终状态。

反向代理和浏览器还有三个坑
- 代理缓冲会让多个事件成批到达,服务端和客户端都不能把一次网络读取当作一个事件。
- 心跳只能证明连接还活着,不能证明模型生成已经推进;心跳应使用独立事件类型。
- 用户点“重新生成”时要创建新的
request_id,旧请求的迟到事件不能写进新消息。
哪些情况不适合自动续接
涉及扣款、写库、发送通知这类有副作用的工具调用时,不能因为流断了就自动再次触发动作。先查询业务任务状态,确认没有完成记录,再用幂等键恢复;模型文本可以重建,业务副作用不能靠字符串去重。
如果上游没有事件重放能力,最稳妥的降级是把请求标成“处理中”,通过结果查询接口拿完整响应;查询也失败时明确显示“结果待确认”,并记录请求编号。不要向用户承诺已经完成,也不要把新请求的半截内容伪装成旧回答的续写。
常见问题:AI 流式断线处理
收到重复的 delta,直接比较文本可以吗?
不建议。相同文字可能在不同位置合法出现,使用事件 ID 和请求编号去重更可靠。
没有 sequence_number 还能做断线恢复吗?
可以由自己的网关补序号,但必须保证重试和重放沿用原序号;如果做不到,就退回完整结果查询,不要猜字符断点。
收到 done 事件后还要保存文本吗?
要。done 只说明生成阶段收尾,仍应把最终文本、请求编号和状态一起持久化,供刷新、审计和业务后续使用。
断线后立刻重新请求是不是最快?
只有在旧请求明确失败且没有副作用时才适合。否则先查旧请求状态或重放缺口,避免重复生成和重复触发业务动作。
把“显示了文字”改成“结果可验证”
流式输出的难点不在于把 delta 追加到页面,而在于断线、重复、乱序和完成态都能被记录和复查。事件 ID 保证幂等,序号发现缺口,重放或完整结果查询负责恢复,最终状态负责收口。四层都在,网络抖一下只是一次可处理的传输问题;少了其中任何一层,页面看起来正常也可能已经丢字或重复执行。
-
Golang · Go教程 | 8小时前 | golang · sse · Go教程 · net/http · 接口设计 · HTTP Go SSE FLUSH 流式响应 ResponseController463 收藏
-
Golang · Go问答 | 3星期前 | HTTP · sse · Go问答 · 用户体验 · 流式响应 · Go EventSource SSE Go问答 Server-Sent Events 长任务进度 http.Flusher293 收藏
-
Golang · Go问答 | 2星期前 | EventSource · sse · Go问答 · http.Flusher · 实时推送 · EventSource FLUSH text/event-stream Go问答 http.Flusher Go SSE333 收藏
-
Golang · Go问答 | 2天前 | net/http · Go问答 · HTTP重试 · 请求体 · 接口稳定性 · net/http Go HTTP重试 Request.GetBody Request.Clone 请求体复用273 收藏
-
488 收藏
-
254 收藏
-
497 收藏
-
188 收藏
-
科技周边 · 人工智能 | 2天前 | go · openai · AI接口 · Responses API · Go OpenAI Responses API background mode 异步轮询 大模型接口388 收藏
-
科技周边 · 人工智能 | 2天前 | go语言 · 异步任务 · 人工智能 · openai · API工程化 · Go 异步任务 轮询 数据保留 OpenAI Responses API background mode183 收藏
-
202 收藏
-
科技周边 · 人工智能 | 4天前 | API · go · 人工智能 · 工程实践 · 工具调用 · Go Anthropic Messages API tool_use tool_result Claude工具调用368 收藏
-
243 收藏
-
195 收藏
-
186 收藏
-
333 收藏
-
419 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习