编码智能体的本地 Vector Search:从 507 MB 到 7 行循环

发布时间:2026 年 11 月 12 日

本周,我们推出了 Modern Web Guidance 1.0,这是一项开源技能,可帮助编码智能体使用现代 Web 平台功能,而不是旧版权宜解决方法。本文将介绍本地搜索引擎,它能为您的任务找到最相关的指南,而不会浪费您的 token。

如果您告诉工程师,您为他们的编码智能体构建了一个包含 1.7 MB Markdown 文本的知识库,他们会提出两个问题:

  1. “这会向我的上下文窗口中转储多少个 token?”
  2. “它需要什么样的臃肿向量数据库?”

在过去六个月中,我们三次重建了本地检索引擎。我们从 507 MB 的 C++ 绑定,缩减到 23 MB 的纯 JavaScript(使用 TensorFlow.js),再缩减到 7 MB 的软件包,其中查询嵌入是一个 7 行的 for 循环,同时还提高了准确率。

智能体 RAG 和渐进式披露

将 1.7 MB 的 Markdown 文本(包含 200 多个 HTML/CSS/Web API 指南)转储到 AGENTS.md 中是不切实际的。我们相信渐进式披露,但知道 Markdown 索引在规模化时将不再具有 token 效率。

为了解决这个问题,我们使用由搜索 CLI 支持的智能体 RAG:

  1. 在静止状态下,智能体只能看到技能的说明(约 205 个 token)。该说明遵循 Anthropic 的技能触发指南,以便智能体在相关前端任务中调用该技能。
  2. 在前端任务中,智能体可以使用 CLI 进行搜索:modern-web-guidance search "animate height auto",获得 10 个相关匹配项(约 530 个 token)。
  3. 智能体运行 modern-web-guidance retrieve animate-to-intrinsic-sizes 来提取整个指南(约 800 个 token)。它现在配备了有关兼容性、后备和技术的实现最佳实践。

为什么选择 CLI 技能,而不是 MCP 服务器或网页搜索?

  • MCP 工具要么会浪费 token,要么触发机制不可靠(由于动态工具发现机制)。技能说明经过精心优化,可确保智能体仅在适当的时候使用指导。
  • 内置的网页搜索功能会显示 2015 年的 Stack Overflow 答案和参考文档,而不是最新的、经过精心挑选的 DO / DO NOT 实现模式。此外,通过网络运行需要几秒钟,并且在离线沙盒中会失败,而本地 CLI 在 CPU 上运行只需几毫秒。

第 1 代:507 MB 的 C++ 原型

第一版 Modern Web Guidance 使用了本地 RAG 堆栈:LanceDB、Apache Arrow 和 Transformers.js,并搭配 onnxruntime-native。

它在开发机器上运行良好,但不太适合作为智能体技能:

  • 当您向项目添加技能(或智能体使用 npx 调用技能)时,您不希望等待 npm install 下载依赖于架构的 .node 二进制文件,也不希望等待 node-gyp C++ 编译(这在云沙盒中通常会失败)。
  • 解压后的 node_modules 目录大小为 507 MB。

第 2 代:将 TensorFlow.js 缩减至 23 MB

我们希望实现零原生编译,并在运行 Node 的任何位置即时执行。因此,我们在 4 月份将原生堆栈替换为在 @tensorflow/tfjs 上运行 all-MiniLM-L6-v2 的纯 JavaScript CPU 引擎:

  • 在 esbuild 中对 onnxruntime-node 进行了填充,并将 Hugging Face 分词器替换为一个微型树内 BERT WordPiece 分词器(0 个 token ID 不匹配)。
  • 在探索了 source-map 之后,pruned @tensorflow/tfjs-backend-cpu 的 400 多个模块缩减为 MiniLM 实际使用的 26 个内核,从而将 JS 引擎从 6 MB 缩减为 830 KB。
  • 弃用了 LanceDB,改为使用构建时经过 gzip 压缩的向量,并对所有大约 200 个指南进行内存中 <1 ms 点积扫描。
  • 将嵌入器置于 import() 调用之后,以便 retrieve 和 list 在 10 毫秒内完成。

