跳至内容
AI Coding 如何保证代码质量?从代码生成到验证工程

AI Coding 如何保证代码质量?从代码生成到验证工程

AI Coding Agent 完成了上一篇文章里的订单 CSV 导出功能。它给出的交付说明很让人放心:

已实现订单导出。
新增 12 条测试。
所有测试通过。
没有 Lint 错误。

Pull Request 很整洁,测试数量增加了,所有检查都是绿色。可到了验收阶段,你却发现三个问题:

  • 员工修改请求里的 departmentId,就能导出其他部门的订单;
  • 导出任务运行期间切换筛选条件,最终结果也跟着变了;
  • 浏览器测试只检查“下载”按钮有没有出现,从未检查下载的 CSV 里到底是什么。

Agent 不一定在撒谎。它确实为自己提出的问题提供了证据。真正的问题是:它提出的问题,比这个任务的实际风险更弱。

这就是 AI Coding 最核心的质量问题:生成只是提出主张,验证才决定这个主张值得多少信任。 生成更多测试,不会自动得到更强的证据。测试可能执行了错误场景、断言了错误结果,也可能和实现代码共享同一个错误假设。

代码只是主张,分层证据决定它能否交付

这篇文章会把“验证”变成一套可执行的工程流程。完成后,你将能够:

  • 正确地把 TDD 用在 AI Coding 中,而不是简单要求“先生成测试”;
  • 根据风险选择单元、集成、契约和端到端测试;
  • 让 Codex 或其他 Coding Agent 提供可以审计的交付证据;
  • 把 AI Code Review 当成额外的传感器,而不是质量裁判;
  • 用 CI 门禁阻止未经验证的修改进入发布流程。

一、代码质量不是生成文本自带的属性

代码质量包含多个相互独立的维度:

问题常见证据检查通过仍然不能证明什么
代码能否解析和构建Formatter、编译器、类型检查业务行为正确
一条局部规则是否成立单元测试、属性测试多个组件正确集成
组件之间是否达成一致集成测试、契约测试真实用户能够走完整流程
关键用户旅程是否可用E2E 测试、运行时证据所有边界和安全风险都已覆盖
这次修改是否可以合并Diff Review、CI 门禁、必要审批生产环境永远不会出错

没有任何一层可以单独叫作“质量检查”。真正的信心来自多份相互独立的证据,它们以合理成本覆盖了最重要的失败方式。

可以用下面这个模型思考:

交付信心 = 需求覆盖
         × 证据强度
         × 环境真实性
         × 评审独立性

这不是用来计算分数的公式,而是一套诊断工具。只要其中一个因素接近零,大量通过的测试仍然可能只提供很弱的信心:

  • **需求覆盖不足:**测试从未问过“能不能导出其他部门”。
  • **证据强度不足:**断言只检查 HTTP 200,没有检查返回了哪些订单。
  • **环境真实性不足:**Mock 出来的任务队列与真实环境行为不同。
  • **评审独立性不足:**同一个 Agent 根据同一个错误假设生成了需求解释、实现、测试和最终结论。

解决方法不是要求穷尽所有测试,而是让证据与风险匹配。

二、先建立一份“验证合同”

写代码和测试之前,先把验收标准整理成一张小型验证矩阵。继续使用订单导出案例:

验收标准主要风险成本最低且足够强的证据
员工只能导出自己有权查看的部门越权泄露服务层权限测试 + API 集成测试
导出使用点击时的筛选快照数据不一致不可变快照单元测试 + 并发操作 E2E
CSV 中的中文可以正确打开数据损坏CSV 编码器测试 + 下载文件断言
大数据量异步导出超时、页面卡死任务集成测试 + 浏览器冒烟流程
旧版 API 客户端继续可用兼容性回归契约测试 + Diff Review

这张表把上一篇文章的规格与验收标准连接到了可执行证据。它可以避免 Agent 只选择最容易运行的检查。

每一行都要回答四个问题:

  1. **必须成立的可观察行为是什么?**不要写“功能正常”。
  2. **它可能在哪一层边界失败?**纯逻辑、数据库、API、浏览器、第三方、权限还是部署?
  3. **哪一种测试能以最低成本诚实地观察这个边界?**优先从低层开始,只有能增加不同证据时才加入宽测试。
  4. **谁或什么负责独立判断?**确定性工具、独立 Review、领域 Owner 还是安全人员?

