15636 words
78 minutes
AI Coding 之后,如何验证 AI 写的代码

最近一段时间,我一直在关注 AI Coding 容易出现的问题,也在尝试整理一些更适合 AI 生成代码的审查和测试方法。

至少从我的感受来看,大概从 2025 年 10 月前后开始,Claude Code、Codex、Cursor 这类工具就已经逐渐进入了很多人的日常开发流程。到了现在,AI 能不能写代码、开发者应不应该使用 AI,似乎已经没有太多讨论的必要了。

很多人已经使用 AI Coding 一段时间,也逐渐形成了自己的使用习惯。比较常见的流程大概是:

描述需求
→ 让 AI 阅读项目并修改代码
→ 让 AI 补充测试
→ 运行测试和编译
→ 让 AI Review 一遍自己的修改
→ 修复发现的问题
→ 提交代码

相比早期单纯把一段需求扔给 AI,复制代码以后直接使用,这种流程当然已经完善了很多。现在的 Coding Agent 通常也会主动读取项目说明、查看 Git Diff、运行测试,并根据错误输出继续修改。

一些项目还会通过 AGENTS.mdCLAUDE.md 或其他说明文件,要求 AI 在完成修改后执行固定的验证命令,例如:

Terminal window
mvn test
mvn verify
npm run lint
npm run typecheck

这些做法确实可以拦截不少问题,至少能够避免 AI 在代码无法编译、测试明显失败的情况下直接宣布任务已经完成。

不过实际使用了一段时间以后,我逐渐觉得,这套流程依然很容易产生一种虚假的安全感。

AI 写完代码以后告诉我们测试已经通过,我们可能会认为这次修改基本没有问题;再让它 Review 一遍,它又能列出几个风险并完成修复,看起来整个过程已经形成了完整的闭环。

但这里有一个很容易被忽略的问题:代码实现、测试编写、问题修复和最终 Review,往往全部由同一个模型,在同一个上下文中完成。

如果 AI 从一开始就错误理解了需求,后面的实现、测试和 Review 可能会沿着同一个方向继续推进。它可以写出一套与实现完全一致的测试,再用这些测试证明实现没有问题,最后在 Review 时继续基于同样的假设检查代码。

所以现在真正值得讨论的问题,可能已经不再是如何使用 AI 写代码,而是:

在 AI Coding 已经普遍使用的情况下,我们应该怎样验证 AI 写出的代码?

这篇文章主要想讨论下面几个问题:

  1. 现在常见的 AI Coding 流程还存在哪些验证盲区
  2. AI 生成的代码通常容易出现哪些问题
  3. 为什么让 AI 自己写测试、再 Review 自己的代码仍然不够
  4. 如何结合代码审查、自动化测试和运行验证,提高 AI 代码的可信度

AI 写、AI 测、AI Review,问题在哪里#

现在很多人已经不会让 AI 写完代码就直接结束了。

比较常见的做法是要求 AI 在完成修改后运行测试,检查代码能否编译,再重新 Review 一遍刚刚生成的 Diff。部分 Coding Agent 甚至会主动执行这些步骤,最后给出一份看起来很完整的结果总结:

Implemented order idempotency.
Added comprehensive tests.
All tests passed.
No critical issues found during review.

从表面上看,需求实现了,测试也补了,代码还经过了一次 Review,整个过程似乎已经足够可靠。

问题在于,这些结论之间可能并没有真正形成相互独立的验证关系。

假设需求要求实现一个带有幂等性的订单创建接口。AI 可能将幂等性理解为:请求执行完成后,将请求 ID 保存下来,之后遇到相同请求时直接返回。

随后它可能按照下面的方式工作:

  1. 根据这个理解实现幂等逻辑
  2. 编写一个连续发送两次相同请求的测试
  3. 确认第二次请求没有创建新订单
  4. 在 Review 时看到代码里存在幂等判断
  5. 得出“订单接口已经支持幂等”的结论

整个过程中的每一步都有代码或测试作为依据,看起来并不是凭空判断。

但如果两个相同请求同时到达,它们可能都在请求 ID 写入之前通过检查,最后仍然创建两笔订单。

AI 实现了自己理解中的幂等性,测试验证了这个理解,Review 又确认实现和测试保持一致。最终所有步骤都通过了,但真实需求仍然没有被完整满足。

这种现象已经开始受到专门研究。2026 年的一项实验将它称为 错误传播(Error Propagation):当模型先生成了错误实现,再根据这个实现生成测试时,错误可能继续传播到测试断言中,使错误代码和测试保持一致。在该实验中,根据错误代码生成的测试只能发现 14% 的缺陷,而独立生成测试时能够发现 25%。[1]

这说明,测试是否由另一个 Agent 执行并不是最关键的问题。只要后续 Agent 直接继承了实现代码、设计解释和原有结论,它仍然可能受到同一套错误假设的影响。

另一个很常见的问题是,AI 会同时修改生产代码和测试代码。

如果原有测试在代码修改后无法通过,AI 有时会选择修改断言、替换测试数据、增加 Mock,或者放宽异常类型。最后测试重新变绿了,但测试原来保护的行为也可能被一起削弱。

例如原测试明确要求用户权限不足时返回 403

assertEquals(403, response.getStatusCode());

AI 修改权限逻辑后,接口意外返回了 500。为了让测试通过,它可能将断言改成:

assertNotEquals(200, response.getStatusCode());

此时测试依然能够验证“请求失败”,但已经无法区分正常的权限拒绝和服务内部异常。

测试通过了,真正的问题却被隐藏了。

所以,看到 AI 表示“所有测试已经通过”时,还需要继续确认:

  • 测试验证的是不是真实需求
  • 测试有没有到达真正可能出错的边界
  • 测试断言是否足够具体
  • 原来的测试有没有被删除或弱化
  • Mock 是否替代了需要真正验证的组件
  • 新测试能不能在问题重新出现时失败
  • Review 是否独立检查了实现背后的假设

从这个角度来看,AI Coding 目前缺少的通常已经不是更多代码生成能力,而是一套更稳定的验证流程。

AI 代码的问题通常没有那么明显#

现在的模型已经很少会在常规项目中持续生成大量明显的语法错误。

即使第一次生成的代码无法编译,Coding Agent 通常也可以根据编译输出继续修改,直到项目能够正常构建。因此,真正值得关注的问题,往往不会直接表现为“这段代码完全不能运行”。

更多时候,代码在正常输入下能够运行,测试也可以通过,但它可能只实现了需求的一部分,或者在异常、并发、重试和真实运行环境中出现问题。

比如让 AI 实现一个创建订单的接口,它可能生成完整的分层结构:

Controller
→ Service
→ Repository
→ Database

