整理:【Codex项目实战】从模糊需求到可验证交付:Plan、测试、Review与Worktree完整流程

刷到一个挺有意思的话题,结合自己之前的经验,整理了一下核心要点。

🔥个人主页:爱和冰阔乐 📚专栏传送门:《数据结构与算法》 、C++ 🐶学方向:C++方向学爱好者 ⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

文章目录

二、先把一句模糊需求说清楚 三、别急着改代码,先让 Codex 把仓库读明白 四、繁琐任务先 Plan,防止改着改着跑偏 五、动手修改:先保留错误码 六、增加错误码映射,不直接展示服务端 message 七、补测试:不要只测三个“正确答案” 八、Review:代码写完后的第二次思考 8.3 用行内评论让 Codex 精确修改8.4 再用一次 /review 九、Worktree:让 Codex 在后台干活,不碰你正在写的代码 十、Subagents:把调查、测试和审查拆开 十一、几种很容易把 Codex 用跑偏的提示词 十二、几套我认为比较实用的提示词 总结

前言

上一篇文章中,我介绍了 Codex 的 config.toml、权限模式、AGENTS.md、MCP 等配置。配置做好之后,新的问题也就来了:

Codex 配好了,然后呢?

难道只是打开一个项目,对它说一句“帮我写个登录功能”,然后等它把代码吐出来吗?

如果只是这样使用,我感觉多少有点浪费了。

因为真正写一个项目,从来不是简单地敲几行代码。就拿一个很普通的 Bug 来说,咱们至少要经历下面这些步骤:

1. 先把 Bug 复现出来;
2. 找到它经过了哪些函数和模块;
3. 判断根因到底在前端、后端还是请求封装层;
4. 想清楚修改会不会影响其他功能;
5. 修改代码;
6. 补测试;
7. 跑一遍验证命令;
8. 最终再看 Git diff,确认没有改出新的问题。

而 Codex 真正有意思的地方,就在于它不只会回答“这段代码应该怎么写”,还可以直接读取仓库、搜索调用关系、修改文件、运行命令、查看测试结果,最终再对修改进行 Review。

这篇文章不再讲 Codex 的基础配置,而是换一个方向,完整演示一次:

如何把一句模糊的“帮我修一下登录 Bug”,逐步变成一个能够验证、能够 Review、能够真正交付的工程任务。

为了让过程更直观,这篇文章会给出完整提示词、TypeScript 代码、测试代码、终端运行结果、Git diff 和 Worktree 并行开发示例。

这篇文章涉及的 Codex 功能依据 OpenAI 官方文档整理,核对时间为 2026 年 6 月 10 日


一、Codex 不只是“代码生成器”

1.1 咱们平时是怎么让 AI 写代码的?

很多人刚动手使用 Codex,第一句话可能是:

帮我修复登录功能。

这句话有没有问题?

当然有,而且问题还不小。

Codex 并不了解你说的“登录功能”具体哪里坏了,也不了解正确结果是什么,更不了解哪些文件不能动。

它可能会:

  • 修改登录页面;
  • 重写请求封装;
  • 新增一个错误处理类;
  • 顺手换掉 Toast 组件;
  • 最终告诉你“已做好”。
代码确实变了,但它到底有没有解决问题,我们并不知道。

这和 Linux 中只见到一条命令执行成功,却不去检查进程状态、返回值和日志,本质上是一样的。

命令能执行,不代表任务做好;代码能编译,也不代表需求正确。

1.2 Codex 接到任务后到底在做什么?

我们可以把 Codex 的开发过程简单理解为一个循环:

读取任务
   ↓
收集上下文
   ↓
决定下一步动作
   ↓
读取文件 / 搜索代码 / 运行命令 / 修改文件
   ↓
观察结果
   ↓
继续下一步,直到满足完成条件

这和传统聊天模型最大的区别是:传统聊天模型通常告诉你“应该怎么做”,Codex 可以直接进入仓库“把这件事做完”。

