我让 AI 重构 5000 行 Java 代码,最后一周都在修它造的 Bug
这是我接手的一个运行了 6 年的 Spring Boot 单体服务核心模块 5000 行最臃肿的那个类 1800 行47 个方法堆在一起没有注释单测覆盖率 8%。前任的前任离职的时候留了一句话“这块别乱动动了必翻车。”我用 Claude Code 开干了。第一周重构进展顺利AI 拆类、提方法、补测试速度确实快。第二周我开始合并代码准备上灰度然后——连续 3 天每天下午收到告警凌晨定位问题根因全是 AI 重构时悄悄改了的东西。AI 代码不会说我不确定它只会自信地告诉你这样完全等价。等你发现不等价可能已经在生产了。这篇文章记录我踩的 4 个真实坑每个坑都有具体的翻车经过和防坑方法。AI重构遗留代码踩坑复盘四大坑与防坑心法图4个核心踩坑的翻车现场与根因分析坑一AI 删掉的死代码其实是 Spring 运行时注入的 Bean这是最隐蔽的坑也是第一个让我上线后翻车的坑。我让 Claude Code 扫描整个服务找出没有被任何地方引用的类删掉它们。AI 给出了一份 37 个候选类的列表自信满满地说这些都是死代码删掉可以减少代码量、降低维护复杂度。我看了几个确实在项目里搜不到任何直接引用。批准删除。上线之后某个异步消息处理功能直接 500。定位了两个小时发现是 OrderNotificationEventListener 这个类被删了。这个类长这样Componentpublic class OrderNotificationEventListener {Autowired private NotificationService notificationService; EventListener public void handleOrderCreated(OrderCreatedEvent event) { notificationService.sendConfirmation(event.getOrderId()); }}项目里没有任何地方写 new OrderNotificationEventListener()也没有 Autowired OrderNotificationEventListener。但它是活的。Spring 的组件扫描会自动把 Component 标注的类注册到容器里。它监听 OrderCreatedEvent只要 Spring 的 ApplicationContext 发布这个事件它就会被调用。没有显式引用只有运行时反射注入。AI 的 grep 分析找不到调用链——因为调用链在运行时才存在。这不是个例。我们那个服务里被误判的类一共 11 个分属以下注解Component、Service、Repository、EventListener、Scheduled、ControllerAdvice。防坑心法给 AI 加一条硬性规则在 prompt 里明确写清楚。规则带有以下注解的类永远不标记为死代码即使找不到显式引用Component, Service, Repository, Controller, RestController,Configuration, Bean, Entity, MappedSuperclass,Scheduled, EventListener, Async, Aspect,ControllerAdvice, RestControllerAdvice,Converter, JsonComponent,以及任何以 *Initializer 结尾的类名。原因Spring 依赖注入是运行时反射机制静态分析无法追踪这类依赖。还有一类更难发现通过字符串动态加载的 Bean。Class.forName(“com.example.OrderService”)、BeanFactory.getBean(“orderService”)这些都是 AI 的死角。凡是字符串拼接类名的地方AI 永远判不准。过去用静态分析工具找死代码的逻辑在这里完全失效——不是工具不够好是 Java 的反射机制本来就是为了绕开静态分析而设计的。坑二上下文窗口到底AI 的函数语义悄悄漂移了第二个坑更难发现因为代码看起来完全正确测试也过了只有特定场景下才会出问题。我在重构的第 8 天让 Claude Code 整理一个 800 行的订单处理核心类把分散的逻辑提取成独立方法。任务很顺利AI 改了 40 多处代码变得整洁多了。三天后上线收到告警某类订单的优惠计算出现偏差。金额差了几块钱不是每笔都有大约每 200 笔里有 1-2 笔。排查到最后发现问题出在这段逻辑AI 在提取某个方法时把一个本来是局部变量的 BigDecimal discountBase 改成了读取 this.orderContext.getDiscountBase()。AI 注释里写的是“等价重构将局部变量替换为 context 字段读取逻辑不变。”逻辑确实一样——在大多数场景下。但问题在于orderContext.getDiscountBase() 在某些分支下返回的是已经应用了会员折扣之后的值而原来的局部变量是在折扣应用之前算的。两者在 95% 的情况下相等只有当订单同时触发多个折扣叠加的时候值才不同。为什么 AI 会犯这种错误这和上下文窗口的工作原理直接相关。会话开始├── 第 1-15 个文件 ← AI 能完整理解上下文├── 第 16-35 个文件 ← 早期细节开始被压缩└── 第 36-45 个文件 ← AI 对前面文件的记忆已经模糊这里的重构开始出现语义漂移当 AI 在处理第 40 个文件里的 OrderProcessor 时它对第 8 个文件里 discountBase 计算时机的记忆已经非常模糊了。它只知道这里用的是 discount basecontext 里也有一个 discount base就做了等价替换。根据 Augment Code 的实测数据在跨 50 个文件总计 200K token 的重构任务中上下文漂移导致约 40% 的任务出现不一致。这不是模型的问题是架构限制。任何当前的 LLM 都面临同样的问题。防坑心法一分批次重构每批控制在 10-15 个文件。每完成一批要求 AI 输出一份语义变更摘要哪些函数的行为发生了变化变化在哪里。这个摘要不需要人工审读每一行但如果某个函数出现在摘要里那就重点 Review。防坑心法二在业务计算函数旁边写注释标明时序依赖。// 注意此处 discountBase 必须在 applyMemberDiscount() 调用之前计算// 如果读取 context.getDiscountBase()在多折扣场景下值会不同// 修改此函数时请检查 DiscountCalculationFlowTest 的时序测试用例BigDecimal discountBase order.getOriginalAmount();这类注释对人类工程师是废话但对 AI 是关键信号。有了它AI 在重构这个函数时会主动保留时序约束而不是等价替换。防坑心法三对金融计算、状态机转换、时序依赖的代码重构后必须跑专项测试。不是测试通过就 OK是要有覆盖多折扣叠加、边界金额、状态切换顺序的专项用例。这类 case 通常不在普通单测里需要专门写。坑三AI 说仅移动代码不改逻辑但它改了这个坑比上面两个更难防因为 AI 主动打破了你给它的限制而且改的是你没让它改的地方。有一次我明确告诉 AI“只重新组织这个文件的包结构不要修改任何实现逻辑不要做任何优化。”AI 回答“明白我只会移动代码不做任何功能变更。”然后它把代码移好了同时删掉了 3 个冗余的局部变量声明把一个三目表达式改成了 Optional 链式调用还把某个日志格式从字符串拼接改成了占位符格式。每一处单独看都是合理的优化。但合在一起有一处改坏了原来的代码是String errorMsg Order orderId failed: reason;log.error(errorMsg);// 注意errorMsg 这个变量后面还被用到了notifySupport(errorMsg);AI 改成了log.error(“Order {} failed: {}”, orderId, reason);notifySupport(Order orderId failed: reason);两处行为一致但 notifySupport 接收的消息和 log 打印的消息现在是两次字符串拼接变成了两个对象。在原来的代码里两者是同一个字符串引用这在某个地方被 比较过是的这是更早的坑所以行为不同了。这类问题的根源在于AI 不理解不要优化这个指令的真实含义。 对 AI 而言把字符串拼接改成占位符格式不叫改逻辑那叫最佳实践。它没有办法区分你的不改逻辑和它理解的不改逻辑。防坑心法用 git diff 做机械检查不要用眼睛。重构完成后用这个命令过滤掉纯格式变化看实质性的代码变更查看所有改动排除纯空白行变化git diff --ignore-all-space HEAD~1重点检查这些类型的变化git diff HEAD~1 | grep “^” | grep -E “(Optional|stream()|lambda|-)”任何你没有明确要求的变更都需要仔细审查。如果 AI 在只移动代码的任务里修改了表达式不管改得对不对都要 Review。另一个方法是在 prompt 里添加行为验证要求任务完成后列出所有你修改的代码行不包括格式调整。如果你修改了任何我没有明确要求修改的逻辑请在报告里标注原因。大多数时候 AI 会老实报告。偶尔它会遗漏但这比完全不报要好得多。坑四AI 生成的数据库迁移脚本开发 2 秒生产锁表 8 分钟这个坑跟 AI 重构的关系不是直接的但是 AI 辅助重构过程中非常容易触发。我们在重构中新增了一个字段 processed_atAI 生成了对应的 Flyway 迁移脚本ALTER TABLE orders ADD COLUMN processed_at DATETIME;UPDATE orders SET processed_at created_at WHERE status ‘COMPLETED’;CREATE INDEX idx_orders_processed_at ON orders(processed_at);在开发环境1000 条数据2 秒跑完完美。到了生产环境orders 表 5200 万行。ALTER TABLE 在 MySQL 5.7 默认加表级锁8 分钟。这 8 分钟里所有涉及 orders 表的写操作全部阻塞。我们的下单服务直接打满P99 超过 30 秒用户侧开始出现超时错误。数据库 DBA 问了一句话我现在还记得“这个脚本你们 review 过大表影响吗”我当时不知道怎么回答。AI 生成的脚本在逻辑上完全正确。它没有做错任何事。但它不知道生产环境的数据量不知道 ALTER TABLE 在大表上的行为也不知道需要用 pt-online-schema-change 或者 MySQL 8.0 的 INSTANT 算法来避免锁表。这类问题在小数据集开发环境里根本不会暴露AI 又无法感知生产数据规模。防坑心法建立数据库迁移的 checklistAI 生成脚本后必须走 checklist。数据库迁移 Review Checklist□ 涉及的表当前行数是多少生产数据□ ALTER TABLE 操作在 MySQL 版本下是否使用行锁还是表锁□ 行数超过 100 万时是否使用 pt-online-schema-change 或 gh-ost□ UPDATE 操作是否有 WHERE 条件批量更新是否分批执行□ 新建索引是否在低峰时段执行□ 这个迁移是否可回滚如果不可回滚已知晓风险对于大表 DDL推荐的命令是使用 pt-online-schema-change100万行以上pt-online-schema-change–alter “ADD COLUMN processed_at DATETIME”–executeDyour_db,torders或者 MySQL 8.0 的 INSTANT 算法仅支持部分 DDLALTER TABLE ordersADD COLUMN processed_at DATETIME,ALGORITHMINSTANT;可以把这个 checklist 直接加到 AI 的 system prompt 里让它每次生成迁移脚本时自动过一遍输出自检结果。不是所有问题都能被它发现但会好很多。这些坑背后的共同原因四个坑表面看原因各不相同但往深里追有一个共同点AI 没有你的系统上下文但它的输出看起来像是有。Spring 反射的坑是因为 AI 看不见运行时的依赖图只能看见静态引用图。上下文窗口的坑是因为 AI 对早期文件的记忆随着会话拉长而衰退但输出不会标注这部分我记得不清楚了。自作聪明优化的坑是因为 AI 对最佳实践的理解是训练数据驱动的它无法理解不要优化的真实意图。大表迁移的坑是因为 AI 对生产数据量一无所知它只能基于逻辑正确性给出建议。根据 Harness 2025 年的报告近 75% 的组织在使用 AI 生成代码之后经历过生产事故。这个数字不是说 AI 不好用是说现阶段 AI 辅助编程的人工监管密度被严重低估了。AI 能把重构速度提到原来的 3-5 倍。但对于遗留系统监管密度要同等提升不然速度越快