用 EvalArc 跑爬山循环:留下有用的改动,回滚过拟合的提议

evalarc hillclimb-run 用你自己的评测与提议命令驱动爬山循环,评测前先回滚贴入用例答案的补丁。按脚本化示例的五次迭代,讲清一次过拟合改动怎样被回滚,以及各道检查、退出码与安全边界。

循环图:左侧一条椭圆圆环上有四个只写数字 1 到 4 的实心圆,圆之间的顺时针箭头按 1、2、3、4 再回到 1 的顺序连接。右侧是对应的编号列表:1 写出上次保留结果的调优集失败用例,不含留出集;2 propose 命令只能改 allow 内的文件,越界:退出码 2;3 评测前先查补丁:含用例文本则回滚,空补丁:no_change;4 运行 evaluate,与上次保留结果比较,keep 或回滚。标题“hillclimb-run 的一次迭代”位于一条水平线上方,来源说明位于第二条水平线下方。
evalarc hillclimb-run 每次迭代的四个环节,依据 commit c3ed02f 的 docs/hillclimb-run.md 绘制。keep 的结果成为下一次迭代的“上次保留结果”;达到 max_iterations、停滞分诊判定 stopped_below_noise 或出现退出码 2 时循环结束,这些停止条件不在图中。正文第 5 步,即连续 stall_after 次没有 keep 后的停滞分诊,也没有画成单独的环节。

让模型反复修改提示词或规则、每改一次就重跑评测,这样的“爬山”很容易爬偏:把失败用例的答案直接贴进去,或者只修好调优集里的某一个用例,分数都会上升。EvalArc 的 evalarc hillclimb-run 替你驱动这个循环。你提供两条命令:一条评测工作区,写出 Inspect AI、promptfoo 或 JUnit 结果;一条提出改动,通常是用你自己的密钥调用模型。EvalArc 本身不调用模型,它决定每条命令能看到什么、能改什么,以及每次改动留不留。本文依据固定在 commit c3ed02f 的文档和脚本化示例,按示例的五次迭代讲清每一步的检查、产物和退出码,以及 allow 守卫挡不住什么。

本站的 EvalArc:分数变高了,智能体哪里退步了 介绍过逐项比较两次评测,把 EvalArc 报告文件夹交给别人复核:输入副本、记录的选项与 evalarc verify 的边界 讲过怎样离线复核 hillclimb-review 等报告。按文档的说法,hillclimb-review 是事后审计一个爬山循环,hillclimb-run 则在运行时驱动这个循环;本文讲的是后者。下文的行为描述都来自项目文档([O],项目自述);本站没有运行这些命令,文中的示例输出是文档里保存的结果。

两条命令与一份配置

示例的配置文件 hillclimb.toml 如下:

workspace = "workspace"            # relative to this file
allow = ["rules.md"]               # globs the propose command may edit
held_out = "split.json"            # evalarc.case-split.v1
evaluate = ["{python}", "evaluate.py", "{output}"]
propose = ["{python}", "propose.py", "{iteration}", "{failures}"]
objective = "quality"              # or "cost"
max_iterations = 5
stall_after = 2
timeout_seconds = 60

两条命令都写成参数数组,不经过 shell 执行,在工作区里运行,并继承你的环境变量。可用的占位符有:{python}、{workspace};evaluate 专用的 {output},即它要写出的结果路径;propose 专用的 {failures}、{allowed}、{iteration}。allow 是 propose 命令允许修改的文件通配符,held_out 指向 schema 为 evalarc.case-split.v1 的用例划分文件。文档还列出了 cost_metric、max_cost_ratio、min_effect、result_suffix(.json、.xml、.eval)、format、threshold、leak_min_chars 和 paste_check 等键(文档在这两个键名后分别标注 12 和 true),但没有逐项解释,本文不展开。

每次迭代要过的五道关

  1. 只给调优集的失败。 EvalArc 写出 steps/NN/tuning-failures.json,内容是上一次保留的结果里失败的调优集用例:输入、期望答案、输出、评分器证据和一个分诊类别。留出集用例从不写进去。
  2. propose 只能改 allow 内的文件。 如果它改了 allow 之外的任何工作区文件(包括 .git;只忽略 __pycache__ 和工具缓存),循环会恢复允许修改的文件、删除它新建的文件,然后以退出码 2 停止,并列出被改过的既有文件,方便你自己恢复。propose 命令本身失败时,这一步记为 propose_failed。被改成符号链接的允许路径,会被换回保留的内容。
  3. 评测之前先查补丁。 对允许文件的改动保存为 patch.diff。如果新增行包含留出集用例的文本,直接回滚为 rollback_leakage;包含调优集用例的文本,回滚为 rollback_pasted_case,即贴入失败用例,而没有修正原因。只检查长度不少于 leak_min_chars 个字符的记录字符串,所以像 billing 这样的短答案拦不住。空补丁记为 no_change。
  4. 评测并与上次保留的结果比较。 通过前三关后才运行 evaluate,再按 hillclimb-review 的规则判为 keep、rollback_regression、rollback_overfit 或 rollback_no_gain。回滚的文件会被恢复。
  5. 停滞分诊。 连续 stall_after 次没有 keep 后,对剩下的调优集失败做分诊;如果调优集的剩余提升空间小于可分辨的变化,循环停止,记为 stopped_below_noise。

第 1 步和第 3 步是配套的。propose 命令能看到调优集用例的期望答案,最省事的“修法”就是把答案抄进规则文件;第 3 步在花评测成本之前就把这种补丁回滚。留出集既不进失败清单,也不允许出现在补丁里,因此它的变化才能用来判断一次改动是否只是拟合了调优集。

示例:五次迭代各停在哪一关

仓库里的示例是一个工单路由工作区:“智能体” workspace/evaluate.py 在 rules.md 里有对应规则时才能把工单路由对;workspace/propose.py 代替模型,每次迭代从 workspace/proposals.json 追加一条预先写好的改动。示例不调用任何模型。下面三行是本站补充的固定版本步骤,不出自这两份文件:

git clone https://github.com/noteflowai/evalarc.git
cd evalarc
git checkout c3ed02f7fb2645c1b8651f16c11a47176c230736

文档自己的命令从 cp -r examples/hillclimb-run /tmp/climb-example 开始;循环会修改工作区,所以先复制一份:

cp -r examples/hillclimb-run /tmp/climb-example
evalarc hillclimb-run /tmp/climb-example/hillclimb.toml --output runs/climb --trust-local

这两份文件没有写安装步骤。检出固定提交只固定了示例和源码,不会固定 PATH 上的 evalarc;请自己确认所用的版本来自同一提交。文档保存的输出是:

  1. keep (tuning +25.0 pp, held out +33.3 pp)
  2. rollback_pasted_case
  3. rollback_overfit (tuning +12.5 pp, held out +0.0 pp) — stalled
  4. no_change
  5. keep (tuning +25.0 pp, held out +50.0 pp)
Stopped: max_iterations | Kept: 05.json | Recommendation: merge
迭代提议的改动结果在哪一关决定
1route refund -> billingkeep:调优集和留出集都提升第 4 步
2把一条失败的调优集工单连同答案贴进去rollback_pasted_case,没有评测第 3 步
3case t06 -> accounts,只适配一个调优集用例rollback_overfit,随后停滞分诊第 4 步,再到第 5 步
4没有改动no_change第 3 步
5route outage -> oncallkeep第 4 步

第 3 次迭代最值得看:调优集提升 12.5 个百分点,留出集为 0,这次改动被回滚。第 2、3 次连续两次没有 keep,达到 stall_after = 2,输出标出 stalled;之后循环继续跑到第 5 次,停止原因是 max_iterations,而不是 stopped_below_noise。最终保留的是 05.json,建议合并。

示例 README 还给了一段后续步骤:把基线和最终结果打包成成对比较,用一个脚本化的评判器打分。judge.py 只是比较输出与期望队列;README 说明,真实的评判命令应调用与被评测模型不同的模型。

evalarc judge-packet pairwise runs/climb/results/00-baseline.json runs/climb/results/05.json \
  --output runs/ab
evalarc judge-run runs/ab --config /tmp/climb-example/judge.toml --output runs/ab-verdicts.json --trust-local
evalarc judge-score runs/ab runs/ab-verdicts.json --require-current-preferred

运行结束后看什么

  • 工作区里是最后一次保留的版本,original/ 是起始文件,final.diff 是净改动。
  • hillclimb.json、summary.md 和 index.html 是对每一个评测过的结果做的 hillclimb-review 报告。
  • loop.json 记录每一步,失败时也会写。
  • 退出码:0 表示建议合并,1 表示不建议,2 表示配置、命令或安全检查失败。

退出码 0 只是循环给出的合并建议。文档建议合并前审阅 final.diff;被回滚的步骤也记录在 loop.json 里,本站建议审阅时和保留的步骤一起看。

allow 守卫不是沙箱

两条命令都以你的权限和环境在本机运行,所以必须显式加 --trust-local。allow 守卫的做法是比较每次 propose 命令前后的工作区,它不隔离命令:工作区之外的改动它看不到,对既有文件的修改它也无法撤销。文档的建议是:在副本或干净的 git 检出上运行,留出集数据不要放在工作区里,合并前审阅 final.diff。

本站再补充两点,属于本站的建议,不是文档原文。第一,两条命令继承你的环境变量,环境里的云凭证对 evaluate 脚本同样可见;调用模型时请使用短期凭证,权限只覆盖所需的模型调用,遇到权限错误时不要靠放宽权限来解决。第二,工作区里的文件可能被 propose 命令读取并随请求发给模型,密钥和其他敏感文件应放在工作区之外。

局限 / 注意

  • 示例是脚本化的:propose.py 回放五条预先声明的改动,输出中的百分点不是模型改进的测量结果。本站没有运行示例,也没有用真实模型试过这个循环。
  • 文档没有说明输出里的百分点是相对上次保留的结果还是相对基线,也没有在本页给出 rollback_overfit、rollback_no_gain 等判定的具体阈值,只说沿用 hillclimb-review 的规则,本文不作推断。
  • 泄漏和贴入检查看的是新增行是否包含记录的用例文本,并且只看不少于 leak_min_chars 个字符的字符串。文档没有说明它能识别改写过的用例,不应把它当作完整的泄漏检测。
  • 本文依据 commit c3ed02f;之后的版本在配置键、判定名称或退出码上可能不同。

链接