已发布资料基础官方资料 + 个人实践
·10 分钟AI 工具实践
文章/AI 与智能体

Codex CLI 实战:从读项目到修复一个可复现的 bug

Codex 系列第 9 篇:用一个可复现的 JavaScript 小项目,完整走过 Codex CLI 的目录确认、失败复现、项目扫描、最小修复、测试、diff、review 和非交互自动化。

文章目录
  1. 一分钟概览
  2. 1. 先准备一个真的会失败的小项目
  3. package.json
  4. src/normalize-tags.js
  5. test/normalize-tags.test.js
  6. 2. 修复前先亲手复现
  7. 3. 交互式 CLI 还是 codex exec
  8. 4. 用正确目录和权限启动
  9. 5. Explore:只读输出项目地图和失败证据
  10. 6. Plan:把验收标准写成能检查的行为
  11. 7. Code:只执行已经确认的最小改动
  12. 8. Verify:用同一条命令证明失败转绿
  13. 9. Review:测试通过不等于任务结束
  14. 10. 一次完整 transcript 应该留下什么
  15. 11. 长任务如何恢复,而不是重新解释一遍
  16. 12. 常用 CLI 控制速查
  17. 13. codex exec:把明确任务放进脚本
  18. 14. 让 codex exec 产出可消费的结果
  19. 保存最终回答
  20. 输出 JSONL 事件
  21. 用 JSON Schema 约束最终结构
  22. 不保留 session
  23. 非 Git 目录的例外
  24. 15. 常见失败模式
  25. 失败模式一:在错误目录启动
  26. 失败模式二:只有报错描述,没有复现命令
  27. 失败模式三:上下文太宽或太窄
  28. 失败模式四:一上来就实现
  29. 失败模式五:测试跑得很多,但没有覆盖 bug
  30. 失败模式六:codex exec 参数位置错误
  31. 16. 30-45 分钟跟做清单
  32. 收藏清单
  33. 参考资料
阅读提要

Codex 系列第 9 篇:用一个可复现的 JavaScript 小项目,完整走过 Codex CLI 的目录确认、失败复现、项目扫描、最小修复、测试、diff、review 和非交互自动化。

#Codex#CLI#调试#Bug 修复#测试

这是 Codex 系列的第 9 篇。

前面的文章已经分别讲过 Prompt、AGENTS.md、工作流和权限。这一篇不再重复讲原则,而是把它们压进一次可以复现的 CLI 修 bug 过程:先看到失败,再让 Codex 定位根因,只改一个函数,最后留下测试和 review 证据。

项目说明
内容类型Codex CLI 实战教程
适合读者会使用终端和 Git,想把 Codex 用进真实修复流程的人
阅读 / 练习时间阅读约 20 分钟,跟做约 30-45 分钟
可带走产物一套可复制的 CLI 修 bug 操作单、一个可复现实验仓库
本地命令验证codex-cli 0.132.0
资料核对日期2026-07-14

版本说明

Codex CLI 更新很快。本文命令在本机 codex-cli 0.132.0 核过参数位置,同时对照了当日官方 Developer commands 和 Non-interactive mode 文档。slash command 会随版本和运行界面变化,实际使用时以终端输入 / 后的菜单和 codex --help 为准。

发布前复测:OpenAI 官方仓库在 2026-07-14 的最新版本已是 0.144.4。本文仍为草稿,正式发布前要先升级本机 CLI,重新核对全局参数位置、/review、non-interactive mode 和 Windows sandbox 行为。

一分钟概览

这篇真正要练的不是“让 AI 写一个函数”,而是下面这条证据链:

text
正确目录
  -> 干净的 Git 基线
  -> 可重复的失败
  -> 项目地图与根因
  -> 有边界的修复计划
  -> 最小 diff
  -> 同一条测试转绿
  -> /diff + /review

读完以后,你应该能回答五个问题:

  1. Codex 到底在哪个目录工作?
  2. bug 在修改前能否稳定复现?
  3. 根因来自代码证据,还是模型猜测?
  4. 修复是否超出验收标准?
  5. 测试通过以后,还剩哪些风险?

