我最初接触流式输出这个概念,是在做大模型应用落地的时候。当时老板提了个需求:让网页像ChatGPT一样,一个字一个字往外蹦,不要等十几秒出整段结果。我第一反应是WebSocket,结果越做越别扭,后来才发现,这里面的最佳实践其实是一套老掉牙的协议——SSE,全称Server-Sent Events,服务端推送事件。这篇文章就围绕“LLM流式输出”和“SSE”展开,梳理清楚Token流式的底层逻辑、实际工程怎么落地、以及我在生产环境里踩过的坑,希望能给正在搞大模型应用、搞Agent、搭RAG服务的朋友一点参考。
1. 为什么大模型输出非要“流式”:用户等待心理与首Token延迟
1.1 非流式输出的致命体验
先看一个最基本的场景。你调用一个LLM接口,假设模型推理需要8秒才能完整生成一段200字的回答。如果走非流式接口,意味着用户发出请求后,页面要白屏或者转圈8秒,然后整段200字一次性出现。
这里有两个痛点。第一是用户的耐心问题,业内有个“2-5-10秒”经验法则:2秒以内反馈,用户觉得流畅;5秒以内还能接受;超过10秒,用户大概率直接关页面。第二是心理反馈问题,哪怕总耗时一样,流式输出让用户看到内容在动、在生成,用户对等待的容忍度会大幅提升。这就像看综艺节目里的答题场景,观众不怕等,怕的是没有进度感。
1.2 首Token延迟比总耗时更重要
在大模型应用里,有一个专门的性能指标叫TTFT(Time To First Token),也就是从发出请求到收到第一个Token的时间。为什么这个指标被反复提及?因为LLM的推理过程是自回归的,首Token之前要做预填充(Prefill),要对整个用户输入做一次完整的注意力计算,这个阶段没有办法吐字,耗时是必付的成本。
而首Token之后,进入解码(Decode)阶段,每生成一个Token都需要一次前向计算,这个过程的耗时理论上和生成长度成正比。所以你会发现,如果总耗时是10秒,里面可能3秒是TTFT,剩下7秒是逐Token解码。
1.3 流式交互带来的产品形态创新
流式输出不只是“好看”,它直接改变产品形态。比如Agent流式输出中间状态,工具调用的进度、思考过程、多步推理的结果都能实时展示给用户。再比如流式输出还能实现“用户打断”机制:用户在生成过程中发现问题,可以直接点停止,节省时间和Token成本。
不能流式的场景就只能干等,用户在整个等待过程中是黑盒状态。这也是为什么现在评测一个LLM应用,流式支持已经成为默认项,不是加分项。
2. SSE协议拆解:它和WebSocket、轮询到底差在哪
2.1 SSE本质上还是一个HTTP请求
很多人听到SSE,第一反应是“又引入了一个新协议”。其实不是,SSE是建立在HTTP之上的,没有新的传输层协议。它做的事情很简单:客户端发起一个普通的HTTP请求,服务端收到后不结束响应,而是持续地一块一块往回写数据。
这里的关键点在于“响应不结束”。传统的HTTP请求-响应模型是:请求发出,响应回来,连接关闭。SSE打破了这个模型的后半段,服务端把响应报文分成多个数据块(chunk)持续发送,但整个HTTP响应一直没有terminated。
2.2 SSE报文格式:分行业务、注释与事件类型
SSE的报文格式非常简洁,核心是text/event-stream这个MIME类型。服务端往响应体里写的数据格式如下:
id: 1 event: message data: 第一行数据 id: 2 event: message data: 第二行数据这里有几个规则:
data:开头的是数据行,多个连续的data:行会被客户端解析为一条消息的不同行,用换行符拼接。event:指定事件类型,如果省略,默认是message事件。id:用于断线重连时的Last-Event-ID追踪。- 一个空行(两个换行符)表示一条消息的结束。
- 以冒号开头的行是注释行,服务端可以用来发心跳包,保持连接不被中间设备断开。
实际一个大模型流式返回的SSE报文,长这样:
data: {"choices": [{"delta": {"role": "assistant"}, "index": 0}]} data: {"choices": [{"delta": {"content": "你好"}, "index": 0}]} data: {"choices": [{"delta": {"content": ","}, "index": 0}]} data: [DONE]OpenAI兼容接口就是这么干的,每个chunk里带一个delta增量,最后用一个data: [DONE]标记整个流结束。
2.3 与WebSocket、传统轮询的对比
我整理了一张对比表,直接看差异:
| 维度 | SSE | WebSocket | 轮询 |
|---|---|---|---|
| 传输层 | HTTP(默认) | TCP | HTTP |
| 方向 | 服务端到客户端单向 | 双向 | 双向(靠多次请求模拟) |
| 协议复杂度 | 低,文本格式 | 较高,有握手、帧格式 | 低 |
| 自动重连 | 原生支持 | 需自己实现 | 需自己实现 |
| 穿透代理/防火墙 | 容易,常规HTTP端口 | 偶尔被策略阻断 | 容易 |
| 适用场景 | 大模型生成、实时通知、股票行情 | 聊天、在线游戏、协同编辑 | 低频轮询 |
大模型输出天然是“单向”的:主要是服务端往客户端推Token,客户端顶多发个停止信号。这种场景用SSE最合适,因为双向通信里客户端真正主动发数据的场景少得可怜,引入WebSocket反而白白增加了复杂度。
3. 从零实现:怎么用Python手动搭一个LLM流式服务端
3.1 为什么不用现成框架先跑一遍
现在FastAPI、Flask这些框架对StreamingResponse都有不错的支持,但在实际动手之前,建议先用Python自带的标准库写一个最原始的HTTP流式服务,理解底层原理。只有把底层原理吃透了,后面用框架出问题时,你才知道该去哪里查。
下面这个例子用Python内置的http.server模块实现一个伪造的LLM流式接口,不依赖任何第三方库:
import json import time from http.server import BaseHTTPRequestHandler, HTTPServer class LLMStreamHandler(BaseHTTPRequestHandler): def do_POST(self): if self.path != '/v1/chat/completions': self.send_error(404) return content_length = int(self.headers.get('Content-Length', 0)) body = self.rfile.read(content_length) self.send_response(200) self.send_header('Content-Type', 'text/event-stream; charset=utf-8') self.send_header('Cache-Control', 'no-cache') self.send_header('Connection', 'keep-alive') self.send_header('X-Accel-Buffering', 'no') self.end_headers() response_text = "你好,这是一个流式输出的示例。" for char in response_text: chunk = { "choices": [ { "delta": {"content": char}, "index": 0 } ] } event = f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" self.wfile.write(event.encode('utf-8')) self.wfile.flush() time.sleep(0.1) self.wfile.write(b"data: [DONE]\n\n") self.wfile.flush() def log_message(self, format, *args): pass if __name__ == '__main__': server = HTTPServer(('0.0.0.0', 8899), LLMStreamHandler) server.serve_forever()这里有几个细节值得注意。
第一,Content-Type必须是text/event-stream,这是SSE协议的核心标识,客户端靠它判断这是个流式响应。
第二,Cache-Control: no-cache必须设置,否则中间缓存代理可能把整个流缓冲起来,导致客户端迟迟收不到数据。
第三,每次写入后必须flush(),否则数据滞留在应用层缓冲区里,不会真正推给客户端。
第四,X-Accel-Buffering: no是给Nginx用的,如果服务端前面挂了Nginx,这个头能告诉Nginx关闭缓冲,相关细节后面再展开。
3.2 Django/Flask/FastAPI里的流式写法
实际项目里大家不太会用标准库裸写HTTP服务,都是用框架。我分别说一下三个主流Python框架的做法。
FastAPI的写法最优雅,直接返回StreamingResponse:
from fastapi import FastAPI from fastapi.responses import StreamingResponse import json import asyncio app = FastAPI() async def generate(prompt: str): # 模拟LLM生成过程 for token in ["我", "是", "一", "个", "大", "模", "型"]: chunk = {"choices": [{"delta": {"content": token}, "index": 0}]} yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" await asyncio.sleep(0.05) yield "data: [DONE]\n\n" @app.post("/v1/chat/completions") async def chat_completions(prompt: str): return StreamingResponse( generate(prompt), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", } )Flask用make_response加Response也可以,不过因为Flask默认是同步WSGI,流式部分建议用生成器函数:
from flask import Flask, Response, request import json import time app = Flask(__name__) def generate(prompt: str): for token in ["我", "是", "一", "个", "大", "模", "型"]: chunk = {"choices": [{"delta": {"content": token}, "index": 0}]} yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" time.sleep(0.05) yield "data: [DONE]\n\n" @app.route("/v1/chat/completions", methods=["POST"]) def chat_completions(): data = request.get_json() prompt = data.get("prompt", "") return Response( generate(prompt), content_type="text/event-stream; charset=utf-8", headers={ "Cache-Control": "no-cache", "X-Accel-Buffering": "no", } )Django从3.2开始支持StreamingHttpResponse,写法类似,不赘述。
3.3 阻断式生成与流式生成的本质差异
可能有人疑惑:同一个模型,怎么一会能流式一会不能流式?区别不在模型本身,而在于调用方式。
如果模型API本身不暴露流式接口,那服务端就只能等完整结果生成完再一次性推给客户端,这时候你哪怕给客户端返回text/event-stream也没用,因为服务端没有增量数据可以发。所以做流式的第一个前提是:底层模型API支持流式。
OpenAI兼容接口的做法是参数里加"stream": true,返回的就是SSE流。而很多自建模型框架(比如llama.cpp的server、vLLM、TGI)都原生支持流式输出。它们内部在生成Token的同时就会通过SSE增量返回。
4. 消费端实战:浏览器原生EventSource与fetch流式解析
4.1 EventSource的局限
浏览器端最省事的方案是用EventSource,它原生支持SSE,自动重连,体验极佳:
const eventSource = new EventSource('/api/llm-stream'); eventSource.onmessage = function(event) { if (event.data === '[DONE]') { eventSource.close(); return; } const chunk = JSON.parse(event.data); const delta = chunk.choices[0].delta.content; if (delta) { outputElement.textContent += delta; } }; eventSource.onerror = function(event) { console.error('SSE连接出错', event); };但注意,EventSource有两个硬伤。
硬伤一是它只能发GET请求,没法自定义请求头。这意味着很多API鉴权方式(比如Authorization头、自定义token头)都用不了,只能通过URL query参数把token拼进去。这在生产环境里极其别扭,一般不建议用EventSource去连那些带鉴权的大模型网关。
硬伤二是它是浏览器原生API,没法用在Node.js后端做服务端转发。
4.2 fetch + ReadableStream逐块解析
更通用的做法是用fetch加ReadableStream来消费SSE:
async function streamChat(messages) { const response = await fetch('/api/llm-stream', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your-token' }, body: JSON.stringify({ messages }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 按空行分割SSE事件 const lines = buffer.split('\n\n'); buffer = lines.pop(); for (const line of lines) { for (const part of line.split('\n')) { if (part.startsWith('data:')) { const data = part.slice(5).trim(); if (data === '[DONE]') return; try { const chunk = JSON.parse(data); const content = chunk.choices?.[0]?.delta?.content; if (content) { handleContent(content); } } catch (e) { console.warn('解析失败', data); } } } } } }这里有个容易被忽略的细节:decoder.decode(value, { stream: true })。HTTP传输是按字节流的,一个中文字符可能被拆分到两个TCP包里到达,所以解码时必须带stream: true,否则多字节字符在被切断的边界上会解码成乱码。
4.3 Node.js后端转发SSE的注意点
如果我们在做Agent网关,最典型的链路是“浏览器 -> 后端Node.js -> 上游LLM API”,后端需要把上游的SSE流转发给浏览器。Node.js里用fetch同样可以消费上游的流式响应:
const upstreamResponse = await fetch('https://api.llm.example.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model, messages, stream: true }), signal: AbortSignal.timeout(120000) }); return new Response(upstreamResponse.body, { headers: { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', } });Node.js 18以后原生fetch返回的response.body就是Web标准ReadableStream,作为新Response的body直接扔出去即可。这里唯一要注意的是超时设置,LLM生成时间长,超时阈值不能设太短,一般建议120秒以上。
5. 鉴权、超时与断线重连:生产环境绕不开的三个问题
5.1 SSE请求的鉴权怎么处理
前文提到,浏览器EventSource不能自定义请求头,所以生产环境里通常有两种做法。
第一种是把token放在query string里,例如/api/llm-stream?access_token=xxx。做法简单,token会出现在Nginx access log和浏览器历史里,有泄漏风险,只适合短生命周期的一次性token。
第二种是客户端用fetch流式请求后端自己的业务接口,业务接口校验完登录态后,由后端代为调用上游LLM API。上游API的密钥保存在后端环境变量里,浏览器永远接触不到。这种模式权限收敛、安全可控,我强烈推荐。
如果担心登录态被恶意前端盗用去刷上游API,可以在后端业务接口里做限流:同一用户在单位时间内最多发起多少次流式请求。这个在上游LLM网关层做也行,但如果你用别人的API,未必有配额接口,自己做更靠谱。
下面是常见的SSE鉴权与传输链路对比:
| 模式 | 前端拿到什么 | 上游密钥暴露面 | 适用场景 |
|---|---|---|---|
| 前端直连上游LLM | 上游API Key或临时token | 大 | 内部工具、demo |
| 前端连业务后端,后端转发上游请求 | 自己的会话凭证 | 无 | 生产环境标准做法 |
5.2 before completion: idle timeout waiting for SSE
这个报错在接入一些大模型API的SDK或者网关时经常出现,含义很直接:在流式响应还没有结束之前,连接处于空闲状态的时间超过了超时阈值。这种情况通常有两种原因。
一种是模型在预填充阶段耗时过长,TTFT超过了网关设置的idle timeout。比如用户上传了一个超长文档,模型要先处理数千Token的上下文,这个阶段可能就要几秒甚至几十秒,网关等不到第一个Token就报错了。
另一种是模型输出临时中断,比如生成过程中服务端在做工具调用(function calling),思考了一段时间没有往外传Token,前端/网关把这个静默期误判成了空闲超时。
解决思路或者说常规优化手段包括这几个:
- 调大idle timeout,比如从默认的30秒调到120秒。
- 服务端主动发心跳注释行。SSE规范里以冒号开头的注释行会被客户端忽略,但能有效“刷新”连接的活跃状态,让网关不把它判定为空闲。
服务端每隔15秒发一个: keep-alive\n\n,就能绕过大部分中间超时限制。
- 检查是不是模型输入过长导致预填充阶段过久,必要时限制单次上下文长度。
- 在应用层收集完整个流式响应后,将结果写入日志。如果从日志看模型端确实长时间无输出,优先排查模型推理侧的瓶颈。
5.3 断线重连与动态游标续传
SSE自带断线重连机制:浏览器原生EventSource断了会自动重连。但如果我们在做后端转发或者自己实现消费端,就得自己设计重连策略。
重度方案是配合Last-Event-ID头实现增量续传。服务端每发出一个事件都带id字段,客户端重连时在请求头里带上最后收到的id,服务端根据这个id决定从哪里继续推。
在LLM流式场景里,这个方法用得不多,因为很多模型API本身不支持从任意位置恢复生成。主流做法是:断线后重新发起一次请求,把已经生成的内容当作对话历史的一部分传给模型。不过这样做会导致生成结果可能变化,所以更稳妥的方案是——前端把已生成的部分先存着,重连后新流里的内容追加显示,重复的部分用算法去重,等整个流结束再做最终展示。
如果对一致性要求极高,那就走“客户端请求取消后,模型服务端缓存已生成Token,重连后根据句柄续推”的定制方案。这个方案实现成本较高,一般只有大厂在核心产品里才做。
6. 服务端框架与网关层的SSE支持:Nginx缓冲问题
6.1 反向代理层如何正确透传SSE
一旦服务端前面挂了Nginx,坑就来了。默认情况下,Nginx会对上游响应做缓冲(proxy_buffering),这意味着Nginx会先攒够一定量数据再统一发给客户端。对于SSE流式响应,这个行为会直接导致客户端长时间收不到数据,直到服务端结束整个流才一股脑收到。
解法是关闭Nginx的proxy缓冲,同时关闭对SSE响应的gzip压缩:
location /api/llm-stream { proxy_pass http://backend_server; proxy_set_header Connection ''; proxy_http_version 1.1; proxy_cache off; proxy_buffering off; gzip off; proxy_read_timeout 180s; proxy_send_timeout 180s; chunked_transfer_encoding on; }几个配置逐条说。
proxy_buffering off是最关键的,关掉缓冲,让上游数据直接透传。proxy_http_version 1.1必配,因为HTTP/1.0不支持长连接,SSE需要保持连接不中断。proxy_read_timeout要设大,因为LLM生成过程中可能出现较长的静默期。gzip off是因为流式响应是逐步到达的,压缩这个行为本身会引入缓冲和延迟,而且Nginx默认在数据量小的时候不会压缩,但为避免极端情况直接禁用。
6.2 网关对idle timeout的判定逻辑
网关层通常根据“连接闲置时间”来判断要不要断开连接。如果某条连接Tag是请求处理中,但一直没有任何字节流动,超过阈值就会被断掉。
这里需要明白:网关看到的是字节流,不是业务事件。只要数据还在流动,不管流动的是业务数据、data: [DONE]还是注释行,网关都会认为连接是活跃的。所以“发心跳注释行”这个技巧本质是在利用网关对数据是否流动的判定逻辑。
6.3 云环境里更稳的单机处理思路
如果你的服务部署在K8s里,前面还挂了一层云负载均衡(CLB/SLB),经常出现流的连接被云LB掐断。要尽量满足这几个条件:
- 云LB的“空闲超时”调到最大值。
- 确保请求路径里每一层(LB、Nginx、Ingress、应用)都不缓冲SSE响应。
- 应用层定期发心跳注释行,覆盖全网关。
实测下来,把心跳间隔设为15-20秒,配合LB空闲超时60秒以上,基本能稳定跑完正常的LLM长对话。
7. 断点排查:一个流式接口不吐字的完整排障思路
这部分我把自己真实排查过一次的完整链路写出来,当时是一个Dify里接入外部LLM,SSE流半天不吐字的问题。
第一个环节:先确认请求有没有到达后端。看后端日志,如果有请求记录但没有输出,说明问题在后端逻辑或模型调用。如果后端根本没有请求记录,问题在网关、Ingress或路由配置。
第二个环节:用curl直接请求后端的内网地址,绕过所有代理层。命令长这样:
curl -N --no-buffer -X POST http://backend:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"prompt": "你好", "stream": true}'-N --no-buffer是curl关闭缓冲的关键参数。如果这里能看到逐步吐字,说明后端本身没问题,锅在反向代理。
第三个环节:逐层检查Nginx配置是否关掉了proxy_buffering,是否存在gzip缓冲,是否限制读取超时。这块最常见的问题都是配置写错位置,没生效。
第四个环节:如果curl都通了,但浏览器里不吐字,重点检查前端解析逻辑。常见问题有两个,一是没有正确处理UTF-8流式解码的边界,中文字符被拆包导致JSON.parse报错,整个流崩掉;二是用EventSource去打POST接口,请求方式不匹配直接404。
第五个环节:看SSL层或协议层有没有干扰。有些WAF产品会拦截text/event-stream的Content-Type或者认为长时间不关闭的响应是异常流量。跳过WAF测试一下,基本能定位。
8. 流式输出背后的模型推理机制与并发考量
8.1 Token是如何一个接一个生成的
刚才一直在聊工程层的流式机制,这里补一下模型层面的原理。大模型生成文本的过程是自回归的:模型每次只生成一个Token,然后把这个Token拼到已有序列后面,再预测下一个Token,如此反复,直到遇到终止符或达到最大长度。
因此“流式输出”对模型推理来说,并不是额外的特殊功能,模型本来就是“一个Token一个Token”往外吐的,只是有些API把中间过程藏起来了,一次性返回全部结果。流式接口本质上只是把这个逐Token生成的过程实时暴露给了调用方。理解这一点非常重要,它会帮助你快速判断问题到底出在哪里——如果模型在预填充、排队、显存不够,那么所有问题都会集中体现在TTFT变长,而不是流式输出本身失效。
8.2 并发请求与流式连接数的折中
流式输出还有一个工程隐患:并发连接数。如果每个用户的每个聊天请求都是一条长连接挂着,网关的连接数、后端线程数、内存占用都会成倍上涨。一个中间的查询或生成任务可能持续几十秒,一百个并发用户就可能压垮单机服务。
在做并发预估时,不要把“请求数/秒”当作唯一指标,要同时估算“峰值并发连接数”和“峰值流阅持续时间”。建议为每个用户的并行流数设上限,一般单用户同时最多2-3个流式生成任务就够了。如果真的有超大规模并发需求,可以考虑把SSE封装成消息队列消费模式,让前端从消息队列拉取增量事件。
8.3 基于Token速率做服务端限速
流式输出本质上也是一种消耗上游Token的手段。有些模型API是按Token计费的,一个用户狂发请求生成超长文本,成本会非常夸张。服务端可以做Token速率限制,也就是对用户的Token消耗速率做统计,超阈值后直接掐断或降级为普通模式。
实现方式并不复杂:在应用层记录每个用户最近N秒内的输出Token总量,用滑动窗口做统计,超过阈值就在流里插入一个特殊事件通知前端“你被限流了”。这种限流没法精确控制模型内部的解码过程,但能确保账单不会失控。
9. 大模型流式生态现状与选型建议
9.1 各家框架的SSE兼容性
现在主流的大模型推理框架,比如vLLM、TGI(Text Generation Inference)、llama.cpp、Ollama、SGLang,基本上都原生支持SSE格式的流式输出。OpenAI兼容接口已经成了事实标准,很多模型网关和开源项目直接对齐这个协议。
在RAG增强LLM链路里,StreamingResponse也很常见。RAG服务先做检索,把检索到的文档片段塞进上下文,然后交给LLM流式生成。这部分会对首次响应延迟有影响,因为检索本身也是TTFT的一部分。
9.2 千万别在原生产品里绕过SSE硬造轮子
我见过一些项目为了“灵活”,直接把模型推理的原始Token流用WebSocket裸传给前端。这就导致前端要自己处理二进制帧、自己实现错误重传、自己实现连接状态管理,工作量翻倍却没有任何收益。除非有双向实时交互需求(比如AI语音对话里需要同时上行音频下行文本),否则SSE永远都是大模型流式输出的首选。
9.3 从“能跑通”到“能上线”要补的功课
一个流式接口demo能跑通,和生产能上线,中间隔着不少工程细节,列一下我日常会重点检查的事项:
- 日志里有没有记录完整请求和响应摘要,方便事后审计。
- 监控里有没有TTFT、Token吞吐、连接时长、断连率这些指标。
- 服务端异常时,有没有给客户端返回一个明确的事件而不是直接断开连接。
- 客户端有没有做“停止生成”按钮,有没有处理服务端异常断开的情况。
- 流式接口有没有做限流和鉴权,防止被人刷接口。
10. 最后再分享几个我在实际项目中常用的调试技巧
第一个技巧,用curl调试真实LLM流式接口时,加上-N参数后如果发现数据不是逐段出现而是一下子全出来,基本可以断定中间有缓冲层在作怪。
第二个技巧,如果前端看不出来到底有没有收到数据,别瞎猜,直接在Chrome开发者工具的Network面板里看,SSE请求在响应标签页里能实时看到数据流的情况,而且能区分是网络层没收到还是JS解析层出了问题。
第三个技巧,很多大模型API的流式输出在最后会附带usage消费统计,但注意它不是标准SSE事件格式,要特殊解析。调试时记得把原始响应体的最后几行打出来看看,很多SDK在上层就把这个字段吞了,导致你想看Token消耗却看不到。
第四个技巧,处理中英文混合内容时,流式解码务必用TextDecoder的stream: true参数,并且保持一个跨多次回调的buffer变量。这个细节决定了你的应用在生成过程中会不会出现乱码或JSON解析崩溃。
第五个技巧,测试SSE断线重连时,不要只在本地模拟。真实网络里运营商NAT超时、公司防火墙、云LB各项策略都会影响连接存活时间,有条件的话在不同网络环境里各测一轮。
流式输出加上SSE,看起来是个小的协议知识点,但在大模型应用里直接决定用户体验和工程稳定性。这篇文章从协议原理、服务端实现、客户端消费、生产环境排障四个角度把整个链路串了一遍,希望能帮你少走点弯路。