AI网关流式响应稳定性与断流重连治理落地

日期:2026-08-08

一、项目背景

这个客户的业务全量走流式输出,用户在界面上看到的是文字一个个吐出来。产品体验上这是对的,等三十秒出一整段和边生成边看,感受完全两码事。但工程上这条链路很脆。

他们上线三个月,用户投诉里排第一的是"答到一半断了"。断在半句话上,前端只能提示重试,用户点重试,模型从头再生成一遍,前面那半段白等。客服那边收集的原话很扎心:一位用户说"我每次问长问题都要祈祷它别断"。

我们介入后先做了两周的链路埋点,把断流按位置归类:客户端网络波动占三成,中间的负载均衡器空闲超时占两成八,网关到模型实例之间的连接异常占两成一,模型侧主动结束异常占一成三,剩下的杂项。也就是说没有单一元凶,每一段都在漏。

客户原来的思路是"哪断了修哪",改了几轮效果有限,因为流式链路的特点是任何一环断,用户看到的都是同一个现象,而修复只能覆盖其中一小块。我们建议换个思路:接受断流会发生,把重点放在断了之后能不能续上。

二、落地场景

方案在 AI 网关上落地,不改业务代码,前端只需要接入一个更新后的 SDK。

网关侧给每一次流式会话分配会话标识,从第一个数据帧开始,网关把已经吐给客户端的内容做增量缓存,同时记录序号。缓存放在内存里,带过期时间,默认五分钟。

客户端断开后重连时带上会话标识和已收到的最后一个序号。网关查缓存,如果这个会话还在生成中,就从断点之后继续推送;如果已经生成完毕,把剩余部分一次性补齐;如果缓存已过期,才回退到重新生成。

长连接保活这块,网关在没有内容产出的间隙定时发送心跳帧,避免中间的负载均衡器和反向代理因为空闲超时把连接掐掉。心跳帧是注释格式,客户端 SDK 直接忽略,不影响内容。

超时做了分级。首字超时、帧间超时、总时长超时三个阈值分别配置,触发不同的处理动作:首字超时直接失败并快速重试,帧间超时先探测后端状态再决定,总时长超时优雅收尾并告知客户端内容可能不完整。

异常帧兜底:后端返回格式错误或非法内容的帧,网关拦下来不透传,避免前端解析崩溃。

三、关键技术挑战与解决思路

增量缓存的内存开销是第一个要算清楚的账。峰值并发流式会话数大约八千,单次响应平均两千个 token,按中文估算平均六千字节,全缓存下来大约不到五十兆,看起来很少。但实际跑起来内存涨到了三百多兆,原因是我们最初用字符串拼接,每来一帧就生成一个新字符串,垃圾回收压力巨大。改成分块链表结构,只在客户端请求续传时才做拼接,内存降回七十兆左右,回收压力也下去了。

续传的内容一致性比想象中麻烦。流式生成是有状态的,同一个会话在后端模型实例上有 KV 缓存。客户端断开后,如果后端也跟着中止生成,续传就无从谈起。我们的处理是把客户端连接和后端生成解耦:客户端断开后,网关不立即中止后端生成,继续接收并缓存,给出一个宽限期(默认三十秒)。宽限期内客户端重连就能无缝续上,超过宽限期还没重连才中止后端并释放资源。这个宽限期的取值调了几轮,太短续传成功率上不去,太长白白浪费算力。最后定在三十秒,是拿真实重连时间分布的九十分位数定的。

重复内容去重是被生产环境教育出来的。上线第一周发现有用户看到重复的句子。排查后发现是客户端 SDK 的序号确认逻辑有竞态:客户端收到帧后先渲染再更新序号,如果在这两步之间断开,重连时上报的序号会偏小,网关就从更早的位置续推,导致重复。改成先更新序号再渲染,并且在渲染层做了幂等,同一序号的帧重复到达直接丢弃。这个 bug 藏得挺深,靠日志比对才找出来。

心跳帧的频率也调过。最初设的是十五秒一次,结果发现客户环境里有一层反向代理的空闲超时是十秒,照样被掐。把频率提到五秒后正常。这类基础设施参数最好在方案设计阶段就摸清楚,我们是上线后才发现的,白折腾了两天。

