SSE(Server-Sent Events)

文章目录

  • 前言
    • ✅ 一、SSE 格式结构
      • 常见字段含义
    • ✅ 二、解析示例(Python 代码)
    • ✅ 三、在前端(JavaScript)解析 SSE
    • ✅ 四、与 AI 应用的结合场景
      • ✅ 1. 服务端示例(Node.js Express)
        • ✅ 2. 前端使用 `EventSource` 接收 SSE
      • ❓如果你有前端已经通过 `fetch/post` 发起请求,如何“模拟 SSE 效果”?
        • ✅ 示例:前端使用 `fetch + ReadableStream` 处理 SSE
      • ✅ 总结
    • ✅ 左边方式(按 `\n` 拆行,逐行处理 `data:`)
      • 优点:
      • 缺点:
    • ✅ 右边方式(维护 `buffer`,提取 JSON)
      • 优点:
      • 缺点:
    • ✅ 建议:推荐使用右边的方案
      • 理由:
      • ✳️ 如果你希望兼容 OpenAI/OpenRouter 标准格式,但也提高健壮性,可以:
      • ✅ 示例 `extractJsonObjects` 实现建议(简化版)


前言

在 AI 应用开发中,SSE(Server-Sent Events)是一种常用于流式传输模型推理结果的协议,比如 OpenAI、Baidu、阿里等大模型服务返回的 token-by-token 响应。理解 SSE 格式对于处理 AI 输出流非常关键。


✅ 一、SSE 格式结构

SSE 的基本传输格式是纯文本,以 event:data:id: 等字段开头,每个字段占一行,事件之间用 两个换行符 \n\n 分隔:

data: {"text":"你好"}
data: {"text":",世界"}
data: [DONE]

常见字段含义

字段名 说明
data: 主体内容(AI输出)
event: 自定义事件类型(可选)
id: 事件 ID(可选)
retry: 重新连接时间(可选)

✅ 二、解析示例(Python 代码)

使用 requestshttpx 等库可以接收 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)

✅ 三、在前端(JavaScript)解析 SSE

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);
};

✅ 四、与 AI 应用的结合场景

场景 用途
ChatGPT 聊天响应 实时逐字响应
文本生成 实时拼接生成内容
文本摘要/改写 渐进式结果输出
语音转写 流式显示识别内容

在前端调用普通的 API 接口(如 POST 请求)之后,主动将响应“转换为 SSE 格式”是不可行的,因为:

❗ SSE 是服务端推送(Server-Sent Events),必须由服务端以 text/event-stream 格式持续推送数据。前端不能“转换”普通响应为 SSE,只能“接收”SSE 流。


✅ 1. 服务端示例(Node.js Express)

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));
});
✅ 2. 前端使用 EventSource 接收 SSE
const 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 处理 SSE
fetch('/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:

优点:

  • 代码直观易懂,模仿标准 SSE 数据格式 (data: ...)。
  • 适合标准 OpenAI / OpenRouter 接口的响应格式(每行一个 data:)。

缺点:

  • 对边界处理能力较弱,比如 chunk 被切在 "data:" 中间时容易出错。
  • 没有处理 JSON 字符串跨 chunk 的拼接容错(虽然在实际情况中较少发生,但并非不可能)。

✅ 右边方式(维护 buffer,提取 JSON)

buffer += chunk;
const jsonObjects = extractJsonObjects(buffer);
buffer = jsonObjects.remainder;

优点:

  • 健壮:可正确处理跨 chunk 的 JSON 数据(如一部分 {"delta":... 在上一个 chunk,另一部分在下一个 chunk)。
  • 对于 非标准流异常格式响应 更具有容错能力。
  • 可以兼容没有 data: 前缀的场景,比如某些第三方 SSE 实现。

缺点:

  • 实现复杂度略高:你需要确保 extractJsonObjects() 函数能正确提取完整 JSON 字符串。

✅ 建议:推荐使用右边的方案

理由:

  1. 更具通用性和鲁棒性
  2. 避免 chunk 分割带来的解析错误。
  3. 可支持未来接入更多模型接口(如 Claude、Gemini、Azure 等 SSE 变体)时的格式变化。

✳️ 如果你希望兼容 OpenAI/OpenRouter 标准格式,但也提高健壮性,可以:

  1. extractJsonObjects() 中判断每一段是否以 data: 开头;
  2. 自动剥离 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 };
}

你可能感兴趣的:(AI应用开发相关,javascript)