← 返回文章列表
所属专题:AI 外骨骼

AI 对话流式响应选型:为什么是 SSE 而不是 WebSocket?

by李奕锦年份:2026字数:2,400阅读时长:7 分钟

技术选型没有崇拜与偏见——AI 文本生成是单向推流、低频、文本级、一次性的场景,SSE 以最低的架构成本对齐了这个业务本质。

TL;DR · 核心结论

  • 1单向推流用 SSE,双向高频用 WebSocket:AI 刷字是典型半双工,SSE 的生命周期与“一次提问 → 一次流式回答 → 结束”天然对齐。
  • 2SSE 是标准 HTTP:状态码、网关鉴权、CDN 直接复用,不需要为长连接重写基础设施。
  • 3原生 EventSource 只支持 GET?用 fetch-event-source 即可自由加 POST body 与自定义 Header,再用 AbortController 一行实现“停止生成”。
SSEWebSocketEventSourcefetch-event-sourceAbortController流式输出单向推流半双工

做 AI 对话,大家最常问的就是:“为什么不用 WebSocket?长连接看起来多高大上啊。”但在真实项目里踩完坑才发现,AI 刷字这个场景,用 SSE 才是真的香。

选型一句话

我们并没有盲目弃用 WebSocket——语音同传那种需要双向传音频的场景,我们依然在用。选型逻辑其实就一句话:单向推流用 SSE,双向高频用 WebSocket。

既然 AI 文本回答是“我发一句,它吐一堆”,那为什么说 SSE 是最优解?下面聊聊我们感受最深的四个点。

维度SSEWebSocket
通信方向服务端 → 客户端,单向推流双向全双工
传输协议标准 HTTP,复用网关 / CDN独立协议,需升级握手
生命周期一次请求,用完即断长连接,需维护心跳与重连
停止请求controller.abort() 一行协商 Close Code
适合场景AI 文本流式输出、通知推送语音同传、多人协作、游戏

一、它是标准的 HTTP,能直接使用现有基础设施

标准 HTTP

WebSocket 看着酷,但它握手升级之后就脱离传统 HTTP 了。一旦报错,你只能收到一个冰冷的数字代码(比如 1008),前端还得额外写逻辑去猜到底发生了什么。

SSE 本质上就是一条不立马断开的 HTTP 响应。这意味着:

状态码(401 没登录、500 服务器崩溃)原样拿来用;
公司现有的网关、网关鉴权、防抓包加密签名、CDN,统统不用重写,直接复用。

二、顺手破解了原生的“巨坑”

破解原生限制

大家可能听说原生 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 篇肉身实战。

阅读时长:7 分钟


文档信息

版权声明:自由转载-非商用-非衍生-保持署名(CC BY-NC-ND 3.0)

原文链接:https://yijinlee.com/articles/article-69

作者:李奕锦

商业用途或修改衍生请联系授权。


李奕锦
李奕锦

全栈工程师,业余马拉松选手。

TL;DR

  • 单向推流用 SSE,双向高频用 WebSocket:AI 刷字是典型半双工,SSE 的生命周期与“一次提问 → 一次流式回答 → 结束”天然对齐。
  • SSE 是标准 HTTP:状态码、网关鉴权、CDN 直接复用,不需要为长连接重写基础设施。
  • 原生 EventSource 只支持 GET?用 fetch-event-source 即可自由加 POST body 与自定义 Header,再用 AbortController 一行实现“停止生成”。
Tags:SSEWebSocketServer-Sent EventsEventSourcefetch-event-sourceAbortController流式输出AI Coding

参考文章:相关链接

权威引文 · 官方文档 · 站内深度文

  1. SSE 协议与 text/event-stream 事件流格式的官方说明。

  2. 官方文档MDN · EventSource

    原生 EventSource 仅支持 GET 且不能自定义 Header 的官方依据。

  3. 基于 fetch 的 SSE 客户端,支持 POST body、自定义 Header 与 AbortController 中断。

  4. 传输层中断请求的标准原语,“停止生成”一行 controller.abort() 的依据。

  5. 同属 SSE 流式输出的完整链路复盘,含 AbortController 三层协作与缓冲策略。

该专题下的阅读路径

AI Coding架构排障

常见问题 FAQ

Q1. 为什么 AI 对话选 SSE 而不是 WebSocket?
AI 文本回答是单向推流、低频、一次性场景,SSE 本质是一条不立即断开的 HTTP 响应,生命周期与“一次提问 → 一次流式回答 → 结束”天然对齐;WebSocket 是双向高频长连接,需要自维护心跳与重连,架构成本更高。
Q2. 原生 EventSource 只能 GET,怎么加 Header 和 POST body?
用基于 fetch 封装的客户端(如 @microsoft/fetch-event-source),既保留 SSE 服务器持续推数据的语义,又能像普通 fetch 一样传 POST body 和自定义加密 Header。
Q3. 为什么反而要关闭断线重连?
AI 对话一次一问,断线重连若重复发送同一句话,服务端可能生成新的消息 ID,导致前端打字机错位。SSE 请求结束连接即断,业务语义更干净。
Q4. 怎么实现“停止生成”?
前端直接调用 controller.abort(),标准 fetch 原语在传输层即可掐断请求;服务端未主动关流时,前端收到结束标记后自己 abort() 兜底。

发表评论

分享你的想法和反馈

支持 Markdown 格式

0/5000