首页
/ codebase-memory-mcp 评测方法论:可复现地量化答案质量、延迟稳定性与 Agent 节省量

codebase-memory-mcp 评测方法论:可复现地量化答案质量、延迟稳定性与 Agent 节省量

2026-09-05 22:18:57作者:谭伦延

本文基于 codebase-memory-mcp(下称 CBM)仓库中的 MEASURING_SAVINGS.md 展开,讲解如何在自己的仓库上独立复现"答案质量、图查询延迟与稳定性、Agent 模型 token / 工具调用节省量"三类指标的完整测量流程。读完并照做之后,你能得到一份带冻结 SHA、隔离会话和原始计数依据的评测记录,而不是把"查询很快"误读成"回答更对"或"更省 token"。

为什么必须把三类问题分开测量

CBM 把代码库索引为持久知识图谱,README 公布的 性能数据(如"五个结构性查询约 3,400 tokens vs 文件逐个探索约 412,000 tokens")与 31 个真实仓库的评估结果(83% 答案质量、token 与工具调用数下降)是发布层面的结论。要在自己的仓库上得出可辩护的结论,必须回答三个彼此独立的问题:

  1. Agent 给出的答案是否更好?
  2. CBM 在该负载下是否快速、稳定?
  3. 基于图(graph)的探索是否比逐文件(file-by-file)探索使用更少的模型 token 或工具调用?

三者不可互相替代:一次快速的图查询不能证明最终答案正确;而 CBM 内部的查询计数器(query_count)只能统计 CBM 侧的工具调用,看不到 Agent 的模型 token 消耗,也看不到非 CBM 的工具调用(如文件列表、文本搜索、文件读取)。因此三类指标必须分开测量、分开报告。README 的 性能章节、语言级基准 BENCHMARK.md(PASS/PARTIAL/FAIL 评分体系)以及更完整的对比方法论 EVALUATION_PLAN.md 提供了评分概念和更广泛的对照实验框架,而 MEASURING_SAVINGS.md 是一份可直接执行的"小配方"。

第一步:冻结实验

文档中所有 shell 示例要求 POSIX 兼容 shell(macOS/Linux,或 Windows 上的 Git Bash),不是原生 PowerShell 语法。所有命令块要在同一个专用 shell 中顺序执行:set -eu 提供 fail-fast 语义,且脚本会先捕获 Git 命令输出再测试,防止内层命令失败被误判为"工作区干净"。

实验前必须记录的信息

在任何条件开始之前,先记录:

  • 仓库 URL 或本地标识,以及精确的 Git commit SHA;
  • 问题集,以及每个问题期望答案的范围(expected scope);
  • CBM 版本、索引模式(index mode)、操作系统与机器配置;
  • Agent 模型/版本、系统提示词、工具说明、上下文上限、每问题预算;
  • 哪个条件先跑,以及任何预热(warm-up)策略;
  • 用于记录 token 与工具调用的客户端或评测 harness。

两个条件必须使用相同的仓库 SHA、问题集、模型、提示词、预算和"干净会话"策略;不要让第二个条件看到第一个条件的答案或工具结果。如果重复实验,重复次数与聚合方法必须在看结果之前定好,并保留每一次运行——包括失败和零结果查询。

为两个条件创建干净的 detached worktree

不能只在"看起来干净"的工作副本上跑实验:冻结实验必须连未跟踪文件(untracked files)一起考虑。因此为两个条件各建一个独立的干净 detached worktree,并把产物目录放在两个 worktree 之外:

set -eu
SOURCE_REPO=/absolute/path/to/source-repository
COMMITISH=main
SHA=$(git -C "$SOURCE_REPO" rev-parse "$COMMITISH^{commit}") || exit 1
MODE=full
RUN_ROOT=$(mktemp -d) || exit 1
GRAPH_REPO="$RUN_ROOT/graph"
BASELINE_REPO="$RUN_ROOT/baseline"
ARTIFACT_ROOT="$RUN_ROOT/artifacts"

git -C "$SOURCE_REPO" worktree add --detach "$GRAPH_REPO" "$SHA" || exit 1
git -C "$SOURCE_REPO" worktree add --detach "$BASELINE_REPO" "$SHA" || exit 1
mkdir "$ARTIFACT_ROOT" || exit 1

