跳到主要内容

内容阅读

验证循环:让 Codex 把修改跑进真实质量门

验证循环:让 Codex 把修改跑进真实质量门

TL;DR

Codex 能读代码、改文件、运行命令,但“它改完了”不等于“工程上可合并”。验证循环的核心是把团队已经认可的质量门写成可执行协议:改动前确认范围,改动后依次运行 lint、typecheck、定向测试、必要的构建或集成测试,再检查 git diff。失败时让 Codex 读取错误、做最小修复、重新验证;连续失败时停止并输出可复现报告。这个循环不追求把所有判断交给模型,而是把原本靠工程师经验补齐的提交前检查前移到 Codex 工作过程中。

读者定位

这篇适合已经把 Codex 用到真实仓库里的中高级开发者、Tech Lead、平台工程师和研发效能负责人。你需要了解项目的包管理器、测试框架、CI 入口、分支策略和代码审查习惯。本文不讨论“如何写一个好看的 prompt”,而讨论一个更硬的问题:当 Codex 真的改了代码,团队怎样判断它没有把风险留给 CI、reviewer 或线上环境。

问题:AI 生成代码最容易停在“看起来对”

人类工程师在本地修 bug 时会自然做很多隐性动作:看测试文件、跑相关 case、看类型错误、检查 diff 是否越界、确认没有顺手改配置。Codex 不会自动知道你们团队把哪些命令当作质量门。它能根据仓库上下文推断,但推断会漏。例如它修了一个登录态刷新问题,只看到了 src/auth/session.ts,没有意识到 src/api/client.ts 里还有调用方;它补了一个测试,却没有运行 typecheck;它让单测绿了,但用 any 绕开了类型约束。

这类问题在个人试用时看起来只是返工,在团队里会变成系统性成本。CI 变红后,reviewer 要重新理解 Codex 的意图;如果变更来自批量任务,错误还会被复制到多处。更麻烦的是,Codex 的回答可能很自信地说“测试通过”,但实际只是读了测试或没有跑到正确命令。验证循环就是为了把这条链条改成可观察的:命令是什么、在哪里跑、结果是什么、失败后改了什么、哪些检查没跑成,都要留下明确记录。

OpenAI 官方 Codex 文档把 Codex 定位为能在本地线程中读写文件和运行命令的编码代理,并强调通过沙箱和审批策略控制执行边界。官方也建议把“怎样构建、测试、review、发布”写进 AGENTS.md,让 Codex 自动加载这些仓库规则。验证循环正是这个建议在工程交付里的落点:把“完成”的定义从自然语言变成命令和证据。

心智模型:验证循环是一个受控反馈系统

不要把验证理解成“最后让 Codex 跑一下测试”。那太晚,也太含糊。更好的模型是反馈控制:

限定任务范围
-> 修改代码
-> 运行最便宜的检查
-> 读取失败输出
-> 做最小修复
-> 逐步扩大验证范围
-> 检查 diff 和剩余风险

这个顺序有成本原因。lint 通常最快,能抓格式、未使用变量、禁止模式;typecheck 能抓接口不匹配、泛型约束、返回值变化;定向测试能抓当前改动的行为;全量测试或构建能抓跨模块影响;git diff 抓的是范围失控。把全量测试放到最前面通常浪费,把 diff 检查放到最后才发现 Codex 改了无关文件也太晚。因此验证循环要分层,而不是一次性把所有命令扔给 Codex。

验证循环还要设置退出条件。很多团队初期会让 Codex “失败就修”,结果它在同一个错误上循环十几次:改生产代码、改测试、再改生产代码,最后引入更大的偏差。更稳的做法是规定同一个验证命令最多修三轮;如果仍失败,输出失败命令、最后错误摘要、已尝试修复、怀疑缺失的信息和建议人工判断的点。这个退出条件不是保守,而是承认模型没有完整业务语义。

详细机制:把验证写成 Codex 能执行的协议

