Toonflow 流式输出 SSE 断连后怎么恢复现场,是靠 Last-Event-ID 续传还是整段重跑?

Toonflow 的 SSE 断连恢复要三处同时配置:服务端按运行标识缓存带 id 的事件并支持 Last-Event-ID 补发,网关关闭响应缓冲并放大读超时,客户端做退避重连与按事件 id 去重。只配其中一处,断连后要么丢中间片段,要么整段输出重跑。

Toonflow 的流式输出断连恢复不是控制台里的一个开关,而是服务端事件缓冲、网关免缓冲与超时、客户端重连去重三层同时到位才成立;只改任意一层,断连后要么丢掉中间已生成的片段,要么整段重跑并重复消耗算力。短输出场景(几秒内跑完)不值得做续传,直接重跑成本更低,这也是判断要不要投入配置的第一道分界线。 SSE 的恢复能力上限,由服务端能记住多少条历史事件决定,而不是由客户端重连次数决定。SSE 是单向 HTTP 长连接,服务端每条事件写 id 与 event 字段,浏览器 EventSource 在连接断开后自动重连,并在请求头带上 Last-Event-ID;服务端读到这个头,就该从缓冲区里把该序号之后的事件按序补发。没有这个头或服务端不认这个头,重连就只是一条新连接,前端看到的输出会从断点处重新开始。 Toonflow 侧要为每次生成分配可重放的运行标识,并按该标识分片存放事件缓冲区。做法是每次运行生成唯一的 run_id,事件缓冲区以 run_id 为键,用环形结构只保留最近若干条事件,并设置一个覆盖单次最长生成时长的过期时间;事件 id 用单调递增整数,不掺时间戳,避免同一毫秒内多事件导致序号重复。缓冲区过期后不要伪造补发,直接引导客户端走状态查询。 续传解决的是“看得到”,状态查询解决的是“算得对”,两者不能互相替代。当断连时长超过缓冲区保留时间、或重连请求落到了另一台实例,客户端应调用按 run_id 查询的状态接口,拿到当前已生成的全量文本、节点执行状态和是否已完成;服务端需要保证同一 run_id 的生成任务不被重复触发,否则用户刷新一次就多跑一次工作流。 大量断连其实与模型无关,而是中间层攒够缓冲才肯吐数据。反向代理要关闭响应缓冲与缓存,并显式下发免缓冲响应头,同时把读超时和发送超时放大到大于单次最长静默期;开了压缩、经过 CDN 或走 HTTP/2 聚合的链路,都可能把多个小事件合并成一个分块,表现为前端长时间白屏然后一次性刷出整段文本。排查时先直连服务端口对比,再逐层加回代理。 心跳事件的作用是让中间层知道连接还活着,但它不能占用业务事件的序号空间。每 15 至 30 秒发一条注释行或独立的 ping 事件即可,心跳不带业务 id,也不进入补发缓冲区;如果把心跳也编进事件序列,Last-Event-ID 会被推到一个没有真实内容的位置,重连时补发就会出现空洞。 客户端的重连必须带退避和上限,否则一次网络抖动会演变成重连风暴。EventSource 可以用服务端下发的 retry 字段控制默认间隔,自研 fetch 加流式读取的方案则要自己实现指数退避加随机抖动,设置最大重试次数和最大单次间隔,并在成功收到任意业务事件后把计数清零;切换网络(例如移动数据与 Wi-Fi 之间)或页面从后台恢复时,应主动重建连接而不是等超时。 补发的事件要按 id 去重和重排,不能按到达顺序直接拼接。客户端维护一个已应用事件的水位值,收到补发事件时丢弃序号小于等于水位的事件;文本增量先按 id 排序再拼接,否则乱序补发会让 Markdown 的代码块和表格渲染错位,用户看到的是已修正又被打乱的半成品。 多副本部署时,进程内存里的缓冲区只在单实例成立的条件下有效,长连接一旦落到别的副本就读不到历史。这种情况下缓冲区必须外置到共享存储或消息队列,或者对同一会话启用粘性会话把连接钉在同一实例;前者通用但多一次读写开销,后者配置简单但实例重启就丢现场,需要按可用性要求取舍。 完成和错误必须有明确的终止语义,否则正常的结束会被客户端当成异常反复重连。服务端在生成结束时发送带完成标记的终止事件,出错时发送带错误码的 error 事件;客户端收到终止事件后主动关闭连接并停止重连,服务端也可以用无内容的成功响应或一个很大的 retry 值来终止重连行为。缺少终止语义的流,会在后台持续堆积无效连接。 参数是否可调以 Toonflow 当前版本的官方说明为准,控制台里没有对应项时就走部署层配置。可核对的观测项建议固定为四个:单次连接的首字节时间、断连率、重连后补发条数、补发重复率;验证方法是用一段足够长的输出跑十分钟,中途主动切断连接两到三次,看最终文本是否与不断连时逐字一致。 恢复粒度要和业务对齐,字符级续传并不总是正确目标。多轮对话场景应按“轮”保存现场,重连只补当前轮,前面轮次从快照读取;一次性长文生成才需要字符级补发。把这两类混在同一套缓冲策略里,往往导致缓冲区被短任务挤占,长任务反而最先丢失现场。