GRAPH_SHA=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
BASELINE_SHA=$(git -C "$BASELINE_REPO" rev-parse HEAD) || exit 1
BASELINE_STATUS=$(git -C "$BASELINE_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA" = "$SHA" || exit 1
test -z "$GRAPH_STATUS" || exit 1
test "$BASELINE_SHA" = "$SHA" || exit 1
test -z "$BASELINE_STATUS" || exit 1

要点:

  • status --porcelain=v1 --untracked-files=all 会把未跟踪文件也算作脏,test -z 断言其输出为空才算干净;
  • 在索引之前、以及每个条件开始前,都要再重复一遍对应的 SHA 与干净性断言;
  • 断言失败时停止并新建 detached worktree,不要用删除未知文件的方式把一个复用过的 checkout"洗"成干净状态;
  • 所有运行产物写入 ARTIFACT_ROOT,避免污染任一条件 worktree。

第二步:图条件预检(Preflight)

每次测量 Graph 条件之前,立即重跑该 worktree 的干净性断言,并要求对这个精确 worktree、这个模式做一次成功的全新索引:

set -eu
GRAPH_SHA=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA" = "$SHA" || exit 1
test -z "$GRAPH_STATUS" || exit 1
codebase-memory-mcp cli index_repository \
  --repo-path "$GRAPH_REPO" \
  --mode "$MODE" || exit 1
GRAPH_SHA_AFTER=$(git -C "$GRAPH_REPO" rev-parse HEAD) || exit 1
GRAPH_STATUS_AFTER=$(git -C "$GRAPH_REPO" status --porcelain=v1 --untracked-files=all) || exit 1
test "$GRAPH_SHA_AFTER" = "$SHA" || exit 1
test -z "$GRAPH_STATUS_AFTER" || exit 1

索引前后各做一次 SHA/干净性断言,防止索引过程改变了仓库状态。CLI 模式本身不启动协调守护进程,只持有命令生命周期的准入租约,详见 README 的 CLI Mode 章节。

从响应中提取项目名,并做状态交叉核对

从成功的索引响应中取 PROJECT_NAME。verbose 的 status 调用只适合做 root/当前 HEAD 的交叉核对:

codebase-memory-mcp cli list_projects
codebase-memory-mcp cli index_status --project PROJECT_NAME --verbose

核对 root_path 是否等于 GRAPH_REPOgit.head_sha 是否等于 SHA、Git 上下文是否 detached。这里有一个关键的证据边界:index_status 描述的是项目根目录及其当前文件系统 Git 上下文,它本身不能证明被索引的记录来自该修订版本。真正的新鲜度依据是"紧邻其前的一次成功的全新 index_repository 调用"。

代表性查询验证

最后跑一条或多条代表性查询,其期望符号必须在记录的 SHA 上直接人工验证过

codebase-memory-mcp cli search_graph \
  --project PROJECT_NAME \
  --name-pattern 'KNOWN_SYMBOL_PATTERN' \
  --limit 10

只有当三件事同时成立才能开始测量:全新索引成功、status 交叉核对指向预期的 detached checkout 与当前 SHA、代表性查询返回期望符号。把索引响应、status 输出和查询输出与本次运行产物一起保存。在跑 file-by-file 条件前,同样对 BASELINE_REPO 重跑一遍 SHA 与干净性断言。

测量一:答案质量(Answer Quality)

构造一个固定的真实开发者问题集,覆盖这些维度(凡适用的话):定义发现、关系或调用路径、定向源码检索、架构、横切关注点问题。每个问题都要记录期望范围,或一份独立推导出的 ground truth。

两个隔离条件的工具面:

条件 允许的探索工具
Graph CBM 图工具:search_graphtrace_pathquery_graphget_code_snippetget_architecturesearch_code
File-by-file 基线 仅文件列表、文本搜索、定向文件读取

评分时对照冻结 SHA 上的源码,而不是对照"听起来是否可信"。BENCHMARK.md 中的精简 rubric 用 PASS (1.0)、PARTIAL (0.5)、FAIL (0.0) 计分,并把真正不适用的题目从分母中剔除。要做更细粒度的比较,则遵循 EVALUATION_PLAN.md:正确性、完整性、具体性分开打分,评分者对"哪个条件产出了哪个答案"保持盲评,A/B 顺序随机化,且评分必须附源码证据。

最后一条纪律:质量分数要与效率指标并排放置但分开统计。token 下降只有当答案仍然达到选定的质量门槛时才有意义。

测量二:延迟与稳定性

索引耗时与查询延迟分开记录

索引时间和查询延迟分开计时,并把索引运行分类为 full-source、artifact-assisted 或 incremental。这里有一个仓库特有的判定细节:当本地没有项目数据库时,index_repository 可以先导入兼容的 .codebase-memory/graph.db.zst 工件,再走增量清单路线(该机制即 README 的 Team-Shared Graph Artifact 章节)。计时之前要记录 GRAPH_REPO 中该工件是否存在、保留索引日志,并且必须在日志中找到一条包含 dbsize_mb 的成功 artifact.import 记录,才能把这次运行归类为 artifact-assisted。

这一点可以直接在源码中验证。src/pipeline/artifact.c 中,导入成功后写入的记录正是:

// src/pipeline/artifact.c#L1201-L1202
cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
             itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));