Codex CLI 修 bug 流程图:从进入正确项目、只读扫描、计划最小改动,到运行测试和 review,所有结论都留在命令、diff 与 transcript 中Codex CLI 修 bug 流程图:从进入正确项目、只读扫描、计划最小改动,到运行测试和 review,所有结论都留在命令、diff 与 transcript 中

图 1:CLI 的价值不是界面更“专业”,而是工程现场、命令输出和修改证据天然在一起。

1. 先准备一个真的会失败的小项目

为了让读者能完整跟做,我没有用一个依赖私有后台、登录态或线上数据库的案例,而是准备了一个只有 3 个文件的 JavaScript 仓库。

它模拟博客系统里常见的标签清洗逻辑:

text
输入: [" Codex ", "AI", "codex", " "]
输出: ["ai", "codex"]

当前实现有三个问题:

  • 缺少 tags 时会抛异常;
  • 空白标签会变成空字符串并被保留下来;
  • 数组里混入 null 或数字时会调用不存在的 .trim()

项目结构:

text
codex-cli-lab/
├─ package.json
├─ src/
│  └─ normalize-tags.js
└─ test/
   └─ normalize-tags.test.js

package.json

json
{
  "name": "codex-cli-lab",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --test"
  }
}

src/normalize-tags.js

js
export function normalizeTags(tags) {
  return [...new Set(tags.map((tag) => tag.trim().toLowerCase()))].sort();
}

test/normalize-tags.test.js

js
import test from "node:test";
import assert from "node:assert/strict";

import { normalizeTags } from "../src/normalize-tags.js";

test("trims, normalizes, and deduplicates tags", () => {
  assert.deepEqual(
    normalizeTags([" Codex ", "AI", "codex"]),
    ["ai", "codex"],
  );
});

test("drops blank tags", () => {
  assert.deepEqual(normalizeTags(["AI", " ", ""]), ["ai"]);
});

test("returns an empty list when tags are missing", () => {
  assert.deepEqual(normalizeTags(null), []);
});

test("ignores non-string values", () => {
  assert.deepEqual(normalizeTags(["AI", null, 42, "Codex"]), ["ai", "codex"]);
});

准备好文件后建立 Git 基线:

bash
cd codex-cli-lab
git init
git add package.json src test
git commit -m "add failing normalize tags fixture"

这里刻意把失败代码提交进去。不是建议平时提交坏代码,而是为了让实验中的 /diff/review 有稳定基线。

2. 修复前先亲手复现

先不要启动 Codex,自己跑一次:

bash
npm test

你应该看到类似结果:

text
✔ trims, normalizes, and deduplicates tags
✖ drops blank tags
✖ returns an empty list when tags are missing
✖ ignores non-string values

tests 4
pass 1
fail 3

具体堆栈和耗时会随 Node.js 版本变化,但三种失败必须存在:

测试失败证据初步线索
空白标签实际数组里多出 ""映射后缺少过滤
缺少 tagsnull.map 报错未检查输入是否为数组
非字符串值tag.trim is not a function假设数组元素全是字符串

这一步不能省。没有失败基线,后面即使测试通过,也可能只是测试环境、命令或输入变了,而不是 bug 被修好。

实战原则

修 bug 的第一个可交付物不是代码,而是一条别人也能重复执行的失败命令。

3. 交互式 CLI 还是 codex exec

Codex CLI 有两种常用工作方式:

方式适合任务交互特点这次是否使用
codex需求还要探索、需要多轮确认、要观察 diff可以持续补充上下文、切权限、review主流程使用
codex exec输入明确、可一次结束、要进脚本或 CI非交互,输出适合重定向或解析后半篇单独演示

这次不知道 Codex 会先看到什么、会提出什么计划,而且我们要练 /status/plan/diff/review,所以主流程使用交互式 CLI。

codex exec 不是“更自动的万能模式”。它适合边界清楚的批处理,而不是把模糊任务和发布权限一起塞进一句话。

4. 用正确目录和权限启动

在项目父目录执行:

bash
codex --cd ./codex-cli-lab --sandbox workspace-write --ask-for-approval on-request

参数含义:

参数作用
--cd ./codex-cli-lab先切到目标仓库,再开始理解任务
--sandbox workspace-write允许在工作区内改文件和运行常规命令
--ask-for-approval on-request越出边界、联网或出现其他批准条件时再询问

