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。
我会按四条线拆:
- @ag-ui 是什么,它和 A2UI、MCP、A2A 各管哪一层;
- Node 端推 SSE 的完整步骤,从设置响应头到错误、取消、心跳;
- 客户端
fetch+ReadableStream的完整步骤,从getReader()到分帧、解析、收尾; - 这条链路里最值钱的十几个坑,其中几个是我在本机复现出来的,不是「听说」。
文中的实测脚本与官方文档原文都留档在 source/a2ui-sse-lab/。SSE 的基础格式、EventSource 与 fetch 的取舍,站内已经有两篇拆过——《SSE 是什么》与《EventSource 是什么,为什么 Agent 流式接收最后都换成了 fetch + ReadableStream》——这里不重复背景,只在需要的地方给出链接。
