流式输出怎么实现?

流式输出怎么实现?为什么大模型可以边生成边显示?
你有没有注意到,AI回复的时候字是一个一个蹦出来的?感觉它在"认真思考"对吧?
其实不是,它只是在流水线生产而已。
上篇聊了上下文长度。越长的输入,Transformer要算的关系就越多。几百个词,可能要处理上万个Token的关系。按理说应该很慢,但你感觉AI回答还挺快的。
秘密就在"边生成边显示"。
"秒回"其实是假象
传统网页加载是这样的:你点个按钮,服务器算半天,然后一下把整个页面扔给你。
大模型不一样。它不是算完再给你,而是边算边吐。
你看到的不是"思考过程",而是"输出过程"。每个字出来的时候,模型已经在生成下一批内容了。就像朋友发微信,不是一口气录完再发,而是一边说一边录,你点开就能听到。
这就是流式输出(Streaming)。
核心问题:模型怎么做到"生成一点就传一点"?
靠三件套:自回归生成 + Python生成器 + SSE协议。
SSE 协议——大模型输出的"快递员"
SSE全称Server-Sent Events,翻译过来就是"服务器推送事件"。
它是一种基于HTTP的轻量级协议。服务器可以主动往客户端发数据,客户端只管接收就行。就像看电视字幕——电视台播什么你就看什么。
SSE的数据包格式是这样的:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"token": "今天"}
data: {"token": "天气"}看到了吗?Content-Type是text/event-stream,这是告诉浏览器"接下来是SSE数据"。
每条数据以data:开头,用两个换行符\n\n分隔。前端收到就知道:这条消息结束了,可以处理了。

为什么大模型选SSE而不是WebSocket?
WebSocket是双向通道,客户端和服务器可以互相发消息。但大模型回复你的时候,你只需要接收,不需要往服务器发指令。既然单向就够用,就不用搞双向那么复杂。
SSE本质上就是个"广播"。简单、干净、基于标准HTTP,不需要特殊库,浏览器原生支持。够用了。
大模型的"流水线工厂"——自回归生成机制
说完传输协议,再看模型内部。
大模型不是一次性生成整段话。它的工作方式是这样的:
先输出第一个Token,然后拿第一个Token当输入,生成第二个Token;再拿前两个Token当输入,生成第三个……
这就是自回归(Autoregressive)生成。每个新Token都依赖前面所有的Token。
打个比方:就像工厂流水线,第一个工人切完菜的1/3,传给第二个工人;第二个工人切完2/3,继续往后传。每个人都在干活,产品(Token)一个接一个从流水线末端出来。

那怎么让这些Token"流"出去而不是憋在内存里?
Python的生成器(Generator)就派上用场了。
def generate_tokens(prompt):
for token in model.generate(prompt):
yield token # 生成一个就吐一个关键就是这个yield。普通函数要等所有结果算完才返回。生成器呢?每算出一个,就"让"出去,然后继续算下一个。
这个"让"的动作,就是SSE推送的触发点。模型生成第一个Token,yield出去,后端立刻把它包装成data:发给你。前端收到,第一个字就显示出来了。
与此同时,模型已经在算第二个Token了。
所以你感觉是"边想边说"。实际上不是模型在思考,而是输出过程被你看见了。
从后端到前端——完整的流式传输链路
光有模型和协议还不够。数据从模型跑到你屏幕上,中间还隔着好几站。
整个流程是这样的:
第一步:模型生成
PyTorch或TensorFlow里的模型跑起来,逐Token输出。每个Token触发一次yield。
第二步:后端封装
FastAPI或者LangChain把这些Token包成SSE格式。代码大概是这样:
@app.get("/stream")
async def stream_response(prompt: str):
async def event_generator():
for token in model.generate(prompt):
yield f"data: {token}\n\n"
return StreamingResponse(event_generator(), media_type="text/event-stream")每个Token后面跟两个换行符,这是SSE的消息分隔符。
第三步:网络传输
有个问题:长连接容易被代理或防火墙"误杀"。你以为在传数据,它以为你死了,直接把连接断了。
解决方案:心跳包。每隔15秒,服务器发一个空行过去,证明"我还活着"。
: keepalive这行开头是冒号,表示注释。客户端收到就知道连接还正常。
第四步:前端接收
浏览器用EventSource接收,这是浏览器原生支持的:
const source = new EventSource('/stream');
source.onmessage = (event) => {
document.getElementById('output').innerHTML += event.data;
};有个细节要注意:网络分片可能把一个Token截成两半送来。比如"今天"可能被拆成"今"和"天"分两次到达。客户端要有个buffer暂存这些"半包",等拼完整了再显示。
第五步:渲染展示
前端把收到的Token追加到页面上。一个字出现了,过一会儿又来一个字……你就看到了"边生成边显示"的效果。