这是一个自己创建、已经纳入 Git 的实验仓库,所以直接使用 Auto 组合是合理的。真实陌生仓库可以先改成:

bash
codex --cd ./unknown-repo --sandbox read-only --ask-for-approval on-request

进入以后先输入:

text
/status

确认至少四件事:

  • 当前工作目录是 codex-cli-lab
  • sandbox 和 approval 符合预期;
  • 可写根目录没有意外扩大;
  • 上下文和模型状态正常。

再让 Codex 检查 Git 现场:

text
先不要改文件。请运行 git status --short,确认当前工作树是否干净;
如果不干净,列出已有改动并停下来。

为什么先看 Git?因为真实项目里最危险的情况之一,是把用户原有改动当成 bug 修复的一部分,最后 review 时已经分不清谁改了什么。

5. Explore:只读输出项目地图和失败证据

第一轮不要说“帮我修复”,而是给一个可核验的探索任务:

text
先不要修改文件。

请完成以下 Explore:
1. 读取 package.json、src/normalize-tags.js 和 test/normalize-tags.test.js;
2. 运行 npm test,记录失败测试名称和关键错误;
3. 说明输入如何流入 normalizeTags,逐条对应三个失败的根因;
4. 列出最小修改范围和不需要修改的文件;
5. 如果失败结果与任务描述不一致,停下来报告,不要猜。

输出格式:项目地图、复现证据、根因、修改边界、验证入口。

也可以用 /mention@ 显式附加关键文件:

text
/mention src/normalize-tags.js
/mention test/normalize-tags.test.js

或者在输入框键入 @src/normalize-tags.js 后从候选项选择。对于小项目,这不是必须;对于大型仓库,它能减少模型在几十个同名文件之间猜测。

一份合格的 Explore 输出应该接近:

text
项目地图
- package.json:测试入口 npm test -> node --test
- src/normalize-tags.js:唯一实现文件
- test/normalize-tags.test.js:4 条行为测试

复现证据
- npm test:1 passed, 3 failed
- blank case:返回数组包含空字符串
- missing case:在 tags.map 处抛 TypeError
- mixed case:在 tag.trim 处抛 TypeError

根因
- 函数默认 tags 一定是数组
- 函数默认每个元素一定是字符串
- trim 后没有过滤空字符串

修改边界
- 修改 src/normalize-tags.js
- 如需锁定行为,可补测试,但不改 package.json 和项目结构

验证入口
- npm test

如果输出只有“可能是空值处理问题”,还不够。项目地图和测试结果的作用,是把模型的判断绑定到文件与命令证据上。

6. Plan:把验收标准写成能检查的行为

输入 /plan 切到计划模式,然后给出验收标准:

text
/plan
text
请基于刚才的证据给最小修复计划。

验收标准:
- 非数组输入返回 [];
- 只处理字符串元素,忽略 null、数字等非字符串值;
- 每个字符串 trim 后转小写;
- 丢弃空字符串;
- 去重并按现有行为排序;
- 不新增依赖,不改 package.json,不扩大公开 API;
- 最终 npm test 全部通过。

计划里写清:改哪些行、为什么、如何验证、还剩什么边界风险。
先不要实现。

一个合理计划应该很短:

text
1. 在 normalizeTags 开头检查 Array.isArray,非数组直接返回 []。
2. 过滤非字符串值,再 trim、lowercase、过滤空字符串。
3. 保留 Set 去重和 sort 排序,避免改变已有输出契约。
4. 运行 npm test;检查 diff 只包含目标函数。

如果计划提出新建 class、引入校验库或重构测试框架,说明它已经偏离“最小修复”。计划阶段最重要的作用之一,就是把这种扩张挡在修改之前。

7. Code:只执行已经确认的最小改动

确认计划后,让 Codex 实现:

text
按计划实现,只修改 src/normalize-tags.js。
完成后先不要继续重构,输出实际 diff 摘要,再运行 npm test。

预期核心 diff:

diff
 export function normalizeTags(tags) {
-  return [...new Set(tags.map((tag) => tag.trim().toLowerCase()))].sort();
+  if (!Array.isArray(tags)) {
+    return [];
+  }
+
+  const normalized = tags
+    .filter((tag) => typeof tag === "string")
+    .map((tag) => tag.trim().toLowerCase())
+    .filter(Boolean);
+
+  return [...new Set(normalized)].sort();
 }

