跳到主要内容

4 篇博文 含有标签「Node.js」

查看所有标签

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,可复现。

舱壁模式在 Node.js 里怎么落地:四道隔离与四组实测

· 阅读需 13 分钟

舱壁模式原理极简,属于 “知道了就觉得理所当然” 的知识;但它属于典型的「简单但极易被忽略」的稳定性手段。 就像保险丝,原理一点不复杂,但是不装,电路一过载就烧设备。

很多稳定性模式都是这个味道:思想朴素,价值巨大,难在主动落地,而不是理解。

舱壁(Bulkhead)来自造船:把船体隔成若干水密舱,一个舱进水,船不沉。搬到软件里,它只有一句话——**给资源划边界:让 A 的故障、拥堵、突发,物理上占不到 B 的资源。**但这句话在 Node.js 里的落点和 Java 生态很不一样。Java 谈舱壁,默认是「线程池隔离」(Hystrix 那套);Node 是单线程的,所以除了连接池,你还得隔离「事件循环」本身——一个 JSON.parse 大字符串或一次同步循环,就能把整个进程的所有接口一起拖下水。

这篇把 Node 里的四道舱壁逐个落成代码 + 本机实测。四组实验的效果都很干净(最夸张的一组是 1416 ms → 2.3 ms)