同时补充 DTO、参数校验、异常处理和单元测试。从文件结构和代码量来看,这个需求似乎已经完成了。

但继续沿着真实执行路径检查,可能会发现:

  • 创建订单和扣减库存没有处于同一个事务中
  • 代码先查询库存再执行扣减,并发情况下可能发生超卖
  • 数据库写入成功后消息发送失败,订单状态无法恢复
  • 客户端超时重试后会创建重复订单
  • 单元测试将数据库和消息队列全部替换成了 Mock
  • 测试只验证 save() 方法被调用,没有验证最终数据
  • 接口字段发生了变化,但旧客户端仍然使用原来的格式

这些问题中的大部分都不会被简单的编译和正常路径测试发现。

AI 可能正确完成了每一个局部步骤,但这些步骤组合起来以后,仍然无法满足完整的业务要求。也可能当前实现暂时没有暴露问题,但对应测试没有真正保护相关行为,后续一次普通重构就会让问题重新出现。

从实际情况来看,AI 生成代码比较容易出现的问题,大致可以分成下面几类。

需求理解发生偏移#

AI 有时会实现一个和需求非常相似的功能,但没有真正满足需求中的限制条件。

比如需求是:

只有管理员可以删除其他用户创建的资源。

AI 可能实现成:

只有已登录用户可以删除资源。

需求是:

接口失败时不能产生任何部分写入。

AI 可能只在正常路径中使用了事务,却没有考虑消息发送、第三方调用或者异常被捕获后的回滚情况。

这类代码看起来通常没有明显 Bug,因为它实现的逻辑本身可能是自洽的,只是实现目标和真实需求发生了偏移。

如果测试也是 AI 根据自己的理解生成的,那么测试通常只会进一步确认这种偏移后的行为。

AI 的最终总结也可能存在类似问题。它可能表示已经完成了全部需求,实际遗漏了某个入口;表示运行了完整测试,实际只运行了单个模块;表示不存在兼容性变化,实际修改了公共字段或错误码。

因此,AI 给出的任务总结更适合被当成一份待核对的索引,用来了解它声称完成了哪些工作,而不能直接作为验收结果。

API、依赖和配置幻觉#

AI 可能调用不存在的方法,使用错误版本的 API,生成已经废弃的配置项,或者把另一个框架中的写法混入当前项目。

在 Java 这类静态类型语言里,一部分问题能够被编译器发现。但涉及配置文件、命令行参数、动态语言、反射和外部服务时,错误可能一直到运行阶段才会暴露。

还有一些情况更加隐蔽。

某个 API 可能确实存在,但它的返回值、线程安全性、异常行为或者生命周期与 AI 假设的不一样。代码可以正常编译,正常情况下也能运行,只有在并发或异常路径中才出现问题。

依赖幻觉还可能进一步发展成软件供应链问题。一项覆盖 16 个模型和 576,000 份代码样本的研究发现,商业模型生成的依赖名称中平均至少有 5.2% 不存在,开源模型的对应比例为 21.7%,研究总共发现了 205,474 个不同的虚构包名。[2]

如果攻击者提前注册了模型经常虚构的包名,开发者或 Agent 又在没有检查来源的情况下直接安装依赖,就可能引入所谓的 Slopsquatting 风险。

所以,验证 API 和依赖真实性时,仅仅确认代码可以编译或依赖可以安装通常还不够,还需要确认:

  • 包是否真实存在
  • 是否来自正确的维护者
  • 包名是否存在拼写错误
  • 当前项目使用的版本
  • 方法签名和返回类型
  • 异常行为
  • 是否阻塞
  • 是否线程安全
  • 是否需要手动释放资源
  • 配置项是否在当前版本中生效
  • 新依赖是否真的有必要
  • Lockfile 是否出现预期之外的变化

只考虑正常路径#

AI 很擅长根据需求中明确给出的示例生成实现。

用户提供一个正常输入,它通常能够写出正常输出;用户提供一个具体报错,它也能够快速让这个报错消失。

问题在于,真实系统中的麻烦通常发生在需求没有明确描述的地方,例如:

  • 输入为空或格式错误
  • 数据已经存在
  • 外部服务超时
  • 数据库写入成功后后续操作失败
  • 同一消息被重复消费
  • 请求执行过程中被取消
  • 多个请求同时修改同一份数据
  • 服务重启后任务重新执行
  • 配置缺失或依赖暂时不可用

正常路径只是系统行为的一部分。很多严重问题都发生在一个流程只执行了一半的时候。

AI 生成的异常处理有时也只是看起来处理了异常:

try {
sendMessage();
} catch (Exception e) {
log.error("send failed", e);
}
return success();

这里确实捕获并记录了异常,但消息发送失败后接口仍然返回成功,调用方无法知道操作只完成了一部分。

所以,检查异常处理时还需要继续确认:

  • 异常是否被错误吞掉
  • 错误类型和错误码是否得到保留
  • 失败后是否留下部分状态
  • 事务能否正确回滚
  • 操作是否能够安全重试
  • 重试会不会重复执行副作用
  • 超时或取消后后台任务是否仍然提交结果
  • 错误能否通过日志、指标和告警被发现

跨模块之间不一致#

AI 在一次需求中可能同时修改前端、后端、数据库、消息格式和配置文件,但这些修改之间不一定完全一致。

常见情况包括:

  • 前端字段名和后端 DTO 不一致
  • 数据库字段类型和代码类型不一致
  • 生产者发送的消息格式与消费者预期不同
  • 接口返回结构发生变化,但旧客户端没有同步修改
  • 本地环境配置正确,容器或生产环境缺少配置
  • 修改了 Schema,却没有更新生成代码
  • 新版本写入的数据无法被旧版本读取

这类问题通常需要集成测试、契约测试或者完整运行环境才能发现。

单独看每一个模块,代码可能都没有问题,但系统之间的约定已经发生了变化。

修改范围不断扩大#

AI 在修改代码时经常会顺便做一些额外工作,例如:

  • 抽取公共接口
  • 统一异常处理
  • 调整目录结构
  • 替换依赖
  • 重命名大量类型
  • 增加扩展点
  • 增加兼容逻辑
  • 顺手清理旧代码

其中一些修改确实有价值,但也有不少并不是完成当前需求所必需的。

这里还有一个比较容易被忽略的原因:对 AI 来说,多写几行代码几乎没有成本。相比人类开发者需要权衡时间、复杂度和维护成本,AI 更倾向于通过增加代码量处理不确定性,例如加入更多防御性判断、兼容分支或兜底逻辑。

这种策略在短期内可能让代码更容易通过测试,但也会带来冗余实现、更复杂的执行路径,以及更大的验证范围。

