- 把夹具任务定义为目标、环境准备和验证器的三元组。
- 对每个任务采样多次运行,计算 pass@1 与 pass@k。
- 把延迟和成本汇总为平均值和第 95 百分位指标。
- 把确定性验证器(文件 diff、退出码、正则匹配)接入可复用函数。
- 输出结构化 JSON 报告,供回归跟踪脚本摄取。
问题
没有评估 harness 的 Agent 基准,会被三类失败模式反复折磨。
第一类是没有验证的通过。Agent 说它修好了 bug,人扫一眼 diff,测试套件标绿,三周后回归测试又把同一个 bug 揪出来。Agent 推理得头头是道,实际上什么都没修。
第二类是没有被发现的回归。提示模板改了一处,Agent 在显眼的那个任务上好 4%,在安静的那个任务上差 14%。没有黄金集和逐任务得分,这次回归就跟着进了主干,直到客户投诉才浮出水面。
第三类是逐任务漂移。周一的评估跑了 100 个任务,周五只跑了 95 个,因为有人重命名了五个夹具。通过率看起来提升了 5%。其实并没有。
harness 就是把这类失败变成事实的程序。它每次都按可复现的顺序跑完每个夹具,交给一个在确定性检查上返回真或假的验证器。
概念
flowchart LR F1[fixtures/task_001/<br/>task.json + expected/] --> Harness F2[fixtures/task_002/<br/>...] --> Harness Harness[Harness<br/>for each task:<br/>setup / run agent k samples /<br/>verify each sample /<br/>record latency, cost] Harness --> Report[EvalReport<br/>pass@1 / pass@k<br/>mean ms / p95 ms<br/>mean cost]
FixtureTask 是一个小 JSON 文件,外加可选的 expected/ 目录。JSON 声明 id、goal(喂给 Agent 的 prompt)、setup 块(要放进临时目录的文件)和 verifier 块。verifier 块指定 harness 验证器注册表中的一个函数,并提供它的参数。
三种验证器形态覆盖了大多数有用的任务。
第一种是 file_equals。Agent 跑完后,把指定文件与期望内容比较。它捕捉「按这个确切方式修好这个 bug」这类任务。
第二种是 regex_match。用正则匹配指定文件的内容。它捕捉「函数必须存在并返回 X」这类有多种可接受解的任务。
第三种是 shell_exit_zero。harness 运行一条 shell 命令(通过第 26 课的沙箱),只有命令以零退出时才判定任务通过。它捕捉「测试必须通过」这类任务。
harness 把每个任务跑 k 次。pass@k 为 1 - (1 - p)^k,其中 p 是经验通过率;harness 同时报告原始计数,方便你发现方差。延迟是每个样本的墙上时钟时间。成本取 Agent 自报的值(token 数、美元,或两者兼有);harness 跨样本求和,给出逐任务与汇总数字。
架构
flowchart TD Harness[EvalHarness] -->|load| Task[FixtureTask<br/>goal / setup / verifier] Harness --> Loop[for each task:<br/>prepare scratch dir from setup<br/>for sample in range k:<br/>run candidate task, scratch_dir -> SampleResult<br/>verify sample, task -> bool<br/>record per-task aggregate] Loop --> TaskReport[TaskReport<br/>task_id / k / passes / pass_rate<br/>mean_latency / mean_cost] TaskReport -->|aggregate| EvalReport[EvalReport<br/>total tasks / pass@1 / pass@k / p95 latency]
候选(candidate)是一个可调用对象:Callable[[FixtureTask, str], SampleResult]。harness 通过 tempfile.mkdtemp() 创建临时目录,并把路径作为普通字符串传入。harness 不关心 candidate 如何工作。candidate 可以是确定性的补丁应用器(用于 harness 自检)、真实的 LLM Agent,或模糊测试器。契约就是 SampleResult。
你将构建什么
main.py 提供:
FixtureTaskdataclass。SampleResultdataclass:success_self_reported、latency_ms、cost_units、edits。TaskReport、EvalReportdataclass,带to_dict()。VerifierRegistry,把验证器名称映射到函数。内置验证器:file_equals、regex_match、shell_exit_zero。EvalHarness类。对一个 candidate 运行一个目录下的任务,返回 EvalReport。tasks/里捆绑的五个夹具任务:
- fizzbuzz 的差一错误
- factorial 缺少 return
- 错误信息里的拼写错误
- 空函数体
- 链表遍历的差一错误
- 一个确定性参考 candidate(
apply_known_fixes),harness 用它演示 pass@1 干净地达到 1.0。 - 演示程序打印 EvalReport JSON 并以零退出。
夹具任务以 JSON 文件的形式捆绑在 tasks/,并在 tasks/<id>/buggy/ 与 tasks/<id>/expected/ 里配有成对的源文件。harness 把 buggy 复制进临时目录,交给 candidate,再对照 expected 验证。
为什么用 pass@k 而不只是 pass@1
真实 LLM Agent 是随机的。pass@1 为 0.6 看起来像失败。pass@5 为 0.95 则说明 Agent 大多数时候能给出正确答案,只是在前几次采样中选错了。解决办法是采样加排序,而不是一味增加训练。pass@k 让这一点变得可见。
pass@k 与 pass@1 并排报告,因为 pass@k 会掩盖一种真实失败:如果一个模型二十次里只对一次,你手上并没有可用的 Agent。harness 两者都展示。
本课如何与 Track A 的其余部分组合
第 25 课产出了门链(gate chain)。第 26 课产出了沙箱。harness 对任何 shell_exit_zero 验证器都使用沙箱。第 28 课把每次 harness 运行包进一条 OTel trace。第 29 课针对某个捆绑夹具运行端到端演示,并断言参考 candidate 的 pass@1 = 1.0。
运行方式
bashcd phases/19-capstone-projects/27-eval-harness-fixture-tasks
python3 code/main.py
python3 -m pytest code/tests/ -v演示程序以 JSON 打印 EvalReport,包括 pass@1、pass@5、平均延迟和逐任务明细。退出码为零。测试覆盖验证器函数、pass@k 数学、夹具加载,以及 harness 针对捆绑参考 candidate 的端到端运行。