
前两天,我们危废系统出了个怪事:库存总数偶尔对不上。不是每次都错,大概百分之五的概率,而且只在上午批量导入的高峰期冒出来。我的第一反应和多数人一样——把报错日志往 AI 对话框里一贴,让它帮我看看哪里出了问题。
它两分钟甩给我一段分析,斩钉截铁:这是事务隔离级别的问题,把 MySQL 的隔离级别调一下就好。我信了,花两小时改隔离级别、写验证脚本,结果问题还在。后来才发现,根因根本不在隔离级别,而是两个入库请求并发打进来,都读了旧库存再各自累加写回,后写的把先写的覆盖了。
那次之后我换了个用法:把 AI 当成一个盲眼的资深工程师。它看不见你的屏幕,也看不到你的日志,甚至不知道你刚改了哪个文件。你得当它的眼睛,把现场证据喂足;它负责列假设、找规律。但所有假设,你都得自己一个个证伪,绝不能直接照单全收。
这篇就讲这套打法,顺便聊聊我踩过的坑。
为什么 AI 在调 Bug 时会瞎猜
先得说清楚 AI 的短板:它对上下文缺失特别敏感。你只丢给它一句报错,没给复现步骤、输入数据、运行环境,它不会老实说“信息不够,我判断不了”,而是用一堆看起来合理的理由把坑填平。
最典型的幻觉是:日志里出现关键字 A,它就顺着说八成是 A 引起的。其实根因是 B。你一旦顺着 A 查下去,只会越查越偏。
所以我调 Bug 时,对 AI 输出的判断标准从来不是“信不信”,而是两件事:它说的原因能不能被验证?能不能被排除?既验证不了也排除不了的输出,再像那么回事都不能当真。
第一步:当它的眼睛,递一份证据包
AI 看不见现场,你得替它看。我每次让它帮忙定位,先整理一份证据包,缺一项都不发:
- 现象:预期是什么,实际是什么,出现的频率(偶发还是必现)
- 复现步骤:怎么操作会触发,触发不了也要说清楚
- 相关日志:贴带请求 ID 和时间戳的真实 日志,别只贴一句报错
- 相关代码:出问题的那段,连同它调用的上下游
- 环境:什么版本、什么隔离级别、压测还是生产
- 已排查线索:你查过 A 和 B 了,请它给新方向,而不是重复你已经做过的
这条最容易被忽视,但也最值钱。很多人只贴一句“测试有时失败”,AI 只能泛泛而谈。你把整段 CI 输出、带堆栈的原始日志直接贴进去,它立刻能锁定到环境变量没配、时区不对这类具体原因。我最大的体会是:你给 AI 的抽象描述,会丢掉它定位最需要的细节;原始数据才是它的粮食。