但是问题来了:

Codex 怎么知道什么时候才算“做完”?

答案是:它主要依赖我们给出的任务描述。

如果提示词中只有“修复登录”,那么“改了代码”就可能被它当作完成。

如果我们明确要求:

完成条件:
1. 密码错误、账号冻结、服务异常显示不同提示;
2. 未知错误有默认兜底;
3. 登录成功流程不受影响;
4. 添加回归测试;
5. 运行测试、类型检查和 lint;
6. 最后检查 Git diff。

那么它的终点就不再是“写完代码”,而是“验证完成”。

这就是这篇文章最核心的一句话:

不要用生成了多少代码来判断 Codex 是否完成任务,而要看结果有没有被验证。

二、先把一句模糊需求说清楚

这一节我们先不写代码,先看一个真实开发中很常见的问题。

2.1 示例 Bug:登录失败永远只显示一句话

假设项目是一个 React + TypeScript 商城。

当前现象是:

密码输入错误     → 登录失败
账号被冻结       → 登录失败
服务器暂时异常   → 登录失败

三个完全不同的问题,前端却只显示同一句“登录失败”。

这对开发者来说可能没什么,但对用户来说区别很大:

  • 密码错误,可以重新输入;
  • 账号冻结,需要联系管理员;
  • 服务异常,只能稍后再试。
服务端实际上已经返回了错误码:

{
  "code": "ACCOUNT_LOCKED",
  "message": "account has been locked"
}

但是页面没有利用 code,只是统一显示默认文案。

2.2 一个任务最好包含四部分

OpenAI 官方最佳实践中,建议用在提示词里尽量给出下面四部分:

部分要解决的问题Goal到底要改变什么行为?ContextBug 在哪里冒出来,怎样复现?Constraints哪些地方不能改?Done when满足什么条件才算完成?

把它们翻译成更直白的话就是:

我要什么?
现在是什么情况?
你不能乱动什么?
最后我要怎么验收?

2.3 改写后的完整提示词

目标:
修复登录失败提示。前端应该根据服务端错误码显示不同文案:

- INVALID_CREDENTIALS:账号或密码错误
- ACCOUNT_LOCKED:账号已冻结,请联系管理员
- SERVICE_UNAVAILABLE:服务暂时不可用,请稍后重试

上下文:
- 项目使用 React + TypeScript
- 登录页面位于 src/pages/LoginPage.tsx
- 登录逻辑可能位于 src/features/auth
- 请求封装位于 src/api
- 当前所有错误都只显示“登录失败”

复现步骤:
1. npm run dev
2. 进入 /login
3. 使用错误密码登录
4. 页面显示“登录失败”

约束:
- 不修改后端接口结构
- 不新增第三方依赖
- 沿用项目现有 Toast 组件
- 不重构与登录无关的代码
- 修改范围保持最小

完成条件:
- 三种错误显示对应文案
- 未知错误码显示默认兜底文案
- 登录成功流程不受影响
- 添加回归测试
- 运行最小相关测试、typecheck 和 lint
- 最后检查 Git diff 并报告修改文件

看起来比一句“帮我修登录”长了不少,但这里的每一句都不是废话。

其中最容易被忽略的是“未知错误码”。

服务端以后很可能增加:

TOO_MANY_ATTEMPTS
PASSWORD_EXPIRED
USER_DISABLED

如果没有兜底,前端可能直接显示空字符串,甚至把服务端内部错误抛给用户。

所以我们不能只写“正常情况”,还要问一句:

如果冒出来了我现在还不知道的情况,程序会怎么样?

这也是写代码时非常重要的边界意识。


三、别急着改代码,先让 Codex 把仓库读明白

3.1 为什么第一步不是打开 LoginPage?

见到登录提示不对,我们第一反应通常是打开:

src/pages/LoginPage.tsx

然后搜索“登录失败”。

但是页面只是错误最终显示出来的地方,根因不一定在页面。

一个登录请求大致可能经过:

LoginPage.tsx
      ↓
useAuth.ts
      ↓
authApi.login()
      ↓
httpClient.ts
      ↓
服务端响应

假设服务端返回了 ACCOUNT_LOCKED,但 httpClient.ts 在构造异常时只保留了 message

throw new ApiError(response.message);

那么错误码在请求层就已经丢了。

这时我们就算把登录页面翻个底朝天,也拿不到 ACCOUNT_LOCKED

3.2 先调查,不修改

我比较建议用先给 Codex 一个只读任务:

先调查,不修改文件。

请定位登录请求从页面到 API 客户端的完整调用链,重点确认:

1. 服务端错误 code 在哪里被保留或丢失
2. 当前 Toast 文案在哪里生成
3. 项目中是否已有错误码映射模式
4. 是否存在登录相关测试

输出:
- 调用链
- 相关文件
- 根因判断
- 最小修复建议

这里特意写了“先调查,不修改文件”。

为什么?

因为在根因没有确认之前,任何修改都可能只是猜。

3.3 Codex 搜索仓库后的结果

它可能会执行类似下面的搜索:

rg -n "login|INVALID_CREDENTIALS|ACCOUNT_LOCKED" src tests
rg --files src/features/auth tests

得到结果:

src/pages/LoginPage.tsx:42
src/features/auth/useAuth.ts:28
src/api/httpClient.ts:61
src/features/payment/paymentError.ts:7
tests/auth/login.test.tsx

从输出中能看出几个关键点:

// useAuth.ts
toast.error("登录失败");

// httpClient.ts
throw new ApiError(response.message);

而支付模块中已经有类似写法:

const paymentErrorMessages: Record = {
  PAYMENT_DECLINED: "支付失败,请更换支付方式",
};

现在根因基本清楚了:

服务端确实返回了 code,但是 httpClient 在转换异常时只传了 message,导致上层无法区分错误类型。

这时候我们再回头看登录页面,就会发现页面并不是根因,它只是最后一个“背锅”的地方。

3.4 让 Codex 先输出调查结论

一个比较好的调查结果应该包含:

调用链:
LoginPage.tsx
→ useAuth.ts
→ authApi.login()
→ httpClient response interceptor

根因:
httpClient 在构造 ApiError 时丢失 response.code。

可复用模式:
支付模块已经使用 Record 维护错误码文案。

最小方案:
1. ApiError 保留可选 code
2. 登录模块增加局部映射函数
3. useAuth 使用映射后的文案
4. 添加错误码和兜底测试

留意,这里说的是“局部映射函数”,而不是一上来就重构全站错误系统。

这也是实际开发中很重要的一点:

能用四个文件解决的问题,就不要先设计一个覆盖全公司的异常框架。

四、繁琐任务先 Plan,防止改着改着跑偏

4.1 Plan 模式到底有什么用?

很多人理解的 Plan 模式,就是让 Codex 先输出一个目录。

其实不是。

Plan 真正的作用是:

1. 在写代码前读取项目;
2. 找出还不确定的地方;
3. 把实现范围锁定;
4. 提前写清楚验证方式。

如果任务只是改一个文案,没必要专门 Plan。

但如果一个 Bug 涉及页面、Hook、请求层、类型和测试,先规划一下还是很有必要的。

4.2 针对本例的计划

1. 修改 httpClient,构造 ApiError 时保留 response.code
2. 新增 loginError.ts,维护登录错误码到用户文案的映射
3. useAuth 捕获异常后调用映射函数
4. 为三个已知错误码和未知错误码补测试
5. 运行登录相关测试、typecheck、lint
6. Review Git diff,确认没有修改后端接口和登录成功流程

计划中还应该明确风险:

风险:
ApiError 可能被其他模块直接创建,因此 code 应该保持可选,
不能强制所有旧代码立刻传入。

这个风险很容易被忽略。

如果我们把构造函数改成:

constructor(message: string, code: string)

那么项目里所有旧写法:

new ApiError("request failed");

都会编译失败。

更稳妥的是:

constructor(message: string, code?: string)

这就是 Plan 的价值:代码还没写,兼容性问题已经提前暴露出来了。

4.3 什么情况下应该打回计划?

如果 Codex 给出下面这种方案:

1. 重写全局请求库
2. 引入新的错误处理依赖
3. 改造所有业务模块
4. 统一全站文案

可以直接回复:

范围太大。

本次只修复登录流程,保留现有请求架构。
复用支付模块的映射模式,不新增依赖,
不修改其他业务模块。

在计划阶段让它收缩范围,比代码写完以后再全部回滚省事得多。


五、动手修改:先保留错误码

根因已经确定,接下来正式看代码。

5.1 修改前的 ApiError

export class ApiError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ApiError";
  }
}

请求失败时:

throw new ApiError(response.message);

这里保存了 message,却没有保存 code

5.2 修改 ApiError

export class ApiError extends Error {
  readonly code?: string;

  constructor(message: string, code?: string) {
    super(message);
    this.name = "ApiError";
    this.code = code;
  }
}

然后修改异常转换:

throw new ApiError(response.message, response.code);

这个改动看起来非常小,但它打通了服务端到业务层之间缺失的信息。

这里有一个值得思考的问题:

为什么不直接把整个 response 都塞进 Error?

例如:

throw new ApiError(response);

这样做当然也能拿到 code,但会让 ApiError 与后端具体响应结构绑定得更紧。

今天接口是:

{
  "code": "ACCOUNT_LOCKED",
  "message": "account locked"
}

明天后端可能改成:

{
  "error": {
    "code": "ACCOUNT_LOCKED",
    "message": "account locked"
  }
}

请求层应该负责把不同的 HTTP 响应整理成稳定的前端异常类型,上层业务只关心:

error.code
error.message

这就是封装的意义。


六、增加错误码映射,不直接展示服务端 message

6.1 为什么不能把服务端 message 原样显示?

有人可能会问:

服务端不是已经返回 message 了吗?直接 Toast 出来不就行了?

代码可能是:

toast.error(error.message);

看起来省事,但有几个问题:

1. 服务端文案可能是英文;
2. 文案可能面向开发人员,不适合直接给用户;
3. 可能暴露内部实现;
4. 后端一改文案,前端显示就跟着变化;
5. 同一个错误在不同页面可能需要不同提示。

例如服务端返回:

failed to query user table: connection timeout

这个信息放日志里很有用,直接展示给用户就不合适了。

6.2 新增映射文件

// src/features/auth/loginError.ts

const loginErrorMessages: Record = {
  INVALID_CREDENTIALS: "账号或密码错误",
  ACCOUNT_LOCKED: "账号已冻结,请联系管理员",
  SERVICE_UNAVAILABLE: "服务暂时不可用,请稍后重试",
};

const DEFAULT_LOGIN_ERROR = "登录失败,请稍后重试";

export function mapLoginErrorMessage(code?: string): string {
  if (!code) {
    return DEFAULT_LOGIN_ERROR;
  }

  return loginErrorMessages[code] ?? DEFAULT_LOGIN_ERROR;
}

这里使用 Record<string, string>,代码并不繁琐。

有人可能会想把它写成 switch

switch (code) {
  case "INVALID_CREDENTIALS":
    return "账号或密码错误";
  case "ACCOUNT_LOCKED":
    return "账号已冻结,请联系管理员";
  default:
    return "登录失败,请稍后重试";
}

两种写法都能用。

当前项目的支付模块已经使用对象映射,所以登录模块沿用相同风格更合适。

在已有项目里,和周围代码保持一致,往往比单独追求“最优雅写法”更重要。

6.3 修改 useAuth

修改前:

try {
  await authApi.login(values);
  navigate("/");
} catch (error) {
  toast.error("登录失败");
}

修改后:

import { ApiError } from "@/api/ApiError";
import { mapLoginErrorMessage } from "./loginError";