这个修复有几个值得保留的判断:

  • Array.isArray 直接对应“缺少 tags”的失败,不扩大到复杂 schema 校验;
  • 先过滤类型,避免在 null 和数字上调用 .trim()
  • .filter(Boolean) 放在 trim() 之后,才能去掉只包含空格的标签;
  • 保留 Setsort(),没有顺手改变原来的去重与排序契约;
  • 没有新增依赖,也没有把一个小函数改造成抽象层。

真正值得学的不是这 10 行 JavaScript,而是 diff 中的每一行都能指回某条失败测试或验收标准。

8. Verify:用同一条命令证明失败转绿

修复后仍然运行最初那条命令:

bash
npm test

预期结果:

text
✔ trims, normalizes, and deduplicates tags
✔ drops blank tags
✔ returns an empty list when tags are missing
✔ ignores non-string values

tests 4
pass 4
fail 0

这里的“同一条命令”很重要。如果修改前跑的是 npm test,修改后只手工调用一次函数,证据链是不完整的。

然后检查工作树:

text
请运行 git status --short 和 git diff --check,确认:
1. 只有 src/normalize-tags.js 被修改;
2. 没有空白错误;
3. 没有未跟踪的临时文件。

最小充分验证可以分三层:

层级这次怎么做什么时候继续扩大
复现路径npm test必须执行
变更卫生git diff --checkgit status --short必须执行
更大范围lint、build、集成测试真实项目里该函数被更多流程使用时

不要为了显得严谨就盲目跑所有测试,也不要只跑最方便的一条。验证范围要覆盖改动的行为和可能影响的调用面。

9. Review:测试通过不等于任务结束

先用:

text
/diff

确认 diff 只包含预期文件。然后输入:

text
/review

再补充 review 重点:

text
请按 bug 风险优先 review 当前未提交改动,重点检查:
- 是否满足 4 条测试对应的行为;
- 是否意外改变排序、大小写或去重契约;
- 是否有无关重构;
- 测试是否遗漏关键边界;
- 如果没有阻塞问题,明确写“未发现阻塞问题”,并列出剩余风险。

对这个实验,一个诚实的 review 结论可能是:

text
未发现阻塞问题。

验证:4/4 tests passed;diff 仅修改 normalize-tags.js。

剩余风险:
- 当前函数会静默忽略对象、数字等非字符串输入;这是本文验收标准,
  但真实业务若要求暴露脏数据,应改为记录或抛错。
- sort() 使用 JavaScript 默认字符串排序;若未来需要中文或 locale-aware 排序,
  需要单独定义契约。

这段“剩余风险”很有价值。它区分了本次 bug 是否修完,以及系统是否从此没有任何问题。

完成定义

“测试绿了”只能证明已覆盖行为通过;“任务完成”还要确认修改范围、契约和未验证风险。

10. 一次完整 transcript 应该留下什么

修复结束时,CLI 线程里至少应该能找到这些证据:

证据为什么需要
git status 基线区分已有改动与本次改动
修改前 npm test证明 bug 可复现
项目地图证明 Codex 读过入口、实现和测试
根因说明证明不是盲改
最小计划约束实现范围
diff让人能审查实际变化
修改后 npm test证明失败路径转绿
/review 结论暴露回归和剩余风险

可以让 Codex 在结束前生成交付摘要:

text
请给出最终交付摘要:
1. 根因;
2. 修改文件与行为变化;
3. 实际运行的验证命令和结果;
4. review 结论;
5. 剩余风险;
6. 明确说明是否执行了提交、推送或远端操作。

最后一项很实用。它能防止“本地改完”和“已经推送/发布”在沟通中混为一谈。

11. 长任务如何恢复,而不是重新解释一遍

交互式任务被中断后,可以恢复最近会话:

bash
codex resume --last

也可以恢复指定 session:

bash
codex resume <SESSION_ID>

非交互任务则可以:

bash
codex exec resume --last "继续上次检查,只输出尚未解决的问题。"