原本只需要修改一个方法的需求,最后可能扩展到十几个文件,并增加新的抽象、配置项和依赖。

更大的 Diff 意味着更大的审查范围,也会增加测试、部署和回滚的成本。如果行为修改和大规模重构混在一起,即使代码最终没有问题,也会让验证变得困难很多。

在 Review 时,不能简单地认为代码越少越好,也不能看到新抽象就直接判断为过度设计。更重要的问题是:

  • 每一项修改是否能够对应当前需求
  • 新增抽象是否有真实使用场景
  • 是否混入了与需求无关的重构
  • 新旧实现是否会长期同时存在
  • 新依赖是否增加了明显的构建或运行成本
  • 修改是否让回滚变得更复杂

测试看起来很多,但没有真正保护行为#

AI 很容易生成大量测试代码,这些测试的覆盖率可能也不低。

但覆盖率只能说明某段代码被执行过,无法说明测试真的验证了它。

例如:

assertNotNull(result);

或者:

verify(repository).save(any());

这类测试可以通过,也会增加覆盖率,但并没有确认最终业务状态是否正确。

有些测试只是把实现逻辑重新写了一遍:

int expected = calculateWithSameLogic(input);
assertEquals(expected, actual);

如果实现和测试使用了相同的错误逻辑,测试自然会一直通过。

还有一些测试会过度依赖 Mock,把真正容易出错的边界一起替换掉。例如事务、数据库约束、序列化、消息队列和文件系统都被 Mock 后,测试能够验证 Service 调用了哪些方法,却无法确认真实系统是否可以运行。

这里需要区分三个概念:

测试数量:写了多少测试
代码覆盖率:有多少代码被执行
故障发现能力:代码出错时,测试能不能发现

AI 很容易提高前两项,但第三项仍然需要单独验证。

一项研究发现,部分由 LLM 生成的测试套件可以达到 100% 的代码覆盖率,但变异测试得分只有 4%。也就是说,测试几乎执行了所有代码,但研究者故意修改生产逻辑后,绝大多数错误仍然没有被测试发现。[3]

因此,覆盖率更适合用来发现哪些代码完全没有被测试触达,不能单独证明测试质量。

安全问题可能藏在功能正常的代码中#

AI 生成的安全问题通常也不会表现为代码完全不能运行。很多时候,功能正是因为缺少限制才显得更加顺畅。

例如:

  • 接口进行了身份认证,却没有检查对象级权限
  • 前端隐藏了按钮,但后端接口仍然可以直接调用
  • SQL、Shell、模板和文件路径直接拼接用户输入
  • 上传文件只检查扩展名,没有检查真实内容
  • Token、密码或个人信息进入日志
  • 使用普通随机数生成安全标识
  • 为了解决连接问题关闭 TLS 证书校验
  • 返回完整堆栈和服务器内部路径
  • 反序列化不可信对象
  • 接口没有大小限制、速率限制或重放保护

一项针对公开 GitHub 仓库中 7,703 个被明确标记为 AI 生成的文件所做的研究,通过 CodeQL 检测到了 4,241 个 CWE 实例,覆盖 77 种漏洞类型。不过研究同时指出,87.9% 的文件没有检测到 CWE 映射漏洞,因此这组结果不适合被简单理解为“AI 写的代码普遍不安全”。它更能说明,即使代码由当前模型生成,常规的软件安全检查仍然不能跳过。[4]

权限、输入处理、文件操作、网络请求、反序列化和敏感信息相关的代码,应该被视为高风险修改,进行专门的安全 Review 和测试。

对抗式代码审查#

基于这些问题,我做了一个名为 adversarial-code-review 的 Skill,主要用于独立审查 AI 生成的代码。

这里的“对抗式”并不是要求 Review Agent 故意找问题,也不是把代码风格差异当成缺陷。

它主要强调一种审查时的默认立场:

代码实现、注释、测试、Commit 信息、PR 描述和 AI 给出的完成声明,都只能作为待验证的信息。

比如 AI 在最终结果中表示:

Implemented order idempotency.
Added comprehensive tests.
All tests passed.

Review 时不能直接把这些内容当成事实,而是要继续确认:

  • 幂等性由什么机制保证
  • 幂等 Key 的作用范围是什么
  • 并发请求是否可能同时通过检查
  • 请求失败后 Key 是否会被错误保留
  • 重试时返回原结果还是重新执行
  • 测试是否覆盖了并发和失败情况
  • 当前环境中重新运行测试是否仍然通过

对抗式审查的核心,是重新建立证据。

整体流程大致包括:

读取项目说明和开发约定
→ 确认准确的审查范围
→ 从需求和设计文档中重建验收条件
→ 检查实际修改范围
→ 跟踪真实执行路径
→ 检查测试是否真正保护生产行为
→ 运行安全且可重复的验证命令
→ 再次检查仓库状态和最终 Diff

首先确认 Review 的边界#

在开始 Review 之前,需要先确认审查对象究竟是什么。

它可能是:

  • 工作区中尚未提交的修改
  • 暂存区中的修改
  • 某一个 Commit
  • 一组 Commit
  • 一个 Pull Request
  • 相对于指定分支的全部差异

如果比较边界没有确认清楚,AI 可能会审查大量无关代码,也可能漏掉需求真正涉及的修改。

这一步看起来很基础,但在实际使用 Coding Agent 时很容易被跳过。很多“帮我 Review 一下代码”的请求,并没有说明要比较哪个分支、哪些 Commit,最后 Agent 只能根据当前工作区猜测审查范围。

从需求重新检查实现#

Review 时不能只沿着代码当前的结构向下阅读,还要从原始需求反向确认每一项要求是否有对应的实现和测试。

如果需求中包含多个条件,可以简单建立一个需求矩阵:

需求生产代码测试状态
重复请求不能重复创建订单幂等逻辑重复请求测试部分验证
库存扣减与订单创建保持一致事务与数据库约束集成测试已验证
消息发送失败可以恢复Outbox 或补偿逻辑故障注入测试未验证

这种方法可以避免 Review 完全被当前实现带着走。

如果只顺着代码阅读,很容易关注“这段代码写得是否合理”;从需求反向检查时,还会看到“有一部分需求根本没有实现”。

根据 Diff 选择风险领域#

不同类型的修改,需要重点检查的问题并不一样。

例如:

  • 修改鉴权、文件处理和用户输入时,需要重点检查安全边界
  • 修改数据库写入时,需要检查事务和数据一致性
  • 修改消息队列、异步任务和重试时,需要检查幂等和重复执行
  • 修改公共 API 时,需要检查契约和兼容性
  • 增加批量操作、缓存和循环查询时,需要检查性能和资源使用
  • 修改部署文件和配置时,需要检查环境差异、可观测性和回滚