try {
  await authApi.login(values);
  navigate("/");
} catch (error) {
  if (error instanceof ApiError) {
    toast.error(mapLoginErrorMessage(error.code));
    return;
  }

  toast.error(mapLoginErrorMessage());
}

这里为什么还要判断 instanceof ApiError

因为 catch 到的东西不一定来自服务端。

它可能是:

  • 网络层抛出的错误;
  • JSON 解析错误;
  • 代码自身的运行时错误;
  • 某个第三方库抛出的异常。
如果不判断类型,直接访问:

error.code

在 TypeScript 严格模式下本身就不安全。


七、补测试:不要只测三个“正确答案”

代码写完以后,最容易冒出来的一句话是:

看起来没问题。

“看起来没问题”恰恰是最不可靠的判断。

7.1 表驱动测试

import { describe, expect, it } from "vitest";
import { mapLoginErrorMessage } from "@/features/auth/loginError";

describe("mapLoginErrorMessage", () => {
  it.each([
    ["INVALID_CREDENTIALS", "账号或密码错误"],
    ["ACCOUNT_LOCKED", "账号已冻结,请联系管理员"],
    ["SERVICE_UNAVAILABLE", "服务暂时不可用,请稍后重试"],
  ])("maps %s", (code, expected) => {
    expect(mapLoginErrorMessage(code)).toBe(expected);
  });

  it("falls back for an unknown code", () => {
    expect(mapLoginErrorMessage("PASSWORD_EXPIRED"))
      .toBe("登录失败,请稍后重试");
  });

  it("falls back when code is missing", () => {
    expect(mapLoginErrorMessage())
      .toBe("登录失败,请稍后重试");
  });
});

这里除了三个已知错误码,还测试了:

未知 code
code 不存在

为什么要测未知 code?

因为接口一定会变化。

今天只有三个错误,明天后端加一个 PASSWORD_EXPIRED,前端至少应该安全地显示默认文案,而不是崩掉。

7.2 登录成功流程也要保护

我们修的是失败路径,但不能只测失败路径。

还应该确认原来的成功逻辑没有被破坏:

it("keeps the successful login path unchanged", async () => {
  authApi.login.mockResolvedValue({
    token: "test-token",
  });

  await login({
    username: "demo",
    password: "correct-password",
  });

  expect(navigate).toHaveBeenCalledWith("/");
  expect(toast.error).not.toHaveBeenCalled();
});

这就是回归测试。

修复一个 Bug 时,不仅要证明“错误路径变正确了”,还要证明“原来正确的路径没有被改坏”。

7.3 运行测试

npm test -- loginError.test.ts
npm run typecheck
npm run lint

运行结果如下:

终端输出:

Test Files  1 passed (1)
Tests       5 passed (5)

npm run typecheck  passed
npm run lint       passed

到这里能不能说任务完成了?

还不能。

因为测试通过,只能说明被测试覆盖的行为没有问题。它不能告诉我们是否顺手修改了其他文件,也不能发现大面积无关格式化。

所以还要看 diff。

7.4 如果全量测试失败,先别急着认定是自己改坏了

实际项目中经常出现这种情况:

npm test

登录相关测试通过了,但另外两个旧模块失败:

loginError.test.ts:通过
UserProfile.test.tsx:失败
NotificationPanel.test.tsx:失败

这时不能简单地说“全量测试失败,所以本次修复失败”,也不能一句“项目原本就有问题”直接跳过。

需要继续判断:

1. 失败文件是否被本次修改;
2. 错误调用链是否经过 auth 模块;
3. 修改前是否已经失败;
4. 单独重跑能否稳定复现;
5. 错误堆栈是否与本次代码有关。

比较完整的报告应该是:

验证结果:
- 登录错误测试:5 个全部通过
- typecheck:通过
- lint:通过
- 全量测试:存在 2 个失败

现有失败:
- UserProfile.test.tsx
- NotificationPanel.test.tsx