第一层是仓库级协议,适合写进 AGENTS.md。官方 AGENTS.md 指南把它视为面向代理的开放格式说明文件,会自动进入 Codex 上下文;更靠近当前目录的文件会覆盖更宽泛的规则。因此验证命令应该按子项目写清楚,不要只在根目录放一句“run tests”。一个前端包、一个后端服务、一个文档目录,命令可能完全不同。

## Verification

- For TypeScript app changes under `packages/web`, run:
  - `pnpm --filter web lint`
  - `pnpm --filter web typecheck`
  - `pnpm --filter web test -- --runInBand`
- For API changes under `services/api`, run:
  - `pnpm --filter api lint`
  - `pnpm --filter api test`
- For documentation-only changes, run:
  - `./scripts/check.sh lint`
  - `./scripts/check.sh links`
- Before finishing any code task, inspect `git diff --stat`.

第二层是任务级协议。不是每个任务都要全量验证。你要在 prompt 里把本次任务的 Done-when 写清楚:

修复 `src/auth/session.ts` 中 token 过期后重复刷新请求的问题。

约束:
- 只修改 `src/auth/session.ts` 和相关测试。
- 不改变公开 API 返回结构。
- 不通过降低类型约束或跳过测试来让检查通过。

完成条件:
- `pnpm lint` 通过。
- `pnpm typecheck` 通过。
- `pnpm test -- src/auth/session.test.ts` 通过。
- 最后报告 `git diff --stat` 和未能运行的检查。

第三层是执行权限。官方文档说明,本地 Codex CLI/IDE 通过操作系统级沙箱限制文件读写和网络访问;workspace-write 允许在工作区内读写并运行命令,网络默认关闭;涉及离开沙箱或需要网络的命令会受审批策略影响。对验证循环来说,这意味着你不能只写“安装依赖并测试”。如果依赖安装需要网络,而当前网络关闭,Codex 要么失败,要么请求审批。更好的做法是把依赖安装视为单独步骤:日常代码修复默认假设依赖已安装;只有在明确需要时再开放网络或使用预置环境。

第四层是证据输出。Codex 最终报告至少要包含四类信息:改了哪些文件、跑了哪些命令、每条命令结果、剩余风险。如果某条命令没跑,不能写成“未测试”,要写明原因:命令不存在、依赖缺失、沙箱禁止网络、数据库不可用、耗时过长、用户没有授权。这样 reviewer 才能判断是环境问题还是变更质量问题。

真实工作流案例:从红灯修复到可审查 diff

假设 CI 报告显示 packages/web 的登录页测试失败。一个不受控的 Codex 任务可能是:

codex "fix login tests"

这条命令问题很多:范围不清、允许改什么不清、完成条件不清、失败后怎么处理不清。更好的做法是先让 Codex 在只读模式里定位:

codex --sandbox read-only \
  "Analyze the failing login test under packages/web. Read relevant files and propose the smallest fix. Do not edit."

只读分析结束后,如果方向可信,再进入实现:

codex --sandbox workspace-write --ask-for-approval on-request \
  "Implement the smallest fix for the login test failure in packages/web. Modify only the component, hook, or test directly related to this failure. Run pnpm --filter web lint, pnpm --filter web typecheck, and pnpm --filter web test -- login. If the same command fails three times, stop and report."

这里的关键不是命令长,而是每个条件都能被检查。Codex 如果想安装依赖、访问网络、改工作区外文件,会触发权限边界;如果测试失败,它有明确的重试上限;如果它想顺手修别的 lint,diff 检查会暴露范围膨胀。

在修复完成后,要求 Codex 输出类似报告:

Changed:
- packages/web/src/login/LoginForm.tsx
- packages/web/src/login/LoginForm.test.tsx

Verification:
- pnpm --filter web lint: passed
- pnpm --filter web typecheck: passed
- pnpm --filter web test -- login: passed

