首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >大模型的"打字机效果"是怎么实现的?SSE 流式输出从协议到代码

大模型的"打字机效果"是怎么实现的?SSE 流式输出从协议到代码

原创
作者头像
用户11136834
发布于 2026-10-04 02:30:22
发布于 2026-10-04 02:30:22
260
举报

前言

用过 ChatGPT 类产品的人都熟悉那个"打字机"效果:回答一个字一个字地蹦出来,而不是等十几秒后"啪"地整段出现。第一次见会觉得很神奇,其实底层是一门 2009 年就定型的老技术——SSE(Server-Sent Events)。

这篇文章把流式输出从下到上拆三层:HTTP 的分块传输、SSE 的文本协议、大模型 API 在其上的封装。文中服务端和客户端代码都经过实测(Node.js 原生实现,零依赖),可直接跑。

一、第一层:HTTP 允许"边生成边发"

普通 HTTP 响应是"攒齐再发":服务器把完整内容准备好,带上 Content-Length,一次性交给客户端。

流式的关键是 HTTP/1.1 的 chunked transfer encoding(分块传输):不声明总长度,把响应切成一个个 chunk,生成一块发一块,直到发出一个零长度块表示结束。浏览器收到每个 chunk 都能立刻交给页面——这就是"边生成边渲染"的物理基础。

二、第二层:SSE 给字节流定了一套"帧格式"

分块传输只管"分段发",不管"内容怎么切"。SSE 在其上定义了极简的文本协议,Content-Type 标记为 text/event-stream,规则三句话说完:

  1. 一行 data: 开头的是数据内容;
  2. 空行(连续两个换行)是帧分隔符,一个空行前的所有行属于同一帧;
  3. id: 给帧编号,retry: 告诉客户端断线后多久重连,注释行以冒号开头。

实测一段真实输出(本文第四节的服务端代码,curl 原样抓取):

代码语言:javascript
复制
retry: 3000

id: 1
data: {"delta":"块1"}

id: 2
data: {"delta":"块2"}

data: {"done":true}

每帧一个 JSON——这就是大模型"流式 token"的传输形态:模型每生成一小段,服务端就包成一帧 SSE 发下来。

三、为什么是 SSE 而不是 WebSocket?

两个方向不同:WebSocket 是双向全双工,适合聊天室、协同编辑;SSE 是单向的服务端推送,搭在普通 HTTP 上——天然过代理、过网关、带标准的 HTTP 鉴权头,服务端实现成本几乎为零。

大模型对话的场景恰好是"一次请求、持续收增量"的单向流,SSE 完全够用。OpenAI、Anthropic、腾讯混元的流式接口都是 SSE。

四、服务端 30 行:Node.js 原生实现

零依赖,新建 sse-demo.mjs:

代码语言:javascript
复制
import { createServer } from 'node:http';

createServer((req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive',
  });
  res.write('retry: 3000\n\n');
  let i = 0;
  const timer = setInterval(() => {
    i += 1;
    res.write('id: ' + i + '\n');
    res.write('data: ' + JSON.stringify({ delta: '块' + i }) + '\n\n');
    if (i >= 3) {
      res.write('data: ' + JSON.stringify({ done: true }) + '\n\n');
      clearInterval(timer);
      res.end();
    }
  }, 100);
  req.on('close', () => clearInterval(timer));
}).listen(8787, () => console.log('sse on 8787'));

三个细节值得注意:

  1. 三个响应头是标配:Content-Type 声明协议、no-cache 防中间层缓存、keep-alive 保长连接;
  2. 每帧以 \n\n 结尾:少了空行,客户端会把两帧粘成一帧解析失败;
  3. req.on('close') 清理定时器:客户端断开后服务端继续写会抛异常,流式服务的资源回收是必修课。

五、客户端:为什么大模型应用都弃用 EventSource

浏览器原生有 EventSource API,但主流大模型应用几乎都用 fetch + ReadableStream 手写。原因很实际:

  • EventSource 只支持 GET,而对话接口要 POST 请求体(消息历史、参数);
  • EventSource 不能自定义请求头(Authorization 就带不上);
  • fetch 流可以随时 AbortController 中断生成——"停止按钮"靠它实现。

标准写法(实测通过):

代码语言:javascript
复制
const res = await fetch('http://127.0.0.1:8787/', {
  method: 'POST',
  headers: { Authorization: 'Bearer xxx' },
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = '';
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buf += decoder.decode(value, { stream: true });
  let idx;
  while ((idx = buf.indexOf('\n\n')) >= 0) {
    const frame = buf.slice(0, idx);
    buf = buf.slice(idx + 2);
    const dataLine = frame.split('\n').find(l => l.startsWith('data: '));
    if (dataLine) console.log('收到:', dataLine.slice(6));
  }
}

注意那个 buf 缓冲区:TCP 分块和 SSE 帧边界不对齐,一次 read 可能收到半帧或两帧半。先攒进 buf,再按 \n\n 切——这是流式解析最容易写错的地方,也是所有"偶发 JSON 解析失败"的根源。

六、大模型 API 在 SSE 上又包了什么

各家 API 的流式响应都是"SSE 帧里装 JSON 增量",结构大同小异:

  • 每帧 data: 里是一个 JSON:包含本次新增的 token 片段(delta/content);
  • 一帧可能只有一两个字,也可能是一小段;
  • 最后一帧通常是结束信号(finish_reason=stop 或事件类型 stop),有的家还会在末帧附上 token 用量统计。

所以"打字机前端"的实现就是:解析每帧 JSON → 取出增量文本 → 追加到页面。模型侧的流式生成(逐 token 采样天然可分段)和传输侧的 SSE,刚好严丝合缝。

七、踩坑清单(生产环境高发)

  1. Nginx 缓冲:反向代理默认攒满缓冲区才转发,表现为"卡很久然后一坨全出来"。要加 proxy_buffering off(或响应头 X-Accel-Buffering: no);
  2. 忘了 text/event-stream:Content-Type 不对,浏览器可能等整个响应结束才处理;
  3. 帧分隔符不严格:\n 写成 \r\n 或漏掉空行,客户端粘帧;
  4. 没处理客户端断开:服务端定时器/查询停不下来,连接泄漏;
  5. 心跳缺失:长空闲被中间层掐断连接,实践上每 15-30 秒发一行注释帧(: ping)保活;
  6. 解码用 TextDecoder 必须 stream: true:多字节中文被切在字节边界时,不加这个参数会解出乱码。

小结

打字机效果没有黑科技,三层旧零件的组合:HTTP chunked 传输管"分段发",SSE 协议管"帧格式",模型 API 管帧里装 token 增量。写一次 30 行的服务端和一个带缓冲的 fetch 解析器,这条链路就彻底通了——下次流式输出抽风(粘帧、乱码、整段憋出来),你直接就知道该查哪一层。

参考

  1. MDN:Server-Sent Events / Using server-sent events
  2. WHATWG HTML 规范:Server-sent events 一节
  3. OpenAI / Anthropic API 文档 Streaming 章节

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 前言
  • 一、第一层:HTTP 允许"边生成边发"
  • 二、第二层:SSE 给字节流定了一套"帧格式"
  • 三、为什么是 SSE 而不是 WebSocket?
  • 四、服务端 30 行:Node.js 原生实现
  • 五、客户端:为什么大模型应用都弃用 EventSource
  • 六、大模型 API 在 SSE 上又包了什么
  • 七、踩坑清单(生产环境高发)
  • 小结
  • 参考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档