首页
/ CodeGraph A/B 基准实测:37 个语言 × 规模单元格,量化 codegraph 带来的 76% 文件读取削减

CodeGraph A/B 基准实测:37 个语言 × 规模单元格,量化 codegraph 带来的 76% 文件读取削减

2026-09-05 09:27:20作者:殷蕙予

本文解读 CodeGraph 仓库中 2026-05-24 完成的 with/without A/B 基准矩阵(记录于 docs/benchmarks/codegraph-ab-matrix.md):同一个 headless agent 对 37 个不同语言、不同规模的真实仓库各回答一道"canonical flow"问题,唯一变量是有没有接入 codegraph MCP 服务。读完本文,你将掌握这套 A/B 协议如何做到单变量控制与防污染、每个指标(R/G/Gl/B/Ag、cg-calls、reads saved)的精确读法,以及如何用仓库自带 harness 在自己的仓库上复现同样的对照实验。

基准设计:codegraph 是唯一变量

该实验的核心约束是:同一模型、同一 prompt,唯一变量是 codegraph MCP 服务。具体设置(见原文档头部,由 scripts/agent-eval/run-all.sh 实现):

  • 被测 agent 是 headless 模式的 Claude Opus,--permission-mode bypassPermissions,每道题跑两个 arm:with(接入 codegraph MCP)与 without(空 MCP,仅内置 Read/Grep/Glob/Bash);
  • 每个单元格先 rm -rf .codegraph && codegraph init -i 重新全量索引(针对当前 main HEAD 的 dist/ 构建),确保 with-arm 反映的是 codegraph 0.9.4 已发布的解析器,而不是过期索引;
  • 每个仓库只问一道 canonical flow 问题(例如"请求如何从路由到达 handler、再到达 service"),两个 arm 问的是同一道题;
  • 日期 2026-05-24,分支 main,版本 codegraph 0.9.4。

run-all.sh 的源码可以确认几个关键实现细节,它们保证了"唯一变量"承诺的可信度:

  • with-arm 写入的 MCP 配置只有 codegraph 一个服务:{"command":"$CG_BIN","args":["serve","--mcp","--path","$REPO"]};without-arm 写入的是 {"mcpServers":{}},两个 arm 都用 --strict-mcp-config 启动 claude -p,内置 Read/Grep/Bash 在两边都可用——所以对比的是"图检索"与"文件检索",而不是"有工具"对"没工具";
  • 脚本显式 export CODEGRAPH_NO_PROMPT_HOOK=1,注释写明原因:环境中的 CodeGraph prompt-hook 会向每个 prompt 注入上下文,会污染 without-arm(白得结构上下文)、同时重复计入 with-arm;
  • 更隐蔽的漏洞是 CLI 泄漏:without-arm 虽然没有 MCP,但它有 Bash,而目标仓库里就放着 .codegraph/ 索引、PATH 上就有 codegraph 二进制。据 scripts/agent-eval/no-cli-shim.sh 头部注释,一次 7 仓库遍历中 14/15 的 without-arm 跑法都通过 Bash 调用了 codegraph explore——那测的其实是"codegraph-over-CLI",不是"没有 codegraph"。因此 harness 用两层封锁:一层是替换 PATH 目录(对 codegraph 所在目录做符号链接镜像、唯独去掉 codegraph,保持 PATH 顺序与优先级),另一层是 PreToolUse hook 拒绝任何命令位置的 CLI 调用(因为曾有 agent 被拒后用 find / 找到二进制并以绝对路径执行)。README 中 2026-08 的复测也记载了未封锁 harness 的污染率:28 次 without 跑法中 26 次通过 Bash 摸到了 CLI。

关键结论(Headline)

37 个单元格合计,codegraph 把总文件读取从 159 次降到 38 次——减少 76%,且没有任何一个单元格里读取数反而变多(0 次回归)。 机制是:几次亚毫秒级的 codegraph 调用替代了"读取+grep"式的探索。

成本大致持平,且在本实验中 with-arm 略高(37 单元格求和:with $15.4 vs without $13.8)。原文档对此的解释值得注意:在这些短的单流问题上,without-arm 通常 10 次调用内就能解决、从不膨胀,还没到 codegraph 成本节省能复利累积的区间;而 with-arm 要支付固定的 MCP 开销(工具定义常驻上下文 + 工具加载),短任务摊不薄。真正的收益是 工具调用更少(189 vs 321,−41%)+ 墙钟时间更短(均值 38s vs 48s)——这才是设计目标。在更难的多轮调查中,随着 without-arm 累积的上下文膨胀,成本会翻转为净节省(见 docs/benchmarks/call-sequence-analysis.md)。