因此,Review 不适合每次机械执行完全相同的一份长清单。

更合理的方式是先从 Diff 中识别风险信号,再深入检查与当前修改相关的领域。这样既能避免遗漏高风险问题,也不会在无关方向上浪费大量上下文。

这里还需要注意:给 AI 提供更多上下文不一定会自动提高 Review 质量。

一项针对 350 个真实 Pull Request 的评测发现,8 个前沿模型在只查看 Diff 时,只能发现人工标记问题中的 15%~31%;当实验继续加入完整文件和更多结构化仓库上下文时,所有模型的表现反而持续下降,研究认为主要原因之一是长上下文中的注意力稀释。[5]

另一项针对 AGENTS.md 等仓库级上下文文件的研究也发现,这类文件总体上没有提高任务成功率,同时让推理成本平均增加超过 20%。研究者并没有因此认为上下文文件完全无用,而是建议其中只保留当前项目真正必要、准确且可执行的要求。[6]

所以,比起一次性让 AI 读取整个仓库,更合适的方式通常是:

  1. 先提供明确的需求和验收条件
  2. 确认精确的 Diff 和比较边界
  3. 根据 Diff 识别高风险领域
  4. 按需读取相关调用方、配置和测试
  5. 对关键执行路径进行深入跟踪
  6. 避免无关文档和历史内容淹没当前任务

单独检查范围膨胀#

对于比较大的修改,我认为有必要额外进行一次范围和过度设计检查。

这一阶段需要重新确认每一个新增文件、抽象、依赖和配置是否与当前需求有关,例如:

  • 新增接口是否有多个真实实现
  • 新增扩展点是否只是为未来需求提前设计
  • 是否混入了无关的重命名和格式化
  • 是否同时保留新旧两条容易分歧的实现路径
  • 新依赖带来的构建和运行成本是否合理
  • 大范围重构是否让行为变化更难验证和回滚

重点依然是具体影响。

代码量增加本身并不是缺陷,抽象层次多也不一定是问题。只有当这些额外设计带来具体的维护、兼容、部署或正确性风险时,才有必要作为问题提出。

单独检查测试完整性#

另一个需要独立检查的问题,是测试是否真正有效。

测试 Review 时可以重点关注:

  • 原来的测试是否被删除或禁用
  • 断言是否变得更宽松
  • 是否从具体异常改成了任意异常
  • 测试数据是否被修改,从而避开原来的问题
  • Mock 是否替代了真正容易出错的组件
  • 是否只验证方法调用,没有验证最终状态
  • 新测试是否会在原问题重新出现时失败
  • 旧功能的保护是否仍然存在

一个比较直接的问题是:

如果删除生产代码中的关键判断,这个测试会失败吗?

如果不会,那么测试可能只是在执行代码,并没有真正验证行为。

代码生成速度可能超过 Review 速度#

AI 可以在很短时间内生成多个文件和大量测试,但人的审查速度不会同步提高。

这会让 Review 逐渐成为整个开发流程中新的瓶颈。

当 Diff 很大时,人和 AI 都更容易:

  • 只检查最明显的文件
  • 默认相信看起来标准的样板代码
  • 将注意力放在风格问题上
  • 忽略跨模块和失败路径
  • 因为测试全绿而降低警惕
  • 没有时间完整理解新增加的抽象
  • 由调用 Agent 的开发者自己快速确认结果

一项结合 38,709 篇工程博客和 Reddit 讨论的研究,从中进一步分析了 3100 篇与 AI Code Review 有关的内容。研究将 Review 视为决定 AI 最终提高还是降低软件质量的关键控制点,并把审查负担、理解债务、Reviewer 能力和验证深度列为社区反复讨论的核心因素。[7]

这意味着控制 Diff 大小、拆分任务和标记高风险文件,不只是为了让 Git 历史更整洁,也是在提高后续验证真正生效的概率。

对于比较大的需求,可以尽量拆成:

接口和数据结构
→ 核心业务逻辑
→ 持久化与迁移
→ 外部集成
→ 测试
→ 部署配置

每一步保持相对独立,更容易确认需求、实现和测试之间的关系。

理解债务#

AI Coding 还可能产生一种不容易立即发现的债务:代码已经进入项目,但团队中没有人真正完整理解它。

开发者知道自己提出了什么需求,却不一定了解 AI 选择了什么实现;Reviewer 可能只快速看过 Diff;AI 当前上下文中的解释也不会自动成为项目长期知识。

短期来看,代码可以正常工作。

但以后发生下面这些事情时,问题可能逐渐暴露:

  • 需要增加新功能
  • 需要排查线上故障
  • 依赖需要升级
  • 数据结构需要迁移
  • 原来的 Agent 上下文已经消失
  • 新维护者需要修改隐含约束
  • AI 再次生成了一套不同的实现

这种情况下,团队需要重新理解一段没有人真正拥有过的代码。

所以,测试通过和代码可维护是两个不同维度。对于关键模块,开发者至少应该能够解释:

  • 主要执行路径是什么
  • 核心状态存在哪里
  • 哪些约束由代码保证
  • 哪些约束由数据库或外部系统保证
  • 失败后如何恢复
  • 为什么选择当前设计
  • 修改和回滚时需要注意什么

AI 可以帮助生成这些说明,但最终仍然需要有人确认它们与实际代码一致。

不一定要等 AI 写完以后再开始测试#

前面的内容主要在讨论 AI 写完代码以后,如何通过 Review 和各种测试方法确认结果是否可靠。

但从实际开发流程来看,测试不一定只能发生在代码完成之后。

如果已经知道 AI 容易根据自己的实现补测试,并让实现和测试共享同一个错误理解,那么一个比较自然的改进方式,就是在 AI 开始写生产代码之前,先把需要满足的行为固定下来。

这正好对应了 TDD,也就是测试驱动开发(Test-Driven Development)的思路。

TDD 通常会被概括为三个阶段:

Red:先写一个会失败的测试
→ Green:编写最少的代码让测试通过
→ Refactor:在测试保护下重构代码

传统上,TDD 主要用于帮助开发者逐步设计接口、控制实现范围,并在修改过程中持续获得反馈。

放到 AI Coding 中,它还有另一个比较重要的作用:避免 AI 完全根据自己刚刚写出的实现来定义测试。

先写测试,再让 AI 写实现#

假设需要修复一个金额为负数时没有被拒绝的问题。

比较常见的 AI Coding 流程是:

让 AI 修复问题
→ AI 修改生产代码
→ AI 根据修改结果补充测试
→ 测试通过

这种方式的问题是,AI 可能只针对用户提供的具体输入进行修复。

