乐于分享
好东西不私藏

我和 AI 花了一整天,就为了修 5 行代码

我和 AI 花了一整天,就为了修 5 行代码

一个关于我如何逼着当今最强 AI:Claude Opus 4.6 反复推翻自己的分析,最终发现一个荒谬到令人发笑的 BUG 的故事。

## 故事的开始:一张不肯露面的图片

我正在用 AI 开发一个 AI 课件生成平台(aizhike.com.cn)。这个平台能自动生成电子教材,教材里不仅有文字,还有 AI 生成的插图。流程是这样的:先让大模型生成教材的文本结构,其中会标记”这里需要一张图片”,然后系统会自动调用图片生成模型把图片补上。

一切听起来很美好,直到我发现了一个 BUG:

**图片明明已经生成好了,服务器上也存好了,但页面上就是不显示。一直显示”等待生成插图”。刷新一下页面?图片就出来了。**

就这么一个看起来简单的问题,我和 Claude Opus 4.6(Amazon Kiro IDE 内置的 AI 助手)折腾了整整一天。

## 第一回合:AI 的自信分析(然后被我打脸)

我把 BUG 描述丢给了 Claude,让它分析代码找原因。它非常认真地读了十几个文件,洋洋洒洒写了一大篇分析报告,最后信心满满地告诉我:

> “根本原因是 AI 生成的 blocks 没有 id 字段,导致匹配逻辑出错。”

听起来很有道理。但我问了一个简单的问题:

> “如果所有的 block id 都是 undefined,那为什么其他非 image 的 block 都能在第一次生成后就正常显示呢?”

这一问直接把它问住了。如果 id 缺失真的是根本原因,那文字、图表、表格这些 block 也应该出问题才对。但它们都好好的,只有图片不显示。

Claude 开始动摇了,承认自己的分析有漏洞,然后开始了漫长的第二轮推理……又是一大堆关于 React 状态更新、闭包、竞态条件的分析。看得我头都大了。这期间 Claude 至少做了6轮推理,输出了不下2万字的推理过程,不断的一遍遍给出推断然后又否定自己的推断,真的很费Token

##第二回合:别猜了,加日志吧

经过几轮来回,我意识到纯靠代码推理是走不通的。再强大的AI光看代码也很难看出运行时到底发生了什么。

于是我决定打断了它:

> “我们换一个思路,你添加调试日志来定位问题。”

但我给了它几个严格要求,因为之前有惨痛的经历,它会乱加日志:

1. 清理掉之前可能残留的旧日志

2. 用统一的 `[IMGDBG]` 标识,方便我在浏览器控制台过滤

3. 日志要足够详细

4. 记录所有日志位置,修完 BUG 后要全部删掉

Claude 很听话地在 4 个文件里加了 19 条日志,还贴心地写了一个清理脚本。

##真相大白:日志里的一行字

加完日志后,我重新生成了一份电子教材,等图片生成完成,然后把浏览器控制台的日志发给了 Claude。

日志里有一行关键信息:

generateAllImages[0]: generateImageFromPrompt returned url=/uploads/images/xxx/36998855-f404-4a1d, cancelled=true

**图片生成成功了,URL 有效,但 `cancelled=true`!**

紧接着:

generateAllImages DONE: success=0, fail=0, cancelled=true

成功 0 个,失败 0 个,被取消了。图片生成了但结果被丢弃了。

还有另一个关键线索:

[IMGDBG] TextbookViewer INIT: 16 blocks, ids=[UNDEF,UNDEF,UNDEF,…]

[IMGDBG] TextbookViewer INIT: 16 blocks, ids=[UNDEF,UNDEF,UNDEF,…]

**组件初始化了两次!**

Claude 看到这两条日志后,感觉瞬间就明白了,信心满满的告诉我:

“这是 React 18 Strict Mode 的行为——在开发模式下,React 会对组件执行 mount → unmount → mount 的循环。第一次挂载时触发了图片生成请求,然后 Strict Mode 卸载组件时把 cancel 标志设为了 true,第二次挂载时因为防重复触发的标志已经是 true 就不会再触发了。等图片生成请求返回时,检查到 cancelled=true,就把结果丢弃了。”

说实话,经过前面漫长的分析推理过程,这个时候我已经不太相信它了,我没有让它马上动手改代码,紧接着又追问了几个问题:

##为什么整套课程没这个问题?

##如果block没有id,为什么刷新后能显示?