Diff:
- 2 files changed, 31 insertions, 12 deletions

Residual risk:
- Did not run full web test suite; only login-related tests ran.

这个报告比“已修复”有用得多,因为它把质量状态转成 reviewer 能接手的材料。

操作清单

  • AGENTS.md 中写清每个子项目的 lint、typecheck、test、build 命令。
  • 在任务 prompt 里明确允许修改的文件或目录。
  • 把 Done-when 写成命令,不写成“确保没问题”。
  • 先跑低成本检查,再跑高成本检查。
  • 定向测试通过后,再根据风险决定是否跑全量测试或构建。
  • 限制同一失败的修复轮数,避免无限循环。
  • 禁止为了绿灯而降低类型、跳过测试、关闭 lint 规则,除非任务本身就是调整规则。
  • 最终报告必须列出命令、结果、未运行项和原因。
  • 完成前检查 git diff --stat 和关键文件 diff。
  • 对支付、权限、认证、数据迁移、安全边界等高风险改动,即使命令全绿也要人工审查设计语义。

权衡与风险

验证循环会增加耗时和上下文消耗。对一个两行文档修复跑全量测试没有意义;对共享认证逻辑只跑一个快照测试又太弱。团队应该把验证分级:文档改动跑 Markdown 检查和链接检查;局部工具函数跑定向单测和 typecheck;跨模块公共接口跑相关包测试;构建配置、权限、认证、数据迁移跑更大的集成检查。

另一个风险是测试本身不可信。Codex 可能发现测试失败后直接改测试期望值,或者删除不稳定断言。任务里必须写清“不要修改测试期望来迎合当前行为,除非需求明确改变行为”。如果确实需要改测试,要求 Codex 解释旧测试为什么过时、新行为的业务依据是什么。

还有环境一致性问题。Codex 在本地、云端或 CI 中跑到的结果可能不同。官方文档说明 Codex Cloud 任务在隔离容器里运行,setup 阶段和 agent 阶段的网络、secrets 可见性也有边界;本地 CLI 则受本机沙箱、网络和审批策略影响。因此报告里要写“在哪里验证通过”。“本地 PowerShell 通过”和“CI Linux runner 通过”不是同一件事。

最后,不要把验证循环当成责任转移。工具能提供证据,不能承担业务责任。涉及钱、权限、隐私、生产凭据和数据删除的变更,最终仍要由人判断边界是否正确。

常见误区

误区一:只让 Codex “run tests”。这句话太模糊。它不知道你要跑哪个包、哪个测试框架、是否需要数据库、是否允许安装依赖。写成具体命令。

误区二:把 codex exec 当成万能 CI。官方非交互模式适合脚本、CI、预合并检查、定时任务和结构化输出,但它默认只读沙箱;需要写文件时要显式设置权限。自动化里要按最小权限配置,不要为了省事直接给 danger-full-access

误区三:只看最终自然语言总结。Codex 的总结要和命令输出、退出码、diff 一起看。对自动化任务,可以使用 --json 获取 JSONL 事件流,或用 -o 保存最终消息。

误区四:失败后立刻全量重跑。先读错误,缩小命令范围,确认是代码问题、测试问题还是环境问题。全量测试适合确认,不适合盲目调试。

误区五:把 git diff --stat 当形式。它能快速暴露范围漂移:本来只改 auth,结果动了配置、锁文件和无关页面。范围漂移是 AI 生成变更里很常见的风险信号。

团队落地模板

验证循环要落地,最好不要靠每个工程师临场补充 prompt。可以在仓库里维护一份“验证矩阵”,按改动类型列出命令、最低证据和人工审查要求。矩阵不必复杂,但要覆盖日常高频场景。