例如当前报错使用的是 -1,AI 最后可能写出:

if (value == -1) {
throw new IllegalArgumentException();
}

随后再补充一个输入为 -1 的测试。实现和测试完全一致,当前问题也确实消失了,但其他负数仍然可以进入系统。

使用 TDD 时,可以先根据需求写出测试:

@ParameterizedTest
@ValueSource(ints = {-1, -2, -100, Integer.MIN_VALUE})
void shouldRejectNegativeValues(int value) {
assertThrows(
IllegalArgumentException.class,
() -> service.process(value)
);
}

确认测试在当前实现下失败,再让 AI 修改生产代码,使测试通过。

这种顺序会给 AI 一个更明确的行为边界。它不再只需要让某一个报错消失,而是需要满足测试中表达的一组规则。

TDD 可以减少测试对实现的依赖#

AI 在看到完整实现后生成测试时,很容易围绕当前代码结构编写测试。

比如生产代码调用了三个 Repository 方法,AI 可能生成:

verify(repository).findById(any());
verify(repository).save(any());
verify(eventPublisher).publish(any());

这种测试验证了实现过程,却没有直接验证最终业务结果。

如果在实现之前先从需求编写测试,测试更容易关注外部行为,例如:

  • 接口应该返回什么
  • 数据库最终保存了什么
  • 非法输入应该产生什么错误
  • 重复请求是否产生重复副作用
  • 操作失败后数据是否保持一致

也就是说,测试会更接近“代码应该做什么”,而不是“当前代码是怎么做的”。

这对于 AI Coding 尤其重要,因为 AI 很容易将当前实现方式误认为需求本身。

TDD 也可以限制修改范围#

AI 在没有明确边界时,可能为了实现一个小功能修改大量代码,增加新的抽象和配置。

如果先有一组明确的失败测试,AI 的目标会变得更加具体:

让这些测试通过
同时保持原有测试不失败

这并不能完全避免范围膨胀,但至少为修改提供了一个可观察的结束条件。

当 AI 开始引入与测试和需求无关的大规模重构时,也更容易判断这些改动是否真的必要。

对于修复 Bug,可以使用下面的流程:

复现问题
→ 编写失败测试
→ 确认测试因目标问题而失败
→ 让 AI 修改最小范围的生产代码
→ 确认新测试通过
→ 运行完整回归测试
→ 再进行代码 Review

其中“确认测试为什么失败”很重要。

如果测试是因为环境没有启动、Fixture 错误或依赖不可用而失败,它并不能作为目标问题的有效证据。只有当失败确实来自需要修复的行为,后面的测试通过才有意义。

对新需求也可以先写验收测试#

TDD 不只适合修复 Bug,新功能同样可以先从验收条件开始。

例如需要实现订单接口的幂等性,可以先定义:

  • 相同请求顺序执行时只创建一笔订单
  • 相同请求并发执行时只创建一笔订单
  • 第一次请求失败后可以重新执行
  • 第一次请求成功后,重试返回相同结果
  • 不同用户使用相同请求 ID 时互不影响

然后先编写相应测试,再让 AI 设计和实现幂等机制。

这样会迫使实现面对真正的边界,而不是只在代码中加入一个看起来像幂等判断的 if

对于复杂功能,也不一定要在开始时写完所有测试。可以先确定最核心的行为,再逐步增加边界和失败路径:

正常路径测试
→ 边界测试
→ 异常测试
→ 并发和重试测试
→ 集成和端到端测试

这仍然符合 TDD 的增量开发思路,也比一次性让 AI 生成完整实现和完整测试更容易检查。

让谁来写测试#

在 AI Coding 中,即使采用 TDD,也需要考虑测试由谁生成。

最理想的情况当然是开发者先理解需求,并亲自确定最重要的验收条件。但这并不意味着所有测试都必须手写。

一种比较实际的方式是:

  1. 人先确认需求和关键约束
  2. 让独立 Agent 根据需求生成失败测试
  3. 人检查测试是否准确表达需求
  4. 再让实现 Agent 编写生产代码
  5. 使用新的 Review 上下文检查实现和测试

关键并不在于测试代码由人还是 AI 敲出来,而在于测试的依据应该来自需求和外部行为,而不是来自已经完成的实现。

也可以让 AI 先列出测试用例,不立即写代码。例如:

请先根据需求整理验收条件和测试用例,不要读取或假设最终实现方式,也不要修改生产代码。

人确认这些测试用例后,再让 AI 编写测试和实现。

这样至少可以在生产代码出现之前,将一部分需求转化为明确的验证条件。

TDD 不能解决所有问题#

TDD 很适合减少“先写错误实现,再用同样的理解补测试”的问题,但它并不能自动保证代码正确。

如果最开始写出的测试就错误理解了需求,那么后面的实现仍然会围绕错误目标展开。

如果测试只覆盖局部逻辑,它也无法发现:

  • 数据库事务没有生效
  • 前后端契约不一致
  • 消息发送和数据库提交之间出现部分失败
  • 生产环境配置缺失
  • 并发情况下产生竞态
  • 性能随着数据量快速下降
  • 权限和安全边界被绕过

TDD 也可能被 AI 机械执行。比如为了让测试快速通过,AI加入大量针对测试输入的特殊分支,或者修改测试本身。

所以,在 AI Coding 场景中,更合适的方式不是把 TDD 当成完整答案,而是把它放在整个验证流程的前面:

需求和验收条件
→ TDD 固定核心行为
→ AI 编写实现
→ 独立代码 Review
→ 分层自动化测试
→ 环境、并发和故障验证
→ 灰度发布和线上观测

TDD 主要负责尽早限制实现方向,让一部分验证发生在生产代码之前。

后面的集成测试、契约测试、E2E、故障注入和线上观测,则继续验证单元测试无法覆盖的系统行为。

从这个角度来看,TDD 和后面的测试方法并不冲突。

它们分别解决的是两个阶段的问题:

TDD:在代码生成过程中,降低实现偏离需求的概率
后续验证:在代码完成后,检查仍然存在的系统性风险

对于 AI Coding 来说,把验证提前,通常比在 AI 一次性生成大量代码后再努力补救更加有效。

如何测试 AI 写的代码#

代码审查主要用于发现风险和缺少证据的地方,测试负责将这些问题转化为可以重复执行的验证。

不同测试解决的问题并不一样,因此通常需要根据代码风险组合使用。

编译、类型检查和静态分析#

这是成本最低,也最适合最先执行的一层。

编译和类型检查可以发现:

  • 不存在的类和方法
  • 参数类型错误
  • 返回值不匹配
  • Import 错误
  • 部分 API 和依赖使用错误

