跳到主要内容

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》——这里不重复背景,只在需要的地方给出链接。

ParadeDB 的混合检索与 RRF:文本和向量装在同一个索引里,但融合那一段要自己写 SQL

· 阅读需 21 分钟

Postgres 上做混合检索,过去的标准姿势是「pgvector 管向量 + tsvector 管关键词 + 一条 SQL 自己融合」——本站 pgvector 入门 里那条 WITH ... RRF 就是这个写法。

ParadeDB 想解决的是这套姿势里最别扭的部分:两套索引、两份候选、两个计划。它的答案是——把文本列和向量列放进同一个索引,让 BM25 和向量距离在同一趟扫描里完成。

但有个反直觉的事实,我这次实测才确认:

ParadeDB 不提供 rrf() 函数,也不提供 hybrid_search() 之类的 API。 它的「RRF」是一段要你自己写的 SQL 模式——RANK() 窗口函数排名次、weight / (k + rank) 算权重、sum() 融合。

我查了 pg_proc,pg_search 0.25.10 里不存在任何名字含 rrf / fuse 的函数。这和 Milvus 是两个路线:Milvus 内置 RRFRanker()(本站 Milvus 入门 验算过它的分数就是教科书公式),ParadeDB 则把融合层的控制权原样交回 SQL。

这篇把它的实现路径跑了一遍,所有数字都来自真实执行(脚本与日志留档在 source/paradedb/)。

命名说明:你说的「RFF 算法」,业界标准名是 RRF(Reciprocal Rank Fusion,互惠排名融合),本文统一用 RRF。

vLLM 在 Tesla V100 上能跑到哪个版本?—— 3090 + V100 混合机器的安装复盘

· 阅读需 20 分钟

这台机器上插着 1 张 RTX 3090(cc 8.6)和 2 张 Tesla V100(cc 7.0)。原以为"装个 vLLM、起个 xinference"是半天的事,实际从 05-02 折腾到 05-05,最后拆成两套互不干扰的 conda 环境才稳下来。

写这篇时我把当时的结论重新核了一遍,其中一条被实测推翻了:无论是当时的记录、还是社区里常见的说法,都是"新版 vLLM 在 V100 上根本起不来"。今天在同一台机器、同一张卡上实测:0.19.1 能起,也能出字——只是 V100 上的默认后端从 xformers 换成了 TRITON_ATTN,比 0.10.0 慢约 25%。所以这篇分两半:这台机器上真实发生了什么,以及"V100 到底支持到哪一版"这个问题的代码级答案。

三条结论先摆出来:

  1. V100 跑不了新版 vLLM,卡点不在编译产物,而在 Python 侧的 attention backend 门槛。 0.19.1 的 _C.abi3.so 里明明编进去了 sm_70,照样被 Python 层的 supports_compute_capability() 挡在候选之外——排查时别去 .so 的架构列表上找。
  2. V100 的可用区间不是"只有 0.10.0",而是 0.10.0 ~ 0.19.x。 0.10.0 会自动降级到 xformers(最省心,也实测最快);0.19.x 会自动降级到 TRITON_ATTN(能跑,慢一些)。真正断掉这条路的是 0.20.0 起官方构建删掉了 sm_70,有 PR diff 为证。
  3. 最阴的坑跟 V100 无关:pip install "xinference[all]" 会静默把 transformers 升到 5.x,把刚装好的 vLLM 0.10.0 弄坏——不报冲突、pip check 干净、vllm --version 通过,直到真正推理才炸。

AI 应用前端选型核对:React 19 的 Server Components、shadcn/ui、React Router 的嵌套路由

· 阅读需 18 分钟

一次选型讨论里落下来三条论断:

  1. React 19 的 Server Components 对 AI 应用有特殊意义——可以在服务端调用 AI 接口,减少客户端暴露
  2. shadcn/ui 风格现代、可定制性强,适合快速搭建 AI 产品界面
  3. React Router 的嵌套路由适合复杂的管理后台场景

三条听起来都对。但「听起来对」和「能拿出依据」之间差着一层:官方文档究竟怎么写的?哪部分是官方原话,哪部分是我自己的推论,哪部分是实测出来的?

这篇文章把三条论断逐条拆开核对。核对办法是三件事:curl 抓一手原文逐字比对、本机跑脚本实测、把推论明确标成推论。

大模型流式 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,可复现。

官方为什么把 supervisor 库换掉了:官方只说了一句,剩下的靠实测

· 阅读需 15 分钟

上一篇记录了官方的推荐变更:从 createSupervisor(worker 是图节点)改成 subagents pattern(子 agent 包成 tool)。这篇回答「为什么」。

⚠️ 更正说明(2026-09-21 追加)​

本文初版把「context engineering / context isolation」当成了官方的理由来引用。这是错的:我后来拿到了官方迁移文档的原文(用 curl 直取,此前误判为被网络策略挡住),逐字核对后发现——

  • 官方文档里 context engineering 出现 0 次、context isolation 出现 0 次、more control 出现 0 次;

  • 官方给出的理由只有一句,而且纯粹是维护状态(原文逐字):

    The langgraph-supervisor package is no longer actively maintained. Instead use the subagents pattern: a main agent coordinates specialized workers by calling them as tools.

  • 「context engineering / isolation」那些措辞来自第三方社区资料(搜索命中的 skill 文档与技术 PDF),不是 LangChain 官方。

因此本文的定位是:官方没有给技术论证,只说了「不再积极维护」——为什么该换,官方是留白的;下面全部机制解释都是本文自己的实测分析,不是官方立场。

官方没给理由,那理由得自己找。下面用实测数据给出一个工程师视角的解释:

