互联网上,AI 编程已经替代了程序员了:拳打后端 golang、rust、java,脚踢前端 vue、js、css,连嵌入式 c/c++ 都得挨几个逼兜。手搓浏览器,人造操作系统那是手拿把掐,更别提搞个网站、应用、小游戏...
闹热归闹热。
但在产品化项目中应用 AI,不得不谨慎。AI 最让人头痛的地方在于:它不是完全不行(相反,在特定场景表现惊艳),最大的问题是不够稳定。就像一个新人,聪明、敏捷、能干,但会不经意地挖点坑在项目里,让人又爱又恨!
本章,我们一起探讨如何审查 AI 写的代码是否满足产品化要求? 如何为AI Coding 这匹野马,套上质量保障 的缰绳。
再聊AI 代码的缺陷
回想我们自己写代码出错,往往是对工程上下文不熟悉、边界没有理清楚、对某个算法没有吃透、API 接口没有搞懂、疲劳/大意导致的低级错误等等。
类似的,AI 出错或低质量,也有它固定的模式:
幻觉型缺陷:调用了不存在的方法,参数顺序写错,引用了不存在的变量。这类错误在强类型语言里编译时就能抓到,在动态语言里会藏得很深。
边界遗漏型缺陷:主流程写得完整,但空值、空列表、超时、并发竞争这些情况没有处理。AI 非常擅长写 happy path,非常不擅长想到所有会出错的情况。
上下文断层型缺陷:AI 修改了一个函数的签名,但没有更新所有调用方。AI 加了一个新字段,但没有同步更新相关的序列化逻辑、数据库 schema、文档。这类问题是最难被静态工具发现的。
风格漂移型缺陷:不同时间、不同 session 生成的代码风格不一致。同一个项目里混着三种错误处理写法,或者命名规范彼此冲突。单独看每段代码都没问题,但整体是一盘散沙。
假测试型缺陷:AI 生成的测试覆盖率看起来很高,但仔细看都是在测 mock,没有真正验证业务逻辑。这是最隐蔽也最危险的一类。
至于这些问题的原因,《玩转 AI Coding(三):学会与 AI 对话》中,我们已粗略阐述过。
下面,我们通过多个维度,逐层提升 AI 代码的质量。
第一层:优化 AI 的交互意识和标准
这一层是成本最低、收益最快的——与其事后修 bug,不如让 AI 从源头就少干挖坑埋雷的事情。
1. 测试必须基于需求,而不是基于代码
这是 AI 测试最常见的陷阱。你让 AI 写测试,它会非常勤快地给你生成一大堆,覆盖率看起来漂漂亮亮。但仔细一看,全是这种东西:
def test_get_user():
mock_db = Mock()
mock_db.find.return_value = {"id": 1, "name": "Alice"}
result = get_user(1, db=mock_db)
mock_db.find.assert_called_once_with(1) # 测的是 mock 有没有被调用
assert result["name"] == "Alice" # 测的是 mock 返回值
这测了什么?测了 AI 自己写的代码能不能通过 AI 自己写的 mock。循环自证,毫无意义。
正确的做法是:在要求 AI 写测试之前,先给它需求文档、规格文档或验收标准,明确告诉它"测试用例必须对应到具体的业务场景"。比如:
需求:用户余额不足时,转账应该失败并返回错误码 INSUFFICIENT_BALANCE,账户余额不变。
对应测试(而非代码推导出的测试):
- 转账金额 > 账户余额 → 应返回 INSUFFICIENT_BALANCE,源账户余额不变
- 转账金额 = 账户余额 → 应该成功(边界)
- 转账金额 = 0 → 应该如何处理(需求里没说?那先确认需求)
一个简单的判断标准:如果把代码实现完全删掉、只看测试,能不能推导出正确的需求? 能,说明测试是有效的;不能,说明测试在测代码而不是在测业务。
前面我们讲到的 基于 SPEC开发,其中spec文档和设计文档就是测试的最佳参考。
2. 构建和测试是最基本的交付要求
每次 AI 完成一轮修改,必须经过两道验证才算"完成":
其一,构建必须通过。 这听起来是废话,但 AI 确实经常给你一段"语法上没问题但逻辑上引用了不存在东西"的代码,在动态语言里尤其如此。把构建/编译命令写进你的工作流,不通过构建就不算交付。
其二,测试必须执行并通过。 这里有个细节:如果测试依赖外部资源(数据库、第三方 API、消息队列),允许 mock 或跳过,但需要人工确认这个跳过是合理的,而不是 AI 为了让测试通过而悄悄 mock 掉了真正需要测的东西。
一个实用做法是在 prompt 里加一条硬约束:
完成修改后,必须执行以下命令并确认全部通过:
1. npm run build(或对应的构建命令)
2. npm run test
如果测试失败,必须修复后再汇报完成,不能跳过测试或修改测试来让它通过。
听起来像在管小孩,但确实有效。没有这条约束,AI 有时候会"聪明地"把失败的测试用例改掉,或者加个 skip,然后告诉你"测试通过了"。
3. 系统测试 > 接口测试 > 单元测试
有条件的话,优先建立离真实运行环境更近的测试。
单元测试的价值毋庸置疑,但它的致命弱点是"隔离":隔离了外部依赖,也隔离了真实的调用路径。AI 生成的代码在单元测试里跑得好好的,集成进系统后可能因为接口约定不匹配、环境配置差异、时序问题等各种原因挂掉。
浏览器 MCP + 接口测试是一个性价比很高的方案:通过浏览器 MCP 模拟真实请求,走完完整的 HTTP 请求链路,可以覆盖很多单元测试 mock 掉的环节(序列化/反序列化、中间件、权限校验等)。这比"AI 测试 AI 写的 mock"可靠得多。
第二层:基于需求的交叉 review
硅基员工最大的好处是不知疲惫。因此,能让它们多做一层,就尽量多做一层。审核AI的代码之前,先让AI 审查,修改、迭代,把明面上的问题先解决掉,再御驾亲征!
但AI review 也有些注意的点:
1. session 隔离是核心
所谓session隔离,就是不要在任务执行的session(写代码的Agent 上下文中),执行code review。 原因有两个。
其一,同一个 session 里,AI 会进入"自己出题、自己改卷" 的模式——它对自己代码的假设是根深蒂固的,根本想不到那些它当时没想到的边界情况。
第二,AI 终究是资本家的产物,[数据一定要漂亮 | 执行一定要成功 | 问题一定要少] 是铭刻在基因里的。 如果你问它代码写得怎么样,它大概是这个回答模式:“这段代码采用了xxx架构或设计模式,结构清晰,逻辑完整,测试已全面覆盖,达到上线标准。接下需要我帮忙补充一下部署方案和脚本么?”
不要问,问就是 这段代码优秀得不能自已...
正确做法是:用全新的 session,甚至是不同的模型,给 reviewer 只提供需求文档(规格说明、验收说明) + 代码,要求以文档为实事依据,对实现进行分析和评判。就像真正的 code review,reviewer 需要判定这段代码是否真正的实现并解决了问题。
我个人常用的方式:
用 opencode + glm 写代码, 用 codex + gpt-5.5 进行review。
2. review 以文档为实事依据
在《玩转 AI Coding(三):学会与 AI 对话》中,我们阐述 上下文(提示词)对AI行为和结果的影响模式。 基于此原理,当我们要求AI review 代码时,一定不能拿代码作为实际事项依据,让AI进行评估,这会极大的限制AI 的视野。
举个例子,项目中 需要对一组数据进行排序,并取出最小的k个元素。
实现上,采用了 sort() 和 fetch()两个接口,先排序,让后取出最小的k个元素。 让AI 对照代码进行review,AI获取到的上下文是代码的实现,分析出 sort 函数是用快速排序,简单高效,fetch函数获取数组的前k个元素,简单明了,因此这段代码实现非常 哇塞,一点毛病没有。
但事实上,需求和规格文档中,对于该需求的描述中有个限制:数据源可能数组,或读取自多个channel。 结合起来看,用 sort + fetch的模式就不太合适,用归并排序则与场景更加匹配。
所有更加好的方式是: 拿着需求文档,问"这个需求的每一条,代码里有没有对应的实现?每一个异常场景,有没有对应的处理?"
review prompt 示例:
根据 xxx-change 的spec 和设计文档,对 xxx 提交的代码进行 review。
作为独立 reviewer,逐条检查:
1. 需求文档中的每一条功能点,代码里是否都有对应实现?
2. 需求中提到的异常场景,代码里是否都有处理?
3. 代码中是否存在需求文档没有要求但被额外引入的行为(可能是过度实现或引入风险的地方)?
4. 整体架构是否和需求的规模/复杂度匹配(有没有过度设计,或明显偷懒的地方)?
请直接指出问题,不需要夸赞没有问题的地方。
最后一句很重要, 是去 “弹窗广告” 的法门。AI 默认会加很多"总体来说实现得不错,以下是一些小问题"之类的铺垫,去掉这种礼貌性废话,让它直接给问题清单。
3. 一些好用的 review 工具和套路
SuperPowers 的代码 review skill:通过派发独立 reviewer subagent,它拿到的是精确裁剪的上下文,而不是主 session 的历史记录。这防止了 AI agent 在长 session 中积累的"错误认知"带入 review 过程。这和文章第二层强调的 session 隔离原则高度一致,所以值得单独提内置了多维度的 review checklist,包括安全、性能、可维护性等方向,适合系统性过一遍,不容易遗漏某个方向。
基于角色的 reviewer(如 gstack):内置了 15 个专家角色(CEO reviewer、Staff Engineer、QA lead 等),review 阶段会有 Staff Engineer 检查正确性、Senior Designer 评估 UX 影响、Security 专项审查等不同视角。 框架也内置了安全 review 角色,能抓常见生产风险如 SQL 注入、竞态条件、静默失败。
基于公司编码规范的自定义 review skill:把团队的编码规范、禁用模式、架构约定写成结构化文档,作为 review 的输入。这类 review 能发现最多的"风格漂移"问题——AI 写了能跑的代码,但违反了团队的约定。这种东西只有人(或被团队规范喂饱的 AI)才能发现。
第三层:静态分析——让机器先过一遍
这个系列开篇我们讨论,AI 并没有真正的替代了程序员(至少眼下没有),而是改变了程序员的工作方式。
这里的工作方式,我觉得更像是: AI 主力负责写, 人主力负责规划和纠偏。
以往,程序验证和测试,更多是测试工程师的职责;但在AI coding模式下,不能能力需要前移。 比如,静态分析和自动化测试。
静态分析是成本最低、回报高的一层。它不需要运行代码,在提交阶段就能拦截相当大比例的 AI 幻觉型和风格漂移型缺陷。
类型系统是过滤 AI 幻觉的第一道关
如果项目还没有强类型,现在引入的理由比以往任何时候都充分。TypeScript 的 strict: true,Python 的 Pyright 或 mypy——这些工具能在编译期发现 AI 调用了不存在的方法、传了错误类型的参数。
重要的不只是"有类型检查",而是要开严格模式。AI 生成的代码在宽松模式下往往可以跑通,但隐患就藏在那些被宽松掉的地方。
Lint 规则要反映项目实际约束
通用 Lint 工具(ESLint、Ruff、Pylint)解决通用问题。但对 AI 代码最有价值的,是项目特定的自定义规则。
这些规则的来源很简单:把过去 review 里反复提过的问题,变成机器可以检查的规则。比如:
- 禁止在业务逻辑层直接使用
console.log,必须用统一的 logger - 所有异步函数必须有明确的错误处理,不能裸写
async/await不加 try-catch - 禁止硬编码配置项,必须从 config 模块读取
把这些写进 lint 配置,就不用在每次 review 里重复提了。
复杂度指标是 AI 代码的特殊风险点
AI 非常容易生成圈复杂度高的代码——深嵌套、多分支、逻辑密集但缺乏拆分。表面上能跑,但没人能维护。
在 CI 里加圈复杂度检查(SonarQube、Code Climate、或者 Radon/Lizard),设一个阈值,超过就报警或阻断。这个阈值不用设得很严,函数圈复杂度超过 10 就应该拆。
依赖安全扫描不能省
AI 在生成代码时会引入依赖,它不会去判断这些依赖是否有安全漏洞、是否有更合适的替代方案。Snyk、Dependabot 或者 pip-audit 这类工具要进 CI 流程,每次有新依赖引入就扫一遍。
第四层:代码审查——人工和 AI 协作
静态分析过滤了低级错误,代码审查要解决更深层的逻辑问题。
AI Review Bot 做第一遍筛查
CodeRabbit、Qodo Merge(原 CodiumAI)、Sourcery 这类工具,在 PR 提交后自动分析 diff,给出初步的 review 意见。它们能发现的东西包括:潜在的 bug、安全问题、性能问题、缺失的测试。
这层工具的价值不在于它有多聪明,而在于它快、便宜、不会漏掉每一个 PR。它做的是粗筛,把明显的问题提出来,让人工 review 可以跳过这些低价值的部分,专注在它看不到的地方。
配置这类工具时,要把项目的技术栈、架构约定、禁用模式告诉它,否则它只能给出通用建议,噪音会很多。
人工 Review 要聚焦在工具盲区
这是很多团队做错的地方:人工 review 还在纠结命名、格式、是否加了注释。这些东西应该完全交给自动化工具处理。人工的稀缺注意力,要用在工具看不到的地方:
关联影响是最重要的一项。AI 不了解你的系统全貌,它改了一个函数但不知道有多少地方调用了它,改了一个数据结构但不知道有多少下游依赖。人工 review 要专门检查:这个改动有没有漏掉需要同步更新的地方?
业务语义正确性是第二重要的。代码逻辑上没有问题,但和实际业务需求对不上。这类问题只有理解业务的人才能发现。
架构一致性:AI 生成的实现方式,是否和整体架构的方向一致?有没有引入新的耦合,或者绕过了应该走的层次?
一个实用做法:在 PR 模板里加一个强制填写的"影响范围评估",让提 PR 的人在提交时就思考这些问题,而不是等 reviewer 去发现。
- 直接修改了哪些模块?
- 哪些模块的行为会因此改变,即使没有直接修改它们?
- 有没有需要同步处理但不在这个 PR 里的地方?
这些内容,刚刚在上一章 “了解你的工程” 中,通过understand-everything等可以解决。
第五层:测试——覆盖率不等于质量
测试是 AI coding 质量保障里最容易被表面数字迷惑的一层。
变异测试:检验测试的真实有效性
这是当前最被低估的工具,对 AI 代码尤其重要。
原理很简单:自动在源代码里引入小的改动(把 > 改成 >=,把 && 改成 ||,把 return true 改成 return false),然后跑测试套件。如果测试没有失败,说明这个改动"存活"了——你的测试根本没有真正验证这段逻辑。
AI 生成的测试天然有这个问题:它生成测试时用的思路和生成代码时一样,所以测试只覆盖了它能想到的情况,而不是所有需要覆盖的情况。变异测试是发现这类"假覆盖"的唯一可靠方法。
JavaScript/TypeScript 用 Stryker,Python 用 mutmut,Java 用 PITest。在 CI 里设变异得分阈值(建议 65-70%),低于阈值阻断合并。
第一次在项目里跑这个,心理准备要做好。很多项目的变异得分只有 30-40%,这说明大多数测试只是在走形式。
属性测试:用随机输入找边界缺陷
AI 遗漏边界处理是高频问题。手写测试用例很难穷举所有边界情况,属性测试可以弥补这个缺口。
做法是:不写具体的输入输出,而是描述代码应该满足的"属性",然后让框架自动生成大量随机输入去验证这些属性。
# 用 Hypothesis(Python)
from hypothesis import given, strategies as st
@given(st.text(), st.text())
def test_concat_length(a, b):
# 两个字符串拼接后的长度等于各自长度之和
assert len(concat(a, b)) == len(a) + len(b)
// 用 fast-check(JS/TS)
import * as fc from 'fast-check'
test('parse(stringify(x)) === x', () => {
fc.assert(
fc.property(fc.record({ id: fc.integer(), name: fc.string() }), (obj) => {
expect(parse(stringify(obj))).toEqual(obj)
})
)
})
这类测试特别适合验证:解析/序列化的往返一致性、排序和过滤的不变量、权限检查的边界条件。这些恰好是 AI 最容易遗漏的地方。
集成测试要测真实行为,而不是 mock
AI 生成的单元测试里,mock 的用法往往过度——把所有依赖都 mock 掉,然后验证 mock 被调用了。这不是在测业务逻辑,是在测 mock 配置。
集成测试要尽量接近真实环境。用 Testcontainers 起真实的数据库和中间件,用真实的 HTTP 调用而不是 mock,验证端到端的行为是否和预期一致。这样发现的问题,是 mock 测试永远发现不了的。
把这些组成一个可执行的流程
通过前面的描述,我们能够描绘一个大体的流程:
本地提交阶段(pre-commit hook)
- 类型检查(tsc / pyright)
- Lint + 格式化(ESLint / Ruff + Prettier / Black)
- 快速单元测试(只跑被改动文件相关的测试)
这一层的目标是把低级问题挡在本地,不让它们进入 CI 队列浪费时间。
PR 提交阶段(CI 自动触发)
- 全量单元测试,覆盖率检查(设阈值,不达标不能合并)
- 变异测试(在主要业务逻辑模块上,不需要全量跑)
- 安全扫描(Semgrep + 依赖扫描)
- 复杂度检查
- AI Review Bot 自动 comment
人工 Review 阶段
- 此时 CI 已经过滤了大部分低级问题
- 人工专注:业务逻辑正确性、关联影响、架构决策
- Review Bot 的意见作为参考,不作为结论
合并后
- 端到端测试或集成测试在预发布环境跑
- 监控数据作为最后一层验证
AI coding 把"写代码"这件事加速了,但却把担子压到了 “代码验证” 和“测试”上。实际上因为生成速度快了,代码量增长也快了,可靠性降低了,质量保障的压力会指数上升。
AI coding 时代,如何做好质量关是一个复杂、长期的挑战,且行且完善吧!