现在,“完成”有了一份合同。Agent 可以提出验证方案,但不能悄悄重新定义成功。

三、TDD 是设计与反馈闭环,不是文件创建顺序

Martin Fowler 在当前的 Test-Driven Development说明中,把 TDD 概括为三个反复执行的步骤:

  1. 为下一个行为写测试;
  2. 写足够的功能代码,让测试通过;
  3. 重构代码与测试,保持结构整洁。

第三步不能省略。没有重构,TDD 也可能产生一堆“有测试保护的补丁”。使用 AI Agent 时还要增加一道保护:接受绿灯之前,必须亲眼观察红灯。

1. Red:证明测试真的能发现缺失行为

假设 sales 部门的员工尝试导出 finance 部门订单:

import { describe, expect, it } from "vitest";
import { authorizeExport } from "./export-policy";

describe("authorizeExport", () => {
  it("rejects an employee exporting another department", () => {
    const actor = { role: "employee", departmentId: "sales" };

    expect(() =>
      authorizeExport(actor, { departmentId: "finance" }),
    ).toThrowError("EXPORT_SCOPE_FORBIDDEN");
  });
});

实现权限策略前,先运行这条测试,并记录它因为预期原因失败。新增测试如果一开始就是绿色,可能代表它只覆盖了已有行为、没有执行目标代码,或者断言根本不起作用。

2. Green:写出最小且行为正确的实现

export function authorizeExport(
  actor: { role: string; departmentId: string },
  filter: { departmentId: string },
) {
  if (
    actor.role !== "admin" &&
    actor.departmentId !== filter.departmentId
  ) {
    throw new Error("EXPORT_SCOPE_FORBIDDEN");
  }
}

这段代码可以让示例通过,却还不足以证明接口安全。API 路由可能忘记调用它,也可能信任浏览器传来的 actor。还需要在真实授权边界增加集成测试:

it("returns 403 and creates no job for an out-of-scope export", async () => {
  const response = await request(app)
    .post("/api/exports")
    .set("Authorization", employeeFrom("sales"))
    .send({ departmentId: "finance" });

  expect(response.status).toBe(403);
  expect(await exportJobs.count()).toBe(0);
});

单元测试以低成本精确定位权限规则;集成测试证明 HTTP 边界确实执行了这条规则。

3. Refactor:在证据保持绿色时改善设计

行为通过后:

  • 删除重复的权限判断;
  • 给权限策略一个清楚的领域名称,不要把它藏在 Controller 里;
  • 测试 Fixture 要明确展示角色和部门;
  • 同时运行新测试和受影响的回归测试。

AI Coding 中的 TDD 陷阱

如果需求本身仍然模糊,只要求“先写测试”并不可靠。Agent 可能从同一条 Prompt 中推导需求、测试预言和实现,然后把同一个错误写三遍。

可以这样降低风险:

  • 提供已经确认的验收标准和具体反例;
  • 要求 Agent 在实现前展示新测试因预期原因失败;
  • 像 Review 生产代码一样 Review 断言、Fixture 和跳过的测试;
  • 至少加入一个并非由实现代码主动建议的负向或边界案例;
  • 对关键逻辑运行小范围变异测试,或者手工改变条件,确认测试会失败。

变异测试会主动修改生产代码,再观察测试能否发现变化。它适合检查“覆盖率很高,但断言可能很弱”的情况。因为成本较高,不必每次提交都跑,可以聚焦高风险策略,或者安排到定时 CI 任务中。

四、建立测试组合,而不是堆一套巨型 E2E

测试金字塔仍然是一种有用的成本模型:底部保留大量聚焦、快速的测试,顶部只保留少量跨越 UI 和基础设施的宽测试。它不是强制比例。如果一套宽测试足够快速、稳定且维护成本低,测试组合完全可以有不同形状。

让测试范围与独立性匹配需要控制的风险

让每一层回答它最擅长的问题:

