AI应用开发 | 为什么 AI 对话都在用 SSE,而不是 WebSocket?

0 阅读10分钟

你想给产品接 AI 对话,做到前后端怎么传数据这一步,发现绕不开一个选择:

轮询、SSE、WebSocket,该用哪个?

我们在 AI应用开发 | 手写流式输出:把打字机效果背后的数据流拆开看 模拟实现了 AI 对话中的打字机效果,用的是 fetch + ReadableStream。但这只是其中一条路。

数据从服务端到前端,其实有不止一种走法。

这篇把三种走法放在一起比,读完你能在具体场景里直接给出选型判断。顺带还会搞清楚一件事:业界说的「SSE」,有时候指的其实是两种不同的东西

先划重点

轮询是客户端反复问服务端要数据,SSE 是服务端单向推,WebSocket 是双向通道。

AI 对话场景用 SSE 就够了,单向推送满足需求,且对基础设施最友好。

业界说「用 SSE 做流式输出」,指的不一定是同一件事。

轮询:最朴素的方案

我之前做过一个 AI 生成报告的功能,每个报告任务要跑好几分钟

服务端没有流式输出,只有任务完成后才有结果——没法用流式推送,只能轮询。

我的做法是用 ahooks 封装好的轮询方法,每隔 30 秒请求一次接口,刷新报告状态,直到变成「完成」。

轮询就是这个思路。

客户端写一个定时器,每隔一段时间去问服务端:有新数据吗?

setInterval(async () => {
	const res = await fetch('/api/check');
	const data = await res.json();
	if (data.hasNew) updateUI(data.content);
}, 3000); // 每 3 秒问一次,实际项目可以用 ahooks useRequest 的 pollingInterval

其实就是「定时发请求」。

底层用的是普通 HTTP,每次请求都经历完整的建连、发请求、等响应、断开

但是放到 AI 对话场景里,问题就来了。

轮询间隔短了,大量用户同时高频请求会压垮服务器; 间隔长了,流式输出变得卡顿,打字机效果全没了。

不过轮询并不是一无是处。

数据更新频率低、对实时性要求不高的场景它完全够用,比如每 30 秒刷一次邮件列表,每分钟检查一下构建状态。

SSE:服务端单向推送

轮询是客户端不停问。

那能不能反过来——让服务端主动推

想象你点了一份外卖,app 有实时推送:下完单之后你不用刷,骑手每到一个节点 app 自动通知你——「已接单」「已到店」「配送中」。

你只下了一次单,之后所有进度都是 app 主动推给你的。

一次请求,服务端不断推,客户端只管接收。

标准 SSE 和你用过的方式有什么不同

浏览器为这种「服务端推送」定义过一套标准格式:响应头必须是 Content-Type: text/event-stream,每条消息以 data: 开头、\n\n 结尾。

data: 今天\n\n
data: 天气\n\n
data: 不错\n\n

相应配套的,浏览器还提供了一个叫 EventSource 的 API。

你可以把它理解成一个「自动接收器」,只要连上,服务端推过来的消息它会自动按格式解析好,你直接拿字符串就行

const source = new EventSource('/api/stream');
source.onmessage = event => {
	console.log(event.data); // 直接拿到解析后的字符串
};

不用自己读字节、不用自己解码EventSource 全包了。

我们在 AI应用开发 | 手写流式输出:把打字机效果背后的数据流拆开看 里,用 fetch 实现流式输出时,是这么接收的:fetch + res.body.getReader()

这种方式拿到的是原始字节,解码和解析都要自己来。

两种方式的区别,就是「点外卖吃现成的」和「买食材自己做」。

eventsource-vs-fetch.png

那 AI 对话为什么不用 EventSource

省事归省事,EventSource两个硬伤

只支持 GET 请求。 AI 对话需要用 POST 把对话历史发给服务端,GET 做不到。

不支持自定义请求头。 你没法带 Authorization token 做鉴权。

所以 AI 对话场景,客户端基本都用 fetch。Vercel AI SDK 的 useChat 底层也是这个方案。

业界说的 SSE 到底指什么

客户端必须用 fetch,这个是确定的。但服务端格式呢?

要搞清楚这件事,得先知道 「SSE」这个词可以指不同层级的东西

sse-layers.png

HTTP streaming 是基础,对格式没有要求,服务端持续写、客户端持续读,仅此而已

标准 SSE 是在它上面加的规范:data: 前缀、event: 字段、空行分隔。

遵不遵循这套规范,是后端接口对接大模型 API 时的实现选择

你打开几个真实产品的 DevTools,差异立刻就出来了。

DeepSeek:严格的标准 SSE

deepseek-standard-sse.png

注意标签页那一行:出现了一个 EventStream 标签页。

这是 Chrome 的内置功能,只有接口的响应头里包含 Content-Type: text/event-stream 时才会显示,浏览器用它把 SSE 消息解析成结构化表格

有这个标签页,说明是标准 SSE。

再看响应内容,文字逐块推送过来,每条都是标准的 data: 消息。

值得注意的是结尾:它没有让连接静默断掉,而是发了一个命名事件 event: close明确告诉客户端「结束了,不是网络断了」

这是 event: 字段的典型用法。

ChatGPT:同样是标准 SSE,但做了一个有意思的取舍

chatgpt-sse.png

也有 EventStream 标签页。但你会发现,ChatGPT 几乎不用 event: 字段来区分消息类型,而是把 "type" 塞进每条 data: 的 JSON 里,靠内部字段判断类型。