这样,npm 软件包的大小就降到了 23 MB,且没有运行时依赖项。我们发布了该工具,代理人使用它进行了大约 75 万次搜索。

MiniLM 在生产环境中发生故障的位置

几个月后,我们分析了智能体安装了该技能但仍编写旧版代码的轨迹。

在一次 Codex 运行中,一位开发者要求提供删除确认提示。我们有一份完整的指南,介绍了如何使用 HTML <dialog closedby="any"> 构建无障碍确认模态框。

智能体做出了正确的选择,搜索了:

npx modern-web-guidance@latest search "confirm destructive action"

CLI 返回了 []。于是,代理耸了耸肩,写下了 window.confirm()。

为什么返回空结果?在深入分析日志和嵌入后,我们发现了两个明显的检索差距:

  1. all-MiniLM-L6-v2 是在 2018 年至 2020 年左右训练的,因此其 WordPiece 词汇表根本不知道 2020 年之后推出的 Web API。当智能体搜索 fetchpriority、popover 或 startViewTransition 时,2018 年的词元化器会将 fetchpriority 拆分为四个无意义的子词:fetch · ##pr · ##ior · ##ity。
  2. 为了防止超出范围的查询返回随机指南,之前的 PR 添加了严格的 0.38 余弦相似度截止值。由于 MiniLM 无法将 3 词短语“"confirm destructive action"”与“"dialog"”联系起来,导致其相似度得分低于 0.38(得分为 0.349,而“"popover anchor"”得分为 0.362,“"autofill address form"”得分为 0.354),因此门控机制丢弃了有效匹配项。更糟糕的是,每当 search 返回 [] 时,我们的技能指令都会让智能体回退到 modern-web-guidance list,将所有约 200 个指南说明(浪费了约 1,500 个 token)全部转储到上下文中。

一张教育图表,突出显示了两项技术限制。第 1 部分展示了 2018 年的分词器如何将 2020 年之后的 Web API(例如“fetchpriority”)拆分为子词。第 2 部分展示了严格的 0.38 相似度阈值如何屏蔽有效的搜索查询匹配,例如“确认破坏性操作”“弹出式锚点”和“自动填充地址表单”。

修正检索:真实遥测、混合搜索和返回 10 条结果

我们最初的基准是根据指南本身生成的 850 个合成查询,这些查询与指南中的措辞过于接近,无法发现这些失败情况。因此,我们构建了一个全新的评估框架 (pnpm benchmark:rag),其中包含从用户选择退出的 CLI 遥测数据中抽样的约 4,000 次实时搜索(这些数据仅记录智能体 2-5 个单词的 search 参数,绝不记录用户提示或代码库代码,并遵循 DISABLE_TELEMETRY=1),以及从 Claude Code、Codex CLI 和 Gemini CLI 的夜间智能体评估中挖掘的 1,029 次搜索。

通过针对这些保留的查询重放我们的 git 历史记录,我们发现 0.38 截止日期导致空 [] 响应激增了 8 倍。有了基准之后,我们在调整嵌入模型之前,先进行了两项快速改进:

  1. 混合关键字 + 语义搜索:密集向量会模糊确切的 API 名称(:user-invalid、showModal、details-name),而当智能体不知道 API 名称时,关键字搜索会错过意图查询。我们添加了一个零依赖项 BM25(懂的都懂)关键字索引,该索引支持复数词干提取,为每个指南的 web-feature-ids 编制了索引,并将 BM25 与语义排名相结合。现在,只有当语义相似度为 <0.30 且 BM25 的关键字命中数为零时,search 才会返回 []。
  2. 10 个精简结果,而不是 5 个详细结果:技能说明已告知代理忽略 similarity 和 tokenCount。通过剔除这些未使用的字段,我们将 10 个紧凑的 {id, description} 结果压缩到与 5 个冗长结果完全相同的约 530 个词元的预算中:
// Before: 5 verbose results (~105 tokens each = ~525 tokens)
{
  "id": "dialog",
  "category": "html",
  "description": "Build accessible modal and non-modal dialogs with native <dialog>, showModal(), and closedby.",
  "featuresUsed": ["dialog", "dialog-closedby"],
  "tokenCount": 780,
  "similarity": 0.49
}
// After: 10 compact results (~53 tokens each = ~530 tokens)
{"id":"dialog","description":"Build accessible modal and non-modal dialogs with native <dialog>, showModal(), and closedby."}

这两项变更共同将我们对保留遥测查询的搜索命中率从 80.6% 提升至 94.7%,衡量标准为 Hit@K(预期指南是否出现在前 K 个结果中)。

第 3 代:预先准备好的嵌入和 7 行 for 循环

即使命中率达到 94.7%,我们的遥测数据也显示出明显的错配:86.7% 的实时代理搜索仅有 2 到 5 个字词(中位数为 3 个字词)。每当客服人员搜索一个 3 词短语时,Node 都会启动 TensorFlow.js,解析一个 22.6 MB 的神经网络,并消耗 770 毫秒的冷启动延迟和 346 MB 的峰值 RAM。

当被问及是否有更轻量的方法来嵌入 3 个字的查询而无需神经网络时,我们的 Antigravity 智能体给出了一堆不熟悉的机器学习蒸馏技术:Model2Vec、Tokenlearn、Matryoshka 维度、Adagrad。(如果您想了解技术细节,请访问我们的搜索质量页面。)

标准本地 RAG 会强制文档和查询都通过用户 CPU 上的同一个小型模型。但我们的约 200 篇指南在 npm 版本发布后不会发生变化,因此没有理由使用 2018 年的小型模型来嵌入它们。相反,embed-guide-chunks.ts 会在发布时使用 gemini-embedding-001 在云端预先烘焙所有引导向量,并将其作为二进制 256 维 Int8 向量(guide-chunks.i8,335 KB gzipped)进行传送。

一个离线、无需 API 密钥的 CLI 如何将用户的实时查询投影到完全相同的 Gemini 向量空间中?由于代理查询几乎总是 2 到 5 个字词,因此我们在运行时不需要转换器。我们只需要一个已知字词 (table) 的向量对照表,以及一种智能求和方法。在运行时,整个 23 MB 的 TensorFlow.js 流水线会折叠成 中的 7 行 for 循环static-embedder.ts:

// Each word's weight and Int8 scale are pre-folded into `scales[i]`
for (const [i, f] of enc) {
  const wt = scales[i] * f;
  const offset = i * 256;
  for (let k = 0; k < 256; k++) {
    out[k] += wt * table[offset + k];
  }
}

为了构建该对照表 (train-query-words.ts),我们使用了包含 29,165 个字词的词汇表(其中 fetchpriority、light-dismiss、popover 和 scrollytelling 等现代网络术语是独立的完整字词),使用 gemini-embedding-001 对其进行了嵌入,并针对 33,000 个真实网络查询对词向量进行了离线调整,以便多词组合能够落在正确的位置(例如,"light" + "dismiss" 指向 <dialog closedby="any">,而不是照明或配色方案)。整个对照表经过 gzip 压缩后,大小为 Int8 (query-words.i8),压缩比为 5.3 MB。

为什么不使用该静态对照表来嵌入指南,并跳过 Cloud Build 步骤?我们首先尝试了这种方法,但在处理完整段落时效果不佳:对 3 个查询词进行求和可以保持清晰,但对整个指南中的数百个词进行平均,会导致每个指南都坍缩到通用英语平均值附近的狭窄锥体中(以至于 "best pizza in new york" 与我们的 dialog 指南的余弦相似度得分为 0.899,高于查询 "dialog" 本身的 0.844!)。将 3 词静态查询查找 (query-words.i8) 与完整模型 Gemini 指南嵌入 (guide-chunks.i8) 配对,可为我们提供云级准确率,同时查询仍可在 30 微秒内离线运行。