恢复前先确认仍在同一个仓库。--last 默认按当前工作目录选择最近会话;从别的目录执行,很可能接错上下文。

长会话接近上下文上限时,可以使用:

text
/compact

它会压缩可见对话,保留关键上下文。要探索另一个方案而不破坏当前线程时,可以使用 /fork。这比在同一个线程里来回推翻根因更容易审查。

12. 常用 CLI 控制速查

下面只列修 bug 流程中高频、且值得记住的命令。不同版本可能略有差异,输入 / 查看当前菜单最可靠。

命令 / 快捷方式用途典型时机
/status看目录、模型、权限、上下文和用量进入项目后
/permissions在只读和 Auto 等权限间切换Explore 后准备实现
/mention <path>把文件加入对话上下文大仓库里明确关键文件
@搜索并附加工作区文件写 Prompt 时快速引用
/plan切到计划模式多文件或风险不明的修复
/diff查看当前 Git diff实现后、验证前
/review审查工作树或对比基线测试通过后
/compact压缩长上下文长任务接近上下文上限
/fork从当前会话分支探索想比较两种修复方案
/init生成 AGENTS.md 初稿新项目缺少持久说明
/goal设置长期持续目标真正跨多轮的任务,不用于这个小实验

一个常见顺序是:

text
/status -> Explore -> /plan -> Code -> /diff -> Verify -> /review

不需要为了“用上命令”而每一步都敲 slash command。命令是控制面,不是仪式。

13. codex exec:把明确任务放进脚本

非交互模式适合可终止、输入和输出都清楚的任务。例如只检查 Markdown 本地资源:

bash
codex --ask-for-approval never exec \
  --cd ./your-repo \
  --sandbox read-only \
  "检查 Markdown 的本地图片引用和相对链接是否指向存在的文件,只输出失败项,不改文件。"

这里特意把 --ask-for-approval never 放在 exec 之前。在本文验证的 codex-cli 0.132.0 中,它是全局参数;如果把它写在 exec 后面,CLI 会提示 unexpected argument '--ask-for-approval'

never + read-only 的组合适合自动检查:任务不会停下来等批准,也不能通过批准扩大权限。

外链实时可用性是另一类任务。它需要 Web Search 或命令网络,不应伪装成“只读本地检查”。如果只是查外部资料,可以单独开启 --search;如果脚本要主动请求所有链接,还要明确网络权限、超时、重试和目标域名。

14. 让 codex exec 产出可消费的结果

只看终端最终文字,自动化价值有限。codex exec 还支持几种结构化输出方式。

保存最终回答

bash
codex --ask-for-approval never exec \
  --cd ./your-repo \
  --sandbox read-only \
  --output-last-message review-summary.md \
  "只读 review 当前 diff,按严重程度输出问题、测试缺口和剩余风险。"

-o--output-last-message 的短写。它只保存最终回答,适合后续贴进 CI artifact 或 PR 评论草稿。

输出 JSONL 事件

bash
codex --ask-for-approval never exec \
  --cd ./your-repo \
  --sandbox read-only \
  --json \
  --output-last-message review-summary.md \
  "检查当前 diff,只读,不改文件。" > codex-events.jsonl

--json 把运行过程按 JSONL 输出到 stdout;最终自然语言摘要仍可通过 -o 单独保存。CI 可以解析事件,而人可以读摘要。

用 JSON Schema 约束最终结构

先准备 review.schema.json

json
{
  "type": "object",
  "properties": {
    "status": { "enum": ["pass", "needs_changes"] },
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "severity": { "enum": ["P0", "P1", "P2"] },
          "file": { "type": "string" },
          "message": { "type": "string" }
        },
        "required": ["severity", "file", "message"],
        "additionalProperties": false
      }
    }
  },
  "required": ["status", "findings"],
  "additionalProperties": false
}

再运行:

bash
codex --ask-for-approval never exec \
  --cd ./your-repo \
  --sandbox read-only \
  --output-schema review.schema.json \
  --output-last-message review.json \
  "只读 review 当前 diff,按 schema 输出。"

这比让下游脚本正则解析一段自由文本可靠得多。

不保留 session

一次性任务可以加:

bash
codex exec --ephemeral "总结这个仓库的目录结构,不改文件。"

