两个数字让我回到了这个基准测试。codebase-memory-mcp 是一个基于 tree-sitter、通过 MCP 提供的代码图,它的 README 承诺能少用 99% 的 token。我自己之前的一次运行显示,我给它写的一层薄包装 cbm-lean,让编程智能体在 haiku 上比普通 grep 便宜 26%,在 sonnet 上便宜 18%。第一个数字我始终没能复现,第二个则来自我实验设置里的一个缺陷。这是在公开仓库上、修正了那个缺陷之后的重新运行。
设置
每个问题都在三种方案下运行:
- cbm-lean,加上系统提示词里的一条路由规则:结构性问题交给图,精确字符串交给 Grep;
- 原始的 codebase-memory-mcp 服务器,加上同一条规则;
- 只有 Read、Grep 和 Glob。
每个仓库有十二个问题。六个是结构性的:谁调用了某个函数,它又调用了什么,从一个 HTTP 端点到数据库写入的调用链,模块布局。另外六个是精确性的:一个配置值,某个环境变量在哪里被读取,一条错误信息,某个类定义在哪一行。每个问题都列出了正确答案必须包含的子串,每一条我都亲手对照代码核对过。
仓库包括三个小仓库(FastAPI 全栈模板、ltx-2-mlx 和 avoid-ai-writing,图节点数 1.6k 到 5k),以及 dub,一个有 25k 节点的 Next.js monorepo。Haiku 4.5 在所有仓库上运行,sonnet 5 在 dub 上运行:共 252 次运行,按 API 价格计 $14.69。
成本以 tokenEquiv 衡量:输入 token,加 1.25 × 缓存写入,加 0.1 × 缓存读取。一旦开启提示词缓存,原始的计费输入就会误导人,因为碰巧在热缓存上运行的方案,会因为与方案本身无关的原因显得便宜。
结果
每次运行的 tokenEquiv 中位数,括号里是正确答案数:
| 场景 | 运行次数 | cbm-lean | 原始图 | grep |
|---|---|---|---|---|
| 小仓库,haiku 4.5 | 108 | 30,321 (28/36) | 43,846 (32/36) | 31,159 (33/36) |
| dub,haiku 4.5 | 72 | 18,791 (22/24) | 20,687 (22/24) | 14,955 (20/24) |
| dub,sonnet 5 | 72 | 17,227 (22/24) | 23,085 (22/24) | 13,348 (22/24) |
有三个结果站得住:
- 按 token 算,图从来没有赢过。与 grep 相比,在小仓库上(p = 0.13)和用 haiku 跑 dub 时(p = 0.18)都没有可测量的差异;用 sonnet 跑 dub 时,grep 在 12 个问题中的 11 个上更便宜(p = 0.034),而准确率相同,每个方案都是 24 个中对 22 个。
- 包装层在每种场景下都赢过原始服务器(p ≤ 0.021)。原始服务器每条搜索命中返回约 1.3 KB,其中大部分是标识符转储和哈希向量,而 cbm-lean 把一条命中精简到约 250 字节。
- 图唯一明确的胜利,是小模型在大仓库上的准确率。haiku 回答 dub 上的结构性问题,用图时 12 次中对 12 次,用 grep 时 12 次中对 8 次,token 中位数为 23.5k 对 16.0k。
p 值来自对每个问题中位数做的 Wilcoxon 符号秩检验。
每种方案错在哪里
- 来自图的行号。被问到一条错误信息所在的行时,图方案从图的输出里读出答案,给出 login.py:33;而 raise 在第 34 行,33 是它上面的 if。对于一个图本身记录在第 15 到 88 行的类,它报告了 config.py:74;对于一个定义在第 59 行的类,它报告了 client.ts:129。路由规则告诉它精确问题要用 grep,而 haiku 并不总是照做。
- 重名。FastAPI 模板里有两个都叫 create_user 的处理函数。被问到管理员端点时,两种图方案在 6 次运行中有 5 次追踪到了不需要认证的那个。grep 每次都找到了藏在超级用户检查后面的那个。
- 过早停止。在 dub 上,用 grep 的 haiku 有两次漏掉了调用链末端真正的数据库写入。
- 我自己的评分。每一次 sonnet 运行,在每种方案下,都在 dub 的架构答案里漏掉了 packages/tinybird。那个文件夹里放的是 Tinybird 数据文件,没有 package.json,所以可以说模型是对的,是我的检查错了。sonnet 的六次失误全都出在这一个问题上,去掉它,任何比较都不会改变。
挂载了图,不等于用上了图
最有用的结果来自试点,而不是正式运行。Claude Code 2.1.283 默认把 MCP 工具的 schema 延后到一个 ToolSearch 步骤之后:模型能看到工具名,但必须先获取 schema 才能调用任何东西。无头模式下的 haiku 没有这么做。在六次试点运行中,无论有没有路由规则,图一次都没有被调用。在那十二个小仓库问题上:
| 配置 | 调用了图 | 正确 | tokenEquiv 中位数 |
|---|---|---|---|
| 默认加载,有规则 | 12 个中 2 个 | 12 个中 11 个 | 41,008 |
| 已加载 schema,无规则 | 12 个中 4 个 | 12 个中 9 个 | 62,008 |
| 已加载 schema,有规则 | 36 个中 22 个 | 36 个中 28 个 | 30,321 |
加载了 schema 之后(ENABLE_TOOL_SEARCH=false),同一个关于调用方的问题在三轮中经历了两次图调用。挂载了图却没有规则,是所有配置里最贵的:每个会话都要为 schema 付费,而模型大多让它们闲着。
如果你发布了一个 MCP 服务器,并且用默认设置测试它,先确认模型到底会不会调用它。
为什么之前的数字是错的
第一次基准测试是在我的项目文件夹里跑的。那里的 CLAUDE.md 告诉智能体结构性问题要用图,而 Claude Code 会加载上级目录里的 CLAUDE.md 文件,所以这条指令传到了每一种方案,包括没有图的那一种。对照组拿到了实验组的指令,也就是说,这个比较测到的规则和工具一样多。
重新运行时,仓库被放在任何上方有 CLAUDE.md 的目录树之外,跳过用户设置和钩子(--setting-sources project),并且只把规则给有图的方案。其余每一项对照措施,都各自纠正过之前某一轮里的一个错误结论。Bash 和 Agent 在所有方案中都被禁用,因为限定范围的 Bash 授权匹配不上管道命令,而子智能体会隐藏回合,同时照样消耗 token。方案的顺序在每轮重复之间轮换,所以没有哪个方案总是在热缓存上运行。调用了不属于自己方案的工具的运行会被丢弃。答案也要评分,因为在错误答案上省下的 token 不算节省。
我会对正在把图接入智能体的团队说什么
- 在节点数不超过 25k 的仓库上配合 Claude 模型使用代码图,不要指望能省 token。先用你们自己的问题测一测。
- 如果无论如何都要挂载,就精简它的输出,加载它的 schema,并给智能体一条路由规则。缺了后两样,它在每个提示词里都是累赘。
- 不要轻信模型从图结果里读出的行号,用 grep 核对。
- 当小模型在大型代码库上回答结构性问题——架构和调用链——时,图才物有所值。
局限
两个模型,四个节点数不超过 25k 的仓库,两到三次重复,每个仓库十二个问题。没有 Opus,没有长时间的交互式会话,没有超过 25k 节点的仓库。之所以报告中位数,是因为同一问题的重复运行结果差异很大。
复现
所有东西都在 code-graph-vs-grep 里:带固定提交的问题、基准测试驱动程序、包含每个答案的原始运行记录,以及 cbm-lean 本身。
bash bench/setup.sh
node bench/run.mjs --reps 3
python3 bench/stats.py results/*.json