第 1 部分中调整了词向量引导,第 2a/2b 部分中对比了非对称 Gemini 引导嵌入与 256 维球体上折叠的 300 词静态平均值。

三代模型的得分情况

在我们的发布里程碑中,Hit@K 从使用整篇文档嵌入时的 78.2% 提升到使用静态 Int8 嵌入器时的 95.4%(相比之下,使用云端 gemini-embedding-001 API 时为 96.9%):

左侧是各里程碑的 Hit@K 折线图,右侧是冷搜索延迟时间与峰值 RAM 和软件包大小的气泡图。

引入静态嵌入器后,这三代嵌入器的性能对比如下:

指标 第 1 代(LanceDB + 原生 ONNX) 第 2 代(TF.js + MiniLM-L6-v2) 第 3 代(Static Int8 × Gemini + BM25) 净变化(第 2 代 → 第 3 代)
原生 C++ 构建步骤 必填 (node-gyp) 无 无 无需安装
npm tarball / 磁盘大小 507 MB(解压后) 23.4 MB (.tgz) 7.1 MB (.tgz) −70% package size
search.mjs 软件包大小 ~6.0 MB 423.6 KB 5.7 KB JS 软件包大小减少 98.6%
冷 CLI search 延迟时间 约 35 毫秒(构建时) 770 毫秒 185 毫秒 冷启动速度提升 4.2 倍
热查询嵌入时间 约 34 毫秒 ~227 毫秒 30 微秒 (0.03 ms) 速度提升约 7,500 倍
RSS 内存峰值 ~400+ MB 346 MB 145 MB RAM 减少 58%
遥测搜索 Hit@K 未衡量 80.6% (v0.0.195) → 94.7%(调优后) 95.4% 比v0.0.195高 14.8 个百分点
夜间智能体评估 Hit@10 未衡量 94.1%(v0.0.195)→ 99.2%(调优后) 99.4% 比v0.0.195高 5.3 个百分点

如果您运行之前那个有问题的 Codex 查询:

npx modern-web-guidance@latest search "confirm destructive action"

dialog 指南会重新显示在列表顶部。

(您可以在我们的搜索质量信息中心内,探索涵盖所有里程碑、查询长度和 K 范围的交互式图表。)

代码不值钱,衡量才是关键

智能体可以在几分钟内编写出 BM25 排序器或词向量蒸馏脚本。生成候选实现不再是瓶颈。了解一项更改是否确实有帮助(或者是否像我们的 0.38 门控一样,悄悄破坏了 9% 的生产搜索)几乎是整个工作的核心。

我们是这样设计该系统的:

  • 确定北极星目标,并投资于衡量框架,而不是任何单一实现。我们花了更多时间构建 pnpm benchmark:rag、整理 5,000 个真实查询,以及根据人工评分校准自动评分,而不是编写任何检索器。
  • 制定一套评分标准,供您和您的智能体用来评判质量,并预计该标准在接触到真实数据后会不断演变。人们称之为“评估优先”,但这只对了一半:你可以在第一天就编写基本的已知答案检查,但在检查实际失败之前,你无法凭空设计出良好的评估标准(Shankar 等人在《谁来验证验证者?》一文中称之为“标准漂移”)。看到智能体触发 1,500 个 token 的 list 后备机制,迫使我们在评估召回率的同时,也对 token 膨胀进行惩罚。
  • 有了可靠的衡量机制,您就可以大胆尝试不同的实现方式,并让记分板来解决架构争议。一旦基准测试能在几秒钟内对 5,000 个查询进行评分,您就不必在 PR 中争论一个 7 行的 Int8 循环是否能击败一个 23 MB 的 Transformer。在一个下午尝试五种方法,并保留效果最好的方法。

当然,所有代码(包括嵌入器、提取器和基准测试工具)都是开源的。