差距随仓库规模与流程复杂度扩大:在 M/L 仓库上,without-arm 经常陷入 thrash——大量 grep/glob、shell find/grep(Bash),偶尔还派生 sub-agent;而 with-arm 用 2–8 次调用就回答完毕。在极小仓库(几个文件)上,两 arm 打平甚至 codegraph 略慢(MCP/索引开销在"整个流程只占一两个文件"时不划算)——但读取数依然下降。

如何阅读结果矩阵

原文档给出了一份阅读约定,复现时对照表格必须遵守:

  • R / G / Gl / B / Ag = Read / Grep / Glob / Bash / sub-agent(Task)工具调用次数;
  • cg-calls = with-arm 中的 codegraph MCP 调用次数(它换掉的是 reads/greps);
  • dur = 墙钟秒数;files = 被索引文件数(规模代理指标);
  • reads saved = without-reads − with-reads;
  • 每个 arm 只跑一次(snapshot):逐次波动是真实存在的,±1–2 次读取、±10s 应视为噪声,看跨单元格的模式;其中部分流程的 2-runs/arm 数字记录在 docs/design/dynamic-dispatch-coverage-playbook.md §7。

完整结果矩阵(37 单元格)

以下表格完整继承自原文档:

Language Size Repo files with R/G cg-calls dur without R/G dur reads saved
C L c-redis 884 0R / 2G 4 42s 5R / 6G 51s 5
C# S aspnet-realworld 78 0R / 0G 2 27s 5R / 3G / 2Gl 54s 5
C# M aspnet-eshop 262 0R / 1G 5 39s 9R / 2G / 5Gl 58s 9
C# L aspnet-jellyfin 2081 3R / 0G 4 51s 17R / 1G / 2Gl / 17B / 1Ag 212s 14
C++ M cpp-leveldb 134 0R / 0G 3 26s 4R / 2G 37s 4
Dart S flutter_module_books 6 1R / 0G 2 24s 2R / 0G / 1Gl 29s 1
Dart M compass_app 212 2R / 0G / 1Gl 2 42s 3R / 0G / 2Gl 30s 1
Go S gin-realworld 21 0R / 0G 5 35s 4R / 3G / 1Gl 57s 4
Go M gin-vueadmin 625 1R / 1G 4 47s 3R / 3G / 1Gl 44s 2
Go L gin-gitness 4438 4R / 3G 4 64s 8R / 7G / 2Gl 57s 4
Java S spring-realworld 117 2R / 0G 3 35s 8R / 1G / 5B 57s 6
Java M spring-mall 536 1R / 0G 5 39s 2R / 4G / 2Gl 49s 1
Java L spring-halo 2444 1R / 2G 8 60s 4R / 1G / 6B 52s 3
Kotlin S kotlin-petclinic 43 0R / 0G 2 37s 3R / 0G / 1Gl 23s 3
Kotlin M Jetcaster 166 1R / 0G 3 36s 1R / 0G / 2Gl 46s 0
Lua S lualine.nvim 123 1R / 1G 4 48s 4R / 0G / 2Gl 49s 3
Lua M telescope.nvim 84 0R / 0G 1 15s 1R / 0G / 1Gl 20s 1
Luau S Knit 11 0R / 0G 2 30s 5R / 0G / 2Gl 37s 5
PHP S laravel-realworld 114 1R / 0G 6 40s 5R / 1G / 3Gl 39s 4
PHP M laravel-firefly 2047 2R / 1G 4 47s 4R / 5G / 3Gl 75s 2
PHP L laravel-bookstack 2160 1R / 2G 2 41s 2R / 4G / 1Gl 50s 1
Python S django-realworld 44 2R / 1G 2 47s 9R / 0G / 1B 38s 7
Python M django-wagtail 1672 2R / 0G 4 45s 8R / 3G / 3Gl / 1B 66s 6
Python L django-saleor 4429 2R / 2G 4 52s 4R / 6G / 1Gl 64s 2
Ruby S rails-realworld 59 0R / 0G 2 30s 3R / 0G / 2B 33s 3
Ruby M rails-spree 2905 2R / 3G / 1Gl 5 43s 3R / 3G / 2Gl / 1B 55s 1
Ruby L rails-forem 4658 3R / 1G 3 43s 4R / 2G / 3Gl 48s 1
Rust S rust-axum-realworld 13 0R / 0G 2 21s 3R / 0G / 1Gl 38s 3
Rust M rust-actix-examples 176 0R / 1G 3 42s 3R / 0G / 3B 36s 3
Rust L rust-cratesio 1053 1R / 0G 3 22s 1R / 2G 18s 0
Scala S computer-database 10 1R / 0G 2 27s 3R / 0G / 1Gl 25s 2
Swift S vapor-template 14 0R / 0G 2 21s 2R / 0G / 2Gl 22s 2
Swift M vapor-steampress 100 0R / 0G 5 49s 3R / 1G / 2Gl 39s 3
Swift L vapor-spi 542 1R / 1G 4 27s 2R / 5G 34s 1
TypeScript/JS S express-realworld 39 1R / 0G 1 25s 2R / 2G 19s 1
TypeScript/JS M excalidraw 643 1R / 0G 3 55s 7R / 5G / 3Gl / 1B 87s 6
TypeScript/JS L nest-immich 2759 1R / 0G 7 50s 3R / 0G / 1Gl 44s 2