案例片段(已脱敏): 一次成功续传的网关日志:会话 sse-7f3a2c 生成中,已推送 seq=0..184;客户端连接在 t=12.4s 断开(对端重置);后端生成继续,缓存至 seq=311 时生成完成;客户端在 t=15.1s 携带 last_seq=182 重连;网关校验会话有效,从 seq=183 开始补推,一次性补齐 129 帧,客户端无感知续上。这条会话的用户侧总耗时 18.6 秒,若按旧逻辑重新生成需要约 31 秒且前 12 秒作废。 超时分级的配置意图:首字超时 8 秒(超时快速失败并换实例重试),帧间超时 20 秒(超时先探测后端健康,健康则继续等待一个周期),总时长超时 180 秒(优雅收尾,向客户端发送截断标记)。三档独立配置,按业务线可覆盖,长文写作类业务把总时长放宽到 300 秒。

四、效果数据

治理完成后运行两个月的数据。用户侧感知到的断流率,从原来的百分之四点七降到百分之零点三一。这里的口径是"用户看到不完整内容且无法自动恢复"的会话占比,续传成功的不计入。

续传成功率百分之九十三点六,剩下的六点多主要是超过五分钟缓存过期或者宽限期内没重连。首字延迟因为加了一层网关处理略有增加,从平均六百二十毫秒到六百五十毫秒,这个代价可以接受。重复输出率在修掉序号竞态之后从百分之一点二降到接近零,两个月里只出现过三次,都是极端网络抖动场景。

间接效果是算力浪费减少。原来断流后重新生成,那部分算力全白费,按埋点估算每天浪费的 token 生成量占总量的百分之五点八。治理后降到百分之零点七。客服工单里关于"生成中断"的投诉从每周三十多条降到个位数。文中数据为项目复盘口径,已脱敏。

五、可复用经验总结

流式链路的稳定性治理,思路上要从"防止断开"转到"断了能续"。防不胜防,客户端网络这一段网关根本控制不了。把重点放在续传能力上,投入产出比高得多。这个转向是这个项目最关键的决定。

客户端连接和后端生成必须解耦。这两者一旦绑死,任何客户端侧的抖动都会浪费一次完整的生成。宽限期机制虽然会短暂占用一点算力,但对比重新生成的浪费,账很好算。

序号协议要设计得幂等且容错。客户端上报的序号不可信,可能偏大也可能偏小,网关要能处理这两种情况,渲染层也要做幂等。我们在这上面栽过跟头,用户看到重复内容的体验比看到断流还糟糕。

基础设施的超时参数要提前摸清。负载均衡器、反向代理、防火墙,每一层都可能有自己的空闲超时,链路上最短的那个决定了心跳频率的上限。这件事花不了多少时间,但不做就会在上线后被打脸。

缓存的数据结构对内存影响很大。流式场景是典型的高频小块追加,用字符串拼接是个陷阱,看着代码简洁,实际上垃圾回收压力会把服务拖垮。

附:工程落地细节

增量缓存用分块链表,每块固定容量,追加时只操作尾块。会话标识用雪花算法生成,包含时间戳便于过期清理。缓存过期采用惰性清理加定时扫描双保险,定时扫描每三十秒跑一次,只扫过期时间索引不遍历全量。心跳帧走注释格式,符合 SSE 规范,任何标准客户端都会自动忽略。宽限期内的后端生成会被打上标记,如果这段时间内系统整体负载超过阈值,优先中止这类无客户端连接的生成,保证有连接的用户体验。超时配置支持按业务线和按模型两个维度覆盖,配置变更热生效不重启。异常帧的拦截规则包括格式校验和内容长度校验,被拦截的帧记录到独立日志用于排查后端问题。

附:一点实在的提醒

流式功能的可观测性要单独建。常规的请求成功率、延迟这些指标在流式场景下会骗人:一个会话 HTTP 状态码是 200,内容断在一半,监控上看完全正常。我们专门加了"内容完整性"这个指标,通过检查会话是否收到了结束标记来判定。加这个指标之前,客户的监控面板一片绿,用户投诉一片红,双方都觉得对方在胡说。指标不对,后面所有的优化都是盲人摸象。