首页
/ CodeGraph Explore Sufficiency:把 Agent 的下一步动作变成"响应是否够用"的免费真值指标

CodeGraph Explore Sufficiency:把 Agent 的下一步动作变成"响应是否够用"的免费真值指标

2026-09-04 11:44:19作者:戚魁泉Nursing

本文介绍 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 头部注释)。

保持指标诚实的八条规则

这是该指标设计的核心——每一条都对应 classifySufficiencyparse-run.mjs)里一段具体的判定逻辑,也是自测用例覆盖的对象。

1. 只有"更晚消息"中的动作才算反应

在同一条 assistant 消息里与 explore 一起发出的 Read,发出时响应还不存在,因此不构成对该响应的裁决。这些调用会被跳过,并单独计为 concurrent(在指标块的 note 行显示)。源码里对应 parse-run.mjsb.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 的裁决。classifySufficiencythreads Map 按 ev.parent_tool_use_id ?? 'main' 分线程处理(parse-run.mjs)。交互式会话中子代理存在于独立文件里:每个 agent-*.meta.json 携带派生它的 Task 的 toolUseIdparse-session.mjs 用它把各线程拼回同一个事件流再喂给同一个分类器(parse-session.mjs)。

4. 委派按子代理的第一个实质动作判定

Agent / Task 委派会被展开为"子代理的第一个实质调用",显示形如 Agent → Bash search。把委派本身计成 "moved on" 是该指标唯一不能犯的错向:在下方的 excalidraw 运行中,那样会把子代理正在为找文件而 grep 的一次运行报告成 33% sufficient。嵌套委派不做递归展开——子代理若只再派生子代理,则保持 "moved on"。实现见 throughDelegationparse-run.mjs)。

5. Shell 里的文件访问算数

两个 A/B 臂都有 Bash,Agent 用 sed -n '100,200p' lib/x.js 和用 Read 一样自然;grep/rg/find/ls 计为搜索。只统计 ReadGrep 工具,会把这类 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 先查本次响应、再查 earlierparse-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.tsxcomponents/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,最后一次委派子代理,子代理立刻 grep sceneNonce → 67% explore again、33% Grep/Glob0% 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 = insufficientexplore → Bash sed of a returned fileread_returnedexplore → 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.mdexplore-allocation-efficiency.md

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341