改动类型 最低验证 额外验证 人工审查重点
文档 Markdown lint、内部链接检查 外部链接抽查 链接是否存在,标题层级是否清楚
单个工具函数 定向单测、typecheck 包级测试 边界输入、错误处理、兼容性
共享类型或公共接口 typecheck、相关包测试 全量构建 调用方是否被覆盖,是否破坏契约
认证、权限、支付 lint、typecheck、定向测试、集成测试 安全审查、人工设计 review 失败路径、越权、重放、数据泄露
构建和 CI 配置 对应 CI job、本地构建 干净环境复跑 缓存、路径、密钥、平台差异

这张矩阵可以放在 AGENTS.md,也可以放在 docs/engineering/verification.md 后由 AGENTS.md 引用。重点是让 Codex 能读到,而不是把规则藏在人的脑子里。每当 Codex 连续两次在同一类任务里漏跑检查,就应把规则补进矩阵。不要一次写一大堆抽象原则,规则应该来自真实事故和重复摩擦。

一个可复用的完成报告模板也很有用:

## Change summary

- Files changed:
- Behavior changed:
- Tests or checks added:

## Verification

- `command`: passed / failed / not run
- `command`: passed / failed / not run

## Not run

- `command`: reason

## Diff scope

- Expected scope:
- Actual scope:

## Reviewer attention

- Risk 1:
- Risk 2:

让 Codex 每次按这个模板收尾,reviewer 能快速判断验证证据是否充分。如果报告里出现大量 not run,这不一定说明任务失败,但说明不能直接合并。团队可以规定:高风险改动如果有任何关键检查未运行,PR 描述里必须保留原因和人工补跑记录。

边界案例:什么时候不该让 Codex 自己闭环

有些任务看起来能验证,实际不适合完全交给 Codex 自动闭环。第一类是需求不清的任务,例如“优化结算体验”。没有明确期望,测试通过也不能说明优化正确。应先让 Codex 做只读调研和方案比较,再由人确定目标。

第二类是需要外部环境判断的任务,例如“修复生产偶发超时”。本地单测无法复现生产网络、数据库锁、限流和第三方服务波动。Codex 可以整理日志、提出假设、写临时诊断脚本,但不应直接把推测性修复合并。

第三类是验证命令不可信的仓库。很多老项目测试覆盖不足,甚至有大量跳过测试。此时验证循环仍有价值,但只能证明“没有破坏已有检查”,不能证明行为正确。任务 prompt 应要求 Codex 明确测试覆盖缺口,并优先补最小回归测试。

第四类是安全和权限变更。即使 Codex 跑通了测试,也可能把权限判断从服务端移到客户端,或把默认拒绝改成默认允许。安全语义通常需要人看威胁模型、失败路径和数据边界。验证循环在这里提供证据,不提供最终批准。

度量指标

团队可以用几个低成本指标评估验证循环是否有效:Codex 任务提交后 CI 红灯率、reviewer 要求补跑检查的次数、Codex 报告中 not run 的比例、范围外文件改动比例、同一失败命令重复出现次数。指标不是为了考核个人,而是找规则缺口。比如 CI 红灯集中在 typecheck,说明 prompt 或 AGENTS.md 没把 typecheck 放进 Done-when;范围外改动高,说明任务范围和禁止项写得不够具体。

验证循环成熟后,团队会看到一个变化:reviewer 不再从“你到底跑没跑测试”开始问,而是直接看差异、看风险、看业务语义。Codex 的产出也会从“像一段回答”变成“像一份可审查变更记录”。这才是验证循环的价值。

一个完整案例:从文档改动到可审查闭环

假设任务是重写一个工具文档页面。低质量的委派方式是“帮我改好这篇文档并检查一下”。这会把目标、范围、验证和报告全部留给 Codex 猜。更稳的方式是把任务拆成四个明确阶段。

第一阶段是范围确认。让 Codex 先读当前文件、目录页、相邻文章和仓库检查脚本,输出它准备修改的文件列表。这个阶段不写文件。它要回答三个问题:这篇文章在系列中的位置是什么,哪些相邻链接会受影响,修改后需要跑哪些检查。范围确认的价值在于提前发现隐藏依赖。例如文章标题改了,目录页的链接文本可能也要改;文章移动目录,内部相对链接可能失效;引用官方文档,日期和来源要写清楚。

