mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2632 words
7 minutes
一次失败的 AI 编程复盘:代码通过了,业务却错了
2026-06-06

一次失败的 AI 编程复盘:代码通过了,业务却错了#

这是一篇复盘文章。

案例根据 AI 编程中常见的失败模式重新组合,用于说明问题,不对应某一次真实生产事故,也不包含真实用户或业务数据。

需求看起来很简单:

给订单增加取消功能,取消后恢复库存。

AI 很快完成了 Controller、Service 和测试。代码可以编译,新增测试也通过了,接口调用后订单状态确实变成了 CANCELLED

但进一步验证后,我们发现:

  • 已支付订单也能被直接取消;
  • 重复请求会多次恢复库存;
  • 库存服务失败时,订单已经变成取消状态;
  • 用户可以尝试取消不属于自己的订单;
  • 新增测试通过 Mock 绕开了这些真实问题。

代码“完成”了需求中的几个动词,却没有完成真实业务。

1. 最初的 Prompt#

最初给 AI 的任务是:

帮我实现订单取消接口。
取消订单后把库存加回去,并补一下测试。

这条 Prompt 缺少几乎所有关键边界:

  • 哪些状态允许取消;
  • 谁可以取消订单;
  • 重复取消如何处理;
  • 订单状态和库存恢复的先后顺序;
  • 库存失败时是否回滚;
  • 是否需要退款;
  • 并发取消如何避免重复执行;
  • 测试需要证明什么。

AI 只能根据常见写法自行补全。

它生成了这样的核心逻辑:

public void cancel(long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
order.setStatus(OrderStatus.CANCELLED);
orderRepository.save(order);
inventoryClient.restore(order.getItems());
}

从代码表面看,步骤与需求一致:

找到订单 → 修改状态 → 保存 → 恢复库存

但业务系统不是把几个动词按顺序执行就结束了。

2. 第一个问题:没有状态规则#

订单状态通常不是任意修改的字符串,而是一个有限状态机。

简化后的规则可能是:

CREATED ──取消──> CANCELLED
UNPAID ──取消──> CANCELLED
PAID ──申请──> REFUNDING
SHIPPED ────────> 不允许直接取消
CANCELLED ───────> 保持取消,不重复恢复库存

AI 生成的代码没有校验当前状态,因此:

PAID -> CANCELLED
SHIPPED -> CANCELLED
COMPLETED -> CANCELLED

全部可以发生。

正确的规则应该先写进 Spec:

## 状态规则
- 只有 CREATED 和 UNPAID 状态允许直接取消。
- PAID 订单必须进入退款流程,本接口返回业务错误。
- SHIPPED、COMPLETED 状态不能取消。
- CANCELLED 状态重复请求时直接返回成功,不重复恢复库存。

如果业务规则没有进入上下文,AI 不可能从方法名中推断出公司真正的订单状态机。

3. 第二个问题:没有权限边界#

原实现只根据 orderId 查询:

orderRepository.findById(orderId)

如果 Controller 接收当前登录用户,却没有把用户 ID 传到 Service,攻击者可能依次尝试订单 ID,取消他人的订单。

更安全的入口至少应该显式携带操作者:

public CancelOrderResult cancel(long currentUserId, long orderId)

查询也需要包含权限:

Order order = orderRepository
.findByIdAndUserId(orderId, currentUserId)
.orElseThrow(() -> new BusinessException("订单不存在或无权操作"));

有些系统会区分普通用户、客服和管理员,规则会更复杂。但无论如何,权限不能依赖 AI “应该知道”。

4. 第三个问题:事务边界是假的#

AI 给方法加上了 @Transactional

@Transactional
public void cancel(long orderId) {
// 更新本地订单
// 调用远程库存服务
}

然后在总结里写:

使用事务保证了订单状态和库存的一致性。

这句话不成立。

Spring 本地事务可以回滚当前数据库连接中的操作,但无法自动回滚一次已经成功或失败的远程 HTTP 调用。

可能出现:

场景 A:订单保存成功,库存服务超时
结果:订单已取消,库存没有恢复
场景 B:库存恢复成功,数据库提交失败
结果:库存增加,订单仍然未取消
场景 C:客户端没有收到响应并重试
结果:库存被重复恢复

“方法上有 @Transactional”不等于跨系统一致性已经解决。

真实方案可能使用:

  • 本地消息表和异步事件;
  • 可靠消息队列;
  • 库存接口幂等键;
  • 补偿任务和对账;
  • 项目已有的 Saga 或状态推进机制。

应该复用系统已有方案,而不是让 AI 在不知道架构的情况下自行发明。

5. 第四个问题:重复请求会重复恢复库存#