37 单元格总计: with codegraph 38 reads / 22 greps,without 159 reads / 72 greps——读取 −76%,grep −约69%。codegraph 没有任何一个单元格让读取变多;without-arm 额外跑了 52 次 glob + 37 次 shell find/grep(Bash)+ 1 个 sub-agent,而 with-arm 0 Bash、0 sub-agent(74 次 agent 运行,总花费 $29.18)。

观察:收益模式与平局区

原文档的五条观察,归纳出 codegraph 收益的边界条件:

  1. 最大收益出现在 M/L 级、有真实 route→handler→service 流程的后端:aspnet-jellyfin(3R / 51s vs 17R + 17 Bash + 1 个派生 sub-agent / 212s,全矩阵最戏剧化的一格)、aspnet-eshop(0R vs 9R)、django-realworld(2R vs 9R)、spring-realworld(2R vs 8R + 5 Bash)、django-wagtail(2R vs 8R)、excalidraw(1R / 55s vs 7R / 87s)、Luau Knit(0R vs 5R)、aspnet-realworld(0R vs 5R)、c-redis(0R vs 5R)。
  2. 大仓库 + 无 codegraph = thrash:agent 回退到 shell find/grep(全矩阵共 37 次 Bash),jellyfin 甚至派生了 sub-agent——这正是 codegraph 要消灭的行为。with-arm 对这些问题用 2–8 次 codegraph 调用回答,且全局 0 Bash、0 sub-agent
  3. 平局区 = 极小仓库(Jetcaster 1R/1R、cratesio 1R/1R、express 1R/2R、vapor-template 0R/2R):整个流程只占 1–2 个文件,读取本来就便宜;codegraph 在读取上打平,有时还慢几秒(MCP + 索引开销——Kotlin petclinic 37s vs 23s、cratesio 22s vs 18s)。这与"codegraph 的价值随仓库规模增长"的设计判断一致。
  4. 大仓库上墙钟时间跟随读取数(jellyfin 51s vs 212s、excalidraw 55s vs 87s、aspnet-eshop 39s vs 58s、django-wagtail 45s vs 66s),小仓库上则是噪声;均值墙钟 with 38s vs without 48s。
  5. 部分 with 单元格仍读 2–4 个文件(jellyfin、gitness、forem、saleor、django)——原文档把这定义为"已记录的 frontier"(匿名 handler、深层 service 链、动态查找器);codegraph 把 agent 送到正确的文件,它再读一个文件确认细节。

覆盖说明

README 列出的全部 14 个框架与所有流程相关语言均已验证(见 playbook)。规模按索引文件数划分;语料中部分语言没有干净的第三档规模(Dart/Kotlin 只有 S/M,Scala/Luau 只有 S,C 只有 L,C++ 只有 M)——这些单元格被省略而不是伪造

harness 如何统计:parse-run.mjs 的度量口径

