土薯工具 Toolshu.com 登录 用户注册

SSE 原理与实战:AI 打字机效果背后的技术,以及它和 WebSocket 的区别

原创 作者:bhnw 于 2026-04-09 09:48 发布 3次浏览 收藏 (0)

AI 打字机效果是怎么实现的?

用过 ChatGPT 或任何国产大模型的人都见过这个效果:AI 回答不是一次性出现,而是一个字一个字地"打"出来,像有人在实时打字。

这个效果背后用的技术叫 SSE(Server-Sent Events,服务器推送事件)。理解 SSE,不仅能搞清楚 AI 打字机效果的原理,更能在自己的项目里实现实时推送、直播弹幕、股价更新等场景。


SSE 是什么?

SSE 是一种基于 HTTP 的单向实时通信协议,允许服务器主动向客户端推送数据,而无需客户端反复发起请求。

它的工作方式很简单:客户端发起一个普通的 HTTP 请求,服务器不立刻关闭连接,而是保持连接打开,持续往里"写"数据——每次有新内容就推一条,直到流结束。

客户端                    服务器
  |                         |
  |--- GET /stream -------->|
  |                         |
  |<-- data: 第一条消息 ----|
  |<-- data: 第二条消息 ----|
  |<-- data: 第三条消息 ----|
  |         ...             |
  |<-- data: [DONE] --------|  连接关闭

SSE 的数据格式

SSE 的数据格式非常简单,每条消息由若干字段组成,字段之间用换行分隔,消息之间用空行分隔:

data: 这是第一条消息

data: 这是第二条消息

event: update
data: {"type":"progress","value":80}
id: 42
retry: 3000

字段说明:

字段 说明
data 消息内容,必填,可以多行
event 自定义事件类型,默认为 message
id 消息 ID,用于断线重连时告知服务器上次收到的位置
retry 重连等待时间(毫秒),客户端断开后等多久重连

Content-Type 必须是 text/event-stream,否则浏览器不会按 SSE 协议解析。


AI 流式输出的 SSE 长什么样?

OpenAI、通义千问等大模型 API 的流式输出,正是 SSE 格式。每个 token 生成后立刻推送一条:

data: {"id":"1","choices":[{"delta":{"content":"你"},"finish_reason":null}]}

data: {"id":"2","choices":[{"delta":{"content":"好"},"finish_reason":null}]}

data: {"id":"3","choices":[{"delta":{"content":"!"},"finish_reason":null}]}

data: [DONE]

前端收到每条 data 后,解析 JSON,提取 choices[0].delta.content,拼接到界面上,就实现了打字机效果。

调试这类 AI 接口时,如果想实时查看每条 SSE 数据的内容并自动提取指定 JSON 字段,可以用 SSE 在线调试工具,输入接口地址和 Header(如 Authorization token),直接在浏览器里观察流式数据。


前端如何接收 SSE?

浏览器原生提供了 EventSource API,专门用于接收 SSE:

const source = new EventSource('/api/stream');

// 接收默认 message 事件
source.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

// 接收自定义事件类型
source.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('进度更新:', data.value);
});

// 错误处理
source.onerror = (error) => {
  console.error('连接出错', error);
  source.close();
};

局限EventSource 只支持 GET 请求,不能自定义请求头。如果需要 POST 或携带 Authorization header(比如调用需要鉴权的 AI 接口),需要用 fetch + ReadableStream 手动处理:

const response = await fetch('/api/stream', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer your-token'
  },
  body: JSON.stringify({ prompt: '你好' })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  const text = decoder.decode(value);
  // 解析 SSE 格式,提取 data 字段
  const lines = text.split('\n');
  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const data = line.slice(6);
      if (data === '[DONE]') return;
      console.log(JSON.parse(data));
    }
  }
}

后端如何实现 SSE?

Python(FastAPI)

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def event_generator():
    for i in range(5):
        yield f"data: 第{i+1}条消息\n\n"
        await asyncio.sleep(1)
    yield "data: [DONE]\n\n"

@app.get("/stream")
async def stream():
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream"
    )

Node.js(Express)

app.get('/stream', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  let count = 0;
  const interval = setInterval(() => {
    res.write(`data: 第${++count}条消息\n\n`);
    if (count >= 5) {
      res.write('data: [DONE]\n\n');
      res.end();
      clearInterval(interval);
    }
  }, 1000);

  req.on('close', () => clearInterval(interval));
});

SSE vs WebSocket:怎么选?

这是最常见的问题。两者都能实现实时通信,但适用场景不同:

对比项 SSE WebSocket
通信方向 单向(服务器→客户端) 双向
协议 基于 HTTP 独立协议(ws://)
断线重连 浏览器自动重连 需手动实现
实现复杂度 简单 较复杂
兼容性 所有现代浏览器 所有现代浏览器
适用场景 通知推送、AI流式输出、实时日志 在线游戏、聊天室、协同编辑
穿透代理/CDN 更容易(普通HTTP) 需要代理支持 ws

选择原则:如果只需要服务器向客户端推数据,用 SSE——更简单,基于普通 HTTP,不需要特殊的服务器或代理配置。如果需要客户端也主动向服务器发消息(真正的双向实时通信),才用 WebSocket。

大多数 AI 应用的流式输出场景,SSE 完全够用,没必要上 WebSocket。


几个容易踩的坑

1. Nginx 代理缓冲导致数据不实时

SSE 经过 Nginx 时,如果开启了代理缓冲,数据会积攒到一定量才一起发出,实时性全无。需要在配置里关掉:

location /stream {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding on;
}

2. HTTP/2 下的行为差异

HTTP/2 原生支持多路复用,SSE 在 HTTP/2 下表现更好,不会占用额外的 TCP 连接。但部分旧版代理对 HTTP/2 + SSE 的支持不完善,出问题时可以尝试降级到 HTTP/1.1。

3. 连接数限制

浏览器对同一域名的 HTTP/1.1 连接数有限制(通常6个)。如果页面打开多个 SSE 连接,可能耗尽连接数影响其他请求。HTTP/2 下没有这个问题。

发现周边 发现周边
评论区

加载中...