标准 SSE 完全撑得住复杂场景,只是 ChatGPT 选择用 JSON 内部字段区分类型,而不是 SSE 自带的 event: 字段。

Google Gemini:推送思路相同,格式完全自定义

先看 Gemini 的回答:天气卡片、实时数据、搜索来源,非常丰富。

gemini-ui.png

然后你打开 DevTools,EventStream 标签页消失了

响应不是 data: 格式,是一行一行的 JSON 数组

gemini-devtools.png

Google 用的其实也是 SSE 的思路:服务端单向持续推数据。

但它没有遵循 SSE 的格式规范,而是用了自己设计的私有协议。

表面上回答最丰富,底层格式反而最私有。

三个案例放在一起,说明了一件事: 遵不遵循标准 SSE 格式,是接入大模型 API 的那层接口自己的选择,不是技术限制。

自己实现时,服务端格式怎么定、前端怎么解析,是前后端需要一起约定的决定。两个参考方向:

  • 只传文本内容,标准 SSE 就够了。格式简单,浏览器有 EventStream 调试工具,前后端约定也直观。

  • 需要在一个流里传多种数据(文字内容、工具调用结果、token 统计、状态信息),自定义格式更实际。可以像 ChatGPT 那样在 JSON 里加 "type" 字段区分类型,也可以设计自己的格式。核心是前后端约定清楚。

WebSocket:双向实时通道

还是回到点外卖这个场景——轮询是你反复刷 app 看进度,SSE 是 app 主动推送骑手状态给你

WebSocket 呢?

你不光能实时收到骑手位置,还能在配送途中直接给骑手发消息:“放门口就行,不要打电话”。

骑手也能马上回你:“好的”。

双方随时都能说话,不需要等对方先开口。 这就是「全双工」。

连接是怎么建立的

WebSocket 先用 HTTP 敲门,服务端同意后「升级」成 WebSocket 协议:

websocket-handshake.png

从此刻起,通信不再走 HTTP,而是走 WebSocket 自己的帧格式。

代码本身很简洁:

const ws = new WebSocket('wss://example.com/chat');

ws.onmessage = event => {
	console.log('收到:', event.data);
};

ws.send('你好'); // 随时可以发
ws.send('再来一条'); // 在同一条连接上

和 SSE 对比,最大的区别是:

SSE 客户端只能听,要「说话」得另发一次 HTTP 请求。

WebSocket 直接 ws.send(),在同一条连接上双向通信。

WebSocket 适合什么场景

需要双向实时通信的场景:多人聊天室里每个人都在发消息、协同编辑文档时每个人的改动要实时同步、在线游戏里玩家操作和服务器状态需要持续互传。

共同点是客户端不只是在「听」,还需要频繁「说」,而且要低延迟。

三种方案放在一起看

transport-polling.png

transport-sse.png

transport-websocket.png

AI 对话选 SSE:单向推送够用,HTTP 原生支持,不用额外配置基础设施。

多人实时协作选 WebSocket:双方都要频繁说话,需要真正的双向通道。

低频状态查询选轮询:数据更新慢、实时性要求不高,用最简单的方案就够了。

回到开头的问题:AI 对话为什么选 SSE

单向就够了

你发出一条消息,然后等着看 AI 怎么回。

整个回答过程里,你不会往服务端再发任何数据——就是在等,在接收。

单向推送完全满足这个场景。

HTTP 友好

SSE 用的就是普通 HTTP,你现在用的 CDN、反向代理、Serverless 函数全都能直接支持,不需要任何额外配置。

WebSocket 就不一样了。它不走普通 HTTP,CDN、Nginx、云函数这些中间层不一定默认支持,部署到线上要单独配置,稍不注意就连不上。

工程量更小

WebSocket 有一套要自己维护的东西:心跳检测、断线重连、连接状态管理。SSE 不用管这些。

fetch 发一次请求,读完响应,连接自然关闭,用完即走。

WebSocket 和 SSE 不是「谁更好」的关系,它们是为不同场景设计的工具。

什么时候需要 WebSocket

一旦客户端也需要频繁往服务端发数据,SSE 就不够用了。

比如你在做一个「多人协同编辑 + AI 辅助」的产品: 协同编辑部分需要 WebSocket,持续同步多人的操作;AI 建议部分用 SSE 就够了,触发后单向推送回答

同一个产品里不同的通信需求,用不同的方案,这在实际项目中是常见做法。

读完回顾

Q1:你在上一篇用 fetch + ReadableStream 读流式数据,这种方式和标准 SSE 的 EventSource 相比,各自适合什么场景?

💡 想想 AI 对话场景需要 POST 还是 GET,需不需要自定义请求头。

Q2:你的项目里已经有 WebSocket 基础设施(比如用于实时通知),现在要加 AI 对话功能,你会复用 WebSocket 还是单独用 SSE?

💡 想想 AI 对话的数据流向是单向还是双向,复用 WebSocket 多出来的维护成本换来了什么。

Q3:假设你在做一个实时协作文档产品,文档协同编辑和 AI 写作助手这两个功能,分别会选什么传输方案?

💡 想想这两个功能的通信方向有什么不同,同一个产品里能不能混合使用不同方案。


如果这篇帮你理清了通信方案的选型思路,接下来自然会问:

确定用 SSE 之后,代码层面怎么写才算对?

Vercel AI SDK 帮你封装了哪些细节,你自己还需要处理什么?

感兴趣可以关注微信公众号 「前端Fusion」,不错过后续更新。

分享底图_压缩.png