第二步:别要答案,要可验证的假设
这步是关键,也是我和多数人用法不一样的地方。
大多数人问:这段代码哪错了,帮我修。AI 会直接动手改。改完你不知道它为什么这么改,更不知道改得对不对。
我的问法是:基于上面的证据包,给出 3 到 5 个可能的原因。每个原因,单独给我一个能验证的方法。先别写任何修复代码。
这就把任务从“让 AI 证明哪个对”,变成了“让 AI 给我一组能逐个排除的实验”。证伪比证实便宜。你不用说服自己这个假设成立,只要跑一个测试证明它不成立就行。
回到库存那个例子,AI 给了四个假设:并发写回旧值、事务隔离、缓存和数据库不一致、状态机竞态。我没急着改,先问每个怎么验证。然后按验证成本排序:并发写回只需加一把锁压测,最便宜,先证这个。
真实例子:并发入库偶发库存偏差
我们入库接口的流程大概是:读当前库存,加本次入库量,写回。平时没问题,批量导入时两个请求几乎同时进来,都读了 100,一个加 5 写成 105,另一个加 3 也基于 100 写成 103,后写的把先写的覆盖了,库存少了 2。
这属于典型的竞态,复现率跟并发量挂钩,所以只在高峰期露脸。
我用证据包喂给 AI 后,它列的四个假设里,我把并发写回排第一验证。在入库方法上加了同步锁,压测模拟并发,问题消失。根因确认。
修复很简单,把读和写在数据库层面锁成原子:
// 修复前:先查后写,并发下旧值被覆盖
int current = inventoryMapper.selectStock(id);
inventoryMapper.updateStock(id, current + inboundQty);
// 修复后:用数据库行锁,读和写在同一个事务里原子完成
@Transactional
public void inbound(Long id, int inboundQty) {
int current = inventoryMapper.selectStockForUpdate(id); // SELECT ... FOR UPDATE
inventoryMapper.updateStock(id, current + inboundQty);
}
这里有个容易踩的坑:光加 Java 层的 synchronized 不够,多实例部署时锁不住别的节点。得落到数据库的行锁,或者分布式锁。这点 AI 一开始没提醒,是我根据部署架构自己补上的。
第三步:先埋点,别急着改
这是我用得最顺、也最被低估的一招。
定位不清的时候,别让 AI 直接改代码。让它先在可疑路径上加结构化日志:输入值、中间态、返回值,逻辑一个字不动。跑一遍,把日志喂回去,问它这排除了假设 X 吗。
比起听 AI 猜,埋点让你看的是真实运行。我的问题排查基本靠日志输出定位,这招和我的习惯一拍即合。好处是:即使假设错了,你也没有动业务逻辑,顶多删几行日志,干干净净。
举个场景:你怀疑某个状态判断有竞态,让 AI 在判断前后加线程 ID 日志。跑完一看,同一个请求里出现了两个线程 ID 先后进入,竞态就实锤了。比让 AI 改完你再去猜强太多。
第四步:一次只动一个变量
AI 给修复建议时,常一次甩五条:加锁、加日志、改事务、换写法、顺手优化命名。你要是全堆上去,好了不知道哪条生效,坏了不知道哪条惹祸。
我的规矩:一次只验证或改一个,测完再上下一个。慢就是快。改完一个,跑测试、看日志,确认这一刀的作用,再动下一刀。代码里别留死实验,一条改完没用,先撤掉再试下一条,别让它们叠在一起。
第五步:让 AI 补一个回归测试
定位完,让 AI 写一个能复现这个 Bug 的最小测试。修完它变绿,以后它就是看门狗。
这一步和上一期写的行为快照不一样,但思路相通:快照锁的是老代码当下的行为,回归测试 锁的是这个 Bug 修好了就别再犯。我们那个并发问题,我让 AI 写了个多线程同时入库的测试,现在每次提交都会跑,谁再把读和写拆开,它第一个红。
@Test
void 并发入库不会少算库存() throws Exception {
int threadCount = 20;
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
pool.submit(() -> inboundService.inbound(1L, 1));
}
pool.shutdown();
pool.awaitTermination(10, TimeUnit.SECONDS);
assertEquals(20, inventoryMapper.selectStock(1L)); // 20 个线程各加 1,必须正好 20
}
三个别踩的坑
坑一:直接信 AI 的结论。它说是 A,你没验证就改,结果 A 是错的。白改一趟还顺手引入新 bug。AI 列假设快,但哪个成立必须你拍板。
坑二:输入太模糊。只贴一句报错,AI 用看起来像补信息,输出比没有还危险,因为它会把你往错路上带。证据包给足,模糊输入是幻觉的温床。
坑三:让 AI 直接改,不埋点。等于蒙眼换零件,换完你还是不知道坏在哪。先埋日志看真实运行,再动手。
收尾清单
下次让 AI 帮你调 Bug,照这个排:
- 给 AI 的证据包齐了(现象、复现、日志、代码、环境、已排查)?
- 让它列假设和验证方法,而不是给结论?
- 假设按验证成本排了序,先证伪最便宜的?
- 先埋了日志看真实运行,没直接改代码?
- 一次只动一个变量,动完即测?
- 修复后让 AI 补了回归测试?
AI 调 Bug 省的是你列假设、扫日志、想边界的时间,省不掉你判断哪个假设成立的责任。把证据给足,让它飞,但落地前你签字。
下期我打算写:怎么把 AI 定位 Bug 这套证据包,沉淀成团队排障的标准动作,让新人也能照着排。