而失败或跳过路径记录的是 skip(如 schema 版本不匹配)或 err。所以文档的告诫是准确的:仅仅尝试过 bootstrap,或一条包含 skip/errartifact.import 记录,都不构成"工件被使用"的证据。

对查询侧:使用同一固定工作负载、同一顺序,提前标记预热调用;保留每次调用的耗时与退出状态,而不是只留一个平均值。报告时要注明机器、操作系统、CBM 版本、仓库 SHA、索引模式、问题集、索引运行类别,以及查询结果是冷(cold)还是热(warm)。

内置诊断:CBM_DIAGNOSTICS

对于 daemon 支撑的运行,在第一个会话启动之前启用诊断。daemon 在启动时捕获环境;如果它已经在运行,必须先关闭所有 daemon 支撑的会话再改设置。完整环境契约见 CONFIGURATION.md 第 4 节

export CBM_DIAGNOSTICS=1

CBM 会在系统临时目录下新建一个随机化、仅属主可读的诊断目录——不要假设或手工构造旧的、可预测的 /tmp 文件名。精确的 snapshottrajectory 路径要从 ${CBM_CACHE_DIR}/logs/cbm-daemon.log(默认缓存目录为 ~/.cache/codebase-memory-mcp)中的 diagnostics.start JSON 记录中发现。该记录通过控制级日志通道发出,即使配置了抑制普通日志的日志级别也会输出——源码中可以确认这一设计意图(src/foundation/diagnostics.c 的注释明确写着"Discovery must survive CBM_LOG_LEVEL suppression"):

// src/foundation/diagnostics.c#L670-L671
cbm_log_control("diagnostics.start", "snapshot", g_diag_path, "trajectory", g_diag_ndjson_path,
                "interval_s", interval);