矩阵里每个数字不是人工数的,而是从 claude -p 的 stream-json 日志解析出来的。scripts/agent-eval/parse-run.mjsparseSession 把一次会话(多轮会话的多个 segment 文件按序拼接)统计为:

  • 工具家族计数:codegraph / read(Read/NotebookRead)/ search(Grep/Glob)/ bash(Bash/BashOutput)/ other,表格里的 R/G/Gl/B/Ag 即来自 counts.ReadGrepGlobBashTask 调用;grep 列是 Grep + Glob 之和,durcost 分别累加各 segment 的 duration_mstotal_cost_usd;
  • CLI 污染计数器:日志里任何命令位置的 codegraph 调用(注意区分"调用"与"提及"——grep codegraph src/ls .codegraphwhich codegraph 都放行)会被记为一次 attempt;只有真正返回了输出的尝试才被计为 contaminated,因为被 hook 拒绝的尝试没有任何输出进入上下文窗口。这层"计数器"是 PATH 替换与 PreToolUse hook 之外的第三道防线——防堵措施会静默失败,计数器不会;
  • 除此之外,该解析器还输出三项反馈指标:residual context occupancy(会话结束时各工具家族仍占据的上下文窗口)、explore sufficiency(explore 调用之后 agent 的下一个动作)、allocation efficiency(答案实际引用的字节占 explore 返回字节的比例)。前两项指标的定义见 docs/benchmarks/agent-eval-feedback-metrics.md 与 run-all.sh 头部注释。

另外值得注意的一个测量陷阱(记录在 docs/benchmarks/call-sequence-analysis.md 中,2026-08-05 已修入 harness):当前版本 Claude Code 的 result.usage 只报告最后一轮而非累计值,直接读取会系统性低估轮数更多的那一 arm——而那永远是 without-arm。正确做法是按 assistant 轮逐次累加 usage(并按 message.id 去重,因为每个 content block 都会带同一份 usage 的事件)。这一 bug 曾把真实的 62% token 节省测成 19%,甚至在两个仓库上虚构出 token 回归。

复现方法

原文档给出的复现路径:

# 标准 harness:with = codegraph-only MCP,without = 空 MCP,指标从 stream-json 日志解析
scripts/agent-eval/run-all.sh <repo> "<question>" headless

参数与约定(结合 run-all.sh 源码补充):

  • 第一个参数是已完成索引的仓库路径(脚本会检查 .codegraph/ 存在,否则要求先索引);第二个参数是问题,多轮问题用 || 分隔,后续轮次通过 --resume 接续同一会话;
  • 环境变量:CG_BIN(codegraph 二进制,默认取 PATH 上的 codegraph)、AGENT_EVAL_OUT(输出目录,默认 /tmp/agent-eval)、MODEL / EFFORT(该 A/B 的常设政策是 sonnet / high,run-all.sh 注释明确"不要调高");CG_ARMS=with|without|both 可只补跑其中一个 arm;
  • 该矩阵本身的一次性驱动脚本(lang|size|repo|question 矩阵,每格先 rm -rf .codegraph && codegraph init -i 再跑两个 arm)当时放在仓库外的 /tmp/ab-matrix/:run.sh(矩阵驱动)、parse-matrix.mjs(cells → 上表)、compare.mjs(新旧对比 + 汇总)——注意这些文件不在仓库内,仓库内可复用的是 run-all.sh + parse-run.mjs 这条链路;
  • 前提:先从目标 commit 构建 dist/,让 MCP 服务加载的是被测代码(PATH 上的 codegraph 通过 npm link 指向开发 dist/)。

后续证据与边界

这份 2026-05 矩阵之后,仓库里有两笔直接相关的后续记录,可以帮你理解结论的演化:

  1. 为什么读得少不等于等比例快:docs/benchmarks/call-sequence-analysis.md 对同一批 37 单元格日志做了再挖掘(未重跑):总轮次 with 283 vs without 375(−25%),但墙钟只快约 16%——差额在于 with-arm 每轮携带约 18K 的 explore 载荷,抬高了每轮延迟。该文档后续还通过 ablation(arm A–I)验证了 trace/explore/context 三工具的取舍,是理解本矩阵中 cg-calls 列背后工具面演进的一手材料;
  2. 同一协议的后续复测:README 的 Benchmark Results 一节(2026-08-05,Claude Opus 4.8,7 个仓库 × 每 arm 4 次取中位数)在当前构建上复现了"35% cost / 59% tokens / 49% time / 70% tool calls"量级的收益,并确认了双 arm 封锁 CLI 后 0/28 污染;其口径差异(4 次取中位数 vs 本矩阵的单次 snapshot、7 个精选仓库 vs 37 格语言 × 规模矩阵)恰好说明为什么两者数字不同但不矛盾。

适用前提与限制:本文数字来自 2026-05-24 的单次 snapshot 运行(codegraph 0.9.4),单格波动可达 ±1–2 次读取与 ±10s,解读时应看跨格模式而非单格绝对值;成本结论("大致持平、with 略高")是针对短单流问题——原文档明确指出多轮场景下成本翻转为净节省,两个场景不应互相套用对方的成本结论。

登录后查看全文
热门项目推荐
相关项目推荐