静态分析工具可以进一步检查潜在 Bug、代码规范和安全问题。

Java 项目中常见的工具包括:

  • Checkstyle
  • PMD
  • SpotBugs
  • Semgrep
  • OWASP Dependency-Check
  • CodeQL

TypeScript 项目则通常可以使用类型检查、ESLint 和依赖漏洞扫描。

这一层很适合拦截基础问题和一部分 API 幻觉,但无法判断业务逻辑是否正确。

单元测试#

单元测试主要验证一个相对独立的函数、类或业务方法。

在 AI Coding 场景中,单元测试最需要关注的是边界情况。因为 AI 通常可以较好地处理需求中明确给出的例子,对需求没有直接说明的输入则不一定考虑完整。

比如测试优惠计算逻辑,除了正常用户和正常优惠券,还应该考虑:

  • 价格为零或负数
  • 优惠券为空
  • 优惠券已经过期
  • 折扣金额超过商品价格
  • 金额精度和舍入
  • 用户等级不存在
  • 多种优惠规则发生冲突

单元测试适合快速验证局部逻辑,但如果风险发生在数据库、序列化、网络和事务之间,单元测试提供的保证仍然有限。

集成测试#

集成测试主要验证多个真实组件连接后能否正常工作。

例如 Spring Boot 接口的集成测试可能经过:

Controller
→ Service
→ Repository
→ MySQL

也可能包含 Redis、消息队列和第三方服务。

集成测试可以发现很多单元测试无法发现的问题,例如:

  • SQL 与实体字段不一致
  • 数据库约束与代码假设不同
  • 事务注解没有生效
  • 序列化格式不一致
  • Redis Key 使用不一致
  • 消息在事务提交前被发送
  • 环境配置缺失

涉及数据库、Redis 和消息队列时,可以使用 Testcontainers 启动临时的真实组件。这样既能保持测试环境相对独立,也能避免所有依赖都被 Mock 后失去测试价值。

契约测试#

契约测试主要验证不同模块或服务之间约定的数据格式。

例如前端依赖后端返回:

{
"code": 200,
"data": {
"id": 1,
"name": "Tom"
}
}

AI 重构后将其修改为:

{
"status": "ok",
"result": {
"userId": 1,
"username": "Tom"
}
}

后端自身仍然可以正常运行,但现有前端或其他调用方会直接出现问题。

契约测试通常会检查:

  • 请求路径
  • 请求字段
  • 响应字段
  • 数据类型
  • 是否允许为空
  • 状态码和错误码
  • 消息事件格式
  • API 版本兼容性

这类测试很适合前后端分离项目、微服务、SDK、插件系统和 MCP Tool 等存在明确调用边界的场景。

端到端测试#

端到端测试,也就是 E2E 测试,会从真实用户操作出发,验证完整业务链路。

例如电商系统的下单流程可能包括:

打开页面
→ 登录
→ 查询商品
→ 加入购物车
→ 创建订单
→ 支付
→ 查看订单状态

这个过程会经过前端、后端、数据库、缓存和其他服务,因此能够发现模块单独测试时看不到的问题。

E2E 测试通常运行较慢,失败后的定位成本也比较高,因此没有必要用它覆盖所有细节。更合理的方式是用单元测试和集成测试覆盖大量局部行为,再用少量 E2E 测试保护最重要的用户路径。

冒烟测试#

冒烟测试用于快速确认系统是否具备最基本的可用性。

例如后端服务部署后可以检查:

  • 应用能否启动
  • 健康检查接口是否正常
  • 数据库是否可以连接
  • 登录是否可用
  • 核心查询接口是否可用
  • 核心写入接口是否可用

它并不追求覆盖所有业务细节,主要回答“这个版本部署后是否基本可用”。

因此,冒烟测试很适合放在 CI/CD 的部署阶段、测试环境部署后、生产灰度发布后,以及紧急修复上线后执行。

回归测试#

回归测试用于确认新修改没有破坏原本正常工作的功能。

AI 在修复问题时经常会只关注当前报错,为了解决一个具体输入而修改公共逻辑,最终导致其他输入或调用方出现问题。

一种比较实用的流程是:

先编写可以稳定复现问题的失败测试
→ 确认测试失败原因符合预期
→ 修改生产代码
→ 确认新测试通过
→ 运行相关回归测试

相比直接要求 AI“修复这个问题”,这种方式更容易限制修改范围,也能够为以后留下稳定的回归保护。

这里还需要注意代码对现有测试的过拟合。

例如真实规则是所有负数都应该被拒绝,而已有测试只使用了 -1,AI 可能直接生成:

if (value == -1) {
throw new IllegalArgumentException();
}

当前测试通过了,但问题并没有得到完整解决。

因此,回归测试除了复现原始问题,也要检查与原问题等价的输入和相邻边界。

属性测试#

普通单元测试通常由开发者指定具体输入和预期输出。

属性测试则是定义结果始终应该满足的性质,再由测试框架自动生成大量输入。

比如测试排序函数,可以定义:

  • 输出长度与输入相同
  • 输出包含的元素与输入一致
  • 输出顺序满足排序要求
  • 对结果再次排序不会发生变化

测试框架可以自动生成空数组、重复元素、负数、大数组和各种随机组合。

属性测试比较适合算法、解析器、格式转换和数据处理等输入空间较大的代码,也很适合发现 AI 只针对少量示例实现的问题。

模糊测试#

模糊测试通过向程序输入大量随机、异常或畸形数据,检查程序是否崩溃、进入异常状态或产生安全问题。

它比较适合:

  • JSON、XML 和协议解析
  • 文件上传和解压
  • URL 与路径处理
  • 正则表达式
  • 序列化与反序列化
  • 表达式解析
  • 网络请求处理

AI 编写这类代码时容易默认输入格式正确,而真实环境中经常会出现损坏文件、超长字符串、错误编码、嵌套结构和恶意输入。

变异测试#

变异测试可以用来判断测试本身是否有效。

它会自动对生产代码做一些小的错误修改,例如:

  • > 改成 >=
  • true 改成 false
  • 删除一个条件判断
  • 替换返回值
  • 移除一次方法调用

之后重新运行测试。

如果生产代码已经被明显破坏,但测试仍然全部通过,说明现有测试没有真正验证这部分行为。

代码覆盖率只能说明一行代码是否执行过,变异测试进一步检查测试能不能识别这行代码产生的行为差异。

由于 AI 很容易生成覆盖率看起来不错、但断言能力较弱的测试,变异测试在 AI Coding 场景中非常有价值。

故障注入#

对于事务、消息队列、异步任务和外部接口,只测试正常情况通常不够。

