提出一套可复现的编码Agent评测方法论:固定任务集、相同fixture、相同超时时间,避免把「修好了」和「改崩了」都算成赢。
你把两个 coding agent 的评分丢进周五评审。一张 PPT 写 71%,另一张写 68%。有人问该继续付费哪个技术栈,会议室瞬间安静——因为没人能说出那个百分号背后的单位是什么。
一个任务是一次失败测试、一个代码仓库,还是一次偷偷重试了一整夜的对话?如果你明天无法用完全相同的 24 个任务重新跑一遍并落在极小的漂移区间内,那就不是基准,只是一张截图。
这篇文章是一套你可以自己运行的方法论。它不是排行榜。它不会告诉你哪个模型"最好"。它会告诉你某次比较是否站得住脚。
从一个场景开始,而非一个指标
想象一个结账服务,其中有条促销规则不太稳定。你给 Agent A 和 Agent B 相同的失败测试、相同的 fixture 文件、相同的计时器。Agent A 修改了三个文件,测试变绿。Agent B 重写了一个辅助函数,测试还是红,然后第二次尝试删掉一条断言后通过了。
如果你的记分板把两种情况都算作胜利,你就混淆了"修复"和"破坏预言机"。市场喜欢这种混合。你不应该。
在写任何一个数字之前,先定义清楚三个对象。
数据集:一组冻结的任务切片,每个任务包含一个 fixture 目录和一条隐藏的测试命令。
预言机:决定 green 的那条精确命令,加上它被允许读取的文件的校验和。
分析单元:一次尝试对应一个任务,而不是一次拥有无限重试次数的对话。
这三个东西没有固定之前,通过率只是一种情绪。
构建一个你可以真正重放的 24 任务切片
你不需要 500 个任务才能做到诚实。你只需要一组小到能在一台机器上跑两遍的切片。24 个足够让你感受到方差,又小到能手工阅读每一个失败。
使用这样的目录结构:
slice24/
TASKS.md
controls.yaml
tasks/
T01_discount_rule/
prompt.md
repo/
oracle/
test_discount.py
sha256.txt
T02_date_parse/
...
runs/
replay_a.jsonl
replay_b.jsonl
TASKS.md 是人类合约。每一行用一句话描述一个 bug、所使用的语言,以及预言机是否被允许查看 agent 可能编辑的生产文件。如果预言机读取了 agent 可以重写的文件,你的"通过"是不可信的。
刻意保持切片枯燥无聊。选用你能在白板上解释的 bug:off-by-one、时区解析、nil map、错误的状态码、缺失的唯一索引。任何需要联网的都跳过。任何 gold patch 已在公开训练数据集中而你无法审计的都跳过。
一张决策表有助于你在任务混入切片之前就拒绝它。
如果某一行没有通过那张表,它就不进入 tasks/。它不会有脚注。它被剔除。
像对待数据集的一部分一样固定 controls
没有 controls 的数据集只是一堆 prompt 倾倒。先写 controls.yaml,然后在 bake-off 期间拒绝修改它。
# Proposal: example controls for a 24-task replay slice.
# Label this as a template until you fill every field on your machine.
slice_id: slice24-checkout-2026-09-13
unit_of_analysis: one_attempt_per_task
max_attempts: 1
wall_clock_sec: 180
cpus: 2
memory_mb: 2048
network: denied
writable_paths: ["repo/"]
oracle_cmd: "python -m pytest oracle/ -q"
oracle_sha256_file: oracle/sha256.txt
seed_policy: fixed
language_toolchain: "python3.11"
forbidden_edits: ["oracle/", "controls.yaml"]
replay_required: 2
max_replay_drift: 0
大声把那个文件读出来。重要的行是 max_attempts、network 和 replay_required。一次尝试意味着你在给第一个 patch 打分,而不是人类守着循环让 agent 跑出最佳 patch 后的分数。denied network 意味着 agent 不能去网上找答案。两次重放且允许漂移为零意味着环境是一台科学仪器,而不是某天下午恰好空闲的一台笔记本。
如果你后来把 max_attempts 从 1 改到 3,你就开启了一项新的研究。不要把这些运行追加到旧的 JSONL 上面然后称之为更新。
给配对差值打分,而不是一个标题百分比
你仍然会计算通过数。你不会用它来开场。
对于每个任务 t,存储四个事实:
pass_a:Agent A 的尝试让固定的预言机变绿则为 1
pass_b:Agent B 做到了则为 1
oracle_intact:预言机/仍然匹配 sha256.txt 则为 1
replay_match:该 agent 的第一次尝试和第二次尝试一致则为 1
然后报告配对情况:
A 获胜: A 通过且 B 未通过,预言机完好
B 获胜:反过来
平局:都通过或都未通过
无效:预言机被篡改、超时不一致、或重放不匹配
无效不是失败。无效是仪器坏了。如果 24 个任务中有超过两个无效,停止排名,去修测试工具。
你可以引用的数字很窄:"在 slice24 上,一次尝试加一个冻结预言机,A 赢了 5 次,B 赢了 3 次,14 次平局,2 次无效。"这句话很丑陋。但这恰恰是很难被拿来做广告的原因。
一个你可以跑两遍的测试工具
下面是带标注的提案。它不声称有生产级指标。把 run_agent 替换成你真正的 CLI。保留预言机检查。如果你跳过校验和,文件的其余部分就是表演。
# proposal_harness.py — replay gate for a 24-task slice
from __future__ import annotations
import hashlib, json, subprocess, time
from pathlib import Path
SLICE = Path("slice24")
WALL_CLOCK = 180
def sha256_tree(root: Path) -> str:
h = hashlib.sha256()
for p in sorted(root.rglob("*")):
if p.is_file():
h.update(p.relative_to(root).as_posix().encode())
h.update(p.read_bytes())
return h.hexdigest()
def oracle_intact(task: Path) -> bool:
expected = (task / "oracle" / "sha256.txt").read_text().strip()
return sha256_tree(task / "oracle") == expected
def run_oracle(task: Path) -> bool:
proc = subprocess.run(
["python", "-m", "pytest", str(task / "oracle"), "-q"],
cwd=task / "repo",
capture_output=True,
timeout=WALL_CLOCK,
)
return proc.returncode == 0
def run_agent(agent_id: str, task: Path) -> None:
prompt = (task / "prompt.md").read_text()
# Replace this stub with your agent CLI. Keep the timeout.
subprocess.run(
["your-agent", "--id", agent_id, "--prompt", prompt, "--cwd", str(task / "repo")],
timeout=WALL_CLOCK,
check=False,
)
def one_attempt(agent_id: str, task: Path) -> dict:
if not oracle_intact(task):
return {"task": task.name, "void": "oracle_before"}
t0 = time.monotonic()
run_agent(agent_id, task)
elapsed = round(time.monotonic() - t0, 3)
if not oracle_intact(task):
return {"task": task.name, "agent": agent_id, "void": "oracle_mutated", "sec": elapsed}
passed = run_oracle(task)
return {"task": task.name, "agent": agent_id, "pass": int(passed), "sec": elapsed, "void": None}
def replay_agent(agent_id: str) -> list[dict]:
rows = []
for task in sorted((SLICE / "tasks").iterdir()):
first = one_attempt(agent_id, task)
second = one_attempt(agent_id, task)
if first.get("pass") != second.get("pass") or first.get("void") or second.get("void"):
rows.append({"task": task.name, "agent": agent_id, "void": "replay_drift"})
else:
rows.append(first)
return rows
def paired_report(a: list[dict], b: list[dict]) -> dict:
by_a = {r["task"]: r for r in a}
wins = {"A": 0, "B": 0, "tie": 0, "void": 0}
for r in b:
t = r["task"]
left, right = by_a[t], r
if left.get("void") or right.get("void"):
wins["void"] += 1
elif left.get("pass") and not right.get("pass"):
wins["A"] += 1
elif right.get("pass") and not left.get("pass"):
wins["B"] += 1
else:
wins["tie"] += 1
return wins
if __name__ == "__main__":
a_rows = replay_agent("A")
b_rows = replay_agent("B")
Path("slice24/runs/replay_a.jsonl").write_text(
"\n".join(json.dumps(r) for r in a_rows) + "\n"
)
Path("slice24/runs/replay_b.jsonl").write_text(
"\n".join(json.dumps(r) for r in b_rows) + "n"
if False else "\n".join(json.dumps(r) for r in b_rows) + "\n"
)
print(json.dumps(paired_report(a_rows, b_rows), indent=2))
刻意跑两遍。外层的 replay_agent 已经把每个任务跑了两遍。但你仍然应该在重启后重新启动整个流程。如果 JSONL 变了,那你的机器也是分数的一部分。在争论模型之前先修好机器。
调试这个仪器时的有用命令:
find slice24/tasks -name sha256.txt -print
python -m pytest slice24/tasks/T01_discount_rule/oracle -q
sha256sum slice24/controls.yaml
python proposal_harness.py
diff -u slice24/runs/replay_a.jsonl slice24/runs/replay_a.jsonl.bak
如果 diff 不为空,你不讨论 Agent A 对比 Agent B。你讨论时钟、缓存和 pytest 插件。
为什么这些数字不是市场营销
供应商的 PPT 要的是一个百分比和一个大胆的赢家。这个切片在四个方面拒绝那样做。
第一,n 等于 24 个配对任务,而不是 24 次独立的抛硬币。有几个任务会共享一种语言和一种 bug 风格。你仍然可以数胜数。你不能假装你做的是人口研究。
第二,单元是一次尝试。隐藏的重试、超时后的额外工具调用、以及"我在对话中推了它一下"都是协议违规。它们属于另一篇论文。
第三,无效任务是存在的。市场把它们吸收进分母或者直接丢弃。你把它们暴露出来。一个不能在公开场合失败的方法不是方法。
第四,重放漂移是停止条件,不是脚注。如果同一个 agent 在同一个 fixture 上从通过/失败翻转了,分数就是环境噪音。在噪音上发布均值是排行榜腐化的方式。
你可以后续加上成本。你可以后续加上失败分类。在重放变得无聊之前这些都不合法。
免费服务器实际上在哪里有帮助
方法论工作在脚手架上烧 token:broken prompts、oracle 错误、路径 bug。这些流量不应该接触生产预算。
披露:本文是 MonkeyCode 产品推广的一部分。如果你需要模型访问和一台不需要自己搭建 GPU 的机器,MonkeyCode 的免费模型访问和免费服务器选项足以托管这个 24 任务切片,同时你可以在此期间加固测试工具。只用这个来跑重放循环。任何你想展示给财务团队的结论,都要迁移到你控制的硬件和你可以校验的切片上。
这就是整个产品说明。
局限性,以及谁不应该使用这个
这种方法对于几个正经的工作是错误的。
不要用 24 任务切片来选一个公司级的 coding agent。区间太宽、任务都是本地的、语言覆盖很薄。不要用它来做超过几小时的重构、GUI 工作或任何需要公网的东西。如果你的预言机是"审阅者喜欢这个 patch",不要用它。人类口味不是 sha256。
不要比较不共享同一工具表面的 agent。如果 Agent A 可以跑 pytest 而 Agent B 只能输出一个 diff,你测的是工具链。说出来,或者停下。
不要在没有 controls 文件、slice id、无效数量和重放规则的情况下发布通过数。没有这四样的数字只是一个图注。
如果你的目标是周一就能发新闻稿,这个工作流会拖慢你。这就是重点。排名很便宜。重放是过滤器,让你不会买到一个坐不住的百分比。