从 AI 对话应用理解 SSE 流式传输:一项 "老技术" 的新生

从 AI 对话应用理解 SSE 流式传输:一项 "老技术" 的新生

最近在开发一个类似 ChatGPT 的 AI 对话应用,深入学习了 SSE(Server-Sent Events)流式传输技术。本文记录我的学习过程和理解,希望对你有帮助。

一、为什么 AI 应用需要流式传输?

如果你用过 ChatGPT、Claude 等 AI 对话产品,一定注意到它们的回复是逐字显示的,而不是等待几十秒后一次性显示完整答案。

这种体验差异巨大:

方式 用户体验
普通接口 发送消息 → 等待 10-30 秒 → 一次性显示完整回答 😴
流式接口 发送消息 → 0.5 秒后开始显示 → 逐字输出 → 完成 🤩

同样的等待时间,流式输出让用户感觉 AI "在思考",而非 "卡死了"。

这背后的技术就是 SSE(Server-Sent Events)


二、SSE 是什么?

一句话定义

SSE 就是在一次 HTTP 请求会话结束前,服务端多次向客户端推送数据。

与普通 HTTP 请求的对比

erlang 复制代码
普通 HTTP 请求:
客户端 ──请求──► 服务端
客户端 ◄──响应── 服务端(一次性返回,连接关闭)

SSE 流式请求:
客户端 ──请求──► 服务端
客户端 ◄──数据1── 服务端
客户端 ◄──数据2── 服务端
客户端 ◄──数据3── 服务端
...
客户端 ◄──结束── 服务端(连接关闭)

核心特点

  • 单向通信:服务端 → 客户端(如果需要双向,用 WebSocket)
  • 基于 HTTP:不需要特殊协议,复用现有基础设施
  • 长连接:一个请求保持打开,直到服务端主动关闭

三、服务端实现:其实很简单

SSE 服务端的核心就三步:

typescript 复制代码
// 1. 设置 SSE 响应头
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");

// 2. 多次写入数据(不关闭连接)
res.write("data: 第1块数据\n\n");
res.write("data: 第2块数据\n\n");
res.write("data: 第3块数据\n\n");

// 3. 结束连接
res.end();

SSE 消息格式

vbnet 复制代码
event: message
data: {"content": "你好"}

event: done
data: {"content": "", "done": true}

每条消息由 event(可选)和 data 组成,消息之间用 \n\n 分隔。

Express 完整示例

typescript 复制代码
import express from "express";

const app = express();

app.post("/api/chat/stream", async (req, res) => {
  const { message } = req.body;

  // 1. 设置 SSE 响应头
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  // 2. 模拟逐字输出
  const reply = `你好!你说的是:"${message}",这是一个流式响应示例。`;

  for (const char of reply) {
    // 每个字符作为一条消息发送
    res.write(`data: ${JSON.stringify({ content: char })}\n\n`);

    // 模拟打字延迟
    await new Promise((resolve) => setTimeout(resolve, 50));
  }

  // 3. 发送结束标记
  res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
  res.end();
});

app.listen(3001);

四、客户端实现:理解 ReadableStream

浏览器如何感知流式响应?

当浏览器收到响应头 Content-Type: text/event-stream 时,会将响应体包装为一个 ReadableStream 对象,允许我们边接收边处理。

核心代码

typescript 复制代码
async function fetchSSE(message: string) {
  const response = await fetch("/api/chat/stream", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message }),
  });

  // response.body 是一个 ReadableStream
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  // 循环读取,直到流结束
  while (true) {
    const { done, value } = await reader.read();

    if (done) break; // 流结束

    // 解码并处理数据
    const text = decoder.decode(value);
    console.log("收到:", text);
  }
}

关键问题:await reader.read() 会阻塞吗?

这是我学习时的一个疑惑:while (true) 循环不会卡死吗?

答案是:不会!

reader.read() 是一个 Promise,它会:

  • 有数据时:立即返回 { done: false, value: ... }
  • 没数据时:挂起等待,直到服务端发送数据
  • 连接关闭时:返回 { done: true }

这是异步等待,不是忙轮询,不会占用 CPU。

css 复制代码
时间轴:
────────────────────────────────────────────────────────►

前端:       await read()     await read()     await read()
                 │ 挂起等待...    │ 挂起等待...    │
                 ▼                ▼                ▼
服务端:   ──● res.write() ──● res.write() ──● res.end()

五、接入真实 LLM API

