AI 一口气生成几百行代码,我们该怎么高效且正确地处理代码审核
一、当 AI 能在一分钟里吐出一两百行代码时,真正的难点早已不是“写不出来”,而是“接得住、查得动、改得回、负责得起”。我自己一天常常让 AI 写十几轮,单次 50–300 行很常见,一周下来累计几千行并不夸张。这里面能直接合进主干的大概只有一半,剩下要么是风格不一致、要么是越权操作、要么是没覆盖到我期望的边界,真正被我留下来再加工的常常只有三分之一。速度确实上去了,但读、改、回滚这些事没有便宜半分。我需要同时做目标设计者、规则制定者和成果验收者:先把问题、边界和验收条件讲清楚,再让 AI 在明确的范围里多试几种写法、多跑几轮检查,最后由我看差异、看风险、看回滚路径,再决定是否合并。这一顺序不能反过来——如果我自己都不知道“完成”长什么样,再快的生成也只是把模糊需求放大成更难检查的代码。带着这个结论再读这个题目,会更容易看清它真正在问什么:不是“AI 写得太快怎么办”,而是“当写的成本被工具压平以后,程序员到底该把时间花在哪几个动作上,才能既不返工,也不背锅”。
二、过去半年我最大的感受是,AI 把“写一段代码”的价格压下去了,但“看懂一段代码”的价格几乎没变。一个函数能不能写出来,今天大多数时候已经不构成瓶颈;真正占用时间的,是它处在什么上下文、用了哪些约定、隐含了哪些假设、会不会影响别的模块。一个项目里如果突然多出几百行 AI 代码,常见问题不是“明显错误”,而是“看起来很像正确答案”。我整理了几类实际反复遇到的偏差:在 Go 项目里写出 Python 风格的命名(snake_case 夹杂驼峰、把 换成 ),在 React 项目里调用一年前版本的 清理写法,在 iOS 项目里把 写成一串 嵌套,在 SQL 里写出 ORM 不能识别的方言。这些都不算 Bug,跑得起来,过得了 lint,但放进项目里就是和团队既有代码打架。错误处理走的是另一套约定、用了项目里没人熟悉的依赖、或者把一份本该抽出来的逻辑写成了复制粘贴,也都同源。这并不是 AI 不努力,而是它默认按“训练里最常见的写法”回答,而不是按“这一个项目里最合理的写法”回答。读代码一旦要一边猜意图、一边查背景、一边补差异,效率马上塌掉。所以处理 AI 批量生成代码的第一个动作,不是立刻评审,而是先把这些代码放到一个能被人快速消化的上下文里:让它先和现有约定对齐,再让它进入人的视线。我自己的小习惯是:先跑一遍项目的 formatter / import-sort / gofmt-prettier / eslint --fix,把肉眼可见的风格差异抹掉,再开始看逻辑。看起来是小事,能省下 review 时一半的争论。
三、把 AI 当成“一小时能写完一周活”的同事是不够的,更准确的定位是:它负责在明确边界内把重复部分做出来,人负责在模糊地带做判断。越模糊的需求,越不该直接交给 AI;越清晰的接口,越适合让 AI 多试几种实现。具体到我自己:在把任务交给 AI 之前,我会先填一份简短的“任务说明模板”,哪怕只是几行清单:
- 目标:这次改动要解决什么,用一两句话讲清楚;
- 不能破坏的行为:列出已有的功能、API 兼容点、对外契约;
- 验收条件:包括成功路径、失败路径、边界输入、需要的日志或埋点;
- 范围:哪些文件/模块允许改动,哪些只能引用不能改;
- 测试:要求覆盖的关键 case(哪怕只是几条自然语言描述);
- 依赖:允许引入哪些新包,是否需要更新 lockfile、是否要走安全 review。
这份清单写在 issue、PR 描述、或者本地 prompt 模板里都行。我自己最常用的是把它贴在每次开新对话的第一段,让 AI 在生成前自己复述一遍——如果 AI 的复述和我理解的不一致,我先纠正它,再让它写代码。这一步看起来比直接生成慢一点,但它避免了我最怕的那种返工:代码已经写得很长,最后才发现我根本没想清楚要的是什么。一旦让 AI 开始写,我会刻意要“两三种实现思路”,而不是一个最终答案:每种思路分别有什么取舍、依赖、风险点,分别适合什么场景。AI 在这里负责的是“扩宽选择范围”,我负责的是“决定走哪条”。判断做完之后,AI 仍然很有用:写样板、补测试、翻译接口、改一处风格、批量重命名——这些是它擅长、我也乐意外包的部分。把“想清楚”和“写出来”分成两步,比把“想”也外包出去重要得多。
四、让 AI 写完并不等于处理完,高效的关键在于把“该人工看的”和“该机器看的”分开。机器最擅长的是把“看起来不对”的代码挡在前面,人最擅长的是判断“看起来对但其实不该这么做”的代码。我会把以下几类工作尽量自动化,让代码在被我看之前先过几道关,并且尽量都接进 CI,让它在合并前自动跑:
- 格式化与 import 排序:
gofmt/prettier/swiftformat/rustfmt,让风格差异在源头消失; - 静态检查与类型:
golangci-lint/eslint+tsc --noEmit/mypy/ruff,把空指针、未使用变量、类型不一致挡在前面; - 依赖扫描:
govulncheck/npm audit/pip-audit/osv-scanner,挡住带已知漏洞的库; - 单元与集成测试:
go test ./.../vitest run/pytest,覆盖关键路径; - 权限与敏感信息:
gitleaks/trufflehog扫密钥,自定义的 AST 规则挡dangerouslySetInnerHTML/ 原始 SQL 拼接 / 关闭鉴权的中间件; - 架构与体量:
dep-cruiser/madge防止循环依赖,cloc/actionlint控制单 PR 改动规模。
这一串单子不是越多越好,而是按项目风险挑三到五项真正接进流水线。AI 生成的代码能不能进项目,应当先看它能不能稳定通过这些自动检查,而不是依赖某个人通宵逐行 review。自动化检查不是把人换成机器,而是把人从“找低级错误”里解放出来,去看机器判断不了的部分:业务规则是否真的成立、错误处理是否覆盖了真实场景、是否引入了项目不该承担的依赖、是否和现有架构方向冲突、出了事之后能不能定位、回滚、解释。我特别会要求 AI 在 PR 描述里补一段“这次为什么这样改”:边界判断、取舍依据、对哪些假设做了妥协。这些问题没有自动答案,只有我能回答。所以我会刻意为每一批 AI 代码留一段“人工时间”:不读每一行,但读关键差异;不重写每一处,但记录每一处我觉得存疑的地方。自动化把人从体力活里拉出来,人把判断放到自动化够不着的地方,这是我认为最高效的分工。
五、处理 AI 批量生成代码最隐蔽的风险,不是“写错”,而是“看起来对、跑起来对、生产里错”。我自己遇到过几类典型场景,值得单独点名:第一,空值与边界,AI 写了一个解析 JSON 的函数,对字段缺失返回 null,但下游依赖一个非空数组,于是偶发空指针崩溃;第二,并发与时序,AI 写了一个“先查再写”的接口,没有事务也没有唯一索引,并发下出现重复订单;第三,权限与作用域,AI 写了一个后台管理接口,从 token 里取 user_id 但忘了校验是不是同租户,结果越权读取了别家数据;第四,依赖版本漂移,AI 按训练数据写了一个 1.x 的旧 API,但项目已经升到 2.x,新接口签名完全不同;第五,隐式副作用,AI 在函数里顺手打了一行 info 级日志,结果在生产里每请求都打,把磁盘和日志费用悄悄放大好几倍。这些问题都不会让 PR 卡在 CI 里,但会在生产里慢慢咬人。为了避免它们,我会做几件可以立刻上手的事:第一,把任务拆小再合并,让 AI 一次只生成一个文件、一个函数、一段可独立验证的改动,而不是一口气吐出半个模块;第二,要求 AI 同时给出反例和边界——空输入、超长输入、并发输入、权限不足的输入,而不是只给它“正常路径”;第三,关键路径必须有生产可观测的数据埋点:trace_id、关键字段、错误码、耗时,不然 AI 写得再优雅,我也不知道它上线后究竟在做了什么;第四,写一份简短的“这次为什么这样改”的说明留在 PR / 注释 / ADR 里,方便三个月后回来看的人(包括未来的我自己)不需要从零推导意图;第五,灰度与回滚路径必须提前想好,feature flag 默认关闭,新接口先小流量验证,确认指标正常再放量,出问题能在 5 分钟内回滚而不是改 5 小时代码。这些动作加在一起不算复杂,但它们保证了一件事:哪怕 AI 的某一次生成不完全对,我也能在影响扩大之前发现、定位、回滚并解释。回到最初的问题,我对“高效”和“正确”的理解是:高效不是 AI 写得更快,而是我需要看的代码更少;正确不是 AI 永远不出错,而是出错的代价被限制在可以接受的范围里。让 AI 处理重复和初稿,让自动化守住底线,让人负责问题、边界和后果;这三件事各自站对位置,就是我目前能给“AI 写大量代码怎么办”这个问题写下的最诚实的答案。给读者的启示:1. 给 AI 的任务先按“目标 / 不破坏什么 / 验收 / 范围 / 测试 / 依赖”六项写清楚,再让它动笔;2. 让 AI 多给几种实现,由人来选,不要把第一次输出当结论;3. 把格式化、静态检查、类型、依赖扫描、敏感信息扫描至少接入 CI,让人只看机器看不到的部分;4. 关键路径埋点、反例与边界必须由人提供,AI 默认只会写 happy path;5. 灰度与回滚路径提前想好,新接口先小流量再放量,出问题能在分钟级回滚;6. 责任不能下放——AI 是放大器,放大的是判断力,也会放大错误。