验证循环:让 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。这个动作很短,但能拦住很多“看起来完成”的变更。
延伸阅读
- Codex 官方文档
- Codex CLI Reference
- AGENTS.md 官方指南
- Codex Non-interactive Mode
- Agent approvals and security
GitHub 原文:08-verification-loop.md