别让 AI 直接改代码:用 Codex 完成一次可回滚的 Bug 修复
AI 能快速修改代码,但“能改”不等于“能上线”。以一个 Node.js 订单接口 500 故障为例,演示如何让 Codex 完成问题复现、最小补丁、自动测试、差异审查和 Git 回滚,建立一套可验证的 AI 修复流程。
周五下午,订单摘要接口突然开始返回 500:
GET /api/orders/9999
HTTP/1.1 500 Internal Server Error
监控里只有一条熟悉的报错:
TypeError: Cannot read properties of null (reading 'id')
这类问题看起来很适合直接交给 AI:
修复订单接口的 500 错误。
但如果只给这一句话,AI 可能顺手修改异常处理、接口返回结构、日志格式,甚至重构整个服务。Bug 也许消失了,新的兼容性问题却可能一起进入项目。
更可靠的做法,是把 Codex 放进一个受约束的工程闭环:
先复现 → 再定位 → 添加失败测试 → 修改最小范围 → 运行验证 → 审查差异 → 随时可回滚
这次使用一个不依赖第三方框架的 Node.js 示例。代码实际在 Node.js v24.14.0 环境运行验证,项目建议使用 Node.js 20 或更高版本。

一、问题代码:一个空对象把接口打成了 500
订单查询逻辑被简化成两个文件。
src/orders.js:
const orders = new Map([
[
1001,
{
id: 1001,
customer: { name: "Lin" },
items: [
{ price: 199, quantity: 1 },
{ price: 49, quantity: 2 }
]
}
],
[
1002,
{
id: 1002,
customer: null,
items: [{ price: 99, quantity: 1 }]
}
]
]);
export function findOrderById(id) {
return orders.get(id) ?? null;
}
原来的 getOrderSummary() 没有处理非法参数、订单不存在和客户资料缺失:
import { findOrderById } from "./orders.js";
export function getOrderSummary(rawId) {
const id = Number(rawId);
const order = findOrderById(id);
const total = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return {
id: order.id,
customerName: order.customer.name,
total
};
}
这里至少存在三个问题:
| 输入场景 | 实际问题 | 合理响应 |
|---|---|---|
/api/orders/abc | 非法 ID 被直接传入查询 | 400 |
/api/orders/9999 | order 为 null | 404 |
/api/orders/1002 | customer 为 null | 422 或业务约定状态码 |
如果服务器只在最外层统一捕获异常,这三个不同问题最终都会变成模糊的 500。
二、第一步不是启动 Codex,而是保护现场
先查看当前工作区:
git status --short
如果存在与当前故障无关的未提交改动,不要让 AI 一起处理。确认工作区范围后,新建修复分支:
git switch -c fix/order-summary-500
接着运行现有测试,记录基线:
npm test
这一步很容易被跳过,但它决定了后面能不能区分:
- 原项目本来就失败的测试;
- AI 修改后新引入的失败;
- 这次补丁真正修复的问题。
如果项目原有测试就不通过,应先保存失败项和日志,不能把所有红灯都归因于本次修改。
三、先把 500 稳定复现出来
启动服务:
npm start
分别请求正常订单、不存在的订单和资料不完整的订单:
curl -i http://localhost:3000/api/orders/1001
curl -i http://localhost:3000/api/orders/9999
curl -i http://localhost:3000/api/orders/1002
正常订单可以返回:
{
"id": 1001,
"customerName": "Lin",
"total": 297
}
后两个请求则会触发空对象访问。此时已经有了三个关键证据:
- 可重复执行的请求;
- 明确的期望状态码;
- 能定位到函数和行号的错误日志。
有了证据再让 AI 分析,效果通常比直接粘贴一句“接口报错了”稳定得多。
四、给 Codex 的任务必须包含边界和验收条件
Codex CLI 可以在本地仓库中读取文件、修改代码并调用已经安装的测试工具;自动化场景也可以使用 codex exec。具体入口和当前参数应以 Codex CLI 官方文档 为准。
第一次交互先让它分析,不要立即修改:
请分析 GET /api/orders/:id 返回 500 的原因,暂时不要修改文件。
已知复现条件:
1. 订单 1001 应返回 200;
2. 非数字 ID 应返回 400;
3. 不存在的订单 9999 应返回 404;
4. 客户资料缺失的订单 1002 不应返回 500;
5. 响应中不能暴露错误堆栈。
请指出根因、受影响文件、建议的最小修改范围和需要补充的测试。
不要重构无关模块,不要修改接口路径。
确认分析合理后,再下达实施任务:
按刚才的分析实施最小补丁:
- 只修改订单摘要和 HTTP 错误映射相关文件;
- 保持 GET /api/orders/:id 路径不变;
- 不引入第三方依赖;
- 为正常、非法 ID、不存在订单和资料缺失四种场景添加测试;
- 运行 npm test;
- 完成后列出修改文件、测试结果和仍然存在的风险;
- 不执行 git commit,不修改环境变量和密钥文件。
如果需要用非交互模式运行,可以采用官方文档当前支持的工作区写入沙箱:
codex exec --sandbox workspace-write \
"根据已确认的复现步骤修复订单摘要接口,并运行 npm test"
自动模式不代表可以放开全部权限。Codex 官方安全说明也建议根据任务配置文件、网络和命令边界,遵循最小权限原则。参考:Codex 沙箱与审批说明。
五、补丁重点:把业务错误与未知异常分开
可以先定义一个带状态码和错误代码的业务异常:
export class HttpError extends Error {
constructor(statusCode, code, message) {
super(message);
this.name = "HttpError";
this.statusCode = statusCode;
this.code = code;
}
}
修复后的订单摘要函数:
import { findOrderById } from "./orders.js";
export class HttpError extends Error {
constructor(statusCode, code, message) {
super(message);
this.name = "HttpError";
this.statusCode = statusCode;
this.code = code;
}
}
export function getOrderSummary(rawId) {
if (!/^\d+$/.test(rawId)) {
throw new HttpError(
400,
"INVALID_ORDER_ID",
"order id must be an integer"
);
}
const order = findOrderById(Number(rawId));
if (!order) {
throw new HttpError(
404,
"ORDER_NOT_FOUND",
"order not found"
);
}
if (!order.customer?.name) {
throw new HttpError(
422,
"ORDER_DATA_INCOMPLETE",
"customer data is incomplete"
);
}
const total = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return {
id: order.id,
customerName: order.customer.name,
total
};
}
HTTP 层只向客户端返回必要信息:
try {
const summary = getOrderSummary(orderId);
sendJson(response, 200, summary);
} catch (error) {
if (error instanceof HttpError) {
sendJson(response, error.statusCode, {
code: error.code,
message: error.message
});
return;
}
console.error(error);
sendJson(response, 500, {
code: "INTERNAL_ERROR"
});
}
这里没有简单地用可选链把报错“压下去”:
customerName: order?.customer?.name
这种写法虽然可能不再抛出异常,但会让错误数据继续流入后续流程。客户资料缺失究竟应该返回空值、422,还是触发数据修复任务,应由业务约定决定,而不是由可选链替业务做决定。

