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/9999ordernull404
/api/orders/1002customernull422 或业务约定状态码

如果服务器只在最外层统一捕获异常,这三个不同问题最终都会变成模糊的 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
}

后两个请求则会触发空对象访问。此时已经有了三个关键证据:

  1. 可重复执行的请求;
  2. 明确的期望状态码;
  3. 能定位到函数和行号的错误日志。

有了证据再让 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 才会从一个回答问题的工具,变成工程流程中可控的执行者。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