用户双击按钮、网关重试、客户端超时重试都可能造成重复请求。

原代码每次都会执行:

inventoryClient.restore(order.getItems());

如果同一订单调用两次,库存可能增加两次。

最小的状态保护是:

if (order.getStatus() == OrderStatus.CANCELLED) {
return CancelOrderResult.alreadyCancelled(order.getId());
}

但在并发场景中,两个请求可能同时读到 UNPAID,然后都恢复库存。还需要结合项目现有机制,例如:

  • 乐观锁版本号;
  • 条件更新 where status = 'UNPAID'
  • 分布式锁;
  • 使用订单 ID 作为库存恢复幂等键。

一种条件更新方式:

@Modifying
@Query("""
update Order o
set o.status = :targetStatus,
o.version = o.version + 1
where o.id = :orderId
and o.user.id = :currentUserId
and o.status in :allowedStatuses
""")
int cancelIfAllowed(
long orderId,
long currentUserId,
Set<OrderStatus> allowedStatuses,
OrderStatus targetStatus
);

返回值为 0 时,需要重新读取状态并判断是无权限、状态不允许,还是已经取消。

6. 为什么新增测试全部通过#

AI 生成的测试是:

@Test
void shouldCancelOrderAndRestoreInventory() {
Order order = anOrder(OrderStatus.UNPAID);
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
service.cancel(1L);
assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED);
verify(orderRepository).save(order);
verify(inventoryClient).restore(order.getItems());
}

这个测试验证了实现写了什么:

状态被修改
save 被调用
库存接口被调用

但它没有验证需求真正关心的风险:

  • 已支付订单能否取消;
  • 其他用户能否操作;
  • 重复取消是否重复恢复库存;
  • 库存失败后系统处于什么状态;
  • 两个并发请求是否都成功;
  • 远程调用超时后怎样恢复。

Mock 把数据库、事务和远程系统全部变成了理想环境,因此测试通过得非常顺利。

测试通过并不表示测试覆盖了正确的问题。

7. 我们应该先写哪些测试#

状态边界#

@ParameterizedTest
@EnumSource(value = OrderStatus.class, names = {"PAID", "SHIPPED", "COMPLETED"})
void shouldRejectStatusesThatCannotBeCancelled(OrderStatus status) {
Order order = anOrder(status, CURRENT_USER_ID);
when(orderRepository.findByIdAndUserId(ORDER_ID, CURRENT_USER_ID))
.thenReturn(Optional.of(order));
assertThatThrownBy(() -> service.cancel(CURRENT_USER_ID, ORDER_ID))
.isInstanceOf(BusinessException.class);
verifyNoInteractions(inventoryClient);
}

权限边界#

@Test
void shouldNotCancelAnotherUsersOrder() {
when(orderRepository.findByIdAndUserId(ORDER_ID, CURRENT_USER_ID))
.thenReturn(Optional.empty());
assertThatThrownBy(() -> service.cancel(CURRENT_USER_ID, ORDER_ID))
.isInstanceOf(BusinessException.class);
verifyNoInteractions(inventoryClient);
}

重复请求#

@Test
void shouldNotRestoreInventoryForAlreadyCancelledOrder() {
Order order = anOrder(OrderStatus.CANCELLED, CURRENT_USER_ID);
when(orderRepository.findByIdAndUserId(ORDER_ID, CURRENT_USER_ID))
.thenReturn(Optional.of(order));
CancelOrderResult result = service.cancel(CURRENT_USER_ID, ORDER_ID);
assertThat(result.alreadyCancelled()).isTrue();
verifyNoInteractions(inventoryClient);
}

库存接口幂等#

集成或契约测试需要确认两次使用同一个业务键调用库存恢复,不会增加两次库存:

inventoryClient.restore(
"cancel-order:" + order.getId(),
order.getItems()
);

如果库存系统不支持幂等键,这就不是在订单 Service 里加一个 if 能彻底解决的问题,需要跨团队明确协议。

8. 失败是怎样一步步发生的#

回头看,问题并不是 AI 某一行代码“突然变坏”,而是一连串流程缺口叠加。

第一步:需求过于模糊#

只描述了取消和恢复库存,没有状态、权限、一致性和幂等规则。

第二步:没有要求先读项目#

AI 没有发现项目已有的状态机、库存事件和权限查询方式。

第三步:没有先 Review 方案#

如果方案阶段写出“本地事务保证远程库存一致性”,问题本可以在写代码前被发现。

第四步:一次生成所有代码#

Controller、Service、库存调用和测试同时生成,修改范围太大,人工只检查了能否编译。

第五步:测试跟着实现走#

测试验证了 AI 写出的调用顺序,没有从业务风险推导用例。

