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 下没有这个问题。



加载中...