服务器发送事件 SSE

SSE (Server-Sent Events)

服务器发送事件 SSE(Server-Sent Events)是基于普通 HTTP 连接的单向推送技术,大模型接口用它把生成结果逐 Token 实时推送给客户端,响应内容类型声明为事件流、每条数据以 data 字段承载。它比 WebSocket 更轻量,更容易穿透代理与网关,是流式输出体验的底层协议。

详细解释

服务器发送事件 SSE(Server-Sent Events) 是一种构建在普通 HTTP 连接之上的单向推送技术:由客户端发起一个普通的 HTTP 请求,服务端在响应建立后保持这条连接不关闭,持续把文本数据按固定格式推给客户端,直到主动结束或连接中断。它和几种常见做法有本质区别。短轮询是客户端每隔几秒反复发请求去问有没有新数据,绝大多数请求都是空跑;长轮询是客户端发一个请求后服务端挂住不返回,直到真的有数据才响应,再立刻发起下一轮,虽然省掉了空跑,但每来一次数据就要重建一次连接;WebSocket 则在一次 HTTP 握手之后升级成独立的双向长连接,用二进制帧通信,收发都能实时。与此相对,SSE 只支持服务端到客户端的单向推送,数据是纯文本而非二进制帧,并且能直接复用标准 HTTP 端口、代理、鉴权与负载均衡,不需要额外的协议升级,因此在穿透企业防火墙与网关时明显更省心。

在 API 接口 层面,大模型的流式输出几乎都建立在 SSE 之上。客户端发出的是一个再普通不过的 POST 请求,只是多带了一个要求流式返回的参数;服务端的响应头会把内容类型声明为事件流,之后的响应体不再是一次性吐出的完整结果,而是被拆成许多个增量分片持续推送。每一片都写成一行以 data 字段承载的文本,事件与事件之间用一个空行分隔,等于告诉客户端「上一段到这里结束、下一段从这里开始」;当生成全部结束时,服务端再推一个约定好的固定结束标记行,客户端读到它就关闭连接。如果请求里没有要求流式,服务端就会在生成完毕后一次性返回完整的响应体。这套约定的好处是它对中间设施几乎是透明的:一个普通的 HTTP 响应流,任何代理或网关都能转发,客户端只要按行解析、边收边渲染,就能把等待首字的时间从「等完整段话」压缩到「等第一个 Token」,这正是 流式输出 体验的基础,也直接改善了 首 Token 延迟 的体感。

工程上,SSE 的难点几乎都不在协议本身,而在链路中间的各种缓冲与超时。由于它是长连接,任何一个中间层如果把响应攒起来再转发,客户端就会在整段生成结束前什么都看不到,这也是绝大多数「流式失效」的真实原因;同时,连接空闲太久又可能被代理按空闲超时切断,于是需要在生成过程中周期性发送心跳。客户端侧还要自己处理断线与重连,并且把已经渲染出来的片段留存下来,才能在重连之后接着拼接,而不是从头再来。此外,客户端想中途取消生成时,SSE 协议本身不提供取消语义,只能由客户端关闭连接或额外发一个取消请求,这一点在设计交互时必须提前考虑。协议的权威说明可以查阅 MDN 的文档 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events 。对做聚合服务的团队而言,把这些细节收在网关侧统一处理是更划算的选择:唯元智创 在流式网关侧统一管理响应缓冲、心跳保活、超时与断线续传,并把 OpenAI 兼容接口 的分片格式规整为一致的事件流,让上层应用不必为不同上游的差异写各自的适配;同时网关会区分首包与后续分片的时延指标,便于把 延迟 问题定位到究竟是首包慢还是中途卡顿。

实时推送技术对比表

技术通信方向底层协议是否需独立端口代理穿透难度断线重连能力典型适用场景
短轮询客户端到服务端单向拉取普通 HTTP否低由客户端自行控制下一次请求数据更新不频繁的简单场景
长轮询客户端到服务端单向拉取普通 HTTP否中,需要关注连接占用每轮响应后立即重新发起即时性要求不高的消息通知
SSE服务端到客户端单向推送普通 HTTP否低,可复用标准端口与代理由客户端监听断开并重连,可配合续传大模型流式输出、进度与通知推送
WebSocket双向独立协议,握手后升级通常需要额外配置高,部分代理不支持升级需自行实现心跳与重连双向实时交互、游戏、协同编辑
HTTP 双向流双向HTTP 版本 2 或版本 3 的原生流否中,依赖较新的中间设施依赖具体实现,通常需重试现代浏览器与服务端的高效双向通信
唯元智创 流式网关服务端到客户端单向推送基于 SSE,向客户端统一转发否低,网关侧统一处理缓冲与代理差异内置心跳与断线续传策略多上游聚合、需要统一流式体验的场景

SSE 工程配置要点表

配置项推荐做法常见错误失败表现排查手段
响应缓冲关闭在应用层与网关层都显式关闭响应缓冲,强制逐块下发只改了应用没改中间层,或依赖默认的开启状态客户端要等整段生成结束才一次性收到全部内容在中间层抓包观察分片是否被攒批,检查缓冲相关配置
心跳保活在无新分片时周期性发送空事件或注释行作为心跳长时间思考期间一个字节都不发连接被代理按空闲超时切断,客户端报错中断对照代理的空闲超时与生成耗时,观察是否在固定时间点断开
超时设置读取超时按最长生成时间放宽,并区分连接超时与读取超时沿用普通的几十秒读取超时长回答生成到一半被客户端或网关掐断比对断开时刻与超时配置,检查客户端读取超时是否过短
代理缓冲在 Nginx 等代理上关闭缓冲并启用分块传输沿用默认的代理缓冲设置表现为假流式,首字节时间被严重拉长临时绕过代理直连后端对比,若差异明显即为代理缓冲
断线重连续传客户端记录已接收片段与最后进度,重连后从头续接断线即整段重来,没有保存任何进度网络抖动后内容重复或丢失,用户体验断裂模拟断网重连,检查客户端拼接逻辑与去重是否完整
并发连接数限制按浏览器与网关上限规划并发,及时复用与释放连接页面或组件未清理旧连接导致连接堆积达到上限后新请求排队或直接被拒统计活跃连接数,检查是否有未及时关闭的 SSE 连接

实战经验与避坑

  1. 绝大多数「流式失效」都是中间层开了响应缓冲造成的,必须显式关闭,否则客户端要等全部生成完才收到:这个问题极具迷惑性,因为本地开发时请求直连后端,分片一出就被发出,一切正常;一上生产,请求要经过反向代理、网关或 CDN,只要其中任意一层把响应攒够一定大小再转发,客户端就要等到整段生成结束才一次性收到全部内容,表现为首批内容的等待时间被拉长到接近总生成时间。排查的正确顺序是由近到远逐层绕过:先直连后端确认分片正常,再依次加回代理与网关,看在哪一层开始攒批。定位后要在该层显式关闭缓冲并启用分块下发,而且要同时检查应用层与基础设施层,只改一处往往仍会被另一处拦住。

  2. 长思考期间要设心跳保活,否则生成时间超过代理空闲超时就会被掐断:SSE 是长连接,而长连接最怕「长时间没有任何字节流动」。当模型在做长推理、检索或调用慢工具时,可能几十秒都不产生新的分片,中间层看到连接空闲就可能按自己的空闲超时主动关闭它,此时客户端会突然报错中断,用户以为是模型出错,其实是链路被掐了。做法是在空闲期间周期性发送一个不含实际内容的心跳事件,让连接始终有字节流动;心跳间隔要明显小于链路上最短的那个空闲超时,并且要在网关层统一配置,而不是指望每个上游应用都记得发。经验值是把心跳间隔设为链路最短空闲超时的三分之一左右,可以有效避免这类静默断开。

  3. SSE 是单向的,客户端要中途取消生成必须关闭连接或另发一个取消请求,不要指望协议层取消:很多团队第一次做流式交互时会想当然地认为「停止」按钮能通过协议通知服务端,但 SSE 只定义了服务端到客户端的推送,没有从客户端回传取消指令的通道。可行做法有两种:一是客户端直接关闭这条事件流连接,服务端检测到连接断开后终止上游生成;二是在连接之外单独提供一个取消接口,客户端带上任务标识调用,服务端据此中止对应生成并释放资源。更稳的组合是两者并用,客户端断开加显式取消接口双保险,专门应对连接状态未能及时同步的边缘情况,无论用哪种,服务端都要确保上游请求被及时取消,避免计算资源空转和持续计费。

  4. 断线要设计重连续传策略,把已生成的片段记录下来以便拼接:移动网络切换、WiFi 抖动、客户端休眠都会让长连接断掉,如果每次断线都从头重新生成,既浪费成本又破坏体验。工程上要让客户端把已经收到并渲染的片段按序留存,重连后请求续接而不是重来;服务端或网关则要支持从指定进度继续生成,或者至少保证重连后不会重复发送已经发过的内容。要特别注意的是拼接去重:重连时最后一段可能有部分重叠,客户端需要按事件标识或进度标记做去重,否则用户会看到内容重复。这套机制在弱网与移动端场景里几乎是必需的,否则重连体验会很差。

  5. 不要在单条事件里塞过大的数据,会触发缓冲与分片问题:SSE 的事件是按行组织的,一条事件过大时,中间层可能因为缓冲策略而延迟下发,也可能在传输层被拆分,客户端如果按固定长度切分解析就会出错。正确做法是保持单条事件尽可能小,把大产物拆分或改由稳定链接承载,需要传大块内容时通过引用而不是直接塞进事件流。此外,内容里如果出现与协议分隔约定冲突的字符,要在发送前做转义处理,避免客户端把一条事件误读成两条。经验是单条事件控制在几 KB 以内比较稳妥,超过之后流式的实时性和稳定性都会明显下降。

  6. 监控要区分首字节时间与后续分片间隔,才能定位是首包慢还是中途卡顿:流式的用户体验由两段指标共同决定,一是从发请求到收到第一个内容片段的等待,二是后续片段之间的间隔是否均匀。如果只看端到端总时延,出现问题时根本分不清是首包慢还是中途卡顿,排查方向会完全走偏。正确做法是分别埋点:首字节时间反映排队、排队与首包处理的情况,分片间隔反映生成与推送的连续性。典型的判断是,首字节时间正常而分片间隔偶发拉长,问题多半在推流链路或上游抖动;首字节时间整体偏高,则更可能是排队或首包处理慢。把这两段指标分开看,才能把问题快速归因到正确的环节。

常见问题

为什么本地测试流式正常,一上生产就变成一次性返回?
这种「假流式」几乎都是中间层开启了响应缓冲造成的,问题既不在模型也不在客户端。本地开发时请求直连后端服务,分片一出就被发出;上了生产之后,请求通常要经过反向代理、网关或 CDN,只要其中任意一层把响应攒够一定大小再转发,客户端就要等到整段生成结束才会一次性收到全部内容,表现就是首批内容的延迟被拉长到与总生成时间接近。排查方法是由近到远逐层绕过:先直连后端确认分片正常,再依次加回代理与网关,看在哪一层开始攒批,然后在该层显式关闭缓冲并启用分块下发。注意要同时检查应用层与基础设施层,只改一处往往仍会被另一处拦住,改完还要用抓包或时间戳再验证一次分片是否真的逐块到达。
SSE 支持中途取消生成吗,客户端的「停止」按钮怎么实现?
SSE 协议本身是单向的,没有内置的取消语义,客户端要停止生成只能主动断开连接或额外发一个取消请求。常见做法有两种:一是客户端直接关闭这条事件流连接,服务端在检测到连接断开后终止上游生成,这是最简单也最通用的方案;二是在连接之外单独提供一个取消接口,由客户端带上任务标识调用,服务端据此中止对应生成并清理资源。两种方式都建议在服务端做一次善后:即便客户端已经断开,也要确保上游请求被及时取消,否则计算资源仍在空转、还会继续计费。工程上更稳的组合是客户端断开加显式取消接口双保险,专门应对连接状态未能及时同步的边缘情况,例如客户端已经离开但连接尚未被探测到断开。
SSE 和 WebSocket 到底怎么选,做聊天应用一定要用 WebSocket 吗?
做单向的流式输出,SSE 通常比 WebSocket 更合适;只有确实需要客户端高频上行时,WebSocket 才更划算。大模型对话的本质是服务端持续下发、客户端偶尔发送一次请求,正是 SSE 擅长的单向推送形态,它复用标准 HTTP 端口与鉴权体系,穿透代理与网关的难度低,实现与调试也更简单。WebSocket 的优势在于双向实时通信,适合协同编辑、实时游戏、语音这类需要客户端高频上行的场景,代价是额外的协议升级、心跳维护与更复杂的中间设施适配。一个实用的判断标准是看上行频率:请求之间以秒计、内容以文本为主时优先 SSE;需要毫秒级的双向小包交互时再考虑 WebSocket。如果两种需求都存在,也可以组合使用,用 SSE 负责内容下发、用普通 HTTP 请求负责上行指令,这样既保留了流式的简单可靠,又满足了双向交互的需要。