层次最适合回答什么订单导出示例主要局限
静态检查程序结构是否有效类型、Lint、依赖、密钥扫描无法观察业务行为
单元与属性测试一条规则对多组输入是否成立权限、CSV 转义、筛选快照Mock 可能隐藏集成问题
集成测试真实组件之间是否达成一致API、数据库查询、任务创建、存储适配器可能没有覆盖浏览器和部署
契约测试Provider 是否破坏 Consumer导出状态 Schema 与旧客户端兼容只覆盖已经声明的契约
E2E 测试用户能否完成关键旅程请求导出、等待、下载、检查文件较慢、范围宽、定位困难
人工与探索性检查脚本模型遗漏了什么可访问性、恢复体验、异常数据不记录就无法重复执行

两条原则可以避免浪费:

  1. **在能够诚实观察目标行为的最低层测试。**CSV 引号转义应该放在聚焦的编码器测试中,不需要写二十条浏览器测试。
  2. **只有覆盖了不同边界,才增加更高层测试。**一条浏览器流程有价值,是因为它同时验证路由、页面、身份、异步状态和下载链路是否正确连接。

宽测试发现缺陷后,先在靠近根因的位置补一条聚焦回归测试,再修复问题。宽测试保护用户旅程,聚焦测试让未来的故障更容易定位。

五、围绕用户旅程和风险边界设计 E2E

端到端测试经常走向两个极端:完全没有,或者什么都从浏览器测。一套实用的 E2E 应该覆盖少量业务关键旅程和高风险状态变化,而不是重复 UI 以下已经覆盖过的所有排列组合。

订单导出功能可以选择四个浏览器级场景:

  1. **正常路径:**有权限的用户导出当前筛选结果,下载的 CSV 包含预期订单和正确的 UTF-8 中文。
  2. **权限边界:**员工不能提交或读取其他部门的导出任务。
  3. **状态变化:**提交后修改页面筛选条件,不会改变运行中任务保存的快照。
  4. **失败恢复:**任务失败后展示有用原因和安全的重试入口。

一条 Playwright 风格测试可以这样写:

test("export keeps the submitted filter snapshot", async ({ page }) => {
  await seedOrders({ sales: 3, finance: 2 });
  await signInAs(page, employee("sales"));

  await page.goto("/orders?status=paid");
  await page.getByRole("button", { name: "Export CSV" }).click();
  await page.getByLabel("Status").selectOption("refunded");

  const download = await Promise.all([
    page.waitForEvent("download"),
    page.getByRole("link", { name: "Download export" }).click(),
  ]).then(([file]) => file);

  const csv = await readDownload(download);
  expect(csv).toContain("paid-order-001");
  expect(csv).not.toContain("refunded-order-001");
});

不同项目会使用不同 API。真正重要的是可观察合同:以状态 A 提交任务,把页面切换到状态 B,再检查产物仍然来自 A。

Playwright 官方的最佳实践建议测试用户可见行为、隔离测试、控制数据、优先使用面向用户的 Locator,并使用会自动等待和重试的 Web-first Assertion。落到工程中就是:

  • 给每条测试独立账号或独立数据命名空间;
  • 通过 Fixture 或 API 创建已知数据,不依赖测试执行顺序;
  • 只 Mock 无法控制的第三方,不要 Mock 掉本来要验证的边界;
  • 按可访问名称定位 button、link 等角色,不依赖易碎的 CSS 结构;
  • 断言最终业务结果,而不只是 Toast 出现;
  • 在 CI 重试时采集 Trace,把 DOM 快照、网络活动和时间线变成失败证据;
  • 把重试当成诊断工具,不要用它掩盖不稳定测试。

四种实用的 E2E 环境

环境适合验证什么代价
本地应用 + 一次性数据库快速开发和 Agent 自验证可能与部署基础设施不同
CI + 容器化依赖每个 PR 都能确定性回归配置和运行成本
Preview 部署冒烟测试路由、静态资源、Header、部署集成较慢,需要管理环境卫生
Staging 用户旅程与探索测试真实集成和发布信心共享数据与第三方会引入噪声

不要让自治测试对生产数据执行不可逆操作。使用测试租户、受限凭证、合成数据和明确清理步骤。支付、邮件、分析和删除类接口默认应该接 Sandbox 或 Stub,只有受控发布测试确实需要时才连接真实集成。

