1.3 代码审查:AI 写的代码能直接用吗
用 AI 写代码一个月后,我遭遇了第一次差点上生产的事故。一个生成的不重命名脚本,函数名写对了、逻辑也对,就是 os.rename 的目标路径少拼了一层目录——在测试目录里跑没毛病,换到正式照片库,三个文件被改名到了错误的位置。那天我花了两个小时写脚本把文件名改回来,也确立了一条规矩:AI 写的代码,过审查才能进项目,没有例外。事后复盘,问题出在我对生成代码有种不自觉的信任惯性——它前面九次都跑对了,第十次我就不再逐行看。机器不会累,但人会,这正是需要机制而不是意志力的原因。
这篇讲的就是这套审查机制怎么落地:哪些看一眼就行,哪些必须逐行读,以及怎么用自动化工具把机械的部分交给机器。
场景:不敢合并的初版代码
团队的实际情况是,我一个人维护工具箱,没有专职审查员。如果每段生成代码都逐行精读,AI 省下的时间基本赔回去一半;如果完全不看,迟早出事。所以我需要的不是更严格的眼睛,而是一个分级机制——把有限的注意力投到风险最高的地方。机器的部分交给机器,人的注意力留给真正的判断。
我先按脚本被谁使用、跑在哪、能否回滚,给生成代码分了三级。事故当天的那段代码长这样,问题藏得非常深:
import os
def rename_all(src, dst):
for name in os.listdir(src):
os.rename(os.path.join(src, name), os.path.join(dst, name))
看起来人畜无害对吧?我把 src 和 dst 都传成了同一个目录,意图是在目录内部改名,结果这段代码会先改成中间名再覆盖,最后文件全部消失——不是改名失败,是数据没了。bug 不在 AI 写的逻辑里,而在我自己给的参数语义没定义清楚:rename_all 到底是移动还是改名,提示词里没说,生成就照着字面实现了。自那以后我明白,审查的第一步不是看代码,是先确认自己要的到底是什么,这个问题又会绕回 1.2 提示词工程 的验收样例上。分级表如下:
| 级别 | 判定条件 | 审查方式 | 谁来把关 |
|---|---|---|---|
| 一级 | 本地一次性脚本、可重跑 | 跑通即可 | 自己扫一眼 |
| 二级 | 进项目的工具函数 | 逐行读加单测 | 自己过清单 |
| 三级 | 涉及文件修改、网络、数据库 | 双人确认或灰度 | 拉同事看关键段 |
| 三级加 | 影响线上数据 | 先备份再执行 | 必须灰度验证 |
那次照片改名事故,按现在的分级是三级加:改的是正式库的文件名,却没有先备份。分级表贴在工位上之后,类似的事再没发生过——不是因为代码变好了,而是因为危险的操作在动手前就被拦住了。
方案:清单加自动化的双层审查
人工审查=靠清单,机械检查靠脚本,这是整体思路。清单部分一共七条,每条对应一类我实际踩过的坑:
| 序号 | 检查项 | 对应的坑 |
|---|---|---|
| 1 | API 和函数名真实存在 | 幻觉编造的方法 |
| 2 | 边界条件有处理 | 空值、重名、越界 |
| 3 | 异常路径有交代 | 只写正常路径 |
| 4 | 文件操作可回滚 | 改名、删除、覆盖 |
| 5 | 无硬编码的本地路径 | 换机器就崩 |
| 6 | 依赖在 requirements 里 | 新包悄悄进项目 |
| 7 | 敏感信息没有写死 | 密钥进了仓库 |
前六条是通用项,第七条单列出来是因为它关乎安全,放在 1.4 安全与版权 里细讲。清单不长,但七条全是血泪:每一条背后至少一次返工或者事故。用的时候也有技巧——别从头到尾顺着读,先扫文件操作和外部调用这两类(第四、六条),它们出事最疼;纯计算逻辑最后看,错了顶多是结果不对,文件没了就是真没了。
自动化那层用的是三个工具组合,在合并任何生成代码之前跑一遍:
ruff check . # 风格和明显错误
bandit -r toolkit/ # 安全扫描,扫硬编码密钥等
pytest tests/ -q # 单测必须全绿
三个命令的分工很清楚:ruff 抓低级错误,bandit 抓安全问题,pytest 抓行为正确性。生成代码经常在风格上偷懒,比如未使用的变量、过长的函数,ruff 一眼就能挑出来;bandit 最有用的一次是报了一个 subprocess 调用里拼接用户输入——这是妥妥的命令注入风险,而生成代码把它写成了默认用法,不细看根本发现不了。bandit 的输出长这样:
>> Issue: subprocess call with shell=True seems safe, but may be
changed in the future, consider rewriting without shell
Location: toolkit/shell.py:42
看到这类报告我都会进文件读一遍上下文,确认输入来源到底是配置文件还是用户输入,再决定是改写还是加校验。自动化工具给的是疑点清单,最终判断还是人来做——这个分工原则在整本书里反复出现,5.2 Agent 编排 里让模型调用工具时同样适用。
我把它们串成一个 pre-commit 检查,提交时自动执行:
# .pre-commit-config.yaml 片段
# 提交时自动跑 ruff 检查与格式化,把风格问题拦在本地
repos:
- repo: https://github.com/astral-sh/ruff
rev: v0.4.0
hooks:
- id: ruff
- id: ruff-format
这个机制和后续 第四篇的部署实践 是一套体系——代码质量检查在本地能拦住的问题,就不要留到服务器上排查。
踩坑:审查本身也会失灵
清单和工具都配上之后,我还是翻过车,因为审查机制有自己的盲区。
第一个盲区是审查疲劳。连续审了五六段生成代码后,第七段我会下意识加快速度,眼睛在扫但脑子没转。有几次还是盯着 bug 看了三遍愣是没看见,直到第二天早上重新打开文件才叫出声。对策是把审查拆开:一次最多审两段,审不完就停下来,生成代码先丢到分支里,不急着合并。慢一点没关系,漏掉才是真问题。
第二个盲区是单测写得太配合。有一回我让 AI 顺便把单测也生成了,结果单测和实现犯的是同一个错——都假设日期列一定有值,测试跑得全绿,但真实数据里就是有空日期,上线直接报 KeyError。教训是单测要自己写断言,至少验收样例这条必须基于我给的输入,而不是基于模型自己假想的输入。附带的做法是故意造一条脏数据扔进测试集:空字符串、负数、超长文件名各来一条,跑通了才敢合并。这就是 1.2 提示词工程 里强调验收样例的原因——生成和测试用同一个脑子想问题,就起不到交叉验证的作用。
第三个盲区是依赖黑箱。生成代码引入了一个我不熟的小包处理时区,单测通过,审查也看不出毛病,但这个包在夏令时切换那天行为异常,凌晨的任务全部偏了一小时。从此定了个规矩:不认识的依赖先读文档再决定,不行就换标准库或大牌实现,新依赖必须写进 review 记录里说明为什么需要它。
还有一个不算盲区但很烦的问题:生成代码爱过度设计。一个简单的读 CSV,它能给你整出三层抽象加插件机制,跑起来没问题,但可维护性是负的。我审查时有一半的修改是做减法——删掉多余的抽象层,把五个函数合成一个。这不是 bug,但留着就是技术债。判断标准很简单:这段代码三个月后我还看得懂吗?看不懂就让它重写,而不是花时间理解 AI 的炫技。
小结
AI 写的代码能直接用吗?得分场景。一级脚本跑通就走,二级逐行读加单测,三级以上当作同事提交的代码对待:可以先信任,但验证不能省。另一个体会是,越简单的生成代码越要小心——正因为看起来人畜无害,审查时反而容易放行,而恰恰是这类短代码藏着一个函数改变所有文件命名的风险。清单加自动化的双层审查,既保住了 AI 带来的速度,又把风险控制在可回滚的范围内。这套机制在工具箱项目里跑了六个月,生成代码合并后的事故从一个月两次降到零。说到底,审查不是不信任 AI,而是不信任不验证的自己——这个心态摆正了,用 AI 的速度才能真正落袋。
工具箱部署到腾讯云之后,审查习惯还帮我省过服务器费用——提早发现了一个死循环把 CPU 跑满的脚本,相关成本复盘在 5.3 成本控制 那篇。刚开始上云想实惠一点的话,可以去 AI 办公助手活动页 看看新用户特惠。
