上一篇审的是计划,在代码写出来之前。这一篇审的是已经躺在你面前的 diff:要不要留下它。
麻烦在于,你平时判断“这里得仔细看看”,靠的是一个很具体的信号——读起来别扭。名字奇怪、注释含糊、风格和别人不一样、留了个 TODO,这些痕迹说明作者在这里卡过壳,卡壳的地方通常就是 bug 藏身处。AI 生成的代码把这个信号抹平了:命名统一、格式干净、该有注释的地方都有注释,它写对和写错的时候是同一种语气。所以“我看着没问题”这句话,在一份 AI 写出来的 diff 上几乎没有信息量。不是你看得不认真,是这个信号本身失效了 3。
要换的问题是:这段代码在假设什么,而这些假设谁承诺过? 它比“看起来对吗”好用,是因为它指向可以核实的东西,而不是你的感觉。
一、先看它改了什么,而不是先读逻辑
第一个动作是打开 diff,不是打开文件。模型经常动手改它并不需要改的地方——重排一个工具函数、删掉一个它觉得没人用的常量、顺手“简化”一处分支。只读最终文件,这些改动是隐形的。
先看文件列表。一个说好改两个文件的任务动了九个文件,这件事本身就是一条发现,在你读第一行逻辑之前就该记下来:多出来的七个文件,改动理由是什么?答不上来的部分拆掉。
然后先读减号,也就是被删掉的行。这是整套动作里收益最高、也最容易被跳过的一步。模型删掉一个边界判断或一个前置守卫,产出的是一个读起来像“简化”的 diff——新增的那几行里没有任何线索提示你少了什么,而少掉的分支会在线上以别的方式回来。看新增容易,看删除需要刻意,所以顺序要定死。
第三步,把测试文件的改动单独拉出来看一遍。实现和测试同时改是正常的,断言变松不是。assert count == 3 被改成 assert count > 0,夹在五十行实现改动里你多半扫过去看不见;单独拉出来,它一眼就露出来。
这三步排在最前面,理由很单纯:它们不需要判断力,只需要看得见。做完之后,后面要仔细读的范围往往已经缩小了一圈。
二、再核对它对项目的每一句断言
计划里的“复用现有函数”是断言,代码里的 import 也是断言,而且更好核实。
每一个不熟悉的依赖,去包仓库确认它真实存在。 这不是小概率事件:一项覆盖 16 个模型、223 万个被推荐包的研究测得,其中 19.7% 的包是虚构的,商业模型约 5.2%,开源模型约 21.7% 4。更麻烦的是它不是随机噪声——同一项研究发现,用同一个提示再问一遍,约有一半的情况会再次给出同一个不存在的包名。也就是说,你再看一遍、再问一遍,它不会自己消失,只会显得更可信。
方法签名要对着官方文档看,不要对着它的解释看。 它的解释和它的代码来自同一个生成过程,一起错是常态。有个可操作的习惯:抽它总结里的一条事实性断言去核实,如果这条是假的,其余部分要重读一遍——这两件事的可信度是绑在一起的。
它引用的配置项和环境变量,真的存在吗? 这类问题最阴,因为开发环境跑不出症状:配置读不到会走 fallback 分支,程序照常运行;等配置真的加上去,或者别人加了一个名字相近的 key,行为才变。
三、它最常替你拍板的三处
上面两类问题都能直接核实。还有一类是它替你做了决定,而你没意识到这里有决定,集中在三个地方。
共同原因是同一个:这三件事通常不写在你的需求里。你不提,模型就必须填一个值,它填的是最像样的默认值,不是最安全的默认值。这不是它偷懒,是它的工作方式就这样——回到第一篇那个问题,正确性由谁定义。
边界。 模型写的是演示用例:三条记录、每个字段都在、每个请求都返回 200。你要问的是每个函数“它能合法收到的最丑的输入长什么样”——空列表、零字节文件、page=0、缺字段的响应、超长字符串。这些不会报错,只会悄悄给出一个错的结果。
错误处理。 两个极端,都属于“写了一个把错误藏起来的动作”:一是把异常吞掉返回默认值(except Exception: return {}),二是包一层 try/catch 只打日志。危害不在于出错时没处理,而在于错误被推迟到离源头很远的地方才炸出来:调用方拿到 {},理解成“这个用户不存在”继续往下跑,最后在距离原始问题八百里的地方空指针。判断只有一句:这个 catch 是在处理错误,还是在隐藏错误?
权限和信任边界。 这里有份很直接的实验证据:能使用 AI 助手的被试,在五个编程任务里的四个写出了安全性更差的代码,同时更倾向于认为自己写得安全;其中一个任务里,用 AI 的一方有 36% 写出了可被 SQL 注入的实现,对照组是 7% 5。原因不难还原:模型并不恶意,它只是照着你的话做。你说“加一个按 id 查订单的接口”,它就把接口写出来,跳过你没提的归属校验——在它看来那不是缺少,而是你没要求。
所以凡改动涉及认证、钱、用户数据,就不要靠扫一眼,把这些方法单独列出来逐个问:绕过界面直接调这个接口会发生什么?这条规则由谁强制?答不上来,说明这段还没准备好合并 2。
四、要证据,不要解释
“测试全部通过”是一句话,而且是由生成代码的同一个过程写出来的。自己跑一遍,并注意用例数:它应该往上走了,而不是原地不动或少了几条。还有个很便宜的做法能验证测试是否真的有效——把被测的一行逻辑改错,看测试会不会失败;改了逻辑测试还绿着,这个测试是假的。
另外,diff 大到读不完,问题出在任务规模,不在你的耐心。一份你读不完的改动,最后一定会被点通过。这时候正确的动作不是更努力地读,而是让它拆开 2。
五、这套动作适用于哪里
它面向要留下来的代码。一次性的、跑完就丢的脚本不需要这么走流程——但如果你已经开始说“先这样,以后再收拾”,它就已经不是一次性脚本了。
机器能做的部分应该先交给机器:格式化、lint、类型检查、CI 里的静态扫描、第二个模型的自动 review。它们在“局部可以穷举”的问题上比你可靠,也省时间。但它们判断不了“这是不是你要解决的问题”,也判断不了你自己项目里的约定——这些信息只有你有 1。合理的分工是:让机器跑在前面清掉形式问题,把你的注意力花在意图、范围和那些被默默填上的默认值上。