我的结论是:在旧模型里,框架会产生一批「你并不想要」的消息,然后用一堆参数让你把它们再弄出去。新模型从构造上就不产生它们。

注意这句是我的分析,不是官方说法。 但它有官方文档的间接印证——迁移文档里那句关于 output_mode 的原文(见 §二 末),说的正是同一件事。

环境:Node v24.14.1 / @langchain/langgraph@1.4.16 / @langchain/langgraph-supervisor@1.1.2 / langchain@1.5.11。用脚本化模型(自定义 BaseChatModel 按次序返回预置响应)替代真实 LLM——无需 API Key、执行轨迹完全确定。脚本留档在 source/langgraph-supervisor/context-growth.mjs。

LangGraph.js Supervisor-Worker 架构解析:把它拆成四件事,并实测两个成本旋钮

· 阅读需 22 分钟

Supervisor-Worker 是中心化调度最典型的形态:一个调度者负责「派活」,若干专家负责「干活」。听起来像个新概念,但把它拆开只有四件事——而且我用大约 40 行手写代码复现了官方库完全相同的执行轨迹,这就是「它内部到底做了什么」最直接的证据。

更值得一看的是后半篇:这套架构有两个成本旋钮,实测能把共享上下文从 16 条消息压到 4 条(字符数 -71%),而执行轨迹一字不变。

但请先看下面那段更正:这套架构依然有效,官方的推荐实现却换了——从 createSupervisor(worker 是图节点)换成了 subagents pattern(子 agent 包成 tool)。我在 §五 把新写法实现并实测了,结论对新写法有利。

环境:Node v24.14.1 / @langchain/langgraph@1.4.16 / @langchain/langgraph-supervisor@1.1.2。为了让执行轨迹完全确定、可复现,我用脚本化模型(按调用次序返回预置响应的自定义 BaseChatModel)替代真实 LLM——无需 API Key,全部输出都是本机真实运行结果。脚本留档在 source/langgraph-supervisor/。

⚠️ 更正说明(2026-09-20 追加)​

本文初版把 createSupervisor 当作 Supervisor-Worker 的标准做法来推荐,这一点需要更正:官方已经改了推荐路径。

  • LangChain 官方文档已把新代码导向 subagents pattern——把子 agent 包成一个 tool,交给一个普通的 createAgent 调度;Python 侧另有专门的迁移文档 Migrate from langgraph-supervisor,给出了逐项映射(output_mode → 在工具包装层里自己格式化输出、create_handoff_tool → 自己写一个调用 subagent.invoke() 的 tool)。
  • 同时,createReactAgent → createAgent 也是 JS v1 的既定迁移方向(见 JavaScript v1 迁移指南)。本文初版的示例两个都用上了,等于踩在两条旧路径上。

一个精确措辞上的澄清(核实依据分两层):官方文档的原话是「no longer actively maintained」(不再积极维护)——注意它说的是「不积极维护」,不是「已废弃」。工程上的旁证是我自己核对的 npm 元数据:该包没有 deprecated 标记,最近一次发版是 2026-09-12 的 1.1.2(本文写作前 8 天),仍由 LangChain 官方维护者发布。所以:

  • 存量代码不必惊慌,它不是"弃用炸弹";
  • 但新代码确实不该再用——这不是"选哪个都行"的偏好问题,官方已明确表态推荐变了。

我把 subagents pattern 也实现并实测了,结论对它有利:同一个任务、路由用的 LLM 调用次数完全相同(都是 3 次),但共享上下文只有 6 条消息,不到库版本默认值(12 条)的一半,而且不需要调任何参数。完整实现与对比见下文「五、官方已改推荐:subagents pattern」。

本文仍然值得读的部分:§三 把 supervisor「拆成四件事」的机制分析依然成立——理解机制才能看懂两种实现的关系;§四 的两个成本旋钮实测也依然有效,但请注意它的定位变了:在新范式里,这件事由「你的工具包装层返回什么」直接决定,比调库参数更自由。读的时候请把 §二/§三 当作"存量代码的长什么样",把 §五 当作"新代码该怎么写"。

向量数据库怎么选:19 个方案的实测横评,与企业实际在用哪些的数据

· 阅读需 50 分钟

上一篇开源 RAG 知识库横评里我实测了 14 个 RAG 平台的默认向量库,留下一个没展开的问题:这些向量库本身怎么选?

这篇补上。做四件事:先分类(它们不是同一类东西)→ 用两个独立数据源回答「企业实际在用哪些」(结论和赛道热度是反的)→ 逐家拆解 19 个方案 → 两张表讲清真正的技术分水岭(过滤+ANN、量化)。

文中 star、协议、版本、容器数均为 2026-09-20 实测:GitHub API + LICENSE 原文 + docker-compose + PyPI 下载量。厂商自测的性能数字一律单独标注。凡我没能从一手来源核实的,直接写「未证实」——这次有好几处,包括一整个云厂商。

Modal 的 position:fixed 为什么会失效:一次 filter 引发的包含块事故,与 Vue 3 Teleport

· 阅读需 16 分钟

需求很简单:给页面加一个 Modal。遮罩层 position: fixed; inset: 0,铺满视口——教科书写法。

结果它没有铺满视口,而是缩在某个容器里,位置还随着容器走。

这篇就讲这件事:先说清 fixed 为什么失效(不是玄学,是 CSS 规范里一条明确的规则),再用无头浏览器实测 14 个属性看谁触发,最后引出 Vue 3 的 Teleport 方案,以及它自己带来的几个新坑。

文中的实测数据来自本机 Chrome 无头模式,复现用的最小代码在文末附录,可直接贴出来自己验一遍。所有规范条文均标注了出处并可核对。