两个诊断文件的作用:

  • snapshot.json(实时快照):包含 CBM 侧的 query_countquery_errorsquery_total_usquery_avg_usquery_max_us 以及进程资源计数器(见 src/foundation/diagnostics.c#L593-L597 的 JSON 输出);
  • trajectory.ndjson(留存文件):提供资源与查询计数随时间变化的趋势。

这两份数据适合做 CBM 延迟与稳定性分析,但不是 Agent 模型用量或非 CBM 工具调用的记录。README 的 Troubleshooting & Diagnostics 章节解释了文件的完整内容与保留/轮转行为。另外,一个 daemon 可能被多个会话共享:归因计数器到单一工作负载时,应使用"否则处于空闲"的 daemon,记录 before/after 值,或启动专用运行。

标准 Soak 工作负载

从源码 checkout 出发,标准的耐力(endurance)入口是:

scripts/soak-legs.sh build/c/codebase-memory-mcp 10

它依次执行 quick 混合负载与只读的 query-leak 负载,检查存在合法的完成摘要,并写出每次调用的延迟与资源结果。仓库中的 scripts/soak-legs.sh 证实了这一序列的所有权:它把 quickquery-leak 两条 leg 都转发给 scripts/soak-test.sh,并带一个完成摘要守卫——任何一条 leg 即使退出码为零、只要日志中没有 === soak-test: PASSED === 摘要,就按失败处理(见 soak-legs.sh 的 run_leg 守卫)。具体而言:

  • quick leg 写入 soak-results/;query-leak leg(CBM_SOAK_MODE=query-leak --skip-crash-test)写入 soak-results-query-leak/
  • 每个目录包含 latency.csvmetrics.csvsummary.txt
  • 两个 CSV 以如下精确表头开头(可在 scripts/soak-test.sh#L164-L165 中逐字核对):
timestamp,tool,duration_ms,exit_code
timestamp,uptime_s,rss_bytes,heap_committed,fd_count,query_count,query_max_us

不要直接调用 scripts/soak-test.shsoak-legs.sh 拥有发布门禁序列。query-leak leg 的语义值得注意:它完成首次索引后不再索引或变更任何数据,因此任何 RSS 增长都只能来自查询路径的泄漏,而不是 WAL/索引抖动——这是把"稳定性"证据做得干净的一个小技巧。

这些产物只能作为稳定性与 CBM 延迟证据,不能作为答案质量或模型 token 证据。

测量三:Token 与工具调用节省

在同样的冻结输入上跑 Graph 与 file-by-file 两个条件。模型最终输入/输出 token 数与 Agent 总工具调用量只能由 MCP 客户端或评测 harness 捕获——CBM 自己不可能知道。

数据行模式

把每个 run_id 定义为一个成对的实验重复(paired replicate);每个 (run_id, condition) 恰好是一个回答一个问题的隔离客户端会话。对客户端直接测量的每个 usage window 记一行:

run_id,condition,repo_sha,question_id,window,input_tokens,output_tokens,total_tokens,tool_calls,wall_time_ms,answer_artifact,quality_score

行内 tool_calls 的含义是该同一窗口内客户端全部工具调用的计数:包括编排调用、该条件允许的图/文件工具、重试、错误、零结果调用——不要只数 CBM 调用。候选窗口有两个:

  • Answering tokens:围绕答题阶段固定标记之间的输入+输出 token;
  • Full-session tokens:整个隔离会话,包括定位(orientation)、初始探测、死胡同、答案格式化。

full-session 数值最能代表采纳者的总成本;answering 窗口帮助解释差异从何而来。两条红线:不要用 CBM 的 query_count 冒充 tool_calls(它看不见文件搜索、文件读取或其他客户端工具);不要推断缺失的窗口、把会话总量拆分到多个问题、或把一个会话总量复制到多个问题行。

计算口径

对每个 Graph/baseline 对,只比较双方客户端都直接报告过的窗口,每个 (run_id, condition, window) 至多一行;某窗口任一方无法暴露,就省略该窗口或记 N/A 并排除出比较。选定一个窗口 W,只用同一窗口的行计算:

token reduction (%)     = 100 * (baseline tokens - Graph tokens) / baseline tokens
tool-call reduction (%) = 100 * (baseline calls  - Graph calls)  / baseline calls
token ratio              = baseline tokens / Graph tokens
tool-call ratio          = baseline calls  / Graph calls

每个共同测量的窗口都要分别计算并分别标注;绝不混合不同窗口的分子分母。边界规则:baseline token/调用为零时,对应 reduction 百分比记 N/A;Graph token/调用为零时,对应 ratio 记 N/A;不要加伪计数(pseudocount)。发布结果时,比率与百分比必须与原始成对计数一起给出,并同时发布每对的质量结果、运行次数、聚合方法、失败情况和实验控制项。最后一个警告:绝不要把某一个仓库、问题集、模型或机器上的结果变成普适的节省量声明。

复现性清单

原文档以一份检查单收尾,它本身就是文章最重要的自检工具:

  • 记录了仓库身份与精确 SHA;每个条件使用无 tracked/untracked 变更的干净 detached worktree;
  • 保留了 CBM 版本、索引模式、项目名与预检输出;
  • 每次 Graph 运行都紧跟一次对冻结 worktree 和模式的成功全新索引;verbose status 只是 root/当前 HEAD 的交叉核对;
  • 问题、ground truth、提示词、预算、条件顺序全部冻结;
  • Graph 与 file-by-file 运行使用隔离会话与相同控制变量;
  • 质量、CBM 延迟/稳定性、Agent 节省量分开报告;
  • 用量计数直接来自客户端或评测 harness;每个条件运行是一个隔离的问题/会话;共同窗口用独立行;不支持的窗口省略或记 N/A,不做推断;
  • 诊断路径来自 diagnostics.start,而非猜测的临时路径;
  • 保留原始输出、错误、零结果与计算输入;
  • 每个结论都标明其仓库、SHA、问题集、模型、机器与运行次数,不含臆造或外推的基准数字。

小结

这份方法论文档的价值不在于任何单一命令,而在于证据边界:SHA 断言 + 全新索引回答"图来自哪个版本",artifact.import 的成功记录回答"工件是否真的被使用",diagnostics.start 回答"诊断文件在哪",客户端 harness 的 usage window 回答"Agent 实际花了多少"。把这些边界逐一钉死之后,你在自己仓库上得到的质量、延迟与节省量数字,才具备与 README 发布数字对等的可复核性。

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