跳转到内容

第 27 课:Agent 评测体系

用 SWE-bench 思路、golden tasks、repo fixtures、工具调用评估和回归测试,建立能判断 coding agent 是否真的变强的评测体系。

Agent 评测不是问模型“答案看起来对不对”,而是把真实工程任务变成可重复执行、可复盘、可回归的测试集。一个生产级 coding agent 至少要同时评估四件事:任务是否完成、证据是否可信、工具调用是否合理、失败是否可分类。

SWE-bench 的核心启发是:让 agent 在真实仓库里修真实问题,然后用测试和 diff 证明结果。课程里不直接复刻 SWE-bench,而是先做自己的 golden tasks:每个任务固定仓库 fixture、初始状态、用户 prompt、允许工具、成功标准和判分证据。这样你才能比较两个 prompt、两个模型、两套工具权限,哪一个真的让 agent 更强。

阶段四你已经能让 agent 处理权限、worktree、subagent、PR 和 code review。现在的问题变成:

如果今天改了 system prompt、工具描述或 planner 策略,怎么证明 agent 变好了,而不是只是在一个 demo 上表现更顺?

这节课的入口不是“写一个排行榜”,而是建立最小评测闭环:准备一批稳定任务,运行 agent,收集命令证据,按成功标准判分,再把失败沉淀成下一轮改进输入。

评测体系可以分成五层:

第一层是任务层。GoldenTask 描述用户想要什么,最好来自真实 issue、失败测试、代码迁移或文档修复,而不是人工编造的谜题。

第二层是环境层。repo fixture 固定初始仓库、依赖锁文件、测试命令和允许修改范围,保证今天和下周跑的是同一个问题。

第三层是执行层。agent 可以自由读文件、调用工具、编辑代码、运行测试,但每一步都要留下事件、命令和 diff 证据。

第四层是判分层。不要只看最终回答。要检查测试是否通过、diff 是否存在、是否改了禁止路径、工具调用是否满足预期、是否伪造成功。

第五层是回归层。每个线上失败、危险工具调用、错误修复,都应该变成新的 golden task 或 regression task。

SWE-bench 的思想可以简化成:

真实仓库 + 真实问题 + agent 修改 + 测试验证 = 可比较的任务结果

在自己的课程项目里,第一版不要追求大规模。先建 20 个真实工程任务,每个任务只回答一个问题:这个 agent 是否能在受控仓库里交付可验证结果。

一个任务至少包含:

  • id:稳定标识,例如 fix-login-test
  • prompt:交给 agent 的用户任务。
  • fixture:仓库初始目录或可 checkout 的 commit。
  • allowedTools:本任务允许的工具集合。
  • successCriteria:机器可检查或人工可复核的成功标准。
  • evidenceRequired:必须收集的命令、测试输出、diff 或日志。

课程 lab 的起点是 /Users/ryanchen/codespace/pi-agent-course-lab/src/17-benchmark-runner.ts,里面已经有 GoldenTaskRunResultgrade() 的最小形状。

repo fixture 解决的是“评测环境漂移”问题。不要在开发者当前工作区直接跑 benchmark,否则依赖版本、未提交改动、缓存和本地配置都会污染结果。

推荐结构:

benchmarks/
fixtures/
fix-test/
repo/
task.json
expected.patch? # 可选,只用于人工对照,不作为唯一判分

每次运行前复制 fixture 到临时目录或 worktree。agent 只能在临时目录里工作,跑完后收集 git diff、测试输出、trace 和命令日志。

coding agent 的成败不只来自最终 diff。工具调用质量会暴露很多问题:

  • 是否先读上下文再修改。
  • 是否运行了相关测试。
  • 是否使用了禁止工具。
  • 是否在失败后继续声称成功。
  • 是否反复调用昂贵工具但没有新增信息。

Pi 的低层 agent loop 会发出 tool_execution_starttool_execution_updatetool_execution_end 等事件。评测 runner 可以从事件里聚合工具序列,而不是靠解析终端文本猜测发生了什么。

成功标准要分层:

  • must:必须满足,例如 npm test exit 0git diff 非空、禁止路径未改。
  • should:强烈期望,例如先读测试文件、失败后重跑目标测试。
  • diagnostic:不直接判失败,但帮助解释,例如 token、cost、工具调用次数、耗时。

第一版 grade() 可以像 starter 一样用字符串包含判断;生产版应该把 evidence 设计成结构化记录。

每次 benchmark 失败后,不要只改 prompt。先给失败分类:

  • task_understood_wrong:任务理解错。
  • bad_context:没读关键文件。
  • bad_tool_choice:工具选择或参数错误。
  • test_not_run:没有验证。
  • false_success:没有证据却声称完成。
  • unsafe_action:越权修改或危险命令。