判断:
两个失败文件未被本次修改,调用链不经过 auth 模块。
修改前运行同样失败,因此不属于本次引入的回归。

重点不是想办法把失败“说成没问题”,而是把证据说清楚。


八、Review:代码写完后的第二次思考

8.1 先看改了多少文件

git diff --stat

输出:

src/api/httpClient.ts                  |  4 +++-
src/features/auth/loginError.ts        | 16 ++++++++++++++++
src/features/auth/useAuth.ts           |  5 +++--
tests/auth/loginError.test.ts          | 28 ++++++++++++++++++++++++++++

4 files changed, 50 insertions(+), 3 deletions(-)

这个 diff 是比较健康的:

  • 修改 4 个文件;
  • 没有升级依赖;
  • 没有全项目格式化;
  • 没有修改后端接口;
  • 没有重构登录页面;
  • 测试代码占了大部分新增行。

8.2 Review 时我一般看什么?

我会重点看下面几个问题:

1. 是否改了计划外文件?

如果只是修登录,却突然出现:

package-lock.json
src/pages/HomePage.tsx
src/styles/global.css

就需要问一句:为什么?

2. 是否直接展示服务端内部错误?

toast.error(error.message);

这种写法要格外留意。

3. 是否存在兜底?

loginErrorMessages[code]

如果没有 ?? DEFAULT_LOGIN_ERROR,未知 code 可能返回 undefined

4. 测试是否确实断言了行为?

下面这种测试价值很低:

expect(mapLoginErrorMessage).toBeDefined();

它只证明函数存在,没有证明映射是否正确。

5. 是否影响成功流程?

失败提示改得再好,如果成功登录后不跳转了,也是严重回归。

8.3 用行内评论让 Codex 精确修改

Codex App 的 Review 面板可以在具体代码行上留下评论。

例如见到:

toast.error(error.message);

可以评论:

这里不要展示服务端原始 message。
保留 message 用于日志,用户文案根据 ApiError.code 映射。

然后再发:

处理 Review 面板中的行内评论。
保持修改范围最小,完成后重新运行登录测试和 typecheck。

这种反馈比一句“你再优化一下”准确得多。

8.4 再用一次 /review

可以让 Codex 对当前未提交修改做独立审查:

/review

重点检查:
1. 登录成功流程是否可能回归
2. 是否可能泄露服务端内部信息
3. code 为空时是否安全
4. 是否存在遗漏的错误分支
5. 测试是否真正覆盖修改行为

需要留意:

Review 的重点应该是具体 Bug、风险和测试缺口,而不是重新介绍“本次改了四个文件”。


九、Worktree:让 Codex 在后台干活,不碰你正在写的代码

如果你正在本地开发一个功能,同时又想让 Codex 修另一个 Bug,会遇到一个很现实的问题:

两边会不会同时修改同一个项目目录?

如果直接开两个线程,让它们都操作同一份文件,确实可能互相影响。

这时就可以用 Worktree。

10.1 Worktree 可以理解成什么?

如果借用 Linux 中进程隔离的思路,可以简单理解为:

同一份 Git 历史
        ↓
多个相互独立的工作目录

每个 Worktree 都有自己的文件、索引和当前状态,但它们共享仓库的提交对象。

例如:

Local:
我继续写用户资料页

Worktree A:
Codex 修复登录错误提示

Worktree B:
Codex 补认证模块测试

三个任务互不覆盖。

终端中可以看到:

git worktree list

D:/shop-web                         a81c2d9 [feature/profile]
C:/Users/me/.codex/worktrees/a12  a81c2d9 (detached HEAD)
C:/Users/me/.codex/worktrees/b34  a81c2d9 (detached HEAD)

10.2 哪些任务适合放到 Worktree?

适合:

  • 独立 Bug 修复;
  • 补单元测试;
  • 文档更新;
  • 依赖升级调查;
  • 代码审查;
  • 和当前开发文件重叠较少的功能。
