在 AI 应用开发中,SSE(Server-Sent Events)是一种常用于流式传输模型推理结果的协议,比如 OpenAI、Baidu、阿里等大模型服务返回的 token-by-token 响应。理解 SSE 格式对于处理 AI 输出流非常关键。
SSE 的基本传输格式是纯文本,以 event:
、data:
、id:
等字段开头,每个字段占一行,事件之间用 两个换行符 \n\n
分隔:
data: {"text":"你好"}
data: {"text":",世界"}
data: [DONE]
字段名 | 说明 |
---|---|
data: |
主体内容(AI输出) |
event: |
自定义事件类型(可选) |
id: |
事件 ID(可选) |
retry: |
重新连接时间(可选) |
使用 requests
或 httpx
等库可以接收 SSE 响应流:
import requests
response = requests.get(
'https://example.com/ai/stream',
stream=True,
headers={'Accept': 'text/event-stream'}
)
for line in response.iter_lines():
if line:
decoded = line.decode('utf-8')
if decoded.startswith('data:'):
data = decoded[len('data:'):].strip()
if data == '[DONE]':
break
print("AI输出:", data)
const eventSource = new EventSource('/api/chat');
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log("AI输出:", data.text);
};
eventSource.onerror = function(err) {
console.error("连接错误:", err);
};
场景 | 用途 |
---|---|
ChatGPT 聊天响应 | 实时逐字响应 |
文本生成 | 实时拼接生成内容 |
文本摘要/改写 | 渐进式结果输出 |
语音转写 | 流式显示识别内容 |
在前端调用普通的 API 接口(如 POST
请求)之后,主动将响应“转换为 SSE 格式”是不可行的,因为:
❗ SSE 是服务端推送(Server-Sent Events),必须由服务端以
text/event-stream
格式持续推送数据。前端不能“转换”普通响应为 SSE,只能“接收”SSE 流。
app.get('/sse', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
res.flushHeaders();
const interval = setInterval(() => {
res.write(`data: ${JSON.stringify({ text: "Hello world" })}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
});
EventSource
接收 SSEconst es = new EventSource('http://localhost:3000/sse');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("收到数据:", data.text);
};
es.onerror = (err) => {
console.error("连接出错", err);
};
fetch/post
发起请求,如何“模拟 SSE 效果”?✅ 方法是:服务端仍使用
text/event-stream
返回流,前端改用fetch
+ReadableStream
手动读取流(适用于不能用EventSource
的复杂场景,如带 token 的 POST 请求)。
fetch + ReadableStream
处理 SSEfetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your-token'
},
body: JSON.stringify({ message: "你好" })
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
function readChunk() {
reader.read().then(({ done, value }) => {
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop(); // 可能有半截
for (const line of lines) {
if (line.startsWith('data:')) {
const data = line.replace(/^data:\s*/, '');
if (data === '[DONE]') return;
console.log("接收到数据:", JSON.parse(data));
}
}
readChunk();
});
}
readChunk();
});
前端方式 | 服务端要求 | 是否支持 POST | 备注 |
---|---|---|---|
EventSource |
响应必须为 text/event-stream |
❌ 仅支持 GET |
最标准、最简单 |
fetch + ReadableStream |
响应为 text/event-stream |
✅ | 推荐用于需要 POST + token 的场景 |
从你截图中展示的两种处理 OpenRouter SSE 数据的方式来看:
\n
拆行,逐行处理 data:
)data: ...
)。data:
)。"data:"
中间时容易出错。buffer
,提取 JSON)buffer += chunk;
const jsonObjects = extractJsonObjects(buffer);
buffer = jsonObjects.remainder;
{"delta":...
在上一个 chunk,另一部分在下一个 chunk)。data:
前缀的场景,比如某些第三方 SSE 实现。extractJsonObjects()
函数能正确提取完整 JSON 字符串。extractJsonObjects()
中判断每一段是否以 data:
开头;data:
前缀再 JSON.parse。extractJsonObjects
实现建议(简化版)function extractJsonObjects(buffer) {
const objects = [];
let remainder = '';
const lines = buffer.split('\n\n');
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith('data:')) {
const jsonStr = line.slice(5).trim();
if (jsonStr && jsonStr !== '[DONE]') {
objects.push(jsonStr);
}
}
}
remainder = lines[lines.length - 1];
return { objects, remainder };
}