整个链路跑通后,延迟大概是多少?
第一个Token出来叫TTFB(Time To First Byte,首字节时间),优化后能控制在几百毫秒。但体感上你可能觉得更快,因为第一个字一出现,模型已经跑了好几轮了。
SSE vs WebSocket——什么时候该选谁
大模型选SSE,但很多实时应用用的是WebSocket。它俩到底什么关系?
SSE是单向广播。你只能听,不能说话。服务器发来数据,你能收到;你想发数据?没门。
WebSocket是双向对讲机。客户端和服务器可以互相发消息。你说一句,对方说一句,低延迟实时交互。

| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务器→客户端) | 双向 |
| 协议 | HTTP | 独立协议 |
| 自动重连 | 支持 | 需要自己实现 |
| 二进制数据 | 不支持 | 支持 |
| 实现复杂度 | 低 | 中 |
| 适用场景 | 消息推送、实时更新 | 聊天、游戏、协作 |
大模型回答问题,用SSE就够了。你说一句话,服务器回答一堆话,这不就是单向广播吗?
但如果你要"多轮对话"呢?每次回复都要等上一轮结束才能继续——这其实是前端的处理逻辑,不是协议层面的限制。SSE只负责"怎么传",不负责"谁来叫谁"。
什么场景必须用WebSocket?需要客户端实时发指令的时候。比如在线协作文档,你打字别人实时看到——你的每个输入都要立刻发给服务器,服务器再广播给其他人。这种场景SSE就不够用了。
工程师都在用的性能优化技巧
SSE原理不难,但真要上线跑,性能问题就冒出来了。
首字节延迟优化
用户等第一句话出现的体验,比等全部内容更重要。模型可以先输出一个占位符(比如"好的,"),然后立刻推送给前端。你看到这个开头,就知道AI开始响应了。
这招能让TTFB降低50%以上。体感上从"等半天没反应"变成"秒回"。
GPU利用率优化
每个Token生成都要跑一次模型。如果一个请求一个请求地处理,GPU利用率可能只有30%。
动态批处理(Dynamic Batching)可以改善。几个请求一起进模型,共享计算,同时出结果。就像拼车——一个人开车空着两个座,不如再捎两个人。
GPU利用率能提升40%。
流量压缩
每个Token都是一个短字符串,网络传输开销不小。Brotli压缩可以在Token级别压缩数据,节省35%带宽。
用户流量费省了,服务器带宽压力也小了。
流式JSON解析
如果你需要AI返回结构化数据(比如JSON),问题来了:JSON没解析完之前,你是不知道它是不是合法的。
半截JSON强行解析会报错。怎么办?增量解析。等}\n\n出现,说明一条消息结束了,再尝试解析这一段。

写在最后
现在你知道了,AI"思考中"就开始回复,不是AI变聪明了,而是输出过程被你看见了。
SSE协议负责"怎么送",模型生成器负责"怎么吐",前端负责"怎么接"。三个环节各司其职,流水线就转起来了。
不过流式输出解决了"边生成边显示",可生成的内容还是一段自由文本。如果你想让AI稳定返回特定格式,比如"帮我查一下天气,返回JSON",这又该怎么保证?
这就是下一篇要聊的了:结构化输出和Function Calling。
