AI 回答为什么一个字一个字往外跳?流式输出和 SSE 一次讲明白(2026入门)

你有没有注意过,用 Kimi、DeepSeek 或者 ChatGPT 聊天的时候,回答是转圈半天后蹦出一大段,还是一个字一个字往外跳?

我第一次接 API 的时候被这事困扰过。用 curl 调 DeepSeek,等了七八秒啥也没有,我以为是网络问题,反复重试。后来才知道,API 默认不走流式,得自己加一个参数。

这个参数背后是 AI 产品里最常见的一个技术:流式输出,英文叫 Streaming,底层协议叫 SSE。

先说结论:跳出来的不是打字特效

很多人以为这是产品经理为了好看做的动画。不是。

模型生成文本的方式,决定了它只能这样输出。大模型写东西和人不一样,它是逐个 token 往外蹦的:根据你前面说的所有内容,算出下一个 token 的概率,选一个,拼上去,再算下一个。

token 你可以理解成文字的碎片,一个汉字大约对应零点几个 token,英文里一个单词通常是一个 token。(token 和账单的关系,我之前写过一篇专门讲,这里不展开。)

所以当你等了八秒看到一大段话,不是因为模型「憋」了八秒。它其实从你按下回车就开始产出了,只是没流式的接口会把所有内容攒齐了,最后一口气塞给你。中间那几秒,内容一直在生成,只是你看不见。

流式输出做的事很简单:模型每算出一个 token,就立刻推给你。你看到的逐字跳动,就是生成过程本身。

SSE 是什么:一套很老但很好用的规矩

模型每算出一个 token 就推给你,靠的是什么?一套叫 SSE 的协议,全名 Server-Sent Events,服务器主动往浏览器推数据的老规矩。

它的工作方式特别好懂:你发一个普通的 HTTP 请求过去,服务器不关连接,一直保持着,然后一行一行地往回写数据。每一行都以 data: 开头,两块数据之间用空行隔开。客户端就这么一行一行地读。

实际抓包看到的内容长这样:

data: {"delta":{"content":"流"}}
data: {"delta":{"content":"式"}}
data: {"delta":{"content":"输"}}
data: [DONE]

每个 data: 后面是一小块 JSON,delta 里装着这次新增的字。最后那行 [DONE] 是结束标志。

用 curl 直接体验一次最直观(把 sk-xxx 换成你自己的 key):

curl https://api.moonshot.cn/v1/chat/completions \

-H "Content-Type: application/json" \

-H "Authorization: Bearer sk-xxx" \

-d '{

"model": "kimi-k3",

"messages": [{"role": "user", "content": "用一句话解释什么是流式输出"}],

"stream": true

}'

终端里会看到 data: 行一块一块刷出来,跟网页上逐字跳动的效果一模一样。

这套路子不是 AI 发明的,SSE 在网页里推消息、推进度条用了快二十年。AI 产品只是把它捡起来用了,好处是简单:就是一个不关门的 HTTP 连接,不需要 WebSocket 那套握手和心跳。

三行代码开流式,和几个容易踩的坑

代码层面没什么神秘的,OpenAI SDK 里一个参数:

stream = client.chat.completions.create(

model="kimi-k3",

messages=[{"role": "user", "content": "讲个冷笑话"}],

stream=True,

)

for chunk in stream:

if chunk.choices[0].delta.content:

print(chunk.choices[0].delta.content, end="")

原理说完,说几个我踩过的坑。

坑一:usage 不见了。 不开流式时,响应里直接带 usage(本次消耗多少 token)。开了流式,默认每个 chunk 里没有 usage。要加一个参数:

stream=True,

stream_options={"include_usage": True}

加了这个,最后一个数据块(在 [DONE] 之前)会带上本次完整用量。做账单统计的别忘了这个,不然你的用量统计永远是 0。顺带一提,我上次写上下文缓存那篇提过,流式下缓存命中明细也只在最后一个 chunk 的 usage 里。

坑二:delta 和 message 不是一回事。 普通响应里,回复在 choices[0].message.content。流式下变成 delta,每个 delta 只有一小片,得自己拼接。

而且拼接有个讲究:增量片段是「追加」不是「覆盖」。平时写代码习惯了 result = new_value 的覆盖写法,到这儿就成了丢数据,最后拼出来的句子缺字。正确写法是 full_text += piece。

坑三:断线重连得自己做。 官方文档原话是,因为并发限制、网络环境复杂,连接可能意外中断,通常不会持续很久。也就是说流式请求断了,SDK 不会帮你自动续。长回答场景要么自己写重试,要么记录已生成内容从断点继续。

坑四:不要用 n 参数。 想「一次请求要两个版本」的朋友注意,Kimi 现役模型(kimi-k3、kimi-k2.7-code、kimi-k2.6)的 n 固定为 1,传 n=2 直接报 400 错误,流式非流式一样。想多版本就并发发两次请求。

还有个细节:终止输出就是断开连接。用户点了停止按钮,前端其实就是不再读这个流了,模型那边收到断开也就不生成了。省 token 就靠这个。

为什么 ChatGPT 不肯改成一次性返回

有人可能想,既然等齐了再返回更简单,为什么所有 AI 产品都坚持流式?

因为体感差距太大了。

Kimi 官方文档里有个数字:等完整回复通常要数秒,问题复杂、回复长的时候能拉到 10 秒甚至 20 秒。同样是等 15 秒,一种是一直转圈,一种是第 1 秒就开始看到内容往外冒,体感完全是两回事。

人机交互里有个老结论:让用户等待的关键不是总时长,是反馈。界面有任何动静,人就有耐心;界面一动不动,超过 3 秒就开始怀疑卡死了。流式输出把「等待总时长 15 秒」变成了「第 1 秒就有反馈」,这是所有聊天类 AI 产品默认开启它的原因。

还有个实际原因:部分回答可能就够用了。读到一半发现方向不对,直接点停止,后面的 token 不生成也不计费。

什么场景该开,什么场景别开

开了流式,客户端代码复杂一点,要处理拼接、断线、终止。所以不是所有场景都值得开。

适合开的:

  • 聊天界面,逐字出内容,体验好
  • 长文生成,用户可以先读前面的
  • 代码补全场景,IDE 要实时显示

别开的:

  • 后台批处理任务,没人盯着看,攒齐了一起处理更省事(这种场景直接用 Batch API,账单直接打六折)
  • 需要先拿到完整 JSON 再解析的程序化调用,反正都要等全部内容,流式没意义

一句话总结:给人看的,开流式;给程序用的,看情况。

写这篇的时候我又想到一个事:技术圈总有新名词,SSE、流式、打字机效果,听着像三样东西,其实是一回事的三种叫法。很多「新技术」拆开看都是老东西,SSE 是十几年前的协议,HTTP 长连接更老。新问题(AI 生成太慢)加老方案(长连接推送),就成了新产品的标配。下次再遇到听不懂的新名词,可以先查查它是不是个老朋友换了名字。


参考:Kimi 官方文档《使用 Kimi API 的流式输出功能》《Chat Completions API》(platform.kimi.com)、DeepSeek 官方文档(api-docs.deepseek.com)

发表评论