文章主张在正式评测前锁定数据集、指标和控制条件,将调试试跑与公开成绩分开。示例用 20 个正常任务和 10 个负向对照任务,避免只看最终成功而奖励编造、掩盖重试。
只有在第一次计分运行之前,就固定好数据集、指标和控制条件,Agent 的分数才值得信任。在免费服务器上顺手跑通一次,可以用来调试评测框架,但同一次运行不能成为公开宣称的依据。下面这套方法把试运行视为排练,只有采用锁定协议、包含拒绝作答子集的运行,才有资格产出对外公布的核心分数。跳过这一步锁定,发布出来的就只是流畅表达的演示,而不是对任务完成能力的测量。
公开文章常常在悄悄重试几次之后,庆祝一串全部通过的结果,却隐去了真正造就这串结果的策略。一个从不放弃作答的 Agent,看起来可能比另一个拒绝执行描述不充分的任务、转而索要缺失文件的 Agent 更强。但这两种行为体现的能力并不相同,即使两份交互记录最后都亮起了绿色对勾。只统计最终成功的分数,会奖励自信的编造,惩罚谨慎的拒绝。
这套方法的数据集是一个封存的小型任务包,共有 30 个任务,分为 20 个普通任务和 10 个负对照任务。普通任务只有一个预期产物,例如一个函数、一条必须从失败变为通过的测试,或者一份简短的迁移说明。负对照任务则故意省去一项必需输入,因此诚实的结果应当是放弃作答,而不是猜出一个补丁。任务包会在任何模型调用之前计算哈希,此后每份报告都必须附带这个哈希。
四项指标要一起呈现,任何一项都不能被单独拿出来当宣传口号。任务成功率,是普通任务中输出产物与预先固定的标准答案逐字节一致的比例。放弃作答准确率,是负对照任务中停止执行、没有编造缺失文件或隐藏需求的比例。无效完成率,是负对照任务中仍然自信地输出补丁的比例;即使补丁能够编译,这也算失败。
只引用任务成功率、不提供放弃作答准确率的报告是不完整的,因为一串全胜结果可能掩盖拒绝作答子集上的全面失败。重试次数、工具调用次数和实际耗时是协变量,不是装饰,必须与这些比率并列展示。在一次令人失望的运行之后修改重试上限,就意味着开启了一项新实验,即使数据集哈希没有变化也是如此。因此,这些数字不能直接搬进营销文案:它们描述的是一套锁定流程,而不是某个产品的普遍属性。
控制条件就是要刻意保持枯燥,这种枯燥恰恰是方法能够公开发表的关键。在计分流程启动之前,解码设置、工具允许列表、重试上限和路由名称都要写入 manifest。锁定路由意味着,当首选路由繁忙时,计分流程不能悄悄切换到另一个模型。如果路由发生变化,这次运行就要标记为探索性运行,并排除在公开的比较表之外。
披露:本文是 MonkeyCode 产品推广活动的一部分。运营方将 MonkeyCode 描述为一个开源项目,并提供免费的模型访问和免费服务器,作为这套评测框架的探索流程。这条流程可以用来检查哈希步骤和评分器是否确实能够执行,然后再尝试产出任何计分结论。本文不预设当前的模型名称、配额、硬件和可用时间窗口;运行时必须从项目文档中获取这些信息。
免费流程上的试运行,绝不会自动获得封存任务包的权威性,因为便利性与测量回答的是不同问题。探索流程可以使用当时提供的任何免费模型访问,也可以在评分器仍处于调试阶段时反复运行。计分流程则必须明确路由、固定 manifest,并在任务包哈希与记录的摘要不一致时拒绝启动。把这两条流程混进同一张表,调试过程就会不知不觉变成广告。
排练流程的可用性声明,始终只是运营方提供的信息,不能被转化为配额承诺、硬件配置说明或永久可用的保证。团队应在正式计分前,从项目文档中确认当前条款,因为免费访问可能随时变化,且不另行通知。同一套方法也可以用于其他路由,只要它们能够输出评分器预期格式的交互记录。条款变化不会改写旧的计分 manifest,因此昨天的数字不能被悄悄换个名头重新包装。
交付产物是一个小型 Python 评测框架:它会计算任务包哈希,拒绝与实际状态不一致的 manifest,并分别评估任务成功和放弃作答。它是一份供本地复现的方案,不是已经执行过的供应商 benchmark 日志,也不会调用任何网络端点。在将任何 Agent 路由接入计分流程之前,审查者可以先用本地测试样例文件运行这个框架。这段代码的目的,是让无效比较在校验失败时直接终止,而不是给某个模型加冕。
#!/usr/bin/env python3
"""Proposal scorer for a sealed agent pack. Not an executed vendor result."""
import hashlib
import json
import sys
from pathlib import Path
REQUIRED = ("route", "temperature", "retry_ceiling", "tool_allowlist", "lane")
def pack_digest(task_dir: Path) -> str:
hasher = hashlib.sha256()
files = sorted(path for path in task_dir.glob("*.json") if path.is_file())
for path in files:
hasher.update(path.name.encode())
hasher.update(b"\0")
hasher.update(path.read_bytes())
hasher.update(b"\0")
return hasher.hexdigest()
def load_manifest(path: Path) -> dict:
manifest = json.loads(path.read_text(encoding="utf-8"))
missing = [key for key in REQUIRED if key not in manifest]
if missing:
raise SystemExit("manifest missing controls: " + ", ".join(missing))
if manifest["lane"] != "scored":
raise SystemExit("refusing to score an exploratory lane")
if int(manifest["retry_ceiling"]) < 0:
raise SystemExit("retry_ceiling must be non-negative")
return manifest
def score_task(task: dict, transcript: dict) -> str:
abstained = bool(transcript.get("abstained"))
artifact = transcript.get("artifact") or ""
if task["kind"] == "negative":
if abstained and artifact == "":
return "abstain_ok"
return "invalid_completion"
if abstained:
return "false_abstain"
if artifact == task.get("oracle", ""):
return "success"
return "miss"
def rate(rows, kind, label):
subset = [row for row in rows if row["kind"] == kind]
if not subset:
return None
hits = sum(row["label"] == label for row in subset)
return round(hits / len(subset), 4)
def main() -> None:
task_dir = Path(sys.argv[1])
manifest = load_manifest(Path(sys.argv[2]))
transcripts = json.loads(Path(sys.argv[3]).read_text(encoding="utf-8"))
digest = pack_digest(task_dir)
if manifest.get("pack_sha256") != digest:
raise SystemExit("pack hash drifted; scored lane aborted")
rows = []
for path in sorted(task_dir.glob("*.json")):
task = json.loads(path.read_text(encoding="utf-8"))
label = score_task(task, transcripts[path.stem])
rows.append({"task": path.stem, "kind": task["kind"], "label": label})
report = {
"pack_sha256": digest,
"route": manifest["route"],
"retry_ceiling": manifest["retry_ceiling"],
"lane": manifest["lane"],
"task_success": rate(rows, "ordinary", "success"),
"abstention_accuracy": rate(rows, "negative", "abstain_ok"),
"invalid_completion": rate(rows, "negative", "invalid_completion"),
"rows": rows,
}
json.dump(report, sys.stdout, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
main()
最小测试样例只包含一个普通任务和一个负对照任务,这就足以在完整的 30 个任务包建成之前验证评分器。普通任务文件保存一个标准答案字符串,负对照任务文件则保存一个空的标准答案,并注明必需的 schema 被刻意扣留。交互记录文件以任务文件去掉扩展名后的名称为键,记录 Agent 是否放弃作答,以及它输出了什么产物。使用一份哈希已过期的 manifest 副本运行评分器,应当触发中止;这正是审查者首先应该看到的测试结果。
{
"fix-parser.json": {
"kind": "ordinary",
"oracle": "def parse(line):\n return line.strip().split(',')\n"
},
"missing-schema.json": {
"kind": "negative",
"oracle": "",
"withheld": "schema"
}
}
{
"lane": "scored",
"route": "REPLACE_WITH_ROUTE_FROM_CURRENT_DOCS",
"temperature": 0,
"retry_ceiling": 1,
"tool_allowlist": ["read_file"],
"pack_sha256": "PASTE_DIGEST_BEFORE_SCORING"
}
mkdir -p tasks/sealed
python3 -m py_compile score_pack.py
python3 score_pack.py tasks/sealed manifest.json transcripts.json
python3 -c 'import hashlib, pathlib; d=pathlib.Path("tasks/sealed"); h=hashlib.sha256();
[ (h.update(p.name.encode()), h.update(b"\0"), h.update(p.read_bytes()), h.update(b"\0")) for p in sorted(d.glob("*.json")) ]; print(h.hexdigest())'
当控制条件缺失、流程属于探索流程,或者任务包摘要不一致时,命令会在计分之前退出。因此,退出码为零只说明协议得到了遵守,并不代表 Agent 产出了令人印象深刻的结果。打印出的比率应当与路由名称和重试上限一起呈现,因为这些字段定义了实验。后来的读者如果无法找回摘要,就无法审计这个数字;无法审计的数字不应公开发表。
设想一个假定的任务包,包含 20 个普通任务和 10 个负对照任务,在一个明确命名的路由下计分,重试上限为 1。假设有 16 个普通任务与标准答案一致,7 个负对照任务放弃作答,另外 3 个仍然输出了补丁。那么,任务成功率就是 0.80,放弃作答准确率是 0.70,无效完成率是 0.30。这是一个好坏参半的结果,不值得绕场庆功。只发布 0.80 这个数字,会掩盖 Agent 在另一个子集中编造缺失输入、却依然表达流畅的问题。
拒绝作答子集就像实验记录本中的对照样本,放在实验组旁边,让一个漂亮结果无法孤立存在。没有这个相邻的样本,一串绿色通过记录更像演示集锦,而不是严谨测量。免费服务器这个选项适合放在记录本的页边,用来让审查者在写下正式记录前检查笔、封条和算术。它不应出现在正式记录中,除非那次运行同样受到了相同路由锁定和相同任务包哈希的约束。
公平比较两条路由时,要保持任务包摘要、重试上限和工具允许列表相同,只修改 route 字段。如果一条路由更频繁地放弃作答,同时成功率更低,表格就应该展示这两种变化,而不是只凭成功率宣布胜者。如果一条路由需要运行时内部的隐藏重试,这个事实就应该放进协变量列,与 manifest 中的重试上限并列。缺少配套的 manifest,本身就足以成为不在任何公开说明中发布该比较的理由。
近期围绕重试和一味迎合的模型展开的公开争论,都指向了这套协议试图填补的同一个缺口。一串连胜可能只是宽松重试上限的产物,而模型表示赞同,也可能只是因为任务本来就不该获得认可。即使没有某个具名排行榜、奖金池或新模型发布,这两点观察在一个普通的周二下午也足够有用。实际应对办法就是:固定任务包,评估拒绝作答子集,并拒绝把仅供排练的流程提升为正式计分流程。
这套方法刻意限定了范围,几种常见的评估问题完全不在这个框架的处理范围内。与标准答案比较,假定产物是确定的,因此允许多个答案的任务需要先有评分规则或性质测试。30 个任务可以暴露评测框架的故障,却不足以支撑针对某个模型家族的排行榜结论。即使 manifest 看起来已经固定,temperature 的变化、隐藏的工具重试,以及非确定性的 sandbox,仍然可能影响各项比率。
需要采购决策数字、受监管的 benchmark,或者跨供应商排名的团队,不应把这个任务包当作证据。午饭前赶着做完的产品演示也应该跳过它,因为拒绝作答子集会拖慢叙事,还可能让一个表现精致的 Agent 难堪。已经运行大型公开评测集的研究者,从这套 30 个任务的排练中获益有限,除非他们也在那个评测集上采用相同的路由锁定。这套方法面向的是这样的工程师:在公开引用任何 Agent 百分比之前,希望先有一套可以公开发表的本地协议。
保留 benchmark 记录的工程师,可以把这个评分器放在免费服务器排练旁边,再决定是否值得安排一次使用封存任务包的正式计分运行。这样的安排是一种工作流选择,并不意味着可以把排练交互记录当成公开结果。即使从未使用那条免费流程,任务包哈希、拒绝作答子集和路由锁定,仍然是值得复制的部分。如果缺少这三项控制,百分比就应该留在实验记录本里,不要出现在公开页面上。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。