你想给产品接 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()。
这种方式拿到的是原始字节,解码和解析都要自己来。
两种方式的区别,就是「点外卖吃现成的」和「买食材自己做」。
那 AI 对话为什么不用 EventSource
省事归省事,EventSource 有两个硬伤:
只支持 GET 请求。 AI 对话需要用 POST 把对话历史发给服务端,GET 做不到。
不支持自定义请求头。 你没法带 Authorization token 做鉴权。
所以 AI 对话场景,客户端基本都用 fetch。Vercel AI SDK 的 useChat 底层也是这个方案。
业界说的 SSE 到底指什么
客户端必须用 fetch,这个是确定的。但服务端格式呢?
要搞清楚这件事,得先知道 「SSE」这个词可以指不同层级的东西:
HTTP streaming 是基础,对格式没有要求,服务端持续写、客户端持续读,仅此而已。
标准 SSE 是在它上面加的规范:data: 前缀、event: 字段、空行分隔。
遵不遵循这套规范,是后端接口对接大模型 API 时的实现选择。
你打开几个真实产品的 DevTools,差异立刻就出来了。
DeepSeek:严格的标准 SSE
注意标签页那一行:出现了一个 EventStream 标签页。
这是 Chrome 的内置功能,只有接口的响应头里包含 Content-Type: text/event-stream 时才会显示,浏览器用它把 SSE 消息解析成结构化表格。
有这个标签页,说明是标准 SSE。
再看响应内容,文字逐块推送过来,每条都是标准的 data: 消息。
值得注意的是结尾:它没有让连接静默断掉,而是发了一个命名事件 event: close,明确告诉客户端「结束了,不是网络断了」。
这是 event: 字段的典型用法。
ChatGPT:同样是标准 SSE,但做了一个有意思的取舍
也有 EventStream 标签页。但你会发现,ChatGPT 几乎不用 event: 字段来区分消息类型,而是把 "type" 塞进每条 data: 的 JSON 里,靠内部字段判断类型。
标准 SSE 完全撑得住复杂场景,只是 ChatGPT 选择用 JSON 内部字段区分类型,而不是 SSE 自带的 event: 字段。
Google Gemini:推送思路相同,格式完全自定义
先看 Gemini 的回答:天气卡片、实时数据、搜索来源,非常丰富。
然后你打开 DevTools,EventStream 标签页消失了。
响应不是 data: 格式,是一行一行的 JSON 数组:
Google 用的其实也是 SSE 的思路:服务端单向持续推数据。
但它没有遵循 SSE 的格式规范,而是用了自己设计的私有协议。
表面上回答最丰富,底层格式反而最私有。
三个案例放在一起,说明了一件事: 遵不遵循标准 SSE 格式,是接入大模型 API 的那层接口自己的选择,不是技术限制。
自己实现时,服务端格式怎么定、前端怎么解析,是前后端需要一起约定的决定。两个参考方向:
-
只传文本内容,标准 SSE 就够了。格式简单,浏览器有 EventStream 调试工具,前后端约定也直观。
-
需要在一个流里传多种数据(文字内容、工具调用结果、token 统计、状态信息),自定义格式更实际。可以像 ChatGPT 那样在 JSON 里加
"type"字段区分类型,也可以设计自己的格式。核心是前后端约定清楚。
WebSocket:双向实时通道
还是回到点外卖这个场景——轮询是你反复刷 app 看进度,SSE 是 app 主动推送骑手状态给你。
WebSocket 呢?
你不光能实时收到骑手位置,还能在配送途中直接给骑手发消息:“放门口就行,不要打电话”。
骑手也能马上回你:“好的”。
双方随时都能说话,不需要等对方先开口。 这就是「全双工」。
连接是怎么建立的
WebSocket 先用 HTTP 敲门,服务端同意后「升级」成 WebSocket 协议:
从此刻起,通信不再走 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 适合什么场景
需要双向实时通信的场景:多人聊天室里每个人都在发消息、协同编辑文档时每个人的改动要实时同步、在线游戏里玩家操作和服务器状态需要持续互传。
共同点是客户端不只是在「听」,还需要频繁「说」,而且要低延迟。
三种方案放在一起看
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」,不错过后续更新。