第二阶段是内容修改。此时给 workspace-write,但把允许文件写清楚。文档任务通常不应碰脚本、配置、锁文件和资产目录。Codex 写完后要先自查标题层级、表格、链接和术语一致性。自查不是最终验证,只是减少低级错误。

第三阶段是命令验证。文档仓库常见命令是 Markdown lint、内部链接检查和 badge 检查。命令失败时,Codex 应先贴出失败分类:标题层级、空行、列表风格、链接路径、README 计数,还是脚本运行环境问题。不同失败的修复方式不同。标题层级可以直接改文章;链接路径要确认目标文件;脚本环境失败不能伪装成文档失败。

第四阶段是交付报告。报告要包含改了哪些文件、为什么改、跑了哪些命令、哪些没跑、哪些风险留给 reviewer。这个报告越具体,reviewer 越不用重复劳动。比如“内部链接检查通过,外部链接未跑,因为当前环境无网络”比“已检查”可靠得多。

这个案例的关键不是文档,而是边界。验证循环不是把测试命令塞到最后,而是让每一步都有可观察证据。范围确认防止改错地方,命令验证防止破坏规则,交付报告防止 reviewer 猜过程。对代码任务也是同样逻辑,只是命令从 Markdown 检查换成单测、typecheck、构建或集成测试。

验证证据的质量分层

不是所有验证证据都一样。团队可以把证据分成四层。第一层是静态证据,例如 git diff --stat、文件列表、格式检查、类型检查。这些证据成本低,能快速发现范围漂移和结构错误。缺点是它们通常不证明业务行为正确。

第二层是定向行为证据,例如某个模块单测、某个 API 的最小复现、某个页面的截图或某个 CLI 命令输出。这类证据直接对应任务目标,适合小到中等改动。

第三层是系统级证据,例如完整测试套件、构建、端到端测试、迁移演练。这类证据覆盖面更广,但成本也更高。它适合共享接口、基础设施、安全路径和发布前检查。

第四层是人工语义审查。测试无法判断所有需求是否合理,也无法判断一个架构取舍是否值得。Codex 可以准备证据,但不能替代责任人批准。尤其是权限、计费、数据删除、生产配置和迁移任务,最终判断必须回到人。

成熟的验证循环会把这四层组合起来,而不是迷信某一个命令。低风险文档任务可能只用第一层和少量人工抽查;认证逻辑改动至少需要第二层、第三层和安全审查;大规模重构需要先证明公共行为不变,再看 diff 是否降低复杂度。

把失败写进知识库

验证循环的长期价值来自积累。每次失败都应该变成一条更好的仓库规则。比如 Codex 经常漏跑 pnpm typecheck,就在 AGENTS.md 的 Done-when 中加入该命令;经常误改快照,就写明“除非任务要求更新 UI 输出,不要修改快照来迎合当前实现”;经常在 Windows 上因为 CRLF 失败,就把脚本运行环境写清楚。

不要把这些规则写成抽象口号。规则要能被执行,要有命令、路径、例外条件和责任人。修改代码后做充分验证 没有用;修改 src/auth/** 后运行 pnpm test auth 和 pnpm typecheck,无法运行时在报告里写明原因 才能改变行为。

当规则越来越多时,要定期清理。过时命令、已废弃目录、没人维护的检查会让 Codex 产生噪声。验证矩阵不是越长越好,而是越贴近当前仓库越好。

发布前复核

发布前再看一遍验证报告:命令是否真实运行,失败项是否写明原因,未覆盖风险是否交给 reviewer。这个动作很短,但能拦住很多“看起来完成”的变更。

延伸阅读


GitHub 原文:08-verification-loop.md

评论

0
登录后可以参与评论和讨论。

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。