为什么流式加载大模型API接口需要使用Fetch

27 阅读3分钟

前言

大模型流式返回选择 fetch + ReadableStream 而非 WebSocket 或 EventSource,是因为该场景本质是“被拉长的单向请求-响应”,fetch 在支持 POST 与自定义鉴权头的同时,完美复用现有 HTTP 基础设施且实现最轻量;WebSocket 属于功能冗余,EventSource 则因协议限制无法满足 POST 与鉴权需求。

各种方式的对比

维度fetch + ReadableStreamAxios(浏览器端)WebSocketEventSource
请求方法任意(POST ✅)任意(POST ✅)独立协议仅 GET ❌
自定义 Header✅✅❌(浏览器限制)❌
通信方向单向(够用)单向(等完整响应)双向(冗余)单向
流式读取✅ getReader() 逐块读❌ 默认 XHR 不支持(需 1.7+ 切 fetch 适配器)✅ 天然逐帧✅ 原生逐事件
基础设施兼容极好(标准 HTTP)极好(标准 HTTP)需额外适配好
鉴权方式标准 Authorization标准 Authorization(拦截器统一注入)别扭别扭
拦截器❌ 无(需手动封装)✅ 请求/响应拦截器开箱即用❌❌
自动重连需手动实现需手动实现需手动实现✅ 原生支持
开发复杂度中等(需手写 SSE 解析)高(流式场景需绕过自身机制)高低
适用场景大模型对话流 ✅普通 CRUD 接口 ✅ / 流式 ❌双向实时通信公开 GET 推送

总结

大模型交互本质是单向数据流(用户单次提问 → 服务端持续流式推送),不需要双向实时通信。

  1. WebSocket 在此场景下存在明显劣势,全双工能力完全用不上,白白背上额外协议、连接管理、心跳保活的复杂度。
  2. EventSource 是浏览器内置的 SSE 原生 API,代码极简且自带断线重连,但在大模型场景有两个致命硬伤:
  • 只支持 GET 请求:大模型请求需将 messages(含历史对话)、model、stream: true、temperature 等大量参数放入请求体,必须用 POST。EventSource 无法携带请求体,且 URL 长度有限制,塞不下完整上下文。
  • 不能自定义请求头:构造签名仅支持 new EventSource(url, { withCredentials: true }),无 headers 参数,无法添加 Authorization: Bearer xxx 鉴权头,API Key 只能拼到 URL 明文暴露。
  1. Axios 在浏览器端默认基于 XMLHttpRequest(XHR),而 XHR 根本不支持真正的流式读取—— 它必须等整个响应接收完毕才能拿到数据,这与流式输出“来一段处理一段”的需求天然矛盾。

为什么 fetch + ReadableStream 是最佳选择?

该方案本质是用 fetch 发起 POST 请求,手动读取响应体中的 SSE 格式流。它精准覆盖了前两者的短板:

  1. 支持 POST + 请求体:可携带任意大小的 messages 数组和参数。
  2. 支持自定义请求头:Authorization、Content-Type 等随意设置。
  3. 原生流式读取:response.body.getReader() 逐块读取,配合 TextDecoder 解码,实现打字机效果。
  4. 支持 AbortController:用户点“停止生成”可立即中断请求。
  5. 复用 HTTP 基础设施:无需协议升级,天然兼容 Nginx、CDN、防火墙等。
  6. 轻量无额外依赖:无需 WebSocket 框架或连接池管理。

代价:需 手动处理 SSE 协议解析(缓冲拼接、按 \n\n 切分消息、剥离 data: 前缀、处理 UTF-8 多字节字符截断等)。这是被 EventSource 能力边界逼出的工程折中,OpenAI、Anthropic 等主流 SDK 均采用此方案。