
开篇
AI 生成的代码最危险的一类问题,不是语法错误。
语法错误会立刻报错,测试也很容易发现。真正容易翻车的是:
代码能运行,正常流程能通过,但它悄悄漏掉了一条业务规则。
这一篇复盘一个任务 API 中很典型的失败:用户可以把任务标记为完成,却无法把已完成任务恢复为未完成。
接口、代码、测试看起来都没有问题,为什么还是出了问题?
一、表面上一切正常
任务状态接口约定是:
http
PATCH /api/tasks/:id
Content-Type: application/json
{
"completed": true
}
AI 给出的校验代码是:
javascript
const { completed } = req.body;
if (!completed) {
return sendError(res, 400, "INVALID_STATUS", "completed 必须是布尔值");
}
task.completed = completed;
return res.json(task);
第一次看,这段代码似乎很合理:
- 没有传 completed 会报错。
- completed 为 true 可以更新任务。
- 任务不存在时也有 404 处理。
- 所有测试都通过了。
于是代码上线。
二、真正的故障是"无法撤销完成状态"
过了一段时间,用户需要把误完成的任务重新打开,请求是:
json
{
"completed": false
}
服务却返回:
json
{
"error": {
"code": "INVALID_STATUS",
"message": "completed 必须是布尔值"
}
}
这不是接口不支持"撤销完成",而是代码把合法的 false 当成了无效值。
在 JavaScript 中,下面这些值都会被判定为假:
javascript
false
0
""
null
undefined
所以:
javascript
if (!completed)
检查的不是"是否为布尔值",而是"是否为真"。
这就是失败的根因。
三、为什么 AI 代码看起来没问题
这次失败通常由三个因素叠加造成。
1. Prompt 只强调了"必须是布尔值"
如果只告诉 AI:
text
completed 必须是布尔值。
AI 可能会生成最常见的"非空校验",却没有真正检查类型。
更准确的描述应该是:
text
completed 必须存在,且只能是 true 或 false。
false 是合法值,不能视为缺失。
输入约束越精确,AI 越不容易把语言习惯带进业务规则。
2. 测试只覆盖了 true
原测试可能是这样:
javascript
it("可以将任务标记为完成", async () => {
const response = await request(app)
.patch("/api/tasks/1")
.send({ completed: true });
expect(response.status).toBe(200);
expect(response.body.completed).toBe(true);
});
它证明了"可以完成",却没有证明"可以恢复"。
测试通过,只说明已测试的路径正常,不能说明接口规则被完整实现。
3. 代码审查只看了异常分支是否存在
审查时容易问:
text
有没有参数校验?
有没有返回 400?
有没有测试?
这些问题都能得到"有"的答案。
但更应该问:
text
合法值的完整集合是什么?
每一个合法值都测试了吗?
false、0、空字符串在当前语言里会不会被混淆?
四、正确的修复方式
对于明确要求布尔值的字段,应该检查类型:
javascript
const { completed } = req.body;
if (typeof completed !== "boolean") {
return sendError(res, 400, "INVALID_STATUS", "completed 必须是布尔值");
}
task.completed = completed;
return res.json(task);
这样结果才符合契约:
| 请求值 | 结果 |
|---|---|
| true | 合法,更新任务 |
| false | 合法,更新任务 |
| "false" | 非法,返回 400 |
| 0 | 非法,返回 400 |
| 未传字段 | 非法,返回 400 |
修复代码只有一行变化,但前提是先识别出错误的业务边界。
五、把线上问题变成回归测试
修复后,至少要补上这两组测试:
javascript
it("可以把未完成任务标记为完成", async () => {
const response = await request(app)
.patch("/api/tasks/1")
.send({ completed: true });
expect(response.status).toBe(200);
expect(response.body.completed).toBe(true);
});
it("可以把已完成任务恢复为未完成", async () => {
const response = await request(app)
.patch("/api/tasks/1")
.send({ completed: false });
expect(response.status).toBe(200);
expect(response.body.completed).toBe(false);
});
再补充非法输入:
javascript
it.each([undefined, null, 0, "false"])(
"completed 为 %p 时返回 400",
async (completed) => {
const response = await request(app)
.patch("/api/tasks/1")
.send({ completed });
expect(response.status).toBe(400);
expect(response.body.error.code).toBe("INVALID_STATUS");
}
);
这组测试不追求数量,而是把"能完成"和"能撤销"这两个业务状态都固定下来。
六、如何让 AI 帮你做这类复盘
故障出现后,不要只问 AI:
text
这段代码有什么问题?
可以这样提供上下文:
text
请复盘下面的接口故障,先不要修改代码。
接口规则:
- PATCH /api/tasks/:id 只允许修改 completed。
- completed 必须是布尔值。
- true 和 false 都是合法业务状态。
实际故障:
- 请求 { "completed": true } 成功。
- 请求 { "completed": false } 返回 400。
请输出:
1. 已确认事实。
2. 最可能的根因。
3. 需要检查的 JavaScript 语义风险。
4. 最小修复方案。
5. 必须补充的回归测试。
6. 如何修改需求说明和 Prompt,避免再次发生。
这个 Prompt 的重点是明确"false 合法"。否则 AI 也可能重复给出错误的非空校验。
七、从这次失败学到的 4 件事
text
[ ] 正常流程通过,不等于所有合法状态都支持。
[ ] 语言中的真假值,不能替代业务规则校验。
[ ] 测试要覆盖值域,不只是覆盖成功和失败。
[ ] AI 输出中的简洁判断,尤其需要检查边界含义。
以后看到下面这些写法时,建议多看一眼:
javascript
if (!value)
if (value)
value || defaultValue
它们在处理字符串、数字、布尔值时,可能会把"合法的假值"误判成"缺失值"。
总结
这次翻车不是 AI 不会写代码,而是我们把一个精确的业务规则,交给了一个模糊的校验实现。
正确的复盘顺序是:
text
还原真实请求
↓
确认业务规则
↓
定位语言语义差异
↓
最小修复
↓
补充回归测试
↓
更新 Prompt 和接口契约
- AI 代码"看起来合理",不代表它符合所有业务状态。
- 布尔值、空值、零值等边界,需要明确写进需求和测试。
- 每一次线上故障,都应该沉淀成可重复执行的测试。
- 开发者的价值,在于确认规则、识别风险并验收结果。
下一篇文章,我们将继续讨论一个更重要的边界:
《哪些任务该交给 AI,哪些必须由开发者负责?》
✍坚持原创,求关注,点赞,收藏