第六步:把 AI 总结当成验收#

看到“测试全部通过”后,没有检查实际执行命令、测试数量和未覆盖场景。

最终失败可以表示成:

模糊需求
+ 缺少项目上下文
+ 未 Review 方案
+ 一次性大修改
+ 表面测试
+ 没有人工业务验收
= 代码通过但业务错误

9. 如果重新做一次#

先阅读系统#

请阅读订单状态变更、用户权限、库存恢复、消息事件、幂等和相关测试。
只输出当前系统如何处理这些问题,不修改代码。

再写 Spec#

至少明确:

## 允许取消
- CREATED、UNPAID
## 不允许取消
- PAID、REFUNDING、SHIPPED、COMPLETED
## 幂等
- CANCELLED 重复请求返回成功,不重复恢复库存
- 库存恢复使用 orderId 组成幂等键
## 权限
- 普通用户只能取消自己的订单
## 一致性
- 复用项目现有订单事件机制
- 不在数据库事务中假设远程 HTTP 可以自动回滚
## 验收
- 状态、权限、重复请求和库存失败均有测试
- 并发取消只有一次状态迁移成功

Review 方案#

方案必须画清楚数据流:

用户请求
权限与状态校验
条件更新订单状态
写入本地订单取消事件
事务提交
异步恢复库存(使用幂等键)
失败重试与告警

分阶段实现#

  1. 状态和权限规则;
  2. 条件更新与并发保护;
  3. 订单事件;
  4. 库存幂等和失败重试;
  5. 集成测试与故障场景。

每一步都可以独立 Review 和验证。

10. 如何处理已经生成的错误代码#

发现方向错误时,不要继续在错误方案上叠加补丁。

先做这些事情:

git status --short
git diff --stat
git diff

然后区分:

  • 哪些测试表达了正确业务规则,可以保留;
  • 哪些测试只是绑定错误实现,需要重写;
  • 哪些代码可以局部修复;
  • 哪些代码基于错误架构,应完整撤掉;
  • 工作区中是否还有不属于当前任务的用户修改。

如果错误实现已经提交,优先使用可追踪的反向提交或新的修复提交,而不是破坏历史。生产数据已经受到影响时,还要先停止进一步损害,再设计数据修复和审计方案。

复盘的目标不是证明 AI 不可靠,而是找到流程中哪些保护没有生效。

11. 这次失败真正教会了什么#

AI 擅长补全代码,不擅长补全私有业务#

它知道常见订单系统怎样写,但不知道你的退款规则、权限模型和库存协议。

注解不能代替架构#

@Transactional 解决不了所有跨系统一致性,@Retryable 也不能自动保证幂等。

测试必须从风险出发#

测试“方法有没有被调用”很容易,测试“重复调用会不会多恢复库存”才接近真实风险。

小任务也需要边界#

订单取消代码可能只有几十行,但它连接订单、支付、库存和权限,风险远大于代码量。

人工 Review 不能只看 Java 写法#

代码风格正确、命名清楚、测试绿色,都不能代替业务规则审查。

12. 我的失败预防清单#

  • Prompt 中包含允许和禁止的业务状态;
  • AI 修改前已经阅读现有调用链;
  • 权限条件进入真实数据查询;
  • 重复请求和并发请求有明确策略;
  • 本地事务与远程调用边界已经区分;
  • 测试从业务风险推导,而不是从实现反推;
  • 至少包含失败、重试和部分成功场景;
  • 实现方案在写代码前已经 Review;
  • 修改被拆成可以独立验证的小阶段;
  • AI 的完成总结有命令、输出和未覆盖项支撑;
  • 上线前由熟悉业务的人做最终判断。

总结#

这次失败表面上是订单取消代码不完整,根本原因却是我们把太多未定义的业务问题交给 AI 猜测。

AI 没有违反 Spec,因为当时根本没有 Spec;测试没有发现关键问题,因为测试只验证了生成的实现;本地事务没有保证一致性,因为系统跨越了远程服务。

真正可靠的 AI 编程流程应该是:

人定义业务边界
→ AI 阅读现有系统
→ 双方确认实现方案
→ AI 分阶段生成代码
→ 测试覆盖业务风险
→ 人根据证据决定是否交付

AI 可以把一个错误方案实现得非常完整。所以开发者最重要的能力,不只是让 AI 写代码,而是在代码出现之前定义正确的问题,在代码出现之后验证它解决的确实是那个问题。

Share

If this article helped you, please share it with others!

一次失败的 AI 编程复盘:代码通过了,业务却错了
https://mizuki.mysqil.com/posts/ai-coding-failure-retrospective/
Author
梦幻晨风
Published at
2026-06-06
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents