想象你在一家高级餐厅点了一道“佛跳墙”,后厨需要四十分钟才能做好。传统做法是后厨把所有食材炖好、摆盘、淋汁,然后一次性端上桌——你只能对着空桌子干等。但如果大厨每炖好一块鲍鱼、每煨熟一朵香菇,就通过传菜窗先端出一小碟,你边尝边等,四十分钟不知不觉就过去了。 大模型生成回答的原理与此惊人相似:一段 500 字的回答,模型可能要计算二三十秒。传统 HTTP 请求要求等模型算完整个回答才一次性返回,用户只能盯着 loading 转圈。而 SSE(Server-Sent Events,服务器推送事件)技术就像那条“传菜窗”——模型每生成一个词或一个标点,就立刻通过连接推送过来,前端逐字渲染,用户看到的仿佛模型在“实时打字”。 本文沿着一条完整工程链路展开:先讲为什么要流式,再讲 SSE 协议原理、后端分块传输、前端 EventSource 消费、断线重连续传,最后聊生产环境中的工程化细节。读完你不仅能听懂 SSE 流式的完整逻辑,还能亲手搭出一套可运行的大模型流式输出服务。
掌握 SSE 流式输出是构建大模型应用的基本功。聊天机器人、AI 搜索、智能写作等产品都依赖它改善用户等待体验,这也是大模型应用工程师面试中的高频考察方向。
可视化演示
动态演示大模型SSE流式输出工程实现的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
大模型SSE流式输出工程实现:SSSE S1:SSE协议原理与HTTP S2:后端分块传输与LLM推理 核心机制可视化 大模型SSE流式输出工程实现整体架构与关键组件
流式输出价值
想象你在餐厅点了一道需要四十分钟炖煮的佛跳墙。传统做法是后厨全部做好后一次性端上来,你只能对着空桌子玩手机。但若大厨每炖好一味食材就先端出一小碟,你边尝边等,等待就不再是煎熬。大模型输出正是如此——一个几百字的回答可能需要几十秒计算,“最后一次端上来”和“边做边端”的体验天差地别。
传统 HTTP 请求遵循“请求-响应”模式:客户端发送一个请求,服务器必须在计算出完整结果后才返回一次响应。对于大模型推理,一次完整响应少则几秒、多则几十秒,期间用户只能面对加载动画。研究数据表明,超过 2 秒的等待就会显著增加用户流失率——这就像客人等菜超过五分钟就会开始不耐烦。
流式输出的核心思路是边生成边返回。服务器将大模型逐词生成的结果切分成多个小块,每生成一小块就立刻推送给客户端。这样用户看到第一个词的时间被压缩到几百毫秒,后续内容陆续到达,形成“打字机”般的视觉效果。首 Token 延迟由此成为衡量大模型应用体验的核心指标之一。
正如佛跳墙的“边做边端”让客人从第一口就能确认菜的味道,SSE 流式让用户从第一个 token 就能确认模型的回答方向。如果第一句话就跑偏,用户可以立刻停止,不必白等三十秒。
- T_首词(首个 Token 时间)从客户端发出请求到收到模型输出的第一个词块,这段时间决定了用户“最先看到什么”。数值越小,用户越快感到“有响应了”。
- T_剩余生成(剩余内容生成时间)模型逐词生成剩余回答所需的时间。若响应总长度为 L、生成速率为 v,则该时间正比于 (L-1)/v。
- 感知等待直觉传统方式让用户承受完整等待;SSE 方式下用户在第 T_首词 时刻就看到内容,感知等待几乎等于 T_首词,体验大幅改善。
SSE协议原理
传菜窗不是随随便便开一个洞,它有自己的规矩:每道菜附一张小纸条,写清菜名、编号、内容,从窗口递出。收菜的服务员只需认准纸条格式,就知道这道菜是什么、该放哪一桌。SSE 协议就是大模型与前端之间的“传菜纸条规范”。
SSE(Server-Sent Events)是 HTML5 标准定义的一种服务器推送技术,基于 HTTP 长连接。服务器通过 text/event-stream 媒体类型持续发送数据,每个事件由若干字段组成:data(数据内容)、event(事件类型)、id(事件编号)、retry(重连间隔),事件之间用空行 \n\n 分隔。
SSE 是单向信道,只允许服务器向客户端推送。浏览器原生的 EventSource 对象会自动处理连接管理和断线重连,这是相比 WebSocket 的核心优势——不需要引入双向聊天式协议,只需一个 HTTP 接口就能实现实时推送。对比来看:WebSocket 像对讲机(双向、全双工),SSE 像传菜窗(单向、自动重连)。
有了这套“传菜纸条规范”,后厨(服务器)和传菜员(EventSource)就有了共同语言:菜名(event)决定放哪张桌(事件绑定),编号(id)用于重连时确认已经上到哪道菜(断点续传),空行则是两盘菜之间的分隔垫纸。
- R_事件(事件速率)一次流式响应中,每秒平均推送的 SSE 事件个数,直接决定前端渲染的细腻程度。
- N_事件(事件总数)一次流式响应中发送的 SSE 事件个数,每个事件包含一段 data 字段。大模型流式中,一个事件通常对应一个 token。
- T_持续(流式持续时间)从连接建立到流结束的时间跨度,等于首 token 时间加上全部 token 生成时间。
后端分块传输
后厨的传菜流程讲究“炒好一碟端一碟”:大厨把食材下锅,翻炒几下,盛出第一碟递给传菜员,传菜员立刻送出,不需要等整桌菜齐。后端实现 SSE 正是这个模式——用生成器逐段产出数据,每产出一次就立刻推给前端。
在 FastAPI 等 Web 框架中,只需返回一个 StreamingResponse,设置 media_type="text/event-stream",并传入一个生成器函数。生成器内部每次 yield 一段符合 SSE 格式的文本,框架就会把该文本立即写入响应流。同时需要设置三个关键响应头:Cache-Control: no-cache(禁止缓存)、Connection: keep-alive(保持长连接)、X-Accel-Buffering: no(禁用 Nginx 代理缓冲)。
生产环境里,后端传菜还有一个常见陷阱:Nginx 默认会缓冲上游响应,把小块攒成大块再发出去,导致“传菜窗”被堵住。设置 X-Accel-Buffering: no 相当于告诉 Nginx:“这道菜要趁热端,别替我攒着。”
- D_网络(固定网络开销)无论数据块多少,TCP 连接建立、TLS 握手等开销基本固定,通常为几十到几百毫秒。
- S_总 / B(数据量与带宽之比)总数据量越大、带宽越小,传输耗时越长。分块传输让接收端尽早拿到第一批数据,而不是等整包数据。
- 首字节时间对流式而言,首字节时间才是最关键的指标,而它只取决于首块大小而非总大小。
event: tokendata: {"token":"你"} event: tokendata: {"token":"好"} event: donedata: {"done":true}
前端EventSource
传菜窗口的另一侧站着服务员(EventSource)。他的工作很简单:站在窗口前,听到铃声就伸手接菜(onmessage),接到菜就放到对应的餐桌上(渲染 DOM),如果窗口被堵住就大声报告(onerror)。浏览器原生提供的 EventSource 对象正是这位训练有素的“服务员”。
创建连接只需一行代码:new EventSource('/chat')。浏览器会自动发送 GET 请求并保持长连接。随后注册三个关键事件:open(连接成功)、message(收到数据)、error(连接出错)。大模型流式输出中,服务端通常把 token 封装成 JSON 放进 data 字段,前端在 message 回调里 JSON.parse 后追加渲染即可。
优秀的“服务员”不只是机械接菜,还会做好体验细节:在回答开始时显示光标闪烁的光标,在结束时移除;收到半截 JSON(网络分包导致)时先缓存等完整再解析;连接失败时在 UI 上温柔地提示“网络不太顺畅,正在重连…”。
- N_已渲染(渲染 Token 数)UI 上已经展示的内容量,随事件到达持续累加。
- T_渲染窗口(渲染时间窗)从首屏开始到当前结算点的时间长度。
- UI 更新率事件频率越高,UI 更新越丝滑。实际渲染还要受浏览器帧率(约 60fps)约束,因此高频事件会被合并到同一帧渲染。
var es = new EventSource('/chat');es.onmessage = function (e) { var data = JSON.parse(e.data); if (data.token) { document.getElementById('answer').textContent += data.token; }};
断线重连策略
餐厅营业中最怕传菜窗被临时堵住:服务员手里端着菜,窗口却推不开。此时他会放下菜、等几秒再推一次;如果还不行,就等更久再试。SSE 的断线重连策略与之如出一辙——自动重连、退避等待、断点续传。
EventSource 内置了自动重连能力:连接意外断开后,浏览器会自动重新发起请求。但重连后服务器如何知道客户端已经收到了哪些数据?这就用到事件 ID 机制:服务器为每个事件分配递增的 id 字段,浏览器断线后会在重连请求的 Last-Event-ID 头中带上最后收到的事件 ID,服务器据此从断点继续推送。
为了防止频繁重连压垮服务器,重连应采用指数退避策略:第一次等待 1 秒,失败后等 2 秒、4 秒、8 秒……直到封顶值。此外,长时间无数据的连接会被中间代理判定为空闲而切断,因此需要心跳保活——每隔一段时间发送一行注释(如 : ping)保持连接活跃。
还记得传菜单上记的“已上到第几道菜”的编号吗?Last-Event-ID 就是这张传菜单。断线重连后,服务端看一眼编号,就能从下一道菜继续端,用户不会吃到重复的菜,也不会漏掉主菜。
- t_base(基础重试间隔)第一次重连前的等待时间,通常设为 1 秒。
- 2^n(指数增长因子)每重试失败一次,等待时间翻倍:1s、2s、4s、8s……避免服务端被重连风暴压垮。
- t_max(封顶值)等待时间不无限膨胀,封顶后保持恒定频率重试,通常设为 15 到 30 秒。
工程化实践
餐厅生意火爆时,传菜窗要同时服务几十桌客人。后厨必须管理好多条上菜通道的并发、监控每扇窗是否拥堵、验证取菜人的身份、并在后厨故障时启动应急预案。SSE 工程的落地同样离不开这四件事。
第一,并发连接管理。每个 SSE 长连接都会占用服务器内存与文件描述符,连接数过多会耗尽资源。需要通过连接池限制单机最大连接数,并设置空闲超时自动清理僵尸连接。第二,监控告警体系。核心指标包括活跃连接数、事件推送延迟、错误率、重连次数,任何异常都要触发告警。第三,安全与鉴权。EventSource 的 GET 请求无法自定义 Header,令牌通常放在 URL 查询参数或 Cookie 中,服务端校验通过后才建立流。第四,降级兜底方案。当模型服务超时或过载时,返回普通 JSON 响应告知用户“稍后重试”,而不是让连接无限挂起。
就像餐厅会准备备用菜单一样,SSE 工程也要有 Plan B:当流式通道异常时,降级为非流式一次性返回,至少保证用户能得到完整回答。工程化的本质,就是让传菜窗在高峰、拥堵、故障时依然可靠运转。
- M_可用(可用内存)服务器能分配给长连接的内存总量,通常取系统内存的 60% 到 70%。
- S_单连接(单连接开销)每个 SSE 连接占用的内核缓冲区与用户态对象大小,约几十 KB。
- 连接上限超过上限会导致内存耗尽、拒绝新连接,因此需要连接池与并发控制。
AI 应用场景
AI 聊天助手逐字输出
阶段:模型推理与内容交付
解决的问题:传统一次性返回时,用户盯着 loading 转圈 20 秒,近半用户会直接关闭页面;SSE 流式让回答逐字出现,用户感知等待时间从 20 秒降到 1 秒,显著提升对话体验留存。
具体案例:某 SaaS 智能客服机器人接入 SSE 流式输出后,首 Token 延迟从 8 秒降至 900 毫秒,用户满意度提升 28%,人均对话轮次从 3 轮增加到 5 轮。
AI 搜索引擎流式摘要
阶段:检索增强生成
解决的问题:传统 AI 搜索需等待检索和摘要全部完成后才展示,用户焦虑感强。SSE 可以先推送检索到的引用来源,再流式输出摘要,用户边读摘要边扫视引用,信息获取效率翻倍。
具体案例:某 AI 搜索产品中,用户输入问题后 1 秒内先看到参考链接列表,摘要逐句呈现,点击引用即可定位原文;弱网环境通过 Last-Event-ID 续传,保证长摘要不因断网中断。
企业知识库问答流式推送
阶段:知识库问答与文档生成
解决的问题:企业文档问答的回复通常长达 500 至 1000 字,非流式返回令移动端用户长时间白屏。SSE 流式结合断线重连,确保弱网环境下回答不丢失、不重复。
具体案例:某移动端知识库 App 中,员工提问后答案逐段出现,可边读边决定是否需要继续等待;网络切换时自动重连并从断点续传,单次回答完整率从 76% 提升到 99%。
Python 演示
"""大模型 SSE 流式输出完整示例: FastAPI 服务端 + requests 客户端 启动服务端: python sse_demo.py server启动客户端: python sse_demo.py client"""import jsonimport sysimport time import requestsimport uvicornfrom fastapi import FastAPIfrom fastapi.responses import StreamingResponse app = FastAPI() def sse_format(event: str, payload: dict) -> str: """把 dict 包装成 SSE 规范事件块""" data = json.dumps(payload, ensure_ascii=False) return f"event: {event}\ndata: {data}\n\n" def token_generator(): """模拟大模型逐字生成结果, 每次 yield 一个事件块""" answer = "SSE 让大模型边思考边输出, 用户不再干等" for ch in answer: yield sse_format("token", {"token": ch}) time.sleep(0.03) yield sse_format("done", {"done": True}) @app.get("/chat")def chat(): headers = { "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", } return StreamingResponse( token_generator(), media_type="text/event-stream", headers=headers, ) def consume(): """客户端消费 SSE 流: 逐行读取并打印 token""" resp = requests.get("http://127.0.0.1:8000/chat", stream=True) for line in resp.iter_lines(decode_unicode=True): if line and line.startswith("data: "): payload = json.loads(line[6:]) if "token" in payload: sys.stdout.write(payload["token"]) sys.stdout.flush() print() if __name__ == "__main__": mode = sys.argv[1] if len(sys.argv) > 1 else "server" if mode == "client": consume() else: uvicorn.run(app, host="0.0.0.0", port=8000)
iter_lines 逐行消费并实时打印。def sse_format(event, payload)将字典序列化为event:+data:字段并用空行分隔,这是 SSE 规范的核心格式。def token_generator()生成器函数,每次yield一个事件块,time.sleep(0.03)模拟模型逐字生成速度。StreamingResponse(token_generator(), media_type="text/event-stream", headers=headers)FastAPI 将生成器包装为流式响应,media_type告诉浏览器这是 SSE 流。"X-Accel-Buffering": "no"禁用 Nginx 代理缓冲,否则数据会被攒批发送,失去流式效果。resp.iter_lines(decode_unicode=True)requests 流式逐行读取响应,每行以\n为边界切分 SSE 事件。line[6:]去掉data:前缀后得到 JSON 字符串,再json.loads解析出 token 内容。
常见误区
误区SSE 和 WebSocket 不都是实时通信吗?我直接用 WebSocket 就好了,SSE 是不是过时了?
正解两者服务场景完全不同。SSE 是单向传菜窗(服务器到客户端),WebSocket 是双向对讲机。SSE 建立在 HTTP 之上,浏览器原生支持、自动重连,实现极简,非常适合大模型单向内容推送;WebSocket 需要双向通信时才用(协同编辑、在线游戏),握手和协议复杂度明显更高。
为何常见:两者都能实现“实时推送”,初学者容易混淆传输方向和应用边界。
误区EventSource 自带自动重连,我只要创建连接就行,断网恢复后它自己会好。
正解EventSource 确实能自动重连,但重连后服务端并不知道客户端收到了哪些数据。如果不配合 Last-Event-ID 断点续传,服务端会从头推送,导致内容重复或错乱。正确做法是:服务端为每个事件分配递增 id,客户端在重连请求中携带 Last-Event-ID,服务端从断点之后的下一事件继续推送。
为何常见:文档只强调“自动重连”,忽略了上下文断点恢复这一关键环节。
误区只要把 `media_type` 设为 `text/event-stream`,浏览器就会自动逐字显示,前端不用写代码。
正解text/event-stream 只是告诉浏览器数据格式。前端仍需要创建 EventSource、监听 message 事件、解析 data 字段并手动更新 DOM。此外生产环境中的代理缓冲、响应头配置如果不处理好,数据可能会被攒批送达,流式效果荡然无存。
为何常见:把“协议类型标识”和“自动渲染”混为一谈,低估了前后端协同工作量。
误区连接建立后只要不断开就一直有效,不需要额外做保活处理。
正解大多数中间代理(Nginx、负载均衡)都有空闲超时机制,如果超过一定时间没有数据经过,连接会被自动切断。因此需要定时发送心跳数据(如注释行 : ping\n\n),让连接看起来“一直有活动”。这就像传菜窗安静久了,需要隔一会儿敲一敲确认通道还通畅。
为何常见:本地开发网络链路短,连接空闲超时问题难以暴露,到生产环境才频繁掉线。
误区SSE 是纯文本协议,只能广播简单消息,传不了 JSON 这类结构化数据吧?
正解SSE 的 data 字段可以携带任意 UTF-8 文本,把 JSON 序列化成字符串放进 data 字段即可。大模型流式应用恰恰就是用 SSE 传输 {"token": "你"} 这类结构化数据,一个 token 一个事件。二进制数据才需要额外编码(如 Base64),但也不是障碍。
为何常见:听说 WebSocket 支持二进制帧,误以为 SSE 能力受限,其实是协议定位不同。
面试题
Q1. SSE 流式传输使用的 HTTP 媒体类型是?
- A.事件流媒体类型← 正确答案
- B.JSON 数据类型
- C.网页文本类型
- D.二进制流类型
答案:A。SSE 规范定义的媒体类型是 text/event-stream,即“事件流媒体类型”。它是纯文本协议,用于传输以行为单位的事件流。JSON 是应用层数据格式,HTML 是普通网页,二进制流则对应流式下载,均不符合 SSE 规范。
Q2. SSE 连接的数据传输方向是?
- A.服务端推送客户端← 正确答案
- B.客户端发送服务端
- C.双向实时通信
- D.离线文件传输
答案:A。SSE 是单向信道,只允许服务器主动向客户端推送数据,客户端通过 EventSource 接收;客户端如需发送数据,需另发普通 HTTP 请求。双向实时通信是 WebSocket 的能力。
Q3. SSE 事件流中,相邻事件之间的分隔符号是?
- A.单个换行符间隔事件
- B.使用空行间隔事件← 正确答案
- C.使用逗号分隔事件
- D.分号作为事件分隔
答案:B。SSE 规范要求每个事件块以空行 \n\n 结尾,空行前是一个事件的全部字段(data、event、id 等)。单个换行只是字段内换行,逗号和分号不参与事件分隔。这就像传菜窗里两盘菜之间要靠垫纸(空行)隔开。
Q4. SSE 协议内置支持的能力包括?
- A.自动断线重连能力← 正确答案
- B.事件 ID 续传能力← 正确答案
- C.自定义事件类型← 正确答案
- D.双向数据通信
答案:ABC。SSE 规范内置自动重连、id 字段实现断点续传、event 字段自定义事件类型。但 SSE 是单向协议,不支持双向数据通信(那是 WebSocket 的职责)。
Q5. SSE 断线重连实现断点续传的依据是?
- A.使用 HTTP 头传输 token
- B.使用 Last-Event-ID 字段← 正确答案
- C.使用 URL 参数保存状态
- D.服务端无法感知重连
答案:B。EventSource 重连时会在请求头自动携带 Last-Event-ID,值为最后收到的事件 id。服务端读到该头后从对应事件的下一条开始推送。这相当于传菜单上的“已上到第几道菜”的编号。
Q6. 对比 WebSocket,SSE 在大模型输出场景中的优势是?
- A.双向交互性能更强大
- B.基于 HTTP 兼容性更好← 正确答案
- C.传输二进制数据更快
- D.不需要维持长连接
答案:B。SSE 基于 HTTP,天然穿过防火墙和代理,浏览器原生支持,无需额外协议握手;WebSocket 需要专门的握手和协议实现。大模型输出是单向推送,SSE 的自动重连和事件 ID 机制让它在体验上更省心,且并不需要双向通信能力。
Q7. 前端使用 EventSource 消费大模型流式输出的正确操作包括?
- A.监听 message 获取数据← 正确答案
- B.监听 error 事件做降级← 正确答案
- C.使用 fetch 流式读取← 正确答案
- D.用 Blob 对象解析流
答案:ABC。EventSource 通过 message 事件获取数据、error 事件感知连接异常;也可用 fetch 配合 ReadableStream 替代 EventSource 获得更细粒度控制。Blob 对象用于二进制大对象,不适合解析 SSE 文本事件流。
Q8. Nginx 反向代理下,避免 SSE 流被缓冲的关键配置是?
- A.开启 gzip 压缩传输
- B.关闭代理响应缓冲← 正确答案
- C.增大连接超时时间
- D.限制请求体大小
答案:B。Nginx 默认缓冲上游响应,会把小数据块攒成大数据块再转发,破坏流式效果。配置 proxy_buffering off 可让数据一到达就立即转发。gzip 与缓冲无关,超时设置影响的是空闲断开,请求体大小限制影响的是上传。
Q9. 当 SSE 长连接长时间无数据时,中间代理可能切断连接,解决方案是?
- A.定时发送心跳注释保持活跃← 正确答案
- B.加大客户端接收缓冲区
- C.缩短服务端生成间隔
- D.改用 HTTP 短连接轮询
答案:A。空闲超时是代理服务器的自我保护机制。SSE 服务端可以每隔一段时间发送一行注释(如 : ping\n\n),让代理认为连接仍在活跃。这就像传菜窗安静久了,敲一敲确认通道还开着。
Q10. 大模型 SSE 流式输出的工程化落地中,需要重点关注的环节是?
- A.配置代理关闭响应缓冲← 正确答案
- B.设置心跳保活长连接← 正确答案
- C.监控连接数量防泄露← 正确答案
- D.必须使用网络套接字
答案:ABC。SSE 工程化三板斧:代理缓冲关闭(保证数据流实时性)、心跳保活(防止空闲断连)、连接数监控(防止内存泄露)。SSE 基于 HTTP 长连接,根本不需要 WebSocket,因此“必须使用网络套接字”是错误的。