不太适合:
  • 两个任务都要重写同一个核心文件;
  • 一定要共用唯一硬件设备;
  • 强依赖当前本地开发服务器状态;
  • 修改之间存在严格先后关系。

10.3 Worktree 并不是“随便并行”

有人看到 Worktree 以后,可能会同时开五六个任务。

但并行不是越多越好。

如果几个任务最后都要修改:

src/api/httpClient.ts

那么合并时仍然会产生冲突。

Worktree 解决的是“开发目录相互干扰”,它不能消除逻辑冲突。

所以拆任务时依然要考虑模块边界。

10.4 Handoff

Codex App 可以把线程在 Local 和 Worktree 之间 Handoff。

当后台任务完成后,如果需要:

  • 在自己常用的 IDE 中检查;
  • 运行本地唯一的开发服务;
  • 使用本机已有的特殊环境;
就可以把线程带回 Local。

但要注意,Git 不允许同一个分支同时被两个 Worktree 检出。

如果分支已经在 Worktree 中,不要再强行在 Local 中检出同一分支,使用 Handoff 会更合适。


十、Subagents:把调查、测试和审查拆开

11.1 为什么要拆子代理?

一个复杂任务中,可能会产生大量信息:

  • 搜索结果;
  • 调用链;
  • 测试日志;
  • 构建输出;
  • 安全问题;
  • 性能问题;
  • Git 差异。
如果所有内容都堆在主线程里,真正重要的需求和决策容易被淹没。

Subagent 的思路是:

主代理:
负责需求、决策、计划和最终结论

子代理 A:
只调查调用链

子代理 B:
只搜索可复用模式

子代理 C:
只检查测试缺口

最后三个子代理把结论交回主线程。

11.2 一个可以直接使用的提示词

使用三个子代理并行调查,暂时不要修改代码:

代理 A:
追踪登录错误从服务端响应到页面 Toast 的完整调用链。

代理 B:
搜索仓库中已有的错误码映射模式,找出最适合复用的实现。

代理 C:
检查登录相关测试、测试工具和可以运行的最小验证命令。

等待三个代理全部完成后,汇总:
- 已确认事实
- 仍然不确定的问题
- 推荐的最小修改方案
- 风险
- 验证计划

11.3 并行读取很适合,并行写入要谨慎

子代理特别适合:

  • 搜索不同目录;
  • 分析不同模块;
  • 分类测试失败;
  • 从安全、性能、可维护性三个角度 Review。
但是让三个代理同时修改同一个文件,通常不是好主意。

这和多线程程序一样:

只读共享数据比较简单,多个执行单元同时写同一份数据,就要考虑冲突和同步。

所以我更建议用:

子代理负责调查
主代理统一制定方案和修改

或者:

不同 Worktree 修改互不重叠的模块
最后统一 Review


十一、几种很容易把 Codex 用跑偏的提示词

12.1 “帮我优化一下”

帮我优化登录模块。

“优化”到底是什么?

  • 更快?
  • 更安全?
  • 更好看?
  • 代码更短?
  • 减少重复?
  • 提高测试覆盖率?
如果自己都没有定义,Codex 只能猜。

可以改成:

减少登录失败处理中的重复 Toast 逻辑,
保持现有 UI 和 API 不变,
把错误码映射集中到 auth 模块,
并补充未知错误码测试。

12.2 “只改第 83 行”

修改 LoginPage.tsx 第 83 行。

这句话把解决方案写死了。

如果根因在 httpClient.ts,强行改第 83 行只是在错误位置打补丁。

更好的说法:

我怀疑 LoginPage.tsx 的错误提示有问题,
但请先验证完整调用链,不要假设根因一定在页面。

12.3 一次塞进去五个任务

修登录 Bug、升级 React、优化数据库、
重写首页,再把测试补全。

这种任务很难验证,也很难 Review。

应该拆成多个线程,必要时放进不同 Worktree。

12.4 不让它复现