然后决定:这是 prompt 问题、工具描述问题、权限问题、fixture 问题,还是 agent loop / runtime 问题。只有能复现的失败,才是有价值的改进输入。

本课实践基于课程 lab:

Terminal window
cd /Users/ryanchen/codespace/pi-agent-course-lab
sed -n '1,220p' src/17-benchmark-runner.ts

先不要急着接真实模型。第一步是把 starter 里的单个 task 扩展成数组,并把 evidence 从字符串数组升级成结构化证据:

type Evidence =
| { kind: "command"; command: string; exitCode: number; output: string }
| { kind: "diff"; filesChanged: string[] }
| { kind: "tool"; name: string; isError: boolean };

然后让 grade() 只依赖 evidence,而不是依赖 agent 的最终自然语言回答。这样可以避免“回答写得很像完成了,但测试没有通过”的假阳性。

课堂上会把 benchmark runner 拆成三段变量流:

const task = loadGoldenTask("fix-test");
const fixtureDir = copyFixtureToTemp(task.fixture);
const trace = await runAgent({
cwd: fixtureDir,
prompt: task.prompt,
tools: task.allowedTools,
});
const evidence = collectEvidence({
trace,
cwd: fixtureDir,
testCommand: task.testCommand,
});
const result = grade(task, evidence);

这里有三个控制点。

第一个控制点是 copyFixtureToTemp()。它保证 agent 的输入环境固定,避免污染真实仓库。没有这一层,benchmark 结果不能比较。

第二个控制点是 runAgent()。它不直接返回“通过/失败”,只返回 trace、命令和 diff。agent 负责尝试完成任务,runner 负责判分。

第三个控制点是 grade()task.successCriteria 从任务定义流入判分函数,evidence 从执行过程流入判分函数。判分函数不应该重新调用模型,否则评测会变成另一个不可控的 agent。

可以从 starter 的最小版本演进:

const passed = task.successCriteria.every((criterion) =>
evidence.some((line) => line.toLowerCase().includes(criterion.toLowerCase())),
);

再升级成结构化判分:

const hasPassingTest = evidence.some(
(item) => item.kind === "command" && item.command === task.testCommand && item.exitCode === 0,
);
const hasDiff = evidence.some(
(item) => item.kind === "diff" && item.filesChanged.length > 0,
);
return { taskId: task.id, passed: hasPassingTest && hasDiff, evidence };

这段推演的重点不是代码量,而是职责边界:agent 产生行为,trace 记录行为,evidence 摘取证据,grader 按任务标准判分。

  • /Users/ryanchen/codespace/pi-agent-course-lab/src/17-benchmark-runner.ts:课程 lab 的 benchmark runner starter,包含 GoldenTaskRunResultgrade() 的最小实现。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/test-harness.ts at 61babc2:Pi 测试 harness,提供 faux model、事件捕获和一键创建 AgentSession 的测试环境,可作为 golden task runner 的测试设计参考。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/harness.ts at 61babc2:新测试套件的本地 harness,展示如何用 faux provider、临时目录、工具和事件数组构造稳定测试。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts at 61babc2:工具 allowlist 的回归测试,说明“允许工具是否生效”可以成为 agent 评测的一部分。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/src/agent-loop.ts at 61babc2:低层 loop 发出 tool_execution_starttool_execution_updatetool_execution_end 和 tool result message,是工具调用评估的事件来源。
  • /Users/ryanchen/codespace/external-sources/pi/packages/agent/src/types.ts at 61babc2:定义 beforeToolCallafterToolCallToolExecutionMode 等工具执行控制点,可用于实现评测期间的阻断、记录和判分。
  • /Users/ryanchen/codespace/external-sources/pi/packages/coding-agent/test/agent-session-stats.test.ts at 61babc2:围绕 session token、context usage 和统计信息的测试,可作为评测指标测试的参考。

学完本课后,你应该能做到:

  • 解释 SWE-bench 思路为什么适合评估 coding agent,而不是只评估问答质量。
  • 设计一个 GoldenTask,包含 prompt、fixture、允许工具、成功标准和证据要求。
  • 说明 repo fixture 为什么必须固定初始状态,并能把执行目录和真实仓库隔离。
  • 把测试输出、diff、工具调用、失败类型转成可判分 evidence。
  • 区分任务成功、工具调用质量、成本指标和诊断指标。
  • 把一次线上失败沉淀成 regression task,而不是只靠临时 prompt 修补。
  1. 实践题:扩展 /Users/ryanchen/codespace/pi-agent-course-lab/src/17-benchmark-runner.ts,把单个 task 改成 3 个 GoldenTask,并让 grade() 输出通过率和每个任务缺失的 success criteria。
  2. 思考题:如果一个 agent 最终让测试通过了,但它没有先读相关文件、调用了多次无关命令、还修改了无关目录,这个任务应该算完全成功、部分成功,还是失败?请写出你的判分规则。