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 布局。
对照 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 测试固定到上一个已知正常的哈希上然后遗忘它。这会把间歇性变成独立车道上的永久漏洞。
记录每个测试 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 之外运行第二个问题,先隔离语料库和账本;当规则是门而非供应商名称时,免费服务器就足够了。