跳到主要内容

4 篇博文 含有标签「流式传输」

查看所有标签

Fetch 的 response.body 到底特殊在哪:和 XMLHttpRequest 的流式能力边界

· 阅读需 26 分钟

先给结论:

在「浏览器标准里的 HTTP 响应体」这个范围内,这句话基本成立:Fetch 的 Response.body 是一个 ReadableStream,而 XMLHttpRequest 没有直接暴露流对象的能力。

但这句话很容易被读成两个过强的版本,而它们都不对:

  1. 「XHR 完全不能边收边处理」——不对。XHR 在 responseType 为 text 或空字符串时,readyState === 3(LOADING)期间 responseText 会不断增长,你确实可以「边收边处理」文本。只是它给你的是一个不断变长的字符串,不是流对象。
  2. 「只有 Fetch 能产生 ReadableStream」——不对。Blob.stream()、File.stream()、CompressionStream、TransformStream、ReadableStream 构造器都能产生流。准确说法是:在 HTTP 响应体这个位置上,Fetch 是标准里唯一直接把 body 暴露成 ReadableStream 的 API。

这篇就把这句话拆开:Fetch 的 body 模型长什么样,XHR 到底暴露了什么,两者的边界在哪,以及我在 Chrome 里做的一组对照实测。

实测脚本与官方原文留档在 source/fetch-stream-lab/。环境:Chrome 156.0.8078.47、Node v24.14.1、macOS 15.7.3,本机回环,无代理 / CDN。

fetch + ReadableStream 实战:从 a2ui-full 拆一条 AG-UI SSE 全链路

· 阅读需 62 分钟

浏览器里那种「AI 一边想、界面一边长出来」的效果,落到代码其实只有两件事:

  • 服务端把一串事件写进一条不肯结束的 HTTP 响应;
  • 客户端从 response.body 里一块一块读出来,边读边解析、边渲染。

听起来像是一个下午就能写完的活。但真正把它接到 Agent 上,坑会集中在几个边界上:一次网络分片不等于一个事件、响应头发出之后状态码就改不了、连接关闭不等于任务完成、在错误的位置 throw 会让客户端永远等下去。这些都不是 API 会不会用的问题,而是「边界由谁负责」的问题。

这篇拿一个真实项目开刀:bigfe/encode_lesson/a2ui-full。它是一个 A2UI Playground——用户在网页里描述界面,Koa 服务端调用 LLM 生成 A2UI 协议,浏览器通过 fetch + ReadableStream 接收,边收边把界面渲染到右侧预览区。服务端用 @ag-ui/core 与 @ag-ui/encoder 组织事件,客户端手写 SSE 分帧,协议载荷则是 A2UI 的 JSONL。

我会按四条线拆:

  1. @ag-ui 是什么,它和 A2UI、MCP、A2A 各管哪一层;
  2. Node 端推 SSE 的完整步骤,从设置响应头到错误、取消、心跳;
  3. 客户端 fetch + ReadableStream 的完整步骤,从 getReader() 到分帧、解析、收尾;
  4. 这条链路里最值钱的十几个坑,其中几个是我在本机复现出来的,不是「听说」。

文中的实测脚本与官方文档原文都留档在 source/a2ui-sse-lab/。SSE 的基础格式、EventSource 与 fetch 的取舍,站内已经有两篇拆过——《SSE 是什么》与《EventSource 是什么,为什么 Agent 流式接收最后都换成了 fetch + ReadableStream》——这里不重复背景,只在需要的地方给出链接。

大模型流式 chunk 会因「网络波动」乱序吗?——TCP 的顺序保证,与「拼接 / 交错 / 截断」三件真身

· 阅读需 18 分钟

用 Node 搭 Agent 的人,几乎都问过同一句话:

大模型把 token 一帧帧吐回来,路上要过抖动、丢包、代理、网关——chunk 会不会乱序?我是不是得在客户端按时间戳重排一遍?

先给结论,三条:

  1. 一条流内部不会乱序。 这不是"概率很低",而是传输层的投递语义:IP 层确实会丢包、也可能乱序到达,但重排和去重由 TCP 承担,交给应用的永远是有序字节流。所以客户端既没必要重排,也没有重排的依据(你的 chunk 上并没有一个"网络序号"可排)。
  2. 你真正会踩的坑是另外三件事:分帧(chunk 边界不等于字符/帧边界,读错就烂码、丢帧)、交错(一个 Agent 同时跑多路流,合流看起来"跳")、截断(中途断流,拿到一段残文却以为写完了)。这三件里没有一件是"乱序"。
  3. 唯一真能"乱序"的地方,是写出顺序本身——而那时该修的是写出方,不是给客户端加一个重排器(它会忠实地把错误的顺序搬给你,也不会替你纠正)。

下面把规范逐字原文、7 组本机实测、生产清单都摆出来。全文的实测脚本随仓库留档在 source/sse-assembly-lab/(harness.mjs + harness.log),零依赖、不需要 API key,可复现。

EventSource 是什么,为什么 Agent 流式接收最后都换成了 fetch + ReadableStream

· 阅读需 33 分钟

「Agent 为什么都用 fetch + ReadableStream 读流?EventSource 不是也能收 SSE 吗?」

这个问题问得对,但流传的答案「大厂从 SSE 换成了 ReadableStream」说得不准。真实情况是:

EventSource 是浏览器内置的 SSE 客户端,它在「打开一个 URL、持续收通知」这件事上是满分答案。但 Agent 场景需要的几件事——发 POST、带请求头、看状态码——它一件也做不到,重连语义也完全不由你控制。于是大家保留了 SSE 的帧格式,只把「读它的那只手」换成了 fetch + ReadableStream。

这篇按这条线讲透:① EventSource 到底是什么 → ② 为什么 Agent 场景要换掉它 → ③ 换成 fetch + ReadableStream 之后怎么写(含可跑的最小实现)→ ④ 流式端点的三个生产雷区 → ⑤ 这套也不够用时,什么时候必须上 WebSocket / WebRTC。

正文的实测都在本机 Node 上跑出来,脚本随仓库留档在 source/stream-lab/。