SSE(Server-Sent Events)流式输出
在大模型问答(LLM Chat)等实时场景中,用户期待能像 ChatGPT 一样边生成边输出 ,而不是等待完整结果。 实现这一效果的关键技术之一,就是 SSE(Server-Sent Events)流式输出。
为什么需要SSE?
我们知道,在前端开发中,常见的实时通信方案有:
- 轮询(Polling):客户端定时请求接口,简单但性能差。
- WebSocket:双向通信,适合高频交互,但实现和协议较复杂。
SSE 的优势在于:
- 简单:基于 HTTP 协议,不需要自定义协议。
- 浏览器原生支持 :
EventSource
API 开箱即用。 - 轻量:适合服务端 → 前端的单向流,如日志、消息、AI 生成文本。
在大模型问答场景中,SSE 能够让用户实时看到 AI 的逐字生成过程,提升交互体验。
SSE的实现原理
SSE基于http长连接,服务端的响应头通常如下:
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
之后服务端会持续不断地推送数据,每条数据的格式如下:

前端如何使用 SSE
原生 EventSource API
浏览器提供了 EventSource
对象来直接消费 SSE:
ts
const eventSource = new EventSource('/sse/stream');
// 监听 message 事件
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到数据:', data);
};
// 自定义事件类型
eventSource.addEventListener('thinking', (event) => {
console.log('AI 正在思考:', event.data);
});
// 错误处理
eventSource.onerror = (err) => {
console.error('SSE 连接出错', err);
eventSource.close();
};
特点:
- 自动重连(默认 3s)
- 支持事件订阅
- 兼容性较好(除了 IE)
fetch + ReadableStream
在大模型场景中,很多接口是 POST
请求(带上下文参数),而 EventSource
只支持 GET
。
这时我们常用 fetch + ReadableStream:
ts
async function startStream() {
const response = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: '你好' }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
console.log('流数据:', chunk);
}
}
这种方式需要自己解析 event:
/ data:
行,适合更灵活的场景。
SSE 在大模型问答中的应用
类似 ChatGPT 的流式回答
用户提问后,模型逐步返回回答内容:
ts
async function askLLM(question: string) {
const response = await fetch('/llm/answer', { ... });
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let answer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
if (chunk.startsWith('data:')) {
const data = JSON.parse(chunk.slice(5));
answer += data.content;
render(answer);
}
}
}
多事件分发(思考 / 主体 / 结果)
很多大模型服务会把不同阶段拆成事件:
event: thinking
→ 模型推理过程event: mainbody
→ 主体回答event: result
→ 结构化结果
前端可以根据事件类型决定展示形式(如灰色小字显示 "AI 正在思考...")。
技术对比:
- 轮询:前端不断发请求获取结果,延迟大,浪费网络资源,也不适合长任务。
- WebSocket:双向通信,适合即时交互场景,但建立连接、心跳维护、断线重连增加了复杂度。如果只是服务端推送,WebSocket 有点"大材小用"。
- SSE(Server-Sent Events) :轻量 HTTP 协议,天然支持服务端向前端推送流式数据。通过
event
字段,可以区分不同 Agent 的输出,特别适合多 Agent 协作场景。前端可以累积接收到的数据,逐步渲染,大幅降低延迟感。
多 Agent 场景拓展:
- 每个 Agent 对应不同的事件类型,例如
event: agentA
、event: agentB
。 - 前端监听多 event,分别处理每个 Agent 的输出,保证数据结构清晰。
- 如果任务过长,SSE 可以自动重连,支持断点续传策略。
超时处理(每个 Agent 最大等待时间)
思路:为每个 Agent 设置一个定时器,如果超过指定时间还没有返回数据,就标记该 Agent 超时。
ts
interface AgentState {
buffer: string[];
timeoutId?: ReturnType<typeof setTimeout>;
completed: boolean;
error?: string;
}
// 初始化 Agent 状态
const agents: Record<string, AgentState> = {
agentA: { buffer: [], completed: false },
agentB: { buffer: [], completed: false },
};
// 设置超时时间 5s
const TIMEOUT = 5000;
function startAgentTimeout(agentName: string) {
agents[agentName].timeoutId = setTimeout(() => {
agents[agentName].completed = true;
agents[agentName].error = '超时';
renderCombined();
}, TIMEOUT);
}
// 收到 Agent 数据时取消定时器
function handleAgentData(agentName: string, chunk: string) {
clearTimeout(agents[agentName].timeoutId);
agents[agentName].buffer.push(chunk);
renderCombined();
}
效果:如果 Agent 很久没返回数据,前端会自动标记失败,但不影响其他 Agent 的输出。
2重试机制(失败 Agent 单独重发请求)
思路:对失败的 Agent 触发单独请求,保持其他 Agent 输出不受影响。
js
async function retryAgent(agentName: string, retries = 2) {
for (let i = 0; i < retries; i++) {
try {
const chunks = await fetchAgentStream(agentName); // 单独请求
chunks.forEach(chunk => handleAgentData(agentName, chunk));
agents[agentName].completed = true;
return;
} catch (err) {
console.warn(`Agent ${agentName} 第${i+1}次重试失败`);
}
}
agents[agentName].error = '重试失败';
renderCombined();
}
// 触发失败 Agent 重试
function handleAgentError(agentName: string) {
retryAgent(agentName);
}
效果:某个 Agent 出现错误或超时,可以单独重试,其他 Agent 的输出继续。
优先级处理(高优先级 Agent 先渲染)
思路:维护一个优先级数组,高优先级 Agent 的输出立即渲染,低优先级 Agent 继续累积。
js
const priorityOrder = ['agentA', 'agentB', 'agentC'];
function renderCombined() {
let combined = '';
for (const agent of priorityOrder) {
const state = agents[agent];
if (state.buffer.length) {
combined += state.buffer.join('');
}
if (state.error) {
combined += `[${agent} 错误: ${state.error}]`;
}
}
display(combined);
}
- 高优先级 Agent 输出先加入
combined
- 低优先级 Agent 的输出缓存在
buffer
中,但不会阻塞高优先级渲染 - 出错或超时也不会阻塞其他 Agent