六、Codex 应该怎样验证一次修改

Codex 没有一套能自动理解所有仓库的通用测试流程。当前官方 Codex Best Practices建议明确告诉 Codex 什么叫“完成”,并把构建、测试、Lint 和 Review 命令写进 AGENTS.md。这样 Codex 才能按项目实际情况新增或更新测试、运行相关检查、确认行为并审查 Diff。

一套严谨的 Codex 验证闭环应该是:

读取项目规则与验收标准
        ↓
检查现有代码、测试与缺陷复现方式
        ↓
根据修改边界和风险选择证据
        ↓
观察测试失败或复现缺陷
        ↓
实施最小修改
        ↓
先跑聚焦检查,再跑更宽的回归检查
        ↓
行为跨越 API 或 UI 时,操作真实边界
        ↓
Review Diff,报告命令、结果和残余风险

验证闭环把 Agent 输出变成可审计的交付证据

把稳定命令写进 AGENTS.md:

## 验证要求

- TypeScript 修改:`pnpm typecheck`
- 业务逻辑:先运行相关 Vitest 文件,再运行 `pnpm test`
- API 契约修改:`pnpm test:contract`
- 用户可见流程:`pnpm playwright test --project=chromium`
- 交付前:检查 `git diff`,列出所有未运行的检查

## 高风险修改

- 权限、支付、迁移和公开 API 变更必须由人工 Owner 审核。
- 测试中禁止使用生产凭证和生产数据。
- 不得在没有解释的情况下弱化、跳过或删除失败测试。

再为当前任务给出验证合同:

实现导出规格中的 AC-SEC-02 和 AC-DATA-03。

修改前:
1. 用 API 集成测试复现越权缺陷。
2. 展示新测试因为预期原因失败。

修改后:
1. 运行 typecheck 和聚焦的单元/集成测试。
2. 运行现有导出回归测试。
3. 在浏览器中分别验证有权和无权流程。
4. Review 最终 Diff,检查范围漂移、断言弱化、跳过测试、
   仅客户端鉴权、敏感日志和兼容性变化。
5. 报告精确命令与退出状态、观察到的行为、修改文件、
   未运行检查和残余风险。没有在当前环境执行的检查,不得声称通过。

对于本地修改,Codex /review 可以检查未提交修改、Commit 或 Branch Diff,并在不修改工作区的情况下返回按优先级排序的问题。Web 应用还可以通过浏览器能力验证实际页面流程和运行时证据。两者都不能悄悄替代仓库里的确定性测试和责任人的审批。

一份合格的交付报告

## 验证证据

验收:
- AC-SEC-02:通过。sales 员工请求 finance 导出时收到 403。
- AC-DATA-03:通过。下载内容保留提交时的 `paid` 筛选快照。

命令:
- `pnpm typecheck` -> exit 0
- `pnpm vitest run src/export` -> 18 passed, exit 0
- `pnpm playwright test export.spec.ts` -> 4 passed, exit 0

运行时观察:
- 越权请求没有创建导出任务。
- 下载 CSV 包含 3 条 sales 订单,UTF-8 中文正确。
- 浏览器 Console 无错误;导出 API 先返回 202,随后完成。

Diff Review:
- 修改 6 个文件;没有依赖、Schema 或无关格式化变化。

未运行 / 残余风险:
- 本地没有 Safari 项目,CI 将运行 WebKit。
- 生产对象存储生命周期不在当前测试环境中。

“所有测试通过”是一个结论;这份报告才是别人可以复核的证据。

七、AI Code Review 工具能做什么,不能做什么

AI Review 的价值在于:它可以用一双相对新鲜的眼睛阅读 Diff,寻找可疑模式,并在人工 Reviewer 有空之前给出反馈。对于已经明确写进仓库规则、而且经常重复出现的问题,它尤其有帮助。

截至 2026 年 8 月 6 日,常见选择包括:

