在大语言模型应用场景中,流式输出已成为提升用户体验的关键技术。当用户向 AI 发起一次对话请求时,模型并非一次性返回完整答案,而是逐 token 生成并实时推送给前端,这种即时反馈机制能够让用户快速看到思考过程,显著降低等待焦虑。然而,多模型环境下的流式输出带来了新的工程挑战:如何在保证响应速度的同时,妥善处理网络波动、代理超时以及模型端的异常中断。本文聚焦于 Server-Sent Events(SSE)协议在多模型流式场景下的连接管理策略,提供可落地的参数配置与断线续传方案。
流式输出的技术选型与 SSE 优势
在 Web 实时通信领域,实现服务器向客户端推送数据的主流方案包括 WebSocket、全双工 HTTP2 以及 SSE。WebSocket 适用于需要双向通信且传输二进制数据的场景,协议开销相对较高;HTTP2 的 Server Push 特性虽然强大,但在 HTTP/3 普及前的负载均衡兼容性问题较多。SSE 则凭借其轻量级、单向推送、基于 HTTP/1.1 即可部署的特性,成为 LLM 流式输出的首选方案。SSE 本质上是通过 HTTP 长连接,以 text/event-stream 格式逐块发送数据,浏览器原生提供 EventSource 接口进行消费,无需引入额外的 Socket 库或复杂的状态管理。
对于同时对接多个模型供应商的系统而言,SSE 的统一接入层可以抽象出通用的流式接口。前端只需订阅同一个端点,后端根据请求参数动态路由到不同的模型提供商(OpenAI、Anthropic、国产模型等),并统一将响应转换为 SSE 格式转发。这种架构设计降低了前端与具体模型的耦合度,同时也为后续的流量调度、熔断降级提供了统一的控制平面。
连接保持:心跳保活与代理超时配置
SSE 长连接面临的第一个工程问题是中间设备的连接超时。企业的负载均衡器、Nginx 反向代理、云厂商的 API 网关通常设有空闲超时机制,当 HTTP 连接在配置时间内没有任何数据传输时,连接会被强制断开。对于 LLM 流式输出场景,模型在推理过程中可能因为思考时间较长而出现短暂的静默期,如果恰好超过代理的空闲超时阈值,整个流式连接就会意外终止,用户看到的结果往往是界面突然卡住然后报错。
针对这一问题的标准解决方案是心跳保活机制。服务器端每隔固定时间向客户端发送一个空事件或注释行,保持连接的活跃状态。根据业界的最佳实践,推荐的心跳间隔为 30 秒,这一数值能够在大多数代理的默认超时阈值(通常为 60 秒)以下提供足够的缓冲空间。心跳事件的格式可以是简单的冒号空行(如 ": heartbeat\n\n"),这类事件不会触发客户端的 message 回调,但能够有效维持 TCP 连接的活跃状态。
除了应用层的心跳之外,还需要在基础设施层面配置合适的超时参数。以 Nginx 为例,需要特别关注 proxy_read_timeout 参数的设置,该参数默认只有 60 秒,对于需要长时间推理的 LLM 请求明显不足。建议将 proxy_read_timeout 设置为 300 秒甚至更高,具体数值取决于业务预期的最大推理时长。同时必须确保 proxy_buffering 参数被禁用,否则 Nginx 会缓冲服务器的流式响应,导致客户端无法实时看到输出。AWS ALB 等云负载均衡器同样需要检查空闲超时配置,部分产品的默认超时可能低至 30 秒,需要通过修改负载均衡器属性或使用 TCP 监听模式来规避。
断线续传:Last-Event-ID 与事件缓冲区设计
即使做了充分的心跳和超时配置,网络故障仍然可能发生。用户的设备可能进入休眠、WiFi 可能切换到移动网络、代理服务器可能因资源限制主动断开连接。此时流式传输的中断并不意味着请求失败,用户期望的是能够在网络恢复后从上一次的断点继续接收数据,而不是重新开始一次完整的推理过程。
SSE 协议原生支持断点续传机制,关键在于 Last-Event-ID 头部。当客户端重新连接时,可以在请求头中携带上一次接收到的最后一个事件的 ID,服务器据此判断客户端已经接收到的数据位置,从断点之后继续推送后续事件。这一机制的实现需要服务端维护一个有限长度的事件缓冲区,保存最近若干条事件的状态信息。当收到带有 Last-Event-ID 的重新连接请求时,服务器从缓冲区中查找对应位置,将缓冲区中的后续事件重新发送一遍。
缓冲区的大小设计需要权衡内存占用与恢复能力。一般建议保留最近 50 到 100 个事件,对于大多数 LLM 流式输出场景已经足够 —— 即使每个 token 作为一个事件发送,100 个事件也对应上百个 token 的上下文,完全能够覆盖一次短对话的恢复需求。如果业务场景涉及更长的连续输出,可以考虑将缓冲区改为环形结构,或者在 Redis 等分布式缓存中存储事件状态,支持多实例部署时的跨节点恢复。
事件 ID 的生成策略同样重要。最简单的方式是使用递增的整数 ID,但更推荐的做法是结合时间戳和随机后缀,确保在分布式场景下也不会出现 ID 冲突。每发送一个事件时,除了 data 字段外,务必同时带上 id 字段,这是客户端正确维护 Last-Event-ID 的前提条件。
重试策略:指数回退与抖动参数
当 SSE 连接发生错误时,浏览器原生的 EventSource 会自动尝试重新连接。默认的重试间隔由服务器在响应头的 retry 字段指定,首次失败后通常会立即重试,如果再次失败则逐渐拉长间隔。然而,原生的重试策略在面对大规模故障时可能产生 “惊群效应”—— 大量客户端在同一时刻同时发起重试,瞬间冲垮服务器。
合理的做法是在客户端实现指数回退策略。首次重试的延迟可以设置为 1 秒,随后每次重试间隔翻倍,直到达到某个上限(如 30 秒或 60 秒)。单纯的几何级数增长仍然可能导致大量客户端在相同时间点聚集,因此需要在回退计算中加入随机抖动。将每次计算的间隔乘以 0.5 到 1.5 之间的随机系数,能够有效分散重试请求的时间分布。
服务端也可以通过 retry 字段主动控制客户端的重试行为。在建立 SSE 连接的初始响应中携带 retry: 5000(单位为毫秒),可以告知客户端 “至少等待 5 秒后再进行第一次重试”。这一机制配合客户端本地的指数回退逻辑,能够在服务端暂时过载时为系统争取到宝贵的恢复时间。需要注意的是,retry 字段设置过大会影响用户在真正网络故障时的体验,建议根据业务对实时性的要求在 1 秒到 5 秒之间选取初始值。
对于 LLM 场景,还需要考虑模型推理本身的可重试性。部分模型提供商在返回流式响应时会在中途遇到服务异常,此时如果简单地让客户端重新发起请求,可能导致模型重新开始生成而非从断点继续。一种可行的方案是在服务端实现 “流式会话保持”—— 为每次流式请求分配唯一的会话 ID,客户端重连时携带该会话 ID,服务端尝试在同一个模型实例中恢复推理上下文。不过这一方案高度依赖模型提供商的支持程度,在实际落地时需要评估兼容性。
监控指标与工程化落地清单
将上述技术方案投入生产环境,需要建立完善的监控体系来持续评估系统的健康状态。核心监控指标包括 SSE 连接的建立成功率、客户端重连频率、平均单次流式输出的完成时间、以及心跳事件的发送延迟。这些指标可以通过在 SSE 端点埋点采集,或者利用 Nginx 的访问日志进行统计。
对于连接成功率的监控,建议设置告警阈值为 99.5%。如果成功率持续低于该值,说明要么基础设施配置存在问题(代理超时过短、负载均衡策略不当),要么模型端的响应时间异常拖累了整体可用性。客户端重连频率则是一个先行指标,当该指标开始上升时,往往意味着网络或服务端即将出现更严重的故障,此时应当提前介入排查。
在工程落地层面,以下参数清单可作为配置参考:心跳间隔设置为 30 秒,Nginx 的 proxy_read_timeout 不低于 300 秒,负载均衡器空闲超时不低于 60 秒,事件缓冲区保留最近 100 条记录,客户端首次重试延迟 1 秒、最大重试延迟 60 秒、加入 0.5 至 1.5 倍的随机抖动。这些参数并非一成不变,应当根据实际业务量级、模型响应速度以及基础设施能力进行微调。建议在系统上线初期采集详细的连接日志,分析实际发生的断开场景和恢复效果,逐步迭代优化参数。
综上所述,SSE 在多模型流式输出场景下的连接管理需要从协议层、基础设施层和应用层多方位入手。通过心跳保活维持长连接活性、借助 Last-Event-ID 实现断点续传、采用指数回退加抖动控制重试节奏,能够构建出具备容错能力的流式服务。这些工程实践的参数化方法论,为 AI 系统的稳定输出提供了可复用的技术路径。
参考资料
- Solita Dev:Server-Sent Events (SSE) 指南(2024)
- Upstash:使用 SSE 流式传输 LLM 响应最佳实践