故障注入会主动模拟系统某一部分失败,例如:

  • 数据库写入成功后消息发送失败
  • 第三方接口超时
  • 消息被重复投递
  • 消费者处理完成但 ACK 失败
  • 服务执行到一半时退出
  • Redis 暂时不可用
  • 请求取消后后台任务仍然继续执行

通过这些测试可以检查:

  • 数据是否会出现部分写入
  • 操作能否安全重试
  • 是否会产生重复订单或重复扣款
  • 事务和补偿机制是否有效
  • 系统是否能够恢复
  • 日志、指标和告警能否发现问题

很多分布式系统中的严重问题,都发生在流程只完成了一部分的时候。这也是 AI 生成代码最容易遗漏的部分之一。

并发、重试和幂等测试#

并发问题通常很难通过普通单元测试稳定触发,因此值得单独处理。

对于订单、库存、支付、消息和定时任务等逻辑,可以主动验证:

  • 多个相同请求同时到达
  • 多个请求同时更新同一条记录
  • 同一消息重复投递
  • 消费成功但 ACK 失败
  • 客户端、SDK 和服务端同时重试
  • 请求超时后服务仍然继续执行
  • 锁超时、事务冲突和唯一约束失败
  • 不同顺序的消息到达

测试时不能只检查请求是否返回成功,还要检查最终数据库状态、副作用次数和恢复结果。

很多幂等问题的关键也不在于代码中是否存在 if (processed),而在于检查和写入是否由真正的原子机制保证。

性能和压力测试#

代码在少量测试数据下能够运行,不代表它在真实数据规模下仍然可用。

AI 生成代码中比较常见的性能问题包括:

  • N+1 数据库查询
  • 在循环中调用远程接口
  • 重复执行解析和序列化
  • 一次性加载全部数据
  • 队列和缓存没有大小限制
  • 不受控制地创建线程或异步任务
  • 批量操作中缺少分页
  • 日志量随输入规模快速增长

对于关键路径,可以通过 Benchmark、并发测试和压力测试记录:

  • 响应时间
  • 吞吐量
  • CPU 与内存使用
  • 数据库连接数
  • 队列积压
  • 错误率
  • 不同数据规模下的性能变化

性能测试最好保留历史基线。只有一个孤立的测试结果,很难判断新代码是否造成了明显退化。

A/B 测试#

A/B 测试和前面的测试解决的问题有所不同。

单元测试、集成测试和 E2E 测试主要用于验证代码是否符合预期,A/B 测试用于比较两个方案在真实用户中的效果。

例如:

  • 新推荐算法是否提高点击率
  • 新页面布局是否提高转化率
  • 不同 Prompt 是否提高 Agent 的任务完成率
  • 新搜索排序是否让用户更容易找到目标内容

因此,A/B 测试应该放在功能、安全和稳定性验证之后。

代码先要被证明可以正常工作,之后再比较新方案是否更好。真实用户流量不适合用来发现基础功能问题。

如何组合这些测试方法#

不同测试方法覆盖的是不同风险,没有一种测试能够单独解决所有问题。

风险主要验证方式
不存在的 API、类型和依赖编译、类型检查、依赖和官方文档核验
需求理解错误需求矩阵、验收测试、独立 Review、E2E
边界情况遗漏单元测试、属性测试、模糊测试
数据库和组件连接问题集成测试、Testcontainers
前后端或服务契约变化契约测试
测试断言过弱测试审查、变异测试
测试被删除或弱化测试 Diff、历史测试对比
旧功能被破坏回归测试
事务和部分失败集成测试、故障注入
重复请求和重复消息幂等测试、并发测试、重复投递测试
权限和输入安全安全 Review、静态扫描、模糊测试
性能退化Benchmark、压力测试、基线对比
修改范围膨胀精确 Diff Review、范围专项审查
部署后无法运行冒烟测试、临时环境验证
新方案效果是否更好A/B 测试

在实际项目中,没有必要对每一次小修改执行所有测试。

更合理的方法是先根据代码变化识别风险,再选择能够覆盖这些风险的测试。

比如修改一个纯函数时,单元测试和属性测试可能已经足够;修改数据库事务时,需要增加真实数据库集成测试;修改消息消费逻辑时,还要测试重复投递、ACK 失败和重试;修改用户完整操作流程时,则可能需要补充 E2E 测试。

一套适合 AI Coding 的验证流程#

结合前面的代码审查和测试方法,我目前认为一套比较完整的 AI Coding 验证流程可以分成下面几个阶段。

1. 明确需求和验收条件#

在让 AI 修改代码前,尽量明确:

  • 需要实现什么行为
  • 哪些行为不能改变
  • 输入和输出是什么
  • 异常情况下应该怎样处理
  • 是否存在兼容性要求
  • 如何判断任务已经完成

验收条件越模糊,AI 越容易按照自己的理解补全需求。

对于比较重要的需求,可以先让 AI 整理验收条件,但需要由人确认,避免后面的实现和测试全部建立在 AI 自己补充的假设上。

2. 限制修改范围#

要求 AI 在修改前先说明预计修改哪些文件、为什么需要修改,以及是否会影响公共接口、数据库和配置。

这不能完全阻止范围扩大,但可以让后续 Review 更容易识别计划外修改。

大型任务也可以拆成多个较小的步骤,避免一次生成一个难以审查的巨大 Diff。

3. 检查精确 Diff#

代码完成后,先确认实际修改范围,并将重要修改映射到需求。

如果一个文件无法对应当前需求,或者不属于必要的测试、配置和文档,就需要进一步确认它存在的理由。

同时检查:

  • 是否生成了预期之外的文件
  • Lockfile 和快照是否被修改
  • 是否发生了大范围格式化
  • 是否混入无关重构
  • 是否存在只为测试服务的生产代码分支

4. 进行独立代码审查#

Review Agent 应该重新读取需求、项目约定和代码差异,避免直接继承实现 Agent 的结论。

条件允许时,可以使用新的上下文或独立 Agent,减少实现阶段形成的思路对 Review 的影响。

审查重点包括:

  • 需求是否真正落到生产路径
  • 边界和失败行为是否完整
  • API 和依赖是否真实可用
  • 事务、并发、重试和幂等是否正确
  • 是否存在安全和权限问题
  • 修改是否影响兼容性、部署和回滚
  • 测试是否真正覆盖了风险
  • 修改范围是否合理

5. 执行自动化检查#

首先运行成本较低的检查:

编译
→ 类型检查
→ 格式与规范检查
→ 静态 Bug 检查
→ 安全扫描
→ 依赖漏洞检查

之后再根据风险执行单元测试、集成测试、契约测试和专项测试。

命令结果需要来自当前环境中的实际执行,不能只依赖 AI 表示“之前已经运行过”。

