---
title: "AI 对话流式响应选型：为什么是 SSE 而不是 WebSocket？"
date: 2026-09-12
description: "AI 对话为什么选 SSE 而不是 WebSocket？本文从标准 HTTP 复用、fetch-event-source 破解原生 GET 限制、独立请求免状态机、AbortController 一行停止生成四个角度，讲清“单向推流用 SSE，双向高频用 WebSocket”的选型逻辑与真实踩坑。"
canonical_url: "https://yijinlee.com/articles/article-69"
tags:
  - SSE
  - WebSocket
  - "Server-Sent Events"
  - EventSource
  - fetch-event-source
  - AbortController
  - 流式输出
  - "AI Coding"
topic: "AI 外骨骼"
---

METRICS: SSE · WebSocket · EventSource · fetch-event-source · AbortController · 流式输出 · 单向推流 · 半双工

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

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

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

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

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

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

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

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

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

大家可能听说原生 `EventSource` 只支持 `GET` 请求，还加不了自定义 Header，听起来很不灵活对吧？

我们直接用了基于 `fetch` 封装的客户端（比如 `@microsoft/fetch-event-source`）。这样既保留了 SSE“服务器持续推数据”的优点，又能像写普通 `fetch` 一样自由加 `POST` body 和加密 Header，两全其美。

```typescript
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 优雅太多。

```typescript
const controller = new AbortController()

// 用户点击“停止生成”
stopBtn.addEventListener('click', () => controller.abort())
```

## 总结

技术选型真没什么崇拜或者偏见。AI 文本生成就是个**单向推流、低频、文本级、一次性**的场景。用 SSE，架构成本最低，写起来代码最少，Bug 也最少。把复杂的长连接留给复杂的场景，把简单的技术留给日常的业务。

## 参考文章：相关链接

- [MDN · Server-Sent Events 使用指南](https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events)（官方文档）— SSE 协议与 text/event-stream 事件流格式的官方说明。
- [MDN · EventSource](https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource)（官方文档）— 原生 EventSource 仅支持 GET 且不能自定义 Header 的官方依据。
- [Azure/fetch-event-source · GitHub](https://github.com/Azure/fetch-event-source)（官方文档）— 基于 fetch 的 SSE 客户端，支持 POST body、自定义 Header 与 AbortController 中断。
- [MDN · AbortController](https://developer.mozilla.org/zh-CN/docs/Web/API/AbortController)（官方文档）— 传输层中断请求的标准原语，“停止生成”一行 controller.abort() 的依据。
- [让 AI 开口说话：从“整段等待”到“流式反馈”的架构演进](https://yijinlee.com/articles/article-10)（站内深度）— 同属 SSE 流式输出的完整链路复盘，含 AbortController 三层协作与缓冲策略。