工具适合放在哪里有用能力重要边界
Codex Code Review本地 Diff、Commit、Branch 或 GitHub PR按优先级输出问题;可遵循 AGENTS.md 中的 Review 规则它是额外 Reviewer;硬门禁仍由测试和分支规则承担
GitHub Copilot code reviewGitHub 与受支持 IDEPR 反馈、建议修复、仓库上下文和自定义指令GitHub 明确提示它会遗漏或出错,反馈必须验证并补充人工 Review
Cursor BugbotGitHub、GitLab、Bitbucket PR自动或手动 Diff Review、评论、修复入口和 Check 状态Review 成功只证明它运行过,不证明所有缺陷都已发现
CodeRabbit多种 Git 平台的 PR、IDE 与 CLI 流程上下文 Review、提交前反馈、团队规则和反馈闭环厂商生成的发现仍需仓库测试和 Owner 判断

这是一张能力地图,不是基准排名。这些产品使用不同的上下文、模型、策略、价格和集成入口。选择前应该在自己的仓库中运行一组有代表性的评估。

用“植入问题”评估 AI Reviewer

从真实缺陷和安全反例中做一组小型 Review 测试集:

案例期望结果
删除服务端部门权限检查一条高优先级越权问题
重命名公开响应字段指向仓库规则的兼容性问题
行为不变的有效重构不产生阻塞问题
Diff 之外已经存在的问题不声称它是本 PR 引入的
为了让 CI 通过而给测试加 .skip发现验证被弱化

不要只统计“评论数量”,还要看:

  • 植入严重缺陷的召回率;
  • 安全修改上的误报率;
  • **可行动性:**文件、原因、严重程度和安全修复路径是否准确;
  • 延迟和成本;
  • **数据与权限边界:**哪些代码会离开你的边界,集成能访问哪些仓库;
  • **团队结果:**有效发现采纳率、合并前捕获的缺陷、长期 Review 噪声。

不要第一天就把 AI 评论设为阻塞门禁。先以建议模式运行,校准仓库规则,检查误报和漏报,再决定哪些结果可以进入合并策略。确定性检查和责任明确的审批仍然应该是强制执行的基础。

八、Review 测试,而不只是实现代码

AI 生成的测试同样需要一等公民级别的 Review。重点检查这些失败模式:

1. 断言只证明“发生过”,没有证明结果

expect(exportService.create).toHaveBeenCalled();

这无法证明筛选条件、权限范围、编码和最终文件正确。应该同时检查调用参数和可观察产物。

2. Mock 掉了本来要验证的风险

如果权限仓库被 Mock 为永远返回 true,所谓 API“权限测试”只证明 Controller 能返回 200。

3. 测试复制了实现算法

如果预期 CSV 也由生产代码使用的同一个 Helper 计算,双方可能共同把错误当成正确。使用独立样例或固定的预期产物。

4. 正常路径占满测试集

典型输入最容易推断,所以 AI 生成测试往往偏向正常流程。根据风险显式要求空数据、无权限、重复、并发、格式错误、超时和兼容性场景。

5. 为了变绿而弱化测试

重点检查删除断言、扩大容差、增加任意 Sleep、.skip、.only、没有人工检查就更新 Snapshot,以及测试 Helper 吞掉异常。

6. 把覆盖率当成终点

覆盖率只能说明哪些代码执行过,不能说明断言是否能发现错误结果。用它寻找未测试区域,不要把它当成正确性证明。对关键纯逻辑,变异测试或故意破坏实现能够更直接地衡量测试敏感度。

九、把检查变成风险驱动的质量门禁

只有失败能够阻止交付或触发升级,才叫门禁。文档里的“请运行测试”只是指导;受保护分支要求这些检查通过,才是强制执行。

GitHub 的受保护分支文档支持合并前要求 Status Check、PR Review、Code Owner 审批、对话解决和部署成功。

不要给所有修改都配置一条无限大的流水线,可以按风险分级:

修改风险必须提供的证据合并控制
低:文案、文档、隔离样式Formatter、链接、聚焦构建常规 CI
中:业务逻辑、局部 API类型/Lint、单元与集成回归、Diff Review必要检查 + Reviewer
高:权限、资金、个人数据、迁移、公开契约负向安全测试、契约/E2E、Owner Review、回滚方案Code Owner、必要检查、禁止绕过、分阶段发布

