干了这些年工程,我见过不少团队在 AI Coding 工具面前又爱又恨:工具确实猛,搭接口、写单测、改样板代码的速度比我十年前手写快了几十倍。可问题也恰好出在“快”上。AI 生成的代码总带着一种非常自信的气场——语法工整、缩进到位、命名像模像样,看起来完全可以直接上生产。但等它合并进主干之后,边界条件漏判、异常分支被吞、状态转换错位、安全校验被绕过,这些问题才开始一个一个冒出来。越来越多的团队发现,AI Coding 真正难的从来不是“能不能生成”,而是“生成了之后怎么兜住质量”。这事靠人海战术补不完,靠传统流程也压不住,必须重新设计一套针对“机器产出”的质量治理方案。
这篇文章我会把在几个项目里摸出来的完整路子捋一遍:工程规范怎么约束 AI 产出、代码评审对着 AI 代码具体该审什么、风险驱动测试如何用有限的资源掐住最容易出事的环节。适合正在把 AI 编码工具接入生产项目的开发者和技术负责人,也适合吃过亏之后想认真重构质量流程的同学。
1. 先承认一个事实:AI 生成的代码问题不在“语法”,在“语义”
1.1 为什么 AI 代码看起来总是“都对”
先讲一个最容易被忽略的底层认知:AI 编码工具本质上是大规模模式补全器,不是带着全局意图去写程序的“虚拟工程师”。它生成的每一行代码,都是基于大量历史样本推断出来的“高概率结果”,所以语法概率最高、格式最工整、注释也最像人写的。可它并不真正理解这段代码在这个项目里承担什么责任、会被谁调用、在什么极端输入下会崩。
这就好比一份满分试卷让誊写员抄了一遍。誊写员字迹漂亮、卷面整洁、每一题都有完整答案,但他并不知道题目的逻辑是不是自洽,更不知道某个填空题的“标准答案”放在另一个题目语境下是不是错的。AI 的“自信”不是来自推理,而是来自概率拟合。正因如此,它写出来的边界条件、异常分支、状态流转,往往乍看合理、细看经不起推敲。
我在实际 review 中最常碰到的情况是:AI 补全了一个函数,缺失了某个参数的 null 校验,但恰好函数内部有另一个看似无关的判断把这个 null 情况“顺手”接住了,于是单测全绿、功能正常。直到线上某个客户端传入了异常数据,整个调用链才炸开,而且报错位置离真正的漏洞点隔了三层调用。这种“错得很有逻辑”的缺陷,恰恰是最难靠肉眼扫出来的。
1.2 AI 代码的高发质量问题
如果只分类不展开,对治理没有意义。我把这些年收集到的问题按频率高低大致分了几类,每一类都配一个常见场景:
- 边界与空值处理缺失:AI 非常擅长写“正常路径”,但对输入为空、长度为零、数值越界、日期跨年这类情况经常视而不见。常见于时间字符串解析、金额计算、列表截取。
- 异常分支被静默吞掉:生成代码里经常出现空 catch,或者 catch 之后只记录日志但没有任何降级与兜底。更隐蔽的是,AI 会把“捕获异常”和“处理异常”当成一件事。
- 全局状态与共享资源被隐式修改:AI 生成的重构代码容易破坏隐式约定,比如改变了某一方法的执行顺序、把可变对象传给了多个调用方、动了一个共享缓存 key 的生成规则。
- 安全校验把守位置错误:权限判断被放在前端/入口之后,服务端没有二次验证;或者把用户输入直接拼进日志、SQL、文件路径。
- 重复代码与随意抽象:AI 倾向于把相似逻辑复制到新位置,而不是复用原有实现,导致同一个 Bug 会在三处同时存在,修复时漏一处就出大事。
这些问题的共同特征是:静态检查工具大概率能抓到一部分(空值、格式、未使用变量),但语义层面的错误只有靠“对行为预期负责任的评审者”才能发现。所以后续所有的工程规范、评审流程、测试策略,都应该围绕“把语义风险从差距中捞出来”这件事来设计。
1.3 先建立可量化的基线:没有度量就没有治理
在引入 AI Coding 之前,团队通常对“历史缺陷率”“单次变更修改文件数”“线上问题回滚率”这些指标是模糊的。不把基线做出来,你没法判断 AI 投入后质量是变差还是变好。我建议至少先记录四个基础指标:
- 变更体积:单次提交平均改动文件数、新增代码行数。AI 介入后这一项通常会暴涨,如果涨了 3 倍以上,评审压力会增加,风险也会非线性放大。
- 缺陷命中点:Bug 是在代码评审阶段被发现,还是测试阶段,还是线上。它能直接反映评审和测试两个环节谁失效了。
- 回归率:某个模块在改动后出现旧功能被破坏的比例。AI 重构最容易踩这个坑。
- 返工成本:从提交到合入的平均耗时,以及因为 review 不通过导致的修订轮数。 这四个指标不需要建数据平台,先拿项目管理工具里的历史记录统计一遍就行。有了数字,后面一切治理动作才有瞄准点。
2. 工程规范:让 AI 在栅栏里干活
2.1 提示词里先写“宪法”,别靠临场发挥
很多人用 AI Coding 是直接甩一句话让它改代码,改完拿去提交。这是把质量责任完全丢给概率。正确做法是在项目里维护一份“AI 协作规范”,把它当成提示词里最靠前的固定上下文。规范不需要很长,但必须是负面清单加正面约束的混合体。
我见过比较好的模板大概长这样:
- 目标语言与框架版本全局固定,不许擅自升级或替换推荐库。
- 命名遵循项目既有风格,禁止自创缩写。
- 所有可能失败的外部调用必须有明确的异常处理,禁止空 catch。
- 输入参数必须进行 null、空值、类型、长度校验,除非接口契约声明为不可空。
- 不允许修改与本需求无关的模块代码。
- 新增公共方法必须附带注释说明调用方与预期行为。
- 禁止在业务代码中直接输出用户敏感信息。
实际执行的时候,可以把它精简成一段固定文本放在团队的 AI 工具配置里。这样做还有一个额外好处:团队不同成员对 AI 的产出期望会收敛,不会出现 A 让 AI 写代码、B 让 AI 写代码但风格完全两样的局面。
2.2 工具链当警察:格式化、静态检查、类型检查一条龙
规范文件写得再好,AI 也不会自觉遵守,它只遵守“可验证的约束”。所以第二步就是把规范翻译成机器可以强制执行的检查项,并且保证这些检查结果能够反馈回 AI 的生成过程。
这一步我强烈推荐“本地检查优先、CI 拦截兜底”的双层结构。本地层包括统一的格式化配置、ESLint/静态分析规则、TypeScript 类型检查,这些在开发环境里即时跑。关键技巧是:把这些检查器的配置文件路径直接写进 AI 的上下文说明,让 AI 在生成时就避开风格问题;很多 AI 工具支持读取项目配置文件,能明显降低生成代码在格式层面的噪音。CI 层的作用是兜底,任何绕过本地检查的提交都会在合并前被拦截。不要在这件事上留白,一旦允许“先合并,后面再改格式”,AI 生成的代码就会在本该聚焦逻辑风险的评审环节消耗掉大量注意力。
2.3 接口与目录结构:用物理边界锁住 AI 的扩散
工程规范里最容易被忽视的是“物理边界”。AI 在修改一个需求时,常常顺手扩大改动面——它会把相似函数的位置挪一挪、把公共方法重构一下、把数据结构顺手改成更方便的样子。从单次改动看可能没错,但对团队来说,这是不可控变异的源头。
我们的解决方法是对仓库做分层约束。核心接口文件、数据库迁移脚本、公共配置目录标记为“只读区”,AI 任务不授权不进入;业务代码内部按模块划分,一次改动只允许落在与需求相关的模块目录里。为了让这一点可执行,我在评审里加了一条硬性规则:凡是与需求无关的目录变更,必须要开发者在提交说明里写清楚理由,否则直接打回。实践证明,这个规则几乎每周都能挡下两次“顺手改错文件”的事件。
3. 代码评审:人工仍然是质量最后的防线
3.1 AI 时代的评审原则:少看热闹,多看语义
传统代码评审依赖一个隐含前提:作者能够解释自己的设计意图和潜在权衡。代码评审时,开发者会主动说明“我为什么这么写、这里有哪些边界”。但 AI 生成的代码没有“意图”可解释,它只会给出一个看起来自洽的结果。评审者面对的是一个充满语法自信但没有作者在场的产物,这时候如果按传统从头到尾逐行扫,效率会极低,而且容易漏。
我建议把评审重心从“这行代码对不对”转向“这段改动是否满足业务契约、是否引入了未声明的行为变化”。具体来说,评审者先看 diff 范围和需求描述是否吻合,再看关键逻辑分支有没有覆盖到边界,最后从调用方的视角反向验证。特别是那种修改了公共函数或底层数据结构的改动,必须确认所有受影响调用方都不会出现行为变化。
3.2 三个必看维度:语义正确性、风险边界、可维护性
第一维度是语义正确性。AI 最容易在“类型对,但语义错”的地方翻车。比如类型是字符串但内容格式与调用方预期不一致;是数字但在算法上搞反了分子分母;是集合但错误地按数组的索引访问。review 时要把重点放在“这个函数返回的值是否符合调用者未来会做的假设”。
第二维度是风险边界。包括外部输入校验、资源释放、并发控制、安全权限。AI 生成的代码通常对异常路径的考虑非常单薄,尤其是网络请求、文件读写、数据库事务这些边界。我的习惯是拿到一个 AI 改动,先问三个问题:这个函数被调用时最不可能的输入是什么?外部服务返回失败时会发生什么?这个改动是否影响其他并发的执行路径?只要有一个问题答不上来,这个改动就不该过。
第三维度是可维护性。AI 很喜欢“一次性正确”的代码,就是能跑,但命名莫名其妙、抽象过度或无抽象、注释永远在解释“做了什么”而不是“为什么这么做”。这种情况最好当场要求重写,因为 AI 代码一旦进入主干,后续维护就是团队自己来扛,没人愿意在三个月后面对一段写得像谜语的关键逻辑。
3.3 一套可以直接抄的代码评审清单
以下是我实际在用的一套 AI 代码评审清单,按优先级排序,不局限于任何具体技术栈:
| 检查项 | 具体动作 | 最高优先 |
|---|---|---|
| 需求范围 | 本次改动是否只涉及需求描述的文件,存在无关文件变更时必须说明理由 | 是 |
| 输入校验 | 所有外部输入是否有 null/空值/越界校验 | 是 |
| 异常路径 | 外部调用失败是否有兜底,禁止空 catch | 是 |
| 状态变化 | 是否修改了全局状态、共享对象、缓存 key 的逻辑 | 是 |
| 安全 | 敏感信息是否进入日志/URL/响应体;权限判定位置是否正确 | 是 |
| 返回值语义 | 返回类型与调用方预期是否一致,是否隐藏了错误状态 | 是 |
| 资源生命周期 | 连接、流、锁、事务是否都有正确释放 | 是 |
| 并发 | 是否存在竞态条件与重复操作 | 中 |
| 可测试性 | 新增逻辑是否容易构造单测,是否依赖过深 mock | 中 |
| 命名与注释 | 是否沿用项目风格,注释是否解释“为什么” | 低 |
这套清单不需要每一条都在评审时喊一遍,真正的作用是让团队成员形成固定视角。评审者只要对照清单过一遍,就不会被 AI 那种“整整齐齐”的观感带着走。我个人的建议是:对 AI 生成的代码,至少要留出比人工代码多 30% 的 review 时间。省下来的开发时间,必须分一部分给质量把关环节,要不然就是在预支后续的返工成本。
4. 风险驱动测试:把子弹打在故障概率最高的地方
4.1 为什么“全覆盖”在 AI 编码时代反而是伪命题
很多人对质量的第一反应是“提高测试覆盖率”。但在 AI 编码时代这招会失效:因为测试用例本身也在大量用 AI 生成。覆盖率达到 90% 只能说明“每一行代码都被执行过”,不能说明“所有关键行为都被验证过”。AI 生成的测试和 AI 生成的实现拥有同源缺陷,它们会一起犯同一个认知盲区——比如都认为某个边界场景永远不可能发生。这种情况下,覆盖率数字越漂亮,虚假安全感越强。
风险驱动的核心思路是:把测试资源从“追求覆盖率”转移到“押注失败概率”。先评估改动中哪些部分的失效概率最高、失效影响最大,然后把测试设计和执行资源集中在那里。它本质上和给自己买保险是一个道理:你会为低概率高损失的房屋火灾买保险,但不会为高概率低损失的铅笔丢失买保险。测试也是一样,应该是风险优先,而不是行数优先。
4.2 怎样识别改动中的高风险区域
在拿到一个 AI 生成改动的时候,我习惯先给“改动面”做一次风险扫描,不打分不下结论,只标记高风险特征:
- 涉及外部输入解析的,比如 HTTP 参数、文件上传、配置读取。
- 涉及数据转换和单位换算的,尤其是时间、金额、坐标、编号这类。
- 修改了公共依赖或底层工具的,影响面扩散到全部调用方。
- 含并发或异步逻辑的,比如锁、协程、消息队列消费。
- 涉及身份认证、授权、敏感数据输出的。
- 在原有代码上做“重构性改动”的,AI 最容易在重构时悄悄改变行为。
我给团队定的简易评估法是:每个高风险特征记一分,得分超过两分的改动,必须补专门的边界测试和异常路径测试;得分超过三分,则额外要过一轮安全关注度高的评审。这样做不需要复杂权重模型,也能让测试资源明显向风险倾斜。
4.3 三条具体落地的测试策略
策略一:强制高风险区域的“边界用例优先”
对识别出的高风险函数,不先写普通成功路径,而是直接写它最不可能被调用的场景。这个方法叫“先想最坏情况再动笔”。比如一个解析日期字符串的函数,我会先写:输入2024-02-30、输入空字符串、输入带时区偏移的格式、输入非 UTF 编码。这些用例在 AI 生成的测试里基本不会出现,必须由人补。
策略二:人机协作的测试对拍
我推荐的组合是“AI 生成正确路径测试 + 人工补边界与异常测试”。AI 很擅长把正常流程的测试写全,省下大量重复劳动;人的精力集中在 AI 不敢想象的输入上。两个来源的测试合并进同一个测试套件,既能提速,又能避开同源盲区。
策略三:属性测试与模糊测试常驻 CI
对于数据处理类函数,传统固定用例很难覆盖所有意外输入。这部分可以引入属性测试或模糊测试,让测试框架自动生成海量随机输入,并验证“输入数据类型合法时,函数输出永远满足某个不变式”。比如金额计算函数,不管输入怎么变,结果都必须是非负、精度正确、不超过上限。这类测试对 AI 生成的实现特别有效,因为它们能快速暴露出“某个分支压根没被思考过”的事实。
落到流程上,我会在 CI 里专门建一个“风险回归任务”:每次 AI 相关的依赖升级或重构动作后,自动跑一次全量模糊测试。跑完如果发现失败,不要急着修,先定位问题出在语义层还是实现层,再决定是改代码还是补约束。
5. 实操复盘:用久了才会踩到的坑
5.1 三个典型事故复盘
第一个事故是“AI 的隐式状态变更”。某团队让 AI 优化一段缓存读取代码,AI 把缓存过期时间计算从“调用时刻”改成了“创建时刻”,导致一批缓存没能按预期刷新。单测全部通过,直到线上数据出现明显延迟才被发现。复盘的原因是:变更范围内没有涉及“缓存 key 生成规则”的明文约定,AI 从代码里自己推断出了一个错误假设。教训是:涉及缓存、时间、状态这类隐式约定时,必须把约定写成显式注释放在关键代码附近,并加入评审强制检查点。
第二个事故是“重复请求风暴”。AI 在实现一个失败重试逻辑时,没有加最大重试次数和退避间隔,某次外部服务瞬时抖动直接引发了整批请求的重复排队,把下游打满。代码层面看,逻辑每一处都合理,因为单次重试确实是正确行为;但在分布式场景下,90% 的失败同时触发重试,就成了事故。从此以后,凡是 AI 生成的循环、重试、定时任务代码,我都会额外审查“是否有全局熔断和限流意识”。
第三个事故是“异常被吞掉后问题迟到”。AI 在某个异步消息处理函数里 catch 了所有异常并直接 return,结果导致失败的消息被静默丢弃,业务数据缺失后两天才被发现。复盘时发现代码评审阶段就看到了空 catch,但因为当时线上的“看起来没出问题”,被打上了低优先级。这件事让我定下了一条死规矩:空 catch 绝不接受任何形式的延迟修复,评审阶段直接打回。
5.2 复盘模板与排查纪律
AI Coding 引入后的复盘和传统复盘有一个关键区别:传统复盘问的是“谁在什么时间改了什么”,AI 时代必须先回答“这段代码是 AI 生成的还是人写的、生成时用了什么上下文、经过了哪些检查环节”。
我设计过一个简单复盘模板,每次线上问题都按这个顺序问:
- 问题出现在哪段代码,这段代码的生产路径是什么(人写、AI 补全、AI 重构)。
- 如果来自 AI,当时提示词里是否包含项目规范与边界约束。
- 这行代码通过了哪些质量环节(本地检查、CI 静态检查、评审、测试、模糊测试)。
- 失效的是规则本身,还是执行规则的人。
- 下一次应该在哪个环节加一道闸,才能在本类问题进入主干之前拦住它。 按这个模板复盘几次后,团队会自然发现:真正的问题往往不是“AI 写错了”,而是“我们没有一个环节在对语义负责”。静态检查和格式化拦住了风格问题,评审环节因为惯性只看了逻辑主线,测试环节因为覆盖率高产生了虚假安全感。这也是为什么我一直强调,AI Coding 的质量治理不是某一个工具的升级,而是一整套工程流程的重构。
5.3 收网的小技巧:把“人工例外”降到最低
最后分享一个我在实操中特别受益的习惯:给团队立了一套“人工例外机制”。允许开发者在特定场景下绕过某些 AI 相关的质量约束,比如临时排查问题、快速原型验证、内部工具开发。这个机制必须配合两个条件:一是绕过动作必须显式登记,不能悄悄走侧门;二是绕过代码进入主干前,必须补上缺少的测试和评审。
没有这个机制时,团队会为了效率偷偷跳过规则,最后反而比严格走流程更慢。有了它,既保住了效率出口,也把“例外”变成了可追踪、有限次的行为。我后来发现,很多真正有效的质量改进,就是在登记这些例外时发现的——因为它们把“规则哪里不合适”暴露得非常直接。
AI Coding 的质量治理这条路,每个团队都会走出一套自己的版本,但核心逻辑是一样的:让 AI 承担生成速度,让人承担语义责任,让规范与工具随时准备接管那些“看起来对但其实不对”的判断。我在实际项目中体会到,这事的难点从来不是某一种工具或某一条规则,而是能不能坚持在每个环节都问一句:这段代码进入主干后,我是否真的敢为它负责。如果敢,AI 编码就是提速器;如果不敢,它就只是放大缺陷的加速器。