做 AI 对话,大家最常问的就是:“为什么不用 WebSocket?长连接看起来多高大上啊。”但在真实项目里踩完坑才发现,AI 刷字这个场景,用 SSE 才是真的香。
我们并没有盲目弃用 WebSocket——语音同传那种需要双向传音频的场景,我们依然在用。选型逻辑其实就一句话:单向推流用 SSE,双向高频用 WebSocket。
既然 AI 文本回答是“我发一句,它吐一堆”,那为什么说 SSE 是最优解?下面聊聊我们感受最深的四个点。
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 服务端 → 客户端,单向推流 | 双向全双工 |
| 传输协议 | 标准 HTTP,复用网关 / CDN | 独立协议,需升级握手 |
| 生命周期 | 一次请求,用完即断 | 长连接,需维护心跳与重连 |
| 停止请求 | controller.abort() 一行 | 协商 Close Code |
| 适合场景 | AI 文本流式输出、通知推送 | 语音同传、多人协作、游戏 |
一、它是标准的 HTTP,能直接使用现有基础设施
标准 HTTPWebSocket 看着酷,但它握手升级之后就脱离传统 HTTP 了。一旦报错,你只能收到一个冰冷的数字代码(比如 1008),前端还得额外写逻辑去猜到底发生了什么。
SSE 本质上就是一条不立马断开的 HTTP 响应。这意味着:
二、顺手破解了原生的“巨坑”
破解原生限制大家可能听说原生 EventSource 只支持 GET 请求,还加不了自定义 Header,听起来很不灵活对吧?
我们直接用了基于 fetch 封装的客户端(比如 @microsoft/fetch-event-source)。这样既保留了 SSE“服务器持续推数据”的优点,又能像写普通 fetch 一样自由加 POST body 和加密 Header,两全其美。
import { fetchEventSource } from '@microsoft/fetch-event-source'
await fetchEventSource('/api/xxxchat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ prompt }),
onmessage(ev) {
// 逐段追加文本,驱动打字机
},
})三、一次对话就是一个独立请求,不搞复杂状态机
免状态机WebSocket 是长连接,长连接最头疼的就是维护心跳、断线重连、防重复发送。
但 AI 对话的业务逻辑非常简单:一次提问 → 一次流式回答 → 结束。
SSE 的生命周期和这个业务逻辑天然对齐。我们甚至显式关闭了客户端重连——断线重连如果重复发同一句话,服务端可能会生成新的消息 ID,导致前端打字机直接错位。请求结束连接就断,干净利落。
四、点“停止生成”写起来太爽了
一行停止用户随时可能点“停止生成”。
用 SSE,前端直接调一行 controller.abort(),标准 fetch 原语直接在传输层就把请求掐断了。甚至有时候服务端出 Bug 没有主动关流,前端收到结束标记后自己 abort() 一下兜底,比 WebSocket 跑去协商 Close Code 优雅太多。
const controller = new AbortController()
// 用户点击“停止生成”
stopBtn.addEventListener('click', () => controller.abort())总结
选型结论技术选型真没什么崇拜或者偏见。AI 文本生成就是个单向推流、低频、文本级、一次性的场景。用 SSE,架构成本最低,写起来代码最少,Bug 也最少。把复杂的长连接留给复杂的场景,把简单的技术留给日常的业务。
本文属于 AI 实用主义流派 的第 51 篇肉身实战。
发表评论
分享你的想法和反馈