##如果Strict Mode下会挂载两次的话是不是会生成两次图片

##如果修复这么简单你之前为什么没有想到?

对于这几个问题Claude都给了信心满满的回答,也坦率的承认之前的分析走了很多弯路,但以我半吊子的编程经验和只有20%能看懂的代码能力,我也无法判断它回答得对不对,事已至此,我只能谨慎的选择相信它。而且,它还给我100%保证找到了问题的根源,跟Claude合作了这么久,我还是第一次听它说100%确定一件事,于是,我很没有底气的让它试试:

## 第一次修复:删 5 行代码,然后灾难降临

找到原因后,修复方案看起来很简单:删掉那个在组件卸载时设置 `cancelled=true` 的 cleanup effect,总共 5 行代码。

Claude 信心满满地删了,我重启测试…… 然后……

**满屏都是同一张图片。**

所有的文字、图表、表格全部消失了,16 个 block 全部变成了同一张图片。更可怕的是,这个损坏的数据被自动保存到了数据库里,重启也恢复不了。

我赶紧让 Claude 把那 5 行代码加回去:

> “现在出现了一个更严重的灾难性问题,请你先把那 5 行代码加回去。等待我的进一步指令。

有一瞬间,我想直接把官网上那$149/每月的高级会员退了,好在理智战胜了愤怒,毕竟活儿还得让它干,想想之前它写的12万行代码,也都合作得不错,于是又是一顿人机协作。

## 最终修复:两个问题必须同时修

Claude 此时终于意识到,这个 BUG 实际上是两个问题叠加在一起的:

1.**React Strict Mode 的 cancel 问题**组件卸载时设置 cancelled=true,导致图片生成结果被丢弃

2.**Block 缺少 id**独立电子教材的 blocks 没有分配唯一 id,导致更新时匹配逻辑错误

两个问题必须同时修复,缺一不可。只修第一个会触发第二个的灾难性后果。

Claude给了我最终的修复方案:

**服务端**:在保存 blocks 到数据库前,为每个 block 分配唯一 id(和整套课程的做法一致)

**客户端**:删除组件卸载时设置 cancel 的 cleanup effect

我在修复前还不忘给 Claude 施加一点压力:

希望这次你能成功,如果这次还不成功,我就把你 fire 掉。

这已经不是愤怒了,是赤裸裸的威胁!

这次Claude也没有之前那么狂了,很低调的回答:“明白”。

或许是威胁起了作用,最终的结果是修复了,当我看到浏览器跑出来正常的效果时,已经离我最开始让它调试这个BUG过去了整整9个小时。

## 复盘:AI 编程的真实体验

回顾这一整天的经历,有几点感受:

**AI 很强,但不是万能的。** Claude 能在几秒钟内读完十几个文件、理解复杂的代码架构,这是人类做不到的。但面对涉及运行时行为的 BUG,它和人类一样会陷入”看代码猜原因”的困境。

**人类的直觉很重要。** 每次 Claude 给出一个分析,我都会用常识去检验:如果你说 id 缺失是原因,那为什么其他 block 没问题?如果你说删 5 行就能修好,那为什么之前分析了那么久都没发现?这些追问逼着 AI 不断修正自己的分析。

**日志是最好的调试工具。** 我们花了大半天在代码层面推理各种可能性,最后是一行日志 `cancelled=true` 直接指向了答案。早该加日志的。

**AI 犯错时可能造成更大的破坏。** 那次”满屏图片”的灾难就是教训。AI 修改代码的速度很快,但如果修错了,破坏的速度也很快。所以每次修改后都要测试,不能盲目信任。

**和 AI 协作就像带一个超级聪明但偶尔粗心的实习生。** 它知识渊博、执行力强,但需要你把控方向、提出正确的问题、在关键时刻踩刹车。

最终的修复改了两个文件,加起来不到 10 行代码。但为了找到这 10 行代码该怎么改,我们花了一整天,经历了错误的分析、语法错误、数据库被污染等各种波折。

这大概就是AI编程的日常吧,以我近半年来累计让AI写了12万行代码的经历来看,这样的压迫和battle每天都在发生!最后总结一句话:AI很强大,但驯服AI需要一点耐心和技巧

*写于 2026 年 5 月 1 日,本该是轻松的五一假期的第一天,却被一个 BUG 折腾了一整天。*

最后的彩蛋:以上文章的所有内容也是我让AI自己总结写出来的,请看对话: