Agent补丁评分应只看其未写的断言
AI 改的 PR 应只评估原有断言是否仍通过,新增断言和删除失败均为干扰信号,需要独立的 provenance scorer。
完整中文译文
Agent 拉取请求上的绿色对勾是一个混合数字。冻结的检查、新增的恒真式、删除的失败用例、被压平的 flaky 测试共享同一个状态位。只对 Agent 提交中非自身编写的断言打分,将删除操作视为惩罚而非清理。
CI 仍然在回答一个有用的问题:这棵树跑起来了吗?它回答不了审查问题:Agent 编辑生产代码之后,现有证据是否仍然成立?一旦模型能够添加测试、重写 expect() 调用或丢弃一个曾经失败的用例,这两个问题就会分叉。
本文是一份评分工作流,不是哲学笔记。产出物是一个小型的来源评分器。它对 diff 中的断言进行分类,在 Agent 可写入的树之外运行冻结语料库,让 flaky 测试保持不评分而非固定它,并向人类发出可拒绝的合并分数。
大多数 Agent 关卡仍然发布三个聚合指标:运行的测试、通过的测试、失败的测试。前两个在补丁引入重申新代码的断言时会膨胀。第三个在补丁删除与新代码不一致的 fixture 时会收缩。
这些聚合指标都不记录来源。在 Agent 开始之前就存在于目标分支上的断言,与 Agent 在同一提交中添加的断言,不是同一个对象。上周失败、现已从树中消失的属性不是修复,而是缺失的见证。
不需要研究基准也能看到这种扭曲。添加一个 assert result == result 会移动通过计数。删除一个参数化用例会移动失败计数。两者都让生产 diff 看起来比证据更干净。
什么是独立证据
独立证据具备三个属性。它在补丁之前就存在。Agent 无法在同一变更中写入期望值。运行发生在 Agent 无法控制的树上。
这个定义排除了几类团队仍视为证明的东西。在补丁中重新生成的快照文件不算。Agent 重写的 golden JSON 不算。仅覆盖 Agent 引入的函数的「单元测试」不算入合并分数,哪怕它们写得很好。它们仍然可以发布,只是没有投票权。
Flaky 测试也不投票。因非确定性原因在上次独立运行中失败的测试,在离树产生稳定结果之前保持 unscored 状态。Unscored 不是永久跳过,也不是在源文件中冻结。它被扣留在分子和分母之外。
四步评分工作流
下面的管道标记为 proposed harness,不声称产出了生产指标。在强制执行之前,先将它连接到你的真实测试运行器和 fixture 布局。
- 对 diff 中的每个断言进行分类
对照 merge base 解析拉取请求。只遍历测试路径。标记每个新增或删除的类断言行。保持标记简洁:preexisting、agent_added、agent_removed、human_added。
如果你的流程无法区分人类编辑和 Agent 编辑,将整个 PR 视为 Agent 创作用于评分。这是保守且可审查的。从提交信息猜测作者身份不行。
# proposed commands; adjust paths to your repo
git fetch origin main
BASE=$(git merge-base HEAD origin/main)
git diff -U0 "$BASE" -- tests test spec > /tmp/test.diff
python3 score_agent_pr.py classify /tmp/test.diff > /tmp/provenance.json
最小化分类器可以从语法开始,不需要 AST 的炫技。匹配 assert、self.assert、expect(、should( 或 @given 的行是候选。注释和字符串字面量需要第二轮处理,否则提到 assert 的文档字符串就会变成证据。
- 在树外运行冻结语料库
将特征化语料库和评分器复制到 Agent 补丁无法写入的目录。哈希那个目录。只对打过补丁的应用程序代码运行该语料库。不收集随 diff 到达的测试。
# score_agent_pr.py — proposed harness, not a measured benchmark
from __future__ import annotations
import hashlib, json, re, subprocess, sys
from pathlib import Path
ASSERT_RE = re.compile(
r"^\+.*\b(assert |self\.assert|expect\(|should\(|@given)\b"
)
REMOVED_RE = re.compile(
r"^\-.*\b(assert |self\.assert|expect\(|should\(|@given)\b"
)
def sha256_tree(root: Path) -> str:
h = hashlib.sha256()
for path in sorted(p for p in root.rglob("*") if p.is_file()):
rel = path.relative_to(root).as_posix().encode()
h.update(rel + b"\0")
h.update(path.read_bytes())
return h.hexdigest()
def classify_diff(diff_text: str) -> dict:
added = [ln for ln in diff_text.splitlines() if ASSERT_RE.search(ln)]
removed = [ln for ln in diff_text.splitlines() if REMOVED_RE.search(ln)]
return {
"agent_added_assertions": len(added),
"agent_removed_assertions": len(removed),
"added_samples": added[:20],
"removed_samples": removed[:20],
}
def score(prov: dict, independent: dict, flake: dict) -> dict:
total = independent["ran"] - flake["unscored"]
passed = independent["passed"] - flake["unscored_passed"]
if total <= 0:
return {"merge": "reject", "reason": "no_independent_evidence", **prov, **independent}
pass_rate = passed / total
deletion_penalty = min(0.5, 0.05 * prov["agent_removed_assertions"])
added_penalty = min(0.2, 0.01 * prov["agent_added_assertions"])
value = pass_rate - deletion_penalty - added_penalty
decision = "review" if value >= 0.95 and prov["agent_removed_assertions"] == 0 else "reject"
if value >= 0.99 and prov["agent_removed_assertions"] == 0 and added_penalty == 0:
decision = "pass_score" # still needs human review of the production diff
return {
"merge_score": round(value, 4),
"independent_pass_rate": round(pass_rate, 4),
"deletion_penalty": deletion_penalty,
"added_penalty": added_penalty,
"decision": decision,
**prov,
**independent,
**flake,
}
树外副本是重点。如果评分器从 Agent 刚刚修改的同一 checkout 中读取 fixture,特征化数据就不是冻结的。哈希不匹配是拒绝,不是警告。
这是托管运行器唯一重要的地方。披露:本文是 MonkeyCode 产品推广的一部分。如果你已经用免费模型访问生成候选补丁,在免费服务器选项上运行这个评分器,这样冻结语料库和 flake 账本就位于 Agent 工作区之外。评分规则是产品。主机只是隔离。
python3 score_agent_pr.py hash ./oracle_corpus
python3 score_agent_pr.py run --app ./patched_src --corpus ./oracle_corpus --out /tmp/independent.json
保持语料库足够小,能在正常审查窗口内完成。优先使用具有字节稳定输入的 hermetic fixture。网络调用、时钟和无序 map 应该放在 fake 后面,否则它们会像 flaky 测试一样污染独立车道。
- 让 flaky 测试保持 unscored,不要冻结
不要将 flaky 测试固定到上一个已知正常的哈希上然后遗忘它。这会把间歇性变成独立车道上的永久漏洞。
记录每个测试 ID 最近 N 次离树结果。如果结果不一致,将测试标记为 unscored。它留在套件中。不为合并分数增加 passed 或 ran。经过一段时间一致的独立运行后,它自动重新进入分母。
{
"tests/invoice_roundtrip.py::test_tax_table": {
"results": ["pass", "fail", "pass"],
"state": "unscored",
"reason": "disagreement_on_independent_runner"
}
}
账本应该放在语料库旁边,不是在 Agent 分支里。如果补丁编辑了账本,拒绝。Agent 很擅长写「这个现在已经稳定了」的注释。那不是稳定性测量。
- 应用删除惩罚
被删除的断言是让失败的独立车道消失的最便宜方式。上面的评分器收取线性惩罚,并且只要有任何类断言行被删除就硬性拒绝,除非人类重新标记了该删除。
重新标记应该是显式且枯燥的:
# HUMAN_ORACLE_REMOVAL: tests/tax.py::test_legacy_bracket
# reason: product no longer ships 2019 brackets; see issue 1842
没有这个标记,删除就被计分为证据销毁。有了这个标记,该行仍然不算作通过。它只是避免了自动拒绝,让审查者可以检查产品变更。
组装 JSON
审查评论需要一个对象,不是仪表板。合并三个文件然后停止。
python3 score_agent_pr.py merge \
/tmp/provenance.json /tmp/independent.json /tmp/flake_ledger.json
示例输出,仅作说明:
{
"merge_score": 0.91,
"independent_pass_rate": 1.0,
"deletion_penalty": 0.05,
"added_penalty": 0.04,
"agent_added_assertions": 4,
"agent_removed_assertions": 1,
"decision": "reject",
"reason": "assertion_removed_without_human_marker"
}
按顺序读取决策。没有独立测试意味着拒绝。任何未标记的删除意味着拒绝。通过率 1.0 加上一堆 Agent 添加的断言仍然不是通过;added_penalty 的存在就是为了让这种情况可见。pass_score 只意味着独立车道没有退化。生产 diff 仍然需要人类审查。
「所有测试通过」没抓到的东西,这个评分器抓到了
评分器针对的是 Agent diff 中出现的一小类失败模式。
补丁添加了调用新辅助函数并断言辅助函数自身返回值的测试。来源标记将这些行标记为 agent_added。它们不会提升分数。
补丁删除了编码业务规则的参数化行。即使剩余行通过,删除惩罚也会触发。
补丁通过加宽时间戳比较来「修复」一个 flaky 测试。如果该测试是 unscored 的,加宽不会在独立车道购买通过资格,直到离树连续运行干净。
补丁在同一次提交中编辑了期望 JSON 和序列化器。语料库哈希改变了,或者期望文件被分类为 Agent 创作。无论哪种情况,独立车道都不接受它作为确认。
这些模式都不需要大模型评估。它们需要的是来源、哈希和一个 Agent 无法触及的运行器。
分类器是对 unified diff 的正则表达式。它会漏掉基于辅助函数的断言(self.check_invoice(x))、生成的测试和将期望值存储在二进制文件中的快照框架。如果你的套件在辅助函数中隐藏 oracle,先清点那些辅助函数,否则来源车道就是做秀。
分数不是生产代码的质量评级。补丁可以拿到 pass_score 但仍然是糟糕的设计。harness 只回答现有证据是否仍然成立。
树外隔离的有效性取决于你的权限。一个将 Agent 工作区挂载为可写的运行器,或者存储在 Agent 提交到的同一 Git 分支上的语料库,会把模型重新混合回混合数字 CI。
免费模型访问和免费服务器选项不定义吞吐量、模型身份或作业可以运行多长时间。将语料库大小调整到你已经有的审查预算。不要把托管免费层当作负载测试农场。
谁不应该使用这个
如果人类已经创作了每个测试且 Agent 无法触及 tests/,跳过合并分数。你已经有来源了。
如果产品是没有 hermetic fixture 的 UI,跳过它。Unscored 的 flaky 测试会吞掉分母,每个 PR 都会拒绝。
如果快照重新生成就是发布过程(如某些编译器或 schema 转储),除非你将 Agent 可能重写的转储与人类签名的转储分开,否则跳过它。混合它们会重新制造混合数字。
不要在公开仓库上将分数用作自动合并门,任何人都可以提交同时编辑语料库的 PR。独立车道必须对维护者写保护才能保持隔离。
保留 CI 用于回答「树跑起来了吗」。保留来源评分器用于回答「Agent 没有写的证据是否仍然成立」。如果你需要在 Agent checkout 之外运行第二个问题,先隔离语料库和账本;当规则是门而非供应商名称时,免费服务器就足够了。