对于 Web 应用安全,OWASP Application Security Verification Standard提供了带版本的安全需求和验证基础。把相关 ASVS 控制映射到测试和 Review Checklist,比让 AI Reviewer 在没有标准的情况下“看看有没有安全问题”更可靠。

保持三类控制相互独立:

  • **确定性门禁:**编译、Lint、测试、Schema 兼容性、密钥与依赖扫描。
  • **判断型门禁:**人工与 AI Review,关注设计、可维护性、数据边界和隐藏假设。
  • **运行时门禁:**Preview 冒烟、Canary 或分阶段发布、监控和回滚准备。

门禁成本应该与漏掉问题的代价成正比。改错别字不需要完整支付 E2E;修改权限策略则需要。

十、故障诊断:找出证据薄弱的那一层

已经“验证通过”的修改出错时,不要马上再加一条宽测试。先判断是哪一层证据说错了,或者根本缺席:

现象可能薄弱的层最小有效响应
单元测试通过,API 仍泄露数据集成边界用真实 API 与数据库权限测试复现
API 测试通过,页面按钮永远不完成UI / 运行时连接检查 Console 和 Network,再补一条用户旅程
E2E 只在 CI 偶发失败环境或时序检查 Trace、隔离状态、用可观察等待替代 Sleep
新测试在实现前就通过测试敏感度确认目标路径执行,并观察一次真实红灯
AI Review 产生大量样式评论Review 规则和范围把机械规则移进 CI,缩小仓库指导范围
所有检查都通过,需求仍然做错验证合同修正验收标准并补充独立反例

正确修复通常会把证据移到缺失边界附近。更多 E2E 无法修复模糊需求,更多 Review 评论也无法修复一套从未在 CI 运行的测试。

十一、一套可以复用的验证工作流

下一次 AI Coding 任务,可以按这个顺序执行。

生成之前

  • 明确验收标准与非目标;
  • 判断修改边界和失败影响;
  • 建立验证矩阵;
  • 指定命令、环境、凭证和人工 Owner。

实现期间

  • 复现缺陷,或者观察新测试失败;
  • 以小步增量实现;
  • 每个有意义的步骤后运行聚焦检查;
  • 让生产代码与测试修改可以一起 Review;
  • 不得在不公开决定的情况下弱化检查。

交付之前

  • 运行受影响的更宽回归测试;
  • 操作关键 API 或浏览器旅程;
  • 检查 Diff 中的范围漂移和隐藏风险;
  • 必要时运行一次独立 AI Review;
  • 返回精确命令、结果、产物、跳过项和残余风险。

合并与发布之前

  • 让 CI 在干净环境重跑证据;
  • 按风险要求 Status Check 与审批;
  • 让密钥和生产数据远离 Agent 测试环境;
  • 为高风险修改准备发布观察和回滚路径。

十二、验证清单

  • 每条验收标准都映射到可观察证据。
  • 测试组合与修改边界和风险匹配。
  • 新测试曾经因为预期原因失败。
  • 断言验证最终结果,而不只是调用、状态码或 UI 出现。
  • 已考虑无权限、空数据、错误、并发和兼容性场景。
  • 聚焦检查和受影响的回归测试已经通过。
  • 关键用户旅程经过真实 API 或 UI 边界验证。
  • 测试数据、账号和第三方行为受到控制。
  • 最终 Diff 不含跳过测试、弱化断言、密钥泄露和无关修改。
  • AI Review 发现经过验证;高风险决定由人工 Owner 审核。
  • CI 可以在干净环境强制执行必要检查。
  • 交付报告明确说明了未测试内容与残余风险。

总结

AI Coding 没有改变软件质量的定义。它改变的是代码生产速度,以及实现、测试和解释从同一个假设生成的概率。

解决办法不是不信任所有生成代码,也不是要求每个任务运行所有测试,而是建立一套验证系统:

验收标准
    → 风险驱动的测试组合
    → 可观察的红灯/绿灯反馈
    → 运行时与 Diff 证据
    → 独立 Review
    → 可强制执行的 CI 与发布门禁

把测试看成可执行的问题,把 Review 看成寻找遗漏问题的过程,把 CI 看成让约定证据无法被绕过的机制。这样,Agent 的速度才能真正转化为生产力,而“全绿”不会取代工程判断。

权威资料

最后更新于