六、让测试成为 AI 补丁的验收门
使用 Node.js 内置的 node:test,不需要额外安装测试框架。官方接口和版本差异可查看 Node.js Test Runner 文档。
import test from "node:test";
import assert from "node:assert/strict";
import { createApp } from "../src/server.js";
async function withServer(run) {
const server = createApp();
await new Promise((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
const { port } = server.address();
try {
await run(`http://127.0.0.1:${port}`);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => {
error ? reject(error) : resolve();
});
});
}
}
test("returns an order summary", async () => {
await withServer(async (baseUrl) => {
const response = await fetch(`${baseUrl}/api/orders/1001`);
assert.equal(response.status, 200);
assert.deepEqual(await response.json(), {
id: 1001,
customerName: "Lin",
total: 297
});
});
});
test("rejects an invalid order id", async () => {
await withServer(async (baseUrl) => {
const response = await fetch(`${baseUrl}/api/orders/abc`);
assert.equal(response.status, 400);
assert.equal(
(await response.json()).code,
"INVALID_ORDER_ID"
);
});
});
test("returns 404 when the order does not exist", async () => {
await withServer(async (baseUrl) => {
const response = await fetch(`${baseUrl}/api/orders/9999`);
assert.equal(response.status, 404);
assert.equal(
(await response.json()).code,
"ORDER_NOT_FOUND"
);
});
});
test("does not expose a stack trace", async () => {
await withServer(async (baseUrl) => {
const response = await fetch(`${baseUrl}/api/orders/1002`);
const body = await response.json();
assert.equal(response.status, 422);
assert.equal(body.code, "ORDER_DATA_INCOMPLETE");
assert.equal("stack" in body, false);
});
});
执行:
npm test
本次示例实际验证结果:
✔ returns an order summary
✔ rejects an invalid order id
✔ returns 404 when the order does not exist
✔ reports incomplete order data without exposing a stack trace
tests 4
pass 4
fail 0
测试全部通过,只能说明这四个已定义场景符合预期,不代表代码已经覆盖并发、数据库超时、权限校验和真实生产数据等所有情况。
七、测试通过后,还要人工检查 Git Diff
先查看 AI 修改了哪些文件:
git status --short
git diff --stat
然后逐文件审查:
git diff -- src/orderService.js
git diff -- src/server.js
git diff -- test/orders.test.js
重点检查:
- 是否修改了任务范围之外的文件;
- 是否悄悄改变成功响应的数据结构;
- 是否将堆栈、数据库信息或内部路径返回给客户端;
- 是否加入未说明的新依赖;
- 是否删掉原有校验;
- 测试是否真的覆盖失败路径;
- 测试是否为了通过而降低断言标准。
AI 会员解决的是工具使用权限,不能代替代码验证。如果需要长期使用 ChatGPT Plus、Claude Pro 等工具,也可以通过 gpt985了解相关会员充值信息;它的定位是第三方 AI 会员充值平台,并非相关产品的官方网站或授权合作方。使用前应看清套餐说明、账号要求、到账说明和售后规则。
八、怎样让这次修改随时可回滚
审查通过后再提交:
git add src/orderService.js src/server.js test/orders.test.js
git commit -m "fix: handle invalid and missing orders"
记录提交编号:
git rev-parse --short HEAD
如果上线后发现补丁存在兼容性问题,不需要手工删除代码,可以创建反向提交:
git revert <commit_sha>
git revert 会保留完整历史,比直接覆盖文件更适合已经推送或上线的提交。
如果尚未提交,则先保存差异:
git diff > order-summary-fix.patch
保存补丁后,再根据实际工作区状态决定是否撤销。不要在存在其他未提交改动时,随意对整个仓库执行批量恢复命令。
九、把“修好 Bug”改成可核对的交付结果
一次合格的 AI 修复,不应该只交付一句“已经解决”。
| 验收项 | 需要留下的证据 |
|---|---|
| 问题可复现 | 请求命令、输入数据、错误日志 |
| 根因明确 | 具体文件、函数和触发条件 |
| 修改范围受控 | Git Diff 和修改文件列表 |
| 行为经过验证 | 测试命令与完整结果 |
| 异常不泄露 | 错误响应断言 |
| 可以撤销 | 独立分支、提交编号或补丁文件 |
| 剩余风险明确 | 未覆盖场景和部署注意事项 |
Codex 的价值不只是把几行代码改对,而是帮助开发者完成“分析、修改、运行、验证和交付”的循环。
真正决定补丁能不能上线的,仍然是复现证据、测试结果、差异审查和回滚能力。把这些环节固定下来以后,AI 才会从一个回答问题的工具,变成工程流程中可控的执行者。
更多推荐



所有评论(0)