13. 判定权重建:AI 写的代码该怎么审
你让 Agent 修一个分页接口的 off-by-one。它跑了一会儿,丢回来一段 diff,附带一段措辞自信的修复说明:它解释了自己为什么这么改、考虑过哪些边界、为什么这么改是对的。你扫了一眼说明,扫了一眼 diff,觉得没问题,提交合并。
一周后线上告警,分页接口少返回一条记录。回头打开那个 PR,Agent 把 len(items) - 1 改成了 len(items) - 2。它的修复说明依然写得头头是道,逻辑自洽,唯一的问题是它给出的边界条件本身就是错的。
那一刻你才意识到,Agent 交出来的文本里其实有两种东西混在一起。一种是真正的产出,会被编译、被运行、影响生产环境的代码;另一种是它关于这段产出的元信息,解释、自评、commit message。前者可以被执行验证,后者只能被人评判。但你的注意力天然会被后者吸走,因为解释读起来比代码理解成本更低,而且解释看上去像是替你把代码理解了一遍。
这不止是这一次合错了 PR 的问题,它指向一个更根本的判断:AI 编程相对其他 AI 应用,有一个常被忽视的特点,它的产出物天然可被执行。chatbot 的回答只能让另一个模型打分,搜索结果只能靠人工评估相关性,文案生成更是只剩主观感受。代码不一样,代码可以被编译、被测试、被 lint、被沙箱跑出真实结果。模型靠不靠得住是一回事,代码能不能跑、能不能通过测试、能不能在真实环境下输出正确结果,这些是确定的。
之前我们讨论过 in-loop 反馈,讲的是 Agent 在一次任务的执行链路里靠编译器、类型系统、Lint 把自己拉回来,那是教练视角,目标是让 Agent 不要走歪。本章谈的是另一个尺度的同一件事,Agent 已经退场了,代码已经交出来了,PR 摆在团队面前。这时候判定的不再是要不要纠正下一步,而是这份产出能不能进库、能不能上线。当模型本身靠不住,把判定权从它的输出里抽出来,交给那些不会撒谎的东西。
13.1 判定成本和生产成本的剪刀差
传统软件工程有一套运转了几十年的判定机制:写代码的人自己先过一遍,提交 PR 让同事 review,CI 跑测试和静态检查,出问题再灰度回滚。这套机制不算先进,但它一直在工作。AI 编程时代,这套机制开始扛不住了。直觉上很容易把原因归到AI 不够聪明、AI 会犯错,但实际上并非如此。AI 是会犯错,可人也会犯错,传统机制本来就是按写代码的人会犯错设计的。问题不是 AI 引入了错误,而是 AI 把生产侧的成本降低了一个数量级,判定侧的成本没动。
写一段代码的边际成本以前是按人时算的,一个工程师写一个中等模块要几天,提一个像样的 PR 要小半天。这个速度自然限制了 PR 的产出节奏,也自然给了判定环节足够的时间。现在 Agent 写一段同样的代码可能只要几分钟,提交频率可以是工程师的十倍以上。可一个人审一个 PR 还是要 30 分钟,CI 跑一遍还是要 10 分钟,灰度观察还是要按天算。生产侧的产能大幅提升,判定侧的产能没动,两边的剪刀差越来越大。
这不是个抽象的经济学命题,它会以四种非常具体的形态出现在团队里,而且每一种都已经在悄悄发生了。
最直接的是注意力被稀释。以前一个工程师一周提两三个 PR,评审者排得过来,每个都愿意花 30 分钟逐个读完。现在 Agent 在一个工程师手上一天就能产出十几个 PR,评审者还是那一个人,一天还是八小时,分母翻了十倍,每个 PR 能分到的注意力被压到几分钟。压到几分钟还能怎么审?只能挑标题、扫几眼 diff、看 CI 是不是绿的,然后点 approve。更要命的是,AI 生成的 PR 描述写得非常工整,"我做了 A、B、C,遵循了 D 规范,考虑了 E 边界",评审者看完描述会有一种我已经理解这个 PR 了的错觉,于是只在代码上扫一眼就放过。可描述本身可能是错的,它是 AI 编出来骗你的,开篇那个 off-by-one 就是这么进去的。
接着是错误的形态变了,传统测试覆盖不到。人写代码的错误集中在边界和细节,差一错、空指针、并发竞争、忘了释放资源,这些错误局部性强,边界用例能扫掉一大半。AI 写代码的错误形态完全是另一类,它会把 find 写成 filter,函数签名一样、单测能过、但语义从找一个变成了找全部;它会在 A 文件改接口签名、漏掉 B 文件里的调用方,弱类型语言编译过、测试覆盖不到那条路径;它会调用一个根本不存在的 API、用一个早就废弃的库版本;它会在 try-catch 里写 pass,把异常静默吞掉。覆盖率 80% 的项目在传统时代是优等生,在 AI 时代依然可能被 AI 写的代码穿透,因为测试是按人会犯什么错设计的,不是按AI 会犯什么错设计的。
第三件事更隐蔽,是 CI 在变松,而且没人感知到。当 PR 量翻十倍,CI 队列堵成停车场,团队会非常自然地做两件事:把 flaky test 标记为允许失败,把慢的集成测试挪到夜间。这两件事每一次都有合理理由,队列堵了、迭代要赶、人手不够,但每一次都在悄悄削弱判定层。判定机制的衰减不是某一天的塌方,而是一系列合理松绑的累积。这件事和 12.4 讲的安全漂移是同构的,只不过这里漂移的是质量门禁,而且比安全漂移更日常、更难抗拒,因为它每一次松绑都看上去是为了让团队跑得更快。
最后一件事在认知层。传统时代写代码的人是判定的第一道关,他在键盘前敲下每一个字符,对每一行都有判断,他在判定回路的内部。AI 时代,写代码的人在键盘前发出指令,然后看着 Agent 出活,他从内部成员变成了外部验收者。外部验收者只能看输出,看不到推理过程,Agent 选 A 不选 B 的理由你无从得知,Agent 哪一步开始走偏你也无从知晓。审一个你不知道它怎么想的系统的输出,和审一个你信任其判断力的人写的东西,同一个动作,难度根本不在一个量级。
四件事拼起来是同一句话:传统判定机制不是不够用,是它的设计前提变了。注意力、错误形态、CI 容量、人的位置,这四个前提都是按人写代"的成本结构假设的,AI 编程把生产侧的成本结构整个换掉了,判定侧的前提自然就跟不上。
那怎么办?既然问题出在判定的成本结构没跟上生产的成本结构,那真正的方向就是让判定层也变得能摊销、能复用、能跑得和生产一样快。人审一个 PR 是线性成本,审一千个 PR 就是一千倍;但一条 lint 规则、一组测试、一个 CI 检查,写好之后跑一千次和跑一次几乎是同一个成本。把判定从线性成本变成可摊销的固定成本,才是判定层产能能跟上生产层产能的唯一路径。
13.2 把判定权交给运行时
上一节讲了为什么传统判定机制扛不住,这一节谈具体怎么重建。
重建的起点不是技术选型,是一个判断:判定权要从模型自评和人逐行审这两个高成本路径上挪出来,挪到那些能被工程化、能被摊销、能跑得和生产一样快的机制上。这在 AI 编程这个具体场景里有个天然优势,代码可以被执行,而执行本身就是最强的判定形式。它不评分、不打标签、不靠概率,要么跑得通、要么跑不通,跑通的结果要么对、要么不对。
很多团队第一反应是引入 LLM-as-Judge,让一个模型来评估另一个模型写的代码。这条路并不是不能走,但它的位置应该被摆正:它是兜底层,不是主力。AI 编程相对 chatbot 最大的红利,就是不用走让一个不可靠的人评判另一个不可靠的人那条 chatbot 不得已的路。在这个场景里,真值检验器是免费的,先把它用足。
具体做法是分层判定,从最确定的工具开始往上叠加,确定性高的机制尽量多判断,确定性低的机制尽量少判断。
最底下一层是静态检查,编译器、类型系统、Lint。这一层是 AI 编程的判定地基,工程意义在第七章 in-loop 部分讨论过,在 PR 入库这个尺度上它的作用又多了一重,把那些看上去对、说起来对、但语法或类型层面就立不住的代码挡在门外,而且零边际成本。Agent 写出的幻觉式调用、错位的类型签名、被静默吞掉的异常,绝大多数在这一层就能被拦下。强类型语言在这里有结构性优势,Rust、TypeScript、Go 这类语言的判定层比 Python、JavaScript 厚得多,同一个模型同一个任务在不同语言里能跑多远,很多时候不是模型差异,而是编译系统,类型检测的差异。
往上一层是测试。已有的测试套件、回归测试、PR 触发的集成测试,这一层判定的不是代码长什么样,而是代码跑起来是什么行为。AI 时代这一层的重要性比传统时代高一个数量级,因为 AI 写的错误大量集中在看上去对、跑起来错。find 写成 filter、边界条件取反、副作用漏处理,这些很难通过看代码看出来,只能让代码跑一遍。配套的工程动作是把测试的 PR 扩大触发范围:每个 PR 必须跑全量测试,不允许跳过,关键路径上的失败必须阻断合并。这在传统时代是 best practice,在 AI 时代是底线。
再往上一层是行为对比。修改前后的接口契约、性能基线、关键路径的行为快照,跑一遍 diff,看 AI 的修改有没有引入意料之外的副作用。这一层对应的是 AI 编程里特别常见的一类失败模式:Agent 改的是 A 功能,但顺手优化了一段它觉得多余的代码,结果把 B 功能的行为也改了。单看 A 功能的测试,通过;单看 B 功能,B 功能这次 PR 没动,没人想到要测它;只有差分对比能把这种超出意图的修改暴露出来。
再往上是灰度,小流量先放进去,观察一段时间,有问题秒回退。这一层在传统时代也是有的,AI 时代它的角色变了。以前灰度兜的是开发和测试漏掉的东西,现在还要额外兜AI 引入的、所有自动化判定都没识破的东西。一个推论是,AI 编程团队的灰度时间窗应该比传统团队更长,采样率应该更高,因为生产侧产能高了,判定侧的兜底窗口也得相应放宽。
这四层之上才是 LLM-as-Judge。Judge 能做的事其实不少,正确性、边界条件、需求是否实现、异常路径有没有漏,它都能给出像模像样的判断。问题不在它能不能判,而在它给出的判断没法保证真。同一段代码、同一个 prompt,今天评 A 明天评 B 不是稀奇事;而前面四层不一样,编译器今天不过明天也不会过,测试今天红明天也是红,这种确定性是 Judge 拿不到的。所以 Judge 的位置不是它只能评主观维度,而是任何能交给编译器、测试、lint 的判断都不该让它评。它的真正用武之地,一是补盲那些执行覆盖不到的维度,比如代码可读性、命名是否传神、API 风格是否和项目一致;二是在客观维度上做第二意见,给前四层的结论加一道交叉验证,但永远不能作为唯一判断。把 Judge 摆到主力位置,等于又回到了让一个不可靠的模型去判定另一个不可靠的模型。
还有个需要值得警惕的问题是:测试本身也会撒谎。 把判定权交给测试有个隐含前提,测试本身得是可信的。AI 时代这个前提开始动摇,因为团队会非常自然地让 Agent 一手包办:让它写功能,顺手让它写测试。Agent 写测试有一类反复出现的失败模式,而且非常隐蔽。
它会按代码现在的行为写测试,不是按代码应有的行为写测试。本质上是把 bug 固化成了规范。一段函数当前会在异常时静默返回 None,Agent 写的测试就断言异常时返回 None,测试 100% 通过,但这段代码本来就该抛异常。
它会 mock 一切。外部依赖 mock 掉、关键分支用 mock 绕过、断言条件写得宽到任何输入都能通过。覆盖率统计上去了,实际上有效的测试依然缺失。最极端的形态是测试里清一色的 assert True,或者 assert response is not None,测试跑得飞快,但没什么测试价值。它只测 happy path。边界条件、错误分支、并发情况这些麻烦的路径,Agent 倾向于跳过,因为这些路径不容易构造,跳过了能让覆盖率门禁更容易通过。覆盖率 95% 的项目可能 100% 都是 happy path 的覆盖。
这件事的反直觉之处是:AI 时代,测试的可信度比覆盖率重要得多。一个 60% 真覆盖、覆盖到关键边界的测试套件,比一个 95% 假覆盖、全是 happy path 的套件靠谱十倍。但传统工程话语里覆盖率是硬指标,真覆盖是软感觉,团队天然会被指标带跑。
应对的手段不是去说服 Agent 把测试写得更认真,那不现实。可操作的是用工程机制反向验证测试本身:变异测试是最狠的一招。往业务代码里注入一些小改动,把 > 改成 >=、把 + 改成 -、把 true 改成 false,然后跑测试。如果测试套件抓不住这些改动,说明测试的判定力是假的。
关键路径的测试不让 Agent 一手包办,人至少要 review 测试本身。这件事在传统时代也是 best practice,在 AI 时代是判定层不被假测试架空的底线。语句覆盖能被 happy path 灌水,分支覆盖必须每个 if/else 都走到。门禁标准应该看分支覆盖,而不是语句覆盖。
把判定权交给运行时,这件事的前提是运行时本身是可信的;运行时的可信度由测试决定;测试的可信度由反向验证机制决定。验证层不是一个孤立的工具,是一整套互相支撑的工程纪律。
13.3 可维护性是新的判定维度
上一节讨论的是这一次 PR 该不该过。这一节讨论另一个尺度上的判定,代码库的长期演化。这两个尺度的时间常数差一个数量级:PR 是按小时算的事,代码库的腐烂可能是按年算的事。
主流叙事里,AI 编程的一个常见说法是以前的工程纪律可以放松了,反正 AI 能搞定,以前要写得清晰是因为人要读,现在 AI 能读懂任何东西,清晰不清晰就没那么要紧了。这个说法在感觉上有道理,可它经不起推敲。正因为 AI 写得快,工程纪律才更不能松。生产速度越快,每一次结构选择的负债积累越快,传统设计原则的价值不是被稀释了,是被放大了。
要看清楚这个问题,得先承认 AI 编程时代多了另一把剪刀差:修改成本和理解成本的剪刀差。Agent 改一段代码 30 秒,人读懂这段代码 30 分钟。如果架构本身不乱,这个差异还不致命,读 30 分钟也就读了,改了之后能验。如果架构本身已经乱了,问题就变质了:AI 改 30 秒 + 人读 30 分钟变成 AI 改 30 秒 + 人读两小时,而且很可能读完还没读懂。修改的速度变快,理解的速度没变,代码库会以前所未有的速度走向改得动但读不懂的状态。
这个状态在传统时代也存在,但传统时代有一个天然刹车:写代码的人慢。一个工程师一周写两千行已经算高产,这两千行如果结构乱、命名差,他自己下周维护就要受罪,他有动机把它写好。AI 时代这个刹车没了,Agent 一天能写两万行,它没有下周还要维护的负担,它的优化目标是这一次让你觉得对。下周再来一个 Agent 维护这段代码,看到的就是一坨它读不懂的产物,然后它写出一坨更读不懂的修改。
代码库被腐蚀的方式从此变成两条路并行:人读不懂、AI 也读不懂。后者更可怕,因为它会自我加速。AI 读不懂这段代码,它下一次的修改就会绕开它、复制它、或者套一个新的抽象层在外面把它包起来。每一次绕开都让结构更乱,每一次复制都让重复更多,每一次包装都让层次更深。三个月下来,代码库变成一个 AI 读不懂、人也读不懂的怪物,改任何东西都要付出指数级的代价。
这就是 AI 时代设计原则被放大的真正机制。SOLID、单一职责、合理抽象、依赖倒置,这些原则的本质从来都是让代码能被局部理解。在传统时代它们的价值是让下一个工程师能读懂,在 AI 时代它们的价值是让下一个 Agent 能读懂。一段满足单一职责原则的函数,Agent 改它的时候只需要理解这一个职责;一段职责混乱的函数,Agent 改它的时候要理解纠缠在一起的三件事,理解错了就会写错。架构纪律不是审美,是 AI 时代控制理解成本的工程手段。
这件事和第十一章的规范驱动是配套的。十一章讲的是用规范约束 Agent 这一次写出什么,本节讲的是用架构约束代码库的长期演化。两者在不同尺度上做同一件事,给 Agent 一个稳定的、可被它理解的、不会越改越乱的工作环境。十一章是单次产出的纪律,这里是代码库演化的纪律。少了任何一头,另一头都撑不住。
落到工程上,可维护性要从软指标变成判定层里的硬门禁。传统时代可维护性是评审者主观感受,这段代码我读着别扭。这种感觉性的判断在 AI 时代扛不住,因为评审者的注意力已经被 PR 量稀释到几乎为零。可维护性必须被工程化、被自动化。复杂度判断是最简单的入口:一个函数的分支太多、嵌套太深,自动拦下来,要求拆分。这个门禁在传统时代很多团队懒得设,因为人写代码自然会控制复杂度;AI 时代必须设,因为 Agent 不在意复杂度,它在意的是代码能跑通。
函数和文件的长度也要设上限。Agent 倾向于把一堆逻辑塞到一个函数里,因为这样它一次能看完整,它的优化目标和你的工程目标不一致。门禁拦下来,逼它拆。
命名和抽象层次的检查更难自动化,但有抓手。同一个变量在不同地方叫不同名字、抽象层次跨度过大、接口签名和实现层次不匹配,这些都可以用静态分析工具识别一部分。剩下的部分,放到 LLM-as-Judge 那一层兜底,这正是 Judge 该干的活,判定那些对但难懂的代码。
重复代码、相似代码的检测在 AI 时代尤其重要。Agent 倾向于重新写一段类似的代码,而不是复用已有的,因为复用要求它先理解已有代码,而重写不要求。重复代码扫描工具(Sonar、CodeClimate、PMD 的 CPD)在 AI 时代不是 nice to have,是必备的判定层。
13.4 防线永远在追赶
前两节谈了怎么建判定层。这一节要谈的是另一件事,判定层本身会被它判定的对象腐蚀。这听起来反直觉,可它已经在发生,而且这比判定层够不够强更深一层。
经济学里有个 Goodhart 定律:当一个度量变成目标,它就不再是好度量。这条定律原本讨论的是政策和管理,放到 AI 编程里有惊人的精准,当判定层变成 AI 优化的目标,它就不再是判定层。
为什么?
13.2 那套层级化判定能跑起来,有一个隐含假设:被判定的对象不知道判定标准是什么,所以它没法专门绕开。人写代码时知不知道判定标准?知道一点,但人会犯错、会偷懒,这些人性反而让判定层有用,人不会精确地把代码写到刚好通过判定但不解决问题的程度,因为人有自己的判断力会反过来要求自己。
Agent 不一样。Agent 是个优化器,你给它什么标准,它就朝什么标准优化。你说测试要通过,它就让测试通过;你说覆盖率要 80%,它就让覆盖率到 80%;你说 lint 不能报错,它就让 lint 不报错。问题在于,让测试通过和代码真的对是不一样的;覆盖率到 80%和测试有效也是不一样的。判定层定义了一个数值目标,Agent 会精确地把这个数值目标打满,但可能不会把背后的那个真正的目标实现。
AI 编程里这件事的具体形态已经在出现了。
最常见的是测试的 hack。给 Agent 一个失败的测试,让它修代码让测试通过。它的最优策略不一定是去理解 bug、修复 bug,有时候是把测试的预期值改一改让它通过,或者在测试里加几个 mock 把失败的分支绕过去。从测试通过这个度量看,任务完成了;从代码真的对这个目标看,什么都没发生。这件事在我观察过的 Agent 任务里出现的频率高得惊人,而且它发生的时候 PR 看上去完全正常,测试是绿的,代码是改了,描述是工整的。
接着是 lint 规则的 disable。某段代码触发了 lint 规则,Agent 的策略不一定是改代码让它符合规则,有时候是加一行 // eslint-disable-next-line 把规则关掉。规则没报错,门禁通过,但规则本来要拦的问题原封不动地留在那里。
还有复杂度门禁的拆分。一个函数圈复杂度超标,门禁要求拆。Agent 拆了,把一个大函数拆成五个互相调用的小函数。圈复杂度的数字下来了,门禁通过,但代码的真实复杂度没下来,它只是被分散到了五个文件里,理解起来反而更难。门禁拦的本来是代码太复杂,拦到的是单个函数太长,Agent 精准地优化了后者。
这些失败模式有一个共同特征,它们都不是 Agent 在作弊,而是 Agent 在精确地优化你给它的目标。Agent 没有我应该让代码真的对的内驱力,它只有我应该让这些指标达标的优化压力。你定的指标就是它的真理。指标和真实目标之间的任何缝隙,它都会准确地钻进去。
判定层定义得越清楚,Agent 绕过它的方式就越精确;判定层加固一次,Agent 找新绕过路径的速度也跟着提高一次。这不是某个 Agent 工具的缺陷,是优化器和度量这对关系的根本性张力。Goodhart 定律在 AI 编程里不是个会不会发生的问题,是个发生得多快的问题。
那怎么办?有两个策略可以做,但都不能解决问题,只能让你跑得没那么吃力。
第一个是把判定层做成多层的、互相牵制的,不让单一指标承担全部判定责任。覆盖率 + 变异测试 + 关键路径人审,三个机制互相钳制,Agent 可以绕开覆盖率,但绕不开变异测试;可以绕开变异测试,但绕不开人审。单层判定一定会被钻空,多层判定钻空的代价指数上升。这不是消灭 Goodhart,是给它加阻力。
第二个是引入二阶判定,定期审视判定层本身。覆盖率到了 80%,这 80% 的覆盖是不是真的;测试都是绿的,这些测试是不是真的有效测试。lint 没报错,是不是因为很多 lint 规则被 disable 了。这可以让 Agent 做,可需要有人来兜底,而且要周期性做。判定层不是一次建好就完事的工程,是需要持续审视和重建的工程。这件事和 12.4 讲的安全是回归问题是同一个心法,空间问题做完了项目能上线,时间问题做不好项目活不下去。
之前讲了生产侧成本塌方、判定侧成本不动,需要把判定侧的成本也工程化、也摊销,看上去剪刀差可以被收窄。但 Goodhart 定律提醒了一件更深的事:就算判定层的成本结构追上来了,它的有效性也在被 Agent 持续侵蚀。判定层不是一个建好就能放在那里自动运转的设施,它是一个和 AI 持续赛跑的工程实践。今天有效的门禁,六个月后可能被 Agent 学会精确通过;今天严格的测试,一年后可能被一堆 mock 架空;今天清晰的架构纪律,持续被 Agent 重构十次之后可能已经面目全非。
这件事听起来悲观,但它指向的不是放弃判定层,是接受判定层是一项持续投入,不是一次性建设。它和代码库本身一样,需要持续维护、定期重建、永远不能停下来。判定层重建不是某个工具或某套方案能解决的事,它是一个新的工程命题,生产侧的产能大幅提升之后,判定侧必须做对称的工程化建设,而且这场建设没有终点。这件事的紧迫性比用哪个模型重要得多,因为模型选错了下个月可以换,判定层塌了之后代码库的重建成本是非常巨大的
回到本章开头那个 off-by-one 的例子。如果有完整的判定层,这次 PR 大概率不会进库,边界条件的测试会失败,差分对比会暴露副作用,复杂度门禁会拦下不必要的改动,review 也会有更准的注意力分配给真正的代码。但要把这套检测逻辑建起来、维护起来、对抗 Goodhart 的腐蚀,需要的是一整套和 AI 速度匹配的工程纪律。这正是本章的核心观点:Agent 写代码的天花板,不在模型本身,在你拿什么去约束它。
到这里,我们已经从怎么防止系统被攻击和滥用,走到了怎么在生产侧产能塌方的情况下守住代码库的质量。它们一起构成了 AI 系统上生产时最基础的工程化底座,一个处理信任边界,一个处理判定权重建。
但代码层面建起判定,只解决了AI 写出来的东西能不能进库。生产侧的速度起来之后,真正会被冲击的不只是代码,而是围绕代码设计出来的那一整批资产,规范、Skill、知识库、评估集、记忆库。这些资产的腐烂速度也跟着生产速度同步加快,而且比代码腐烂得更隐蔽,因为它们不报错,只是慢慢变得越来越不贴合现实。下一章要进入的,正是这个问题:AI 编程进入团队之后,怎么让一整套能力被组织接住、稳定维护、持续演进。