如果要接入 OpenAI、Claude 等真实 AI 服务,你的服务端需要:

  1. 接收 三方 API 的 SSE 响应
  2. 转发 给前端
scss 复制代码
前端 ◄──(SSE)── 你的服务端 ◄──(SSE)── LLM API

OpenAI 示例

typescript 复制代码
import OpenAI from "openai";

const openai = new OpenAI({ apiKey: "sk-xxx" });

app.post("/api/chat/stream", async (req, res) => {
  res.setHeader("Content-Type", "text/event-stream");

  // 调用 OpenAI,开启流式模式
  const stream = await openai.chat.completions.create({
    model: "gpt-4",
    messages: [{ role: "user", content: req.body.message }],
    stream: true, // 关键:开启流式
  });

  // 遍历流式响应,逐个转发
  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || "";
    if (content) {
      res.write(`data: ${JSON.stringify({ content })}\n\n`);
    }
  }

  res.end();
});

本质就是:LLM 给你一滴水,你就往前端倒一滴。


六、不同环境的流式处理

SSE 不是浏览器专属,任何支持 HTTP 的环境都能实现:

环境 流式 API
浏览器 response.body.getReader()
Node.js response.on('data', callback)
Dart/Flutter response.stream.listen()
Go io.Reader
Python response.iter_content()

原理都一样:收到一块数据 → 处理一块 → 等待下一块 → 直到结束


七、一项 25 年前的 "老技术"

SSE 背后的核心技术------HTTP 分块传输(Chunked Transfer Encoding) ------早在 1999 年 就被纳入 HTTP/1.1 标准(RFC 2616)。

makefile 复制代码
HTTP/1.1 响应头:
Transfer-Encoding: chunked  ← 告诉客户端这是分块传输

这不是什么新发明,而是一项 20+ 年的成熟技术,只是 AI 时代让它重新成为焦点。

AI 之前的应用场景

  • 大文件下载:边读边发,不用先加载到内存
  • 动态网页:边生成边返回,用户先看到框架
  • 实时日志tail -f 式的持续输出
  • 股票行情:实时推送价格变动

你打开任意一个网站,在开发者工具中大概率能看到 Transfer-Encoding: chunked------这技术一直在默默工作。


八、常见误区澄清

误区 1:分片上传也是 HTTP Chunked

错! 前端大文件分片上传是应用层方案,多个 HTTP 请求,每个传一片。

HTTP Chunked 是协议层功能,一个请求内分块传输。

对比 HTTP Chunked 分片上传
方向 服务端 → 客户端 客户端 → 服务端
请求数 1 个 多个
谁来分块 协议自动处理 前端 JS 手动切分

误区 2:SSE 是新技术

错! SSE 规范(EventSource API)2006 年就有了,底层的 Chunked 更是 1999 年的标准。

AI 只是给老技术找到了新的杀手级应用场景。


九、总结

概念 一句话解释
SSE 一次请求内,服务端多次推送数据
服务端 res.write() 多次,res.end() 结束
客户端 reader.read() 循环,done 判断结束
await read() 异步等待,有数据才返回,不是忙轮询
底层原理 HTTP/1.1 Transfer-Encoding: chunked
历史 1999 年标准,2023 年因 AI 翻红

核心认知

技术本身没变,场景变了。很多 "新技术" 只是老技术 + 新包装。

学技术时,理解底层原理比追逐新概念更重要------因为原理不变,概念会反复翻新。


相关推荐
专吃海绵宝宝菠萝屋的派大星3 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
宇擎智脑科技3 小时前
基于 SAM3 + FastAPI 搭建智能图像标注工具实战
人工智能·计算机视觉
爱分享的阿Q3 小时前
Rust加WebAssembly前端性能革命实践指南
前端·rust·wasm
蓝黑20203 小时前
Vue的 value=“1“ 和 :value=“1“ 有什么区别
前端·javascript·vue
F_U_N_3 小时前
效率提升80%:AI全流程研发真实项目落地复盘
人工智能·ai编程
小李子呢02113 小时前
前端八股6---v-model双向绑定
前端·javascript·算法
月诸清酒3 小时前
24-260409 AI 科技日报 (Gemma 4发布一周下载破千万,开源模型生态加速演进)
人工智能·开源
2501_933329553 小时前
技术架构深度解析:Infoseek舆情监测系统的全链路设计与GEO时代的技术实践
开发语言·人工智能·分布式·架构
He少年4 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
X journey4 小时前
机器学习进阶(16):如何防止过拟合
人工智能·机器学习