如果修改前没有复现 Bug,就无法证明修改后 Bug 消失了。

先按复现步骤确认当前行为并记录结果,
修改后重复相同步骤进行验证。

12.5 只说“测试一下”

“测试一下”太模糊。

应该明确:

先运行新增测试文件,
再运行 auth 模块测试,
然后执行 typecheck 和 lint。
如果时间允许再运行全量测试。

12.6 测试通过就直接提交

测试不能发现所有问题。

还要检查:

git diff --stat
git diff
git status

瞧瞧有没有:

  • 无关文件;
  • 调试代码;
  • 本地路径;
  • 临时日志;
  • 大面积格式化;
  • 意外生成文件。

十二、几套我认为比较实用的提示词

13.1 陌生仓库快速理解

先不要修改代码。

请解释  的完整调用流程,包括:
- 入口
- 核心业务逻辑
- 数据验证
- 外部依赖
- 错误处理
- 测试位置

最后输出:
1. 编号流程
2. 涉及文件
3. 修改该功能最容易踩的三个坑

13.2 修复 Bug

Bug:

复现:
1. ...
2. ...
3. ...

期望:

约束:
- 保持接口兼容
- 修改范围最小
- 不做无关重构

先复现和定位根因,再给出最小方案。
实施后补回归测试,并重新执行复现步骤。
最后报告验证命令和结果。

13.3 新增小功能

新增功能:

用户行为:
- ...

技术约束:
- 沿用现有组件
- 沿用现有状态管理
- 不新增依赖
- API 结构保持不变

完成条件:
- 正常路径可用
- 空数据有处理
- 异常路径有处理
- 添加测试
- 运行 typecheck、lint 和相关测试

13.4 Review 当前修改

审查当前未提交修改。

按严重程度检查:
1. 功能错误和回归
2. 安全问题
3. 边界条件
4. 数据兼容性
5. 测试缺口

只报告具体、可以操作的问题。
每个问题给出文件位置、触发条件、影响和修复方向。

13.5 多代理并行调查

使用三个子代理,只读调查,不修改代码:

- 代理 A:分析业务调用链
- 代理 B:搜索仓库中可以复用的实现
- 代理 C:检查测试覆盖和验证命令

等待全部完成后,统一汇总:
- 事实
- 不确定项
- 推荐方案
- 风险
- 验证步骤


总结

这篇文章与上一篇 Codex 配置文章的侧重点完全不同。

【Codex配置实战】从 config.toml 到 AGENTS.md:把 AI 编程助手调成顺手的开发环境

上一篇解决的是:

如何把 Codex 的环境配置好?

这一篇解决的是:

环境已经配置好了,如何让 Codex 真正完成一次能够交付的工程任务?

最后总结几句话:

1. 不要只说“帮我写代码”。先说清楚目标、现象、限制和验收条件。
2. 不要看到页面报错就只改页面。先追完整调用链,找到信息真正丢失的位置。
3. 复杂任务先 Plan。计划的重点不是步骤多,而是修改范围、风险和验证方式是否明确。
4. 代码写完不是完成。测试、类型检查、lint、复现步骤和 Git diff 都是任务的一部分。
5. 测试通过也要 Review。测试看行为,diff 看修改边界,两者缺一不可。
6. 需要后台并行时用 Worktree。不要让多个线程直接在同一个开发目录里互相覆盖。
7. 子代理更适合做并行调查。多个代理同时写同一文件,反而可能增加冲突。

Codex 并不是一个“你说一句,它写一段”的代码补全工具。

当我们把需求、调查、计划、实现、测试和 Review 连成一条完整链路之后,它更像是一个能够真正参与项目的工程搭档。

而我们需要做的,也不再是盯着它输出了多少行代码,而是始终追问:

这个结果,确实被证明是正确的吗?

官方参考资料
Codex Best PracticesCodex WorkflowsCodex PromptingCodex App ReviewCodex App WorktreesCodex Subagents

本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。

评论 (0)

暂无评论