6. 检查测试本身#

除了确认测试通过,还要检查:

  • 测试是否到达真实生产路径
  • 是否过度依赖 Mock
  • 断言是否验证最终状态
  • 原来的测试是否被削弱
  • 新测试能否在缺陷重新出现时失败
  • 是否包含异常、边界和部分失败路径
  • 是否遗漏相邻调用方和历史数据

对于重要逻辑,可以进一步运行变异测试。

7. 在真实运行环境中验证#

通过测试环境或临时环境执行:

  • 集成测试
  • E2E 测试
  • 冒烟测试
  • 故障注入
  • 并发测试
  • 性能测试

这一阶段主要用于发现编译和单元测试覆盖不到的环境、配置和组件交互问题。

8. 灰度发布并观察运行情况#

上线后需要继续关注:

  • 错误率
  • 响应时间
  • 日志和异常
  • 数据库与缓存状态
  • 队列积压
  • 资源使用
  • 业务指标
  • 新旧版本同时运行时的兼容情况

如果修改会影响用户体验或业务效果,可以在确认功能稳定后继续进行 A/B 测试。

整个流程可以概括为:

需求和验收条件
→ AI 生成代码
→ 精确 Diff 审查
→ 独立对抗式 Review
→ 静态检查
→ 分层自动化测试
→ 环境、并发与故障验证
→ 灰度发布和线上观测

从证据角度看,也可以理解为:

需求证据
→ 代码差异证据
→ 审查证据
→ 测试证据
→ 运行证据
→ 线上证据

每一层都在补充上一层无法提供的保证。

需求可以说明代码应该做什么,Diff 可以说明 AI 实际改了什么,Review 可以发现实现中的风险,测试可以重复验证具体行为,运行环境可以暴露组件和配置问题,线上观测则负责确认代码在真实负载下仍然正常。

产出更多代码,不一定代表整体交付更快#

AI Coding 确实能够明显提高代码生成速度,但软件开发的完整成本还包括:

  • 理解需求
  • 检查 AI 输出
  • 修改不合适的实现
  • 运行测试
  • Review Diff
  • 排查环境问题
  • 上线后的维护和返工

METR 在 2025 年进行过一项范围比较有限的随机对照实验:16 名熟悉各自成熟开源项目的开发者,在使用当时的 AI Coding 工具后,完成任务的时间平均增加了 19%。参与者在实验前认为 AI 会让自己快 24%,实验结束后也仍然认为自己快了约 20%。[8]

这项实验的样本和场景都比较有限,不能用来说明 AI Coding 在所有情况下都会降低效率。它更值得注意的地方是,主观上觉得代码生成变快,不一定代表整个任务真的完成得更快。

另一项对开源项目的研究也发现,AI 工具带来的代码产量增长主要来自经验相对较少的外围开发者,但新增代码需要更多返工。核心开发者需要 Review 的代码增加了 6.5%,自身原创代码产出则下降了 19%。[9]

因此,真正需要优化的应该是从需求到上线、再到长期维护的整体吞吐量,而不只是生成了多少代码、提交了多少行,或者一天完成了多少个 PR。

总结#

AI Coding 已经提高了代码的产出速度。

现在一个开发者可以在很短时间内生成过去需要较长时间完成的实现、测试和文档。不过代码产出得越快,需要审查和验证的内容也会越多。

如果验证能力没有同步跟上,AI 只会让项目更快地积累一批“看起来已经完成,但很难确认是否可靠”的代码。

从我目前的使用经验和相关研究来看,验证 AI 代码时最需要避免的是将某一个信号直接当成最终结论:

  • 能编译不代表业务逻辑正确
  • 测试通过不代表测试本身有效
  • 覆盖率高不代表故障发现能力足够
  • AI Review 没有发现问题不代表代码没有问题
  • 提供更多上下文不一定带来更好的 Review
  • 本地能够运行不代表生产环境可以安全部署
  • AI 给出了完整总结,也不代表它真的验证了所有声明
  • 短期生成了更多代码,也不代表整体交付和维护成本更低

adversarial-code-review 主要解决的是审查阶段的问题,让 AI 在 Review 时重新确认需求、Diff、执行路径和测试证据,不直接接受实现者给出的完成声明。

在这之后,还需要通过静态分析、分层测试、故障注入、环境验证和线上观测继续补充证据。

AI 可以参与代码编写、测试和 Review,但这些过程需要尽可能保持独立,也需要依赖能够重复执行和观察的结果。对于高风险修改,人仍然需要理解关键设计、确认验收条件,并对最终结果负责。

当需求、实现、测试和运行结果能够互相对应时,我们才可以比较有把握地认为,一段 AI 生成的代码真正完成了。

参考资料#

[1] Michael Konstantinou, Florian Tambon, Mike Papadakis. On the Risk of Coding Before Testing: An Empirical Study on LLM-Based Test Generation Workflow. arXiv:2607.05139, 2026.

[2] Joseph Spracklen, Raveen Wijewickrama, A H M Nazmus Sakib, et al. We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs. arXiv:2406.10279, 2024.

[3] Guancheng Wang, Qinghua Xu, Lionel Briand, Kui Liu. Mutation-Guided Unit Test Generation with a Large Language Model. arXiv:2506.02954, 2025.

[4] Maximilian Schreiber, Pascal Tippe. Security Vulnerabilities in AI-Generated Code: A Large-Scale Analysis of Public GitHub Repositories. arXiv:2510.26103, 2025.

[5] Deepak Kumar. SWE-PRBench: Benchmarking AI Code Review Quality Against Pull Request Feedback. arXiv:2603.26130, 2026.

[6] Thibaud Gloaguen, Niels Mündler, Mark Müller, Veselin Raychev, Martin Vechev. Evaluating AGENTS.md: Are Repository-Level Context Files Helpful for Coding Agents?. arXiv:2602.11988, 2026.

[7] Shyam Agarwal, Courtney Miller, Christian Kästner, Bogdan Vasilescu. 3100 Opinions on Code Review in an AI World: Building Causal Theory from Practitioner Discourse. arXiv:2607.07980, 2026.

[8] Joel Becker, Nate Rush, Elizabeth Barnes, David Rein. Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity. arXiv:2507.09089, 2025.

[9] Feiyang Xu, Poonacha K. Medappa, Murat M. Tunc, et al. AI-Assisted Programming Decreases the Productivity of Experienced Developers by Increasing the Technical Debt and Maintenance Burden. arXiv:2510.10165, 2025.

AI Coding 之后,如何验证 AI 写的代码
https://contrue.top/posts/how-to-review-code-from-ai/
Author
contrueCT
Published at
2026-07-10
License
CC BY-NC-SA 4.0