大模型场景下的推送技术选型:轮询 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: agentAevent: 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
相关推荐
会豪2 小时前
前端插件-不固定高度的DIV如何增加transition
前端
却尘2 小时前
Server Actions 深度剖析(2):缓存管理与重新验证,如何用一行代码干掉整个客户端状态层
前端·客户端·next.js
小菜全2 小时前
Vue 3 + TypeScript 事件触发与数据绑定方法
前端·javascript·vue.js
Hilaku3 小时前
面试官开始问我AI了,前端的危机真的来了吗?
前端·javascript·面试
shellvon3 小时前
前端攻防:揭秘 Chrome DevTools 与反调试的博弈
前端·逆向
β添砖java3 小时前
案例二:登高千古第一绝句
前端·javascript·css
却尘3 小时前
Server Actions 深度剖析:这就是个披着 React 外衣的 RPC
前端·rpc·next.js
南雨北斗4 小时前
Vue 3 修饰符(Modifiers)
前端
会豪4 小时前
工业仿真(simulation)--前端(七)--消息栏
前端