--ephemeral 不持久化 session rollout,适合不需要后续 resume 的临时检查。

非 Git 目录的例外

--skip-git-repo-check 能让 codex exec 在非 Git 目录运行,但不要把它当默认参数。失去 Git 基线后,diff、回滚和 review 都更弱;只在一次性资料目录或外层已有快照时使用。

15. 常见失败模式

失败模式一:在错误目录启动

表现:Codex 找不到 package.json,或读了相邻项目。

处理:

bash
pwd
git rev-parse --show-toplevel
codex --cd <正确项目路径>

失败模式二:只有报错描述,没有复现命令

表现:Codex 根据一句“标签偶尔为空”猜根因,改完以后也不知道是否覆盖真实输入。

处理:先把输入、失败测试、日志或最小复现固定下来。复现不了时,任务产物应是“还缺什么证据”,不是一份猜测性 patch。

失败模式三:上下文太宽或太窄

太宽:把整个 monorepo 都塞进上下文,输出充满无关架构总结。

太窄:只给一个函数,不给调用方和测试,模型不知道行为契约。

处理:先用搜索找候选,再用 /mention@ 附加入口、实现、测试和项目规则这 2-5 类关键文件。

失败模式四:一上来就实现

表现:根因没复现,Codex 已经改了多个文件并“顺便重构”。

处理:第一轮明确“不要改文件”,要求项目地图、失败证据、根因、边界和验证入口;风险更高时用只读 sandbox 强制约束。

失败模式五:测试跑得很多,但没有覆盖 bug

表现:全仓库 lint 通过,原始失败输入却没重新执行。

处理:先跑复现命令,再补受影响范围的 lint/build/集成测试。验证应从行为向外扩,而不是从最熟悉的命令开始。

失败模式六:codex exec 参数位置错误

表现:

text
unexpected argument '--ask-for-approval'

处理:把全局参数放在子命令前:

bash
codex --ask-for-approval never exec --cd ./repo --sandbox read-only "..."

遇到版本差异先看:

bash
codex --version
codex --help
codex exec --help

16. 30-45 分钟跟做清单

阶段时间完成标准
建仓库5 分钟3 个文件已创建并提交为 Git 基线
复现5 分钟npm test 明确 1 pass / 3 fail
Explore5-8 分钟得到项目地图、三条根因和修改边界
Plan5 分钟计划只改一个函数,验收标准可检查
Code5 分钟diff 与本文核心 diff 等价
Verify5 分钟4 条测试通过,git diff --check 通过
Review5-8 分钟有明确结论和至少一条剩余风险

做完后不要急着删实验仓库。再练两个变体:

  1. 新增测试:传入 undefined,确认仍返回 []
  2. 改变产品契约:非字符串不再静默忽略,而是抛出带索引的错误,让 Codex先改计划再改代码。

第二个变体尤其重要。它会让你看到“技术上能修”与“产品行为怎么定义”是两件事,Codex 不能替你偷偷决定契约。

收藏清单

这段可以直接作为 CLI 修 bug 的默认操作单:

text
[启动]
codex --cd <repo> --sandbox workspace-write --ask-for-approval on-request

[基线]
/status
git status --short
先复现,不改文件。

[Explore]
输出入口、调用链、失败命令、根因、修改边界和验证入口。
必要时用 /mention 或 @ 附加关键文件。

[Plan]
/plan
写清验收标准、改什么、不改什么、如何验证。

[Code]
只做计划内最小 diff;计划不成立时停下来。

[Verify]
先重跑原始失败命令,再跑受影响范围的检查。
记录实际命令和结果。

[Review]
/diff
/review
检查回归、无关改动、测试缺口和剩余风险。

[交付]
总结根因、文件、行为变化、验证结果、剩余风险,
并说明是否执行了 commit、push、deploy 或其他远端操作。

CLI 最有价值的地方,不是把自然语言搬进终端,而是让自然语言决策旁边一直放着代码、Git、测试和命令证据。真正稳定的用法,也不是让 Agent 一口气做完,而是让每一步都有下一步可以验证的产物。

参考资料

下一篇继续进入 GitHub 协作:怎样把 issue 变成可执行任务,让 Codex 产出可审查的分支和 PR,并把 review 结论真正接回修复循环。