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 下沒有這個問題。



加載中...