大模型场景下的推送技术选型:轮询 vs WebSocket vs SSE

SSE(Server-Sent Events)流式输出

在大模型问答(LLM Chat)等实时场景中,用户期待能像 ChatGPT 一样边生成边输出 ,而不是等待完整结果。 实现这一效果的关键技术之一,就是 SSE(Server-Sent Events)流式输出。

为什么需要SSE?

我们知道,在前端开发中,常见的实时通信方案有:

  1. 轮询(Polling):客户端定时请求接口,简单但性能差。
  2. 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
相关推荐
静水leo3 小时前
使用 Whistle 拦截接口并模拟后端响应
前端
lyai0073 小时前
读完 DeepSeek Harness 源码,我给自己画了一张「最小 CLI Agent」施工图
llm·agent
掘金者阿豪3 小时前
SQL Server数据迁移完成后,凌晨的数据库告警该怎么查?
前端·后端
JudithHuang3 小时前
React 嵌套路由与布局
前端·react.js·arcgis·react·react router
htzyl2064 小时前
前端阶梯——第四十三章、实战二:待办清单To-Do
前端·javascript·html
七夜zippoe4 小时前
Agent 错误处理体系:分类、重试、降级与熔断设计
ai·agent·降级·错误处理·熔断设计
武子康4 小时前
固定时间步已经开了,为什么游戏回放还会走出不同位置?
人工智能·后端·agent
EnCi Zheng4 小时前
Agent自动化系统怎么搭-从Super-agent架构到事件驱动调度的完整指南
自动化·agent·领导力·调度系统·super-agent
摹客4 小时前
【3D设计】界面设计中的3D元素:应用场景+5款工具横评(含Three.js)
前端