CodeGraph Explore Sufficiency:把 Agent 的下一步动作变成"响应是否够用"的免费真值指标
本文介绍 CodeGraph 评测体系中 Explore sufficiency(CG-8) 这一反馈指标的完整设计:它如何从 Claude Code 每次运行都会写下的 stream-json 转录中,读出 codegraph_explore 响应之后 Agent 实际执行的下一个动作,并将其归入五个各有修复指向的桶(再次 explore、读了我们返回过的文件、读了没返回的文件、Grep/Glob、直接推进/作答)。读完后,你能复制仓库中现成的评测脚本(run-all.sh / parse-run.mjs / parse-session.mjs)对任意一次探索响应做充足度归因,理解八条"保真规则"背后对应的源码实现,并知道该指标在什么场景下不能下结论。
Explore sufficiency 是 CodeGraph agent-eval 框架在每次运行中报告的三个反馈指标之一,另两个是 Residual context occupancy(CG-7)与 Allocation efficiency(CG-9);三者各自回答不同问题,入口文档见 agent-eval-feedback-metrics.md。
它测量什么:Agent 的下一步是免费的地面真值
指标定义(原文见 explore-sufficiency.md):判断一次 codegraph_explore 响应是否 enough——依据是 Agent 接下来做了什么,而这个信息此前被评测框架直接丢弃。
关键点在于:Agent 每次调用后都会用行为"投票"——它去读一个文件、再次 explore,或者直接作答。这个下一步动作是不需要额外标注的免费真值(free ground truth),且它能自然分裂成指向不同修复手段的桶:
| 下一步动作 | 桶(bucket) | 含义 |
|---|---|---|
| 再次 codegraph 调用 | explore again |
insufficient——响应没有回答问题 |
Read 一个我们返回过的文件 |
Read a file we returned |
allocation 问题:找对了文件,但拿到的字节不对 |
Read 一个我们没返回的文件 |
Read a file we did not return |
recall 问题:该文件从未出现在响应里 |
Grep / Glob |
Grep/Glob |
recall 问题,信号较弱——Agent 还在找文件 |
Edit、构建、最终作答 |
moved on / answered |
sufficient(够用) |
在源码中,这五个桶是一个按"从差到好"排列的常量数组,标签同时充当汇总输出里的行名(见 parse-run.mjs):
export const SUFFICIENCY = [
['explore_again', 'explore again', 'insufficient: did not answer'],
['read_returned', 'Read a file we returned', 'allocation: right file, wrong bytes'],
['read_missed', 'Read a file we did not return', 'recall: file never surfaced'],
['search', 'Grep/Glob', 'recall (weak): still hunting for the file'],
['sufficient', 'moved on / answered', 'sufficient'],
];
桶与修复手段的映射是刻意的:Read a file we returned 直接对应 allocation efficiency(CG-9)会偏低的同一批运行——响应截错了窗口;Read a file we did not return / Grep/Glob 则是 recall 缺口,allocation 指标对此完全失明(信封里本来就缺东西);explore again 按构造就是模糊的,它只说明"没答上来",而后续那条 explore 的 query 通常能揭示到底是 allocation 还是 recall;moved on / answered 说明够用,但不等于答对了。
仅评测侧使用(harness-only)。 产品侧不输出任何相关内容,也没有任何数据离开本机(2026-08-03 的决定);该指标完全从框架本来就会写的转录日志中解析而来。
如何运行
每次运行都会打印该指标块——无论是 run-all.sh 还是任何调用 parse-run.mjs 的入口。两种方式(命令继承自原文档):
scripts/agent-eval/run-all.sh /tmp/codegraph-corpus/express \
"How does res.send decide the Content-Type and ETag?"
# 或者对你已有的日志文件:
node scripts/agent-eval/parse-run.mjs /tmp/agent-eval/run-headless-with.jsonl
典型输出:
Explore sufficiency — what the agent did NEXT (2 answered calls):
1 50% explore again insufficient: did not answer
1 50% Read a file we returned allocation: right file, wrong bytes
0 0% Read a file we did not return recall: file never surfaced
0 0% Grep/Glob recall (weak): still hunting for the file
0 0% moved on / answered sufficient
1. "res.send Content-Type ETag generation" [3 files] → codegraph_explore
2. "response.js res.send function body" [3 files] → Read response.js
交互式会话则用 parse-session.mjs <project-dir> 得到同样的指标块(见 parse-session.mjs,它读取 ~/.claude/projects/<escaped-cwd>/ 下最新的 session 日志及其子代理日志)。
两个实践要点:
- 逐次调用的行与汇总计数同等重要。 计数块只告诉你比例,而逐行明细(
1. "res.send Content-Type ETag generation" [3 files] → codegraph_explore)会点名"哪条 query 没答上来"以及"Agent 转而去读了哪个文件",这通常已经足够用 probe-explore.mjs 复现这次 miss。 - harness 的运行约定:
run-all.sh会屏蔽 codegraph CLI(双层防护,见 no-cli-shim.sh),把 A/B 的唯一变量限定为 MCP server;模型策略是双臂统一--model sonnet --effort high,多轮问题用||分隔(见 run-all.sh 头部注释)。
保持指标诚实的八条规则
这是该指标设计的核心——每一条都对应 classifySufficiency(parse-run.mjs)里一段具体的判定逻辑,也是自测用例覆盖的对象。
1. 只有"更晚消息"中的动作才算反应
在同一条 assistant 消息里与 explore 一起发出的 Read,发出时响应还不存在,因此不构成对该响应的裁决。这些调用会被跳过,并单独计为 concurrent(在指标块的 note 行显示)。源码里对应 parse-run.mjs 中 b.msgId === a.msgId 的分支:同消息内的文件访问只累加 concurrent 计数,不覆盖 reaction。
2. 簿记工具会被跨过
ToolSearch(拉取延迟加载的工具 schema)和 TodoWrite 对响应质量没有任何表态,分类器会越过它们,取其后第一个实质调用作为裁决。实现上就是 TRANSPARENT_TOOLS = new Set(['ToolSearch', 'TodoWrite'])(parse-run.mjs)。
3. 子代理是独立的线程
Claude Code 会把子代理的工具调用交错写入同一个 stream-json 输出流,并以 parent_tool_use_id 打标——这一点已在真实 excalidraw 运行上验证过(被委派搜索的 greps 落在父线程自身调用之间)。因此反应(reaction)只在线程内匹配;否则子代理的第一个 grep 会被记成父线程对一次它根本没见过的 explore 的裁决。classifySufficiency 用 threads Map 按 ev.parent_tool_use_id ?? 'main' 分线程处理(parse-run.mjs)。交互式会话中子代理存在于独立文件里:每个 agent-*.meta.json 携带派生它的 Task 的 toolUseId,parse-session.mjs 用它把各线程拼回同一个事件流再喂给同一个分类器(parse-session.mjs)。
4. 委派按子代理的第一个实质动作判定
Agent / Task 委派会被展开为"子代理的第一个实质调用",显示形如 Agent → Bash search。把委派本身计成 "moved on" 是该指标唯一不能犯的错向:在下方的 excalidraw 运行中,那样会把子代理正在为找文件而 grep 的一次运行报告成 33% sufficient。嵌套委派不做递归展开——子代理若只再派生子代理,则保持 "moved on"。实现见 throughDelegation(parse-run.mjs)。
5. Shell 里的文件访问算数
两个 A/B 臂都有 Bash,Agent 用 sed -n '100,200p' lib/x.js 和用 Read 一样自然;grep/rg/find/ls 计为搜索。只统计 Read 和 Grep 工具,会把这类 explore 误判为 sufficient。源码用两条正则实现(parse-run.mjs):
const BASH_READ_RE = /(?:^|[;&|]|\$\(|`)\s*(?:sudo\s+)?(?:cat|bat|head|tail|less|more|nl|sed|awk)\s+([^\n|;&]*)/;
const BASH_SEARCH_RE = /(?:^|[;&|]|\$\(|`)\s*(?:sudo\s+)?(?:grep|egrep|fgrep|rg|ag|ack|find|fd|ls|tree)\b/;
bashIntent 会先剔除标志位与纯数字参数(sed -n '100,200p' lib/x.js → 取 lib/x.js),再取最后一个"路径形态"的 token。heredoc 与重定向是写而不是读——含 << 或 > file 的命令不会被误读成 Read,explore → Bash cat > /tmp/note.md <<EOF 因此计为 sufficient。
6. "Returned"意味着我们交付了源码
只有响应里带该文件源码段落(即 ** 前缀标记,对应 tools.ts 中定义、全 explore 输出中唯一的 FILE_SECTION_PREFIX = '**'`)才算 returned:
export function exploreReturnedFiles(text) {
return [...String(text ?? '').matchAll(/^\*\*`([^`]+)`\*\*/gm)].map((m) => m[1]);
}
而响应只是提到的文件——流程步骤、blast-radius 条目里的路径——仍然是 recall miss,并标记 (named, not returned):指了但不交付是另一种失败。响应中所有路径形态 token 由 PATH_TOKEN_RE 收集作为 "mentioned" 集合(parse-run.mjs)。
7. 更早的 explore 交付过的文件仍算 returned
重读一个之前 explore 已交付的文件,在哪里交付的都算 allocation miss——把这类重读记成 recall 会把修复指向管线的错误一端。逐行输出会说明来源:Read utils.js (returned by an earlier explore)。实现上,每个线程维护一个 earlier 数组累积先前 explore 交付的文件,readOf 先查本次响应、再查 earlier(parse-run.mjs)。
8. 出错的调用只计数、不归桶
返回 isError 或始终未返回的 explore 没有可裁决的响应体,单独计入 errors 并在 note 行报告(N errored/unanswered calls (not bucketed)),不进入五个桶。
验证:真实转录手检 + 全量 A/B 日志扫描 + selftest
指标的验证路径(继承自原文档 Validation 节)分三层。
1. 真实转录全量扫描。 先对照真实转录手检,再扫过本机全部 A/B 日志:76 次单问题运行(176 次调用),加上 7-repo README 语料中 14 个多轮 with-arm 会话(62 次调用),0 崩溃。该语料覆盖了每个桶:
explore again 29 (47%) · Read a file we returned 7 (11%) ·
Read a file we did not return 1 (2%) · Grep/Glob 14 (23%) · moved on 11 (18%)
原文档明确提醒:把这段数字读作基线而非裁决——这些是三轮会话、且问的是难 flow 问题;"explored again" 里包含在预算为 2–3 次调用的仓库上合法的第二调。
2. 快照警告:那段数字不再可复现。 bench-readme.sh(见 bench-readme.sh)每次 campaign 都会覆盖 /tmp/ab-readme,因此那里现存的日志不是上面扫描过的日志。把 2026-08-05 时点磁盘上的 14 个 with-arm 会话合并得到的是 explore again 47 (76%) · Read a file we returned 1 (2%) · Read a file we did not return 1 (2%) · Grep/Glob 0 (0%) · moved on 13 (21%)(同为 62 次调用)——已与 CG-8 时期的分类器和当前分类器双向核对,结果完全一致,说明分类器在其上未漂移。CG-13 重新建立了 7-repo 基线(单次 campaign、三项指标全部接通):对比应以 CG-13 为基准;若想让某个 campaign 的分布保持可复现,请把日志归档到别处。
3. 手检出的典型 miss 案例(这些是"把 hunch 变成桶"的实例):
cg22/ab-express/run-baseline-1——allocation 桶的手工复现:序列为 explore "res.send Content-Type ETag generation" → explore "response.js res.send function body" →Read /…/t-base/lib/response.js。第二次 explore 明明返回了lib/response.js,Agent 还是去读了它 → 先explore again,再Read a file we returned。这正是 #1500 allocation 缺陷(583 字节 stub 文件)以"桶"而非"感觉"的形式现身。同一问题的新构建臂:一次 explore,moved on / answered,100% sufficient。cg15/ab-express/run-new-2——同一裁决的长运行版本:四次 explore;第四次返回了lib/utils.js,Agent 随后带着offset: 195去读/…/t-new/lib/utils.js。找对了文件,拿错了窗口。ab-readme/excalidraw/run2——recall 桶的手工复现:第三次 explore 返回了components/App.tsx和components/canvases/InteractiveCanvas.tsx;Agent 的下一步是Read components/canvases/StaticCanvas.tsx——兄弟 canvas 文件,响应里点名了但没交付源码,归桶为Read a file we did not return (named, not returned)。同一会话第四次调用是委派,子代理第一步是 shell 读element/src/shape.ts,而这次 explore 返回过它 → allocation,显示为Agent → Bash Read shape.ts。- excalidraw
canvasNonce——recall 桶端到端:一次全新的run-all.sh臂,打在已知的数据流前沿问题上:三次 explore,最后一次委派子代理,子代理立刻 grepsceneNonce→ 67%explore again、33%Grep/Glob、0% sufficient。这与 CLAUDE.md 已记录的该问题的残余 reads/greps 全部是 nonce 数据流(刻意不覆盖)的事实吻合——指标在没人告诉它的前提下自己找到了它。
4. --selftest。 node scripts/agent-eval/parse-run.mjs --selftest 用已知答案的合成转录覆盖分类器:全部五个桶、同消息规则、线程规则、委派、shell 读取与搜索、出错的调用。它刻意放在脚本里而非独立测试文件中——因为新增的 scripts/agent-eval/*.mjs 会进入 self-query 评测的语料(parse-run.mjs)。selftest 中的代表性断言包括:explore → explore = insufficient、explore → Bash sed of a returned file 归 read_returned、explore → Bash that writes a file is not a read 归 sufficient、"parent explore is not blamed for a subagent Read"、"re-read of an earlier explore's file is allocation, not recall" 等(parse-run.mjs)。
它不能说什么
四条边界(继承自原文档 "What it does not say" 节),决定如何解读该指标块:
- Sufficient 不等于 Correct。 Agent 停下来只说明响应足够让它停下,不说明答案是对的。答案质量不在本指标范围内。
- Read 是一张选票,不是证明。 Agent 有时会重读它已经拿到的文件。桶仍然是对的信号——它去读是因为缺了什么——但单次调用噪声大:要看一整遍(a pass)的计数,而不是单次运行的数字。
explore again桶按构造存在歧义。 它只说明"响应没有答上来",不区分是 allocation 还是 recall;后续那条 explore 的 query 通常能给出答案。- 小样本(small-n)。 单次运行只有 1–5 次 explore 调用,百分比很粗糙。做臂对比要跨一个 pass(
RUNS>=2,以及 7-repo campaign),永远不要用 n=1 下结论。
配套阅读:入口文档 agent-eval-feedback-metrics.md 说明三个指标各回答什么、该选哪个 harness(ab-new-vs-baseline.sh 隔离检索变更 / run-all.sh 有无 codegraph 对比 / bench-readme.sh campaign)以及如何读臂对比表;相关指标文档为 residual-context-occupancy.md 与 explore-allocation-efficiency.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00