作者清点自己代码库中204个自动化检查,仅11%用过已知坏输入验证过;其余89%一直是绿的,但无法证明是因为真的没问题还是根本检不出问题。
我想反驳 Michael Amachree(@dev_michael)的「AI 让我成了一个更差的评审」一文。结果我自己数了 204 个自动化检查——其中 89% 从未被要求证明自己能够失败。
Michael 写了一句话,让我无法释卷:AI 没有让我成为一个更差的程序员,它让我成了一个更差的评审。这里有个数字,比他的论点更糟糕:在我的仓库里 204 个得出结论的自动化检查中,只有 22 个能够证明它们可以失败。比例是 11%。其余 89% 从未被展示过一个已知为坏的输入。它们是绿色的。之所以绿色,是因为一切正常,还是因为它们根本没有能力发现任何问题——上周的我无法回答。而这些检查是我自己写的。
先给定义,这样你可以拒绝它或复用它。
带结论的检查(conclusion-bearing guard)指的是任何读取源代码、配置或系统状态,并对其做出断言的测试。不是「这个函数返回 4」——而是「没有工作流通过网络下载缓存」「每个页面使用相同的季度过滤器」「这个功能开关与已部署的规格匹配」。这些测试替代了人工评审。
阴性对照(negative control)是一个探针,向检查输入一个已知为坏的输入,并断言它因为预期的原因被拒绝。我们的惯例是用 KONTROLLE 标记它们:写在测试名称里。
计数是机械性的:三个仓库共 204 个检查文件,其中 22 个至少有一个对照探针,总计 54 个探针。计数器是一个代理——基于标记的,所以未标记的对照和假阳性的检查文件会使真实数字上下浮动几个点。任何修正都会保留这个形状:我的大多数评审从来没有被评审过。
这不是理论。以下三件事都发生在我过去七天的生产工具中。
死于自己药的部署门。一个管道步骤专门用来捕捉一种静默失败模式——工具缺失时回退到空结果。它调用 node -e 来解析健康检查响应。部署运行器没有 Node。六次连续部署失败,退出码 127——检查缺失工具的步骤在缺失工具上失败了,六小时没有任何东西发布。这个步骤在评审中是绿色的,因为没有人曾在它实际运行的地方运行过它。
丢弃了自己工作的收割机。一个自动作业从公共仓库收集数据,并根据退出码评判每次运行。有一次运行写入了七条完全有效的记录,然后遇到一个非致命警告并以非零退出。机器把自己完成的工 BOOK 为「失败,稍后重试」——因为「中断-有部分结果」没有表示,只有成功和失败。我们发现它是因为结果文件就坐在磁盘上,紧挨着否定其存在的退出码。
匹配了错误 500 的模式。一个错误分类器用模式 50[024] 查找服务器错误——在输出的任何位置。它匹配到了「500 of 5000 quota points remaining」里的「500」,把一次成功的运行分类为服务器失败。它读取的每个字段都是真实的。它回答的是与提问不同的问题。
三个不同的系统。同一种形状:检查盯着 messenger——退出码、模式、状态——而重要的 artifact 讲述了另一个故事。
这是我认为 Michael 的文章比他所说的更扎心的地方。
AI 移动了我的工作。我曾经大部分时间花在生产 artifact 上,少量时间验证它们。现在 agent 生产了大部分 artifact,我的工作是验证。这意味着我真正的代码库——我的判断实际通过的那个——是那 204 个检查。
而那个代码库所适用的标准,如果放在应用代码里我绝不会接受。没有测试覆盖率(11%)。没有对评审者的评审。绿色是默认状态,沉默被记为成功。
当 Michael 说 AI 让他成为一个更差的评审时,我会把它尖锐化:AI 让我们所有人都升格为评审,而没有人测试过评审。模型不是薄弱环节。不可证伪的绿色对勾才是。
以上所有内容压缩成一句话,我们现在机械地应用:
判断 artifact,而不是 messenger。
退出码是 messenger。摘要是 messenger。agent 自己的「完成」是 messenger。绿色徽章是 messenger。artifact 是 diff、磁盘上的文件、提供服务的响应体、数据库中的行。当 messenger 和 artifact 不一致时,artifact 是对的——而一个只读取 messenger 的检查应该被视为未经验证的,无论它有多绿。
检查的推论:绿色零是检查能给出的最危险的答案。「未发现违规」和「无法发现违规」产生相同的输出。只有阴性对照才能区分它们。
这是你可以不信我也能用的部分。把它放到仓库根目录——它计算读取源代码或状态的测试文件,以及多少个带有标记的阴性对照(根据你的惯例调整标记):
// count-controls.mjs — node count-controls.mjs
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
const files = [];
(function walk(d) {
for (const n of readdirSync(d)) {
if (n === "node_modules" || n === ".git" || n === "dist") continue;
const p = join(d, n);
statSync(p).isDirectory() ? walk(p) : /\.test\.(t|j)sx?$/.test(n) && files.push(p);
}
})(".");
let guards = 0, withControl = 0, probes = 0;
for (const f of files) {
const t = readFileSync(f, "utf8");
if (!/readFileSync|readdirSync|execSync/.test(t)) continue; // "reads state" proxy
guards++;
const n = (t.match(/KONTROLLE|negative.control|can.?not.?find/gi) ?? []).length;
if (n) withControl++;
probes += n;
}
console.log(`${guards} conclusion-bearing guard files · ${withControl} with a negative control (${guards ? Math.round(100 * withControl / guards) : 0} %) · ${probes} probes`);
如果你的数字超过 30%,我真的很想知道你是怎么做到的——这是我希望在下面看到的讨论。
写这些文章的规则 2 是主动纠正自己,所以在:
构建这篇文章数据来源的功能时,我的等价测试失败了恰好 0.25——而 bug 在我的测试中,不在代码里:min-max 扩展把一列 0 变成一列 0.5 并加上一个常数。我在中间测量那个失败类时,构建了一个探针来回答一个与被问问题不同的问题。
在同一个小时内,一次推送发出了红色测试——因为 npm test | grep 用 grep 的退出码替换了测试的退出码。我的管道读取了一个 messenger。artifact——那个失败的测试——就坐在那里。
告诉你测试你的评审的人,在写这篇文章时没能测试他的评审,两次,同一个晚上。这不是反讽。这是基准率,这就是为什么惯例胜过自律。
一个开发者,三个仓库,一周——这是案例系列,不是样本。11% 是基于标记的且近似。而且我没有证明提高伪证覆盖率会改善下游结果;我只证明了在 11% 的情况下,我无法区分我的工作检查和我的装饰性检查。我不知道重要的数字是 30% 还是 80%——我们在提高我们的数字并边走边测量。
还有一个合理的反对意见:阴性对照本身是会腐坏的测试。对。但是一个腐坏的对照会在检查改变时大声失败——这就是使它们值得编写的非对称性。
所以:你的比率是多少?更有趣的是——你管道中最绿色的检查是什么,你现在怀疑它从来没有能力失败?
我构建了 cachly——AI 编程助手记忆工具,基于 MCP。ChatGPT 和 Claude 记住你的对话。Cachly 记住你的系统:你修的 bug、为什么选择 Postgres、总是坏的部署步骤——以及它与早期哪个决定矛盾。你使用的每个助手都读相同的记忆,每条经验都带有学到它的人的名字——所以没有人需要学两次。
免费层级,托管在欧盟:cachly.dev