AI 改了三轮还没修好?多半是你跳过了复现这一步
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
大多数「AI 修不好这个 bug」的抱怨,根子不在模型能力,而在于你把一个还没复现稳定的问题直接丢给它改代码。 问题现象是间歇性的,你贴了一段报错,它给出一个改法,你跑一遍没复现,就当修好了;下次又冒出来,你再贴一次,它换个改法。三轮之后代码里多了三处互相打架的补丁,原始 bug 一个没死。这不是模型笨,是流程反了——调试的顺序从来是复现、定位、修改、验收,AI 只是把其中几步变快了,没有取消任何一步。
这篇讲的是「调试」这个工程环节本身怎么和 AI 配合:哪一步它能真正省你时间,哪一步必须你来拍板,每一步做完拿什么动作验收。站内另外两篇是从具体场景切的——CI 挂了但 AI 修不动 讲的是流水线环境这一类特定战场,本地跑得好线上就挂 讲的是环境差异这一类特定成因;本篇不重复那些场景,只讲贯穿所有场景的顺序和分工,你可以把它当成前面两篇的上层方法。
一、动手之前先分清:你手里的是哪一类「不对」
「不对」至少有四种,处理路径完全不同,混在一起谈就会白折腾。
**第一类是确定性错误。**同样的输入、同样的代码、同样的环境,每次都错,报错信息稳定。这类最好办,也是 AI 最擅长的一类,因为信息完备:堆栈、输入、期望输出都能一次给全。
**第二类是条件性错误。**只有满足某个条件才错——特定数据、特定并发度、特定时区、特定文件编码。它看起来像偶发,其实是确定性的,只是触发条件你还没找到。绝大多数「玄学 bug」属于这一类,找到条件它就退化成第一类。
**第三类是环境性错误。**代码逻辑没问题,是运行环境不一样:依赖版本、环境变量、网络出口、证书信任链、文件系统大小写敏感性。这类的特征是「换台机器就好了」或者「只有某个人遇到」。
**第四类是认知性错误。**代码行为完全正确,是你或者需求方对「正确」的定义错了。这类你让 AI 改多少轮都没用,因为它只会照着你说的错误期望去改,改完还会很自信地告诉你修好了。
判断自己在哪一类,只需要问三个问题:换个人、换台机器还错吗?改变输入还错吗?如果它按现在这样行为,到底哪里不符合需求?三个问题答完,类别基本定了。这一步 AI 帮不上,因为它没有你的环境和上下文,只能你来。
二、复现:把偶发变成必现,这是 AI 能上场的前提
复现的目标不是「让 bug 再出现一次」,而是做出一个最小的、可重复运行的触发器。它可以是一条 curl、一个单元测试、一段十行的脚本。没有这个东西,后面所有步骤都是猜。
做最小复现有一套很笨但很有效的手法:从能触发的完整场景开始,每次砍掉一半的输入或一半的调用链,看还错不错。错就继续砍,不错就把刚砍掉的那半加回来再砍另一半。二分几轮,剩下的东西通常不到原来的十分之一。
AI 在这一步能帮的是体力活:把你口述的场景写成一个可运行的测试文件、把一段冗长的日志按时间轴整理出关键事件、把手工点击的操作翻译成脚本。它帮不上的是「猜触发条件」——它不知道你的数据长什么样,凭空猜出来的条件多半是幻觉,你验证这些猜测的时间比自己二分还长。
关于时间维度的偶发,有个实用做法是先把不确定因素固定住再复现:随机种子固定、系统时间固定、并发度降到 1、外部依赖换成录制好的假响应。固定完还错,说明和这些无关;固定完不错,说明就跟被你固定的那个东西有关,范围一下子收窄了。
代码变更引入的回归有现成工具,别手工翻提交记录:
git bisect start
git bisect bad HEAD
git bisect good <已知正常的提交>
# 之后每停在一个提交,就跑一遍最小复现脚本,再按结果反馈
git bisect good # 复现不出来用这条;能复现用 git bisect bad
# 反复若干轮,git 报出第一个坏提交后收尾,回到原来的分支
git bisect reset
如果最小复现能写成一条退出码为 0/1 的命令,直接 git bisect run ./repro.sh 让它自动跑完。这一步的产出是一个具体提交,比任何模型的推测都硬。
三、定位:先归因,再决定要不要让 AI 动手
有了稳定复现,才轮到定位。下面这张表是我常用的归因起点,按现象查大概率成因,重点在第三列——怎么验证。没验证过的成因只是假设,拿假设去改代码就是赌博。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 本地正常,服务器上报错 | 依赖版本或环境变量不一致 | 两边分别打印依赖树和关键环境变量,逐项对比 | 锁版本、把配置项显式化,别靠默认值 |
| 接口返回 401/403 | 凭据未加载或权限范围不对 | 用 curl -v 带同一份凭据直连,绕过应用层 | 凭据能直连成功就查应用层加载逻辑,失败就查权限配置 |
| 接口偶发 429 | 触发了服务端限流 | 记录请求时间戳,看错误是否集中在突发窗口 | 加退避重试与并发上限,各家限流规则不同且会调整,以官方最新说明为准 |
| 请求偶发 ETIMEDOUT / ECONNRESET | 网络链路或连接池,不是业务代码 | 换出口重试、把连接池大小调成 1 再试 | 归到网络层处理,别让 AI 去改业务代码 |
| 提示证书链校验失败 | 中间设备换发了证书,或本机根证书库不全 | 用 openssl s_client -connect 主机:443 看实际返回的证书链,签发者是不是你预期的那家 | 签发者被换过就把对应根证书导入信任库,链本身不全就补中间证书,别关掉校验了事 |
| 服务端 500 但日志没有堆栈 | 异常被吞、日志级别过滤掉了 | 临时把日志级别调到最低,或在捕获处补打印 | 先让错误可见,再谈修 |
| 输出中文变成问号或乱码 | 编码或终端字符集不一致 | 直接看文件字节:python -c "print(open('f.txt','rb').read()[:64])" | 统一为 UTF-8,输入输出两端都要改 |
| 测试全绿但线上出错 | 测试用的假数据掩盖了真实分支 | 用生产结构的样本数据重跑同一份测试 | 补真实结构的用例,别在断言上放水 |
| 改完这次好了,换个输入又坏 | 补的是症状不是成因 | 把两次输入的差异拎出来,看是否共用一条路径 | 回退补丁,回到成因层重做 |
用法是:先在表里找到你的行,做完第三列的验证动作,再把验证结果连同最小复现一起交给 AI。这时候它拿到的是事实,不是你的猜测,给出的修改质量会完全不同。
顺带一提,如果你已经陷入「改了没用、再改还是没用」的循环,说明大概率跳过了归因这一步,AI 修不好时的思维循环怎么打破 那篇专门讲这个状态怎么脱身。
四、改:把边界划清楚,让 AI 只做它擅长的那部分
到了改这一步,最重要的不是提问技巧,而是边界。
明确交给 AI 做的:在你指定的文件、指定的函数里做局部修改;写覆盖这个 bug 的回归测试;把一段可疑逻辑的分支穷举出来让你核对;解释某段陌生代码在做什么。
明确留给自己定的:这个改动会影响哪些调用方;要不要改数据结构;错误该在哪一层处理;性能和正确性之间的取舍;这个补丁和历史上那些补丁会不会冲突。这些依赖的是整体设计意图,模型看不到你团队的历史决策。
具体操作上有几个硬约束值得固定下来。改动前先隔离出干净的工作区,别在一堆未提交的修改里让它动手:
git stash -u # 把无关的未提交改动先收起来,工作区归零
git switch -c fix/xxx # 再开一个只放本次修复的分支
给指令时限定改动范围,比如只允许动某一个文件;改完先看 git diff --stat,超出预期范围的立刻打回。这一点被低估得厉害,AI 改动范围失控 那篇有更细的控制手法。
验收动作必须是可执行的,不能是「看起来对」:
- 最小复现脚本从「必错」变成「必对」。
- 把复现条件反着改一遍(换输入、换并发度、换时区),确认没有引入新的错法。
- 全量测试跑一遍,红了的用例一个都不许绕过。
git diff逐行读一遍,读不懂的行让它解释,解释不通就删掉。- 回滚一次补丁,确认 bug 会重新出现——这一步能验证你修的确实是成因,不是巧合。
第 5 条经常被省略,但它是唯一能证明因果关系的动作。改完就好了不代表是你改好的。
五、什么时候该停手:止损点和回滚点
调试最贵的成本不是没修好,是在错误的方向上持续投入。给自己设几条硬线,到了就停:
**第一条线:同一个方向改了三轮还没有任何进展。**注意「进展」的定义不是「有新的改法」,而是「对成因的认识比上一轮更具体」。如果三轮下来你对成因的描述还是同一句话,说明方向错了,回到第三节重新归因。
**第二条线:补丁开始互相依赖。**当你需要保留前两轮的改动,第三轮才生效,基本可以判定前面改的是症状。这时候正确动作是全部回滚到干净状态,从最小复现重来,而不是在补丁上叠补丁。
**第三条线:改动扩散到了你没打算碰的模块。**范围一旦外溢,风险就不再可控。回滚,把问题拆小,一次只解决一个。
**第四条线:线上还在出血。**故障处理和 bug 修复是两件事。线上正在影响用户的时候,第一动作永远是恢复服务——回滚版本、切流量、关掉出问题的开关,然后再慢慢查根因。用 AI 在故障现场直接改生产代码是最糟糕的选项,你既没有复现也没有验收。
回滚本身也要有准备。上线前记下当前版本号和回滚命令,别等出事了才去找。代码层面 git revert <提交> 比 git reset 安全,因为它保留历史、可追溯;配置和数据层面的变更要单独准备回退方案,尤其是不可逆的数据迁移。
还有一种「换条路」的情况值得单说:如果查到最后发现是第三方库的已知缺陷、或者依赖的服务本身不稳定,继续往下修性价比很低。这时候合理的做法是绕过——加一层适配、换个实现、或者把这条路径降级掉,把根因问题记进技术债清单排期处理,而不是死磕。
如果你用的是有区域限制的海外工具或模型,还要多考虑一层:这类服务对中国大陆的可用范围以官方最新说明为准,链路本身就可能不通或不稳定,表现出来就是超时、断连、握手失败,和你的业务代码毫无关系。排查顺序上,先用一条最简单的探测请求确认链路和凭据是否正常,链路这一层排除不掉就别往业务代码里找,不然你会花半天去修一个根本不存在的 bug。至于访问方式本身,请以服务条款和所在地法规为准,这不属于调试范畴。
六、避坑清单:每条都写清楚为什么会踩
**坑一:拿着不完整的报错直接问。**为什么会踩——报错信息在终端里刷得很长,你顺手截了最后几行,而真正的原因往往在第一条异常里,后面全是包装。怎么避——贴堆栈时从最内层的原始异常开始贴,同时附上触发它的那次输入。
**坑二:把猜测当事实描述给 AI。**为什么会踩——你说「应该是缓存没失效」,模型会顺着这个前提往下推,推得又快又像那么回事,你就更确信了。怎么避——描述时严格区分「我观察到」和「我怀疑」,把怀疑单独列出来并且明确说这只是假设,让它给出验证方法而不是直接改。
**坑三:一次让它改多个文件。**为什么会踩——单次改动大看起来效率高,但一旦结果不对,你无法判断是哪一处引起的,只能整体回滚重来。怎么避——一次一个关注点,改完就验收,验收过了再提交,让每个提交都能独立回滚。
**坑四:删掉挡路的校验。**为什么会踩——证书校验失败、类型检查报错、断言不通过,把它们关掉之后程序确实跑通了,看上去像解决了。怎么避——校验失败说明前提不成立,关掉只是让问题延后到更难查的地方爆发。正确动作是让前提成立,比如导入正确的根证书、修正数据结构。
**坑五:在测试断言上放水。**为什么会踩——测试红了,改断言比改代码快得多,尤其当你不确定预期值时。怎么避——先想清楚正确值应该是什么、依据是什么,写不出依据说明你还没定位完。改断言必须写清楚为什么原来的期望是错的。
**坑六:修完不写回归测试。**为什么会踩——bug 已经不出现了,写测试感觉是在给已经过去的事情做功课。怎么避——把最小复现脚本原地转成一个测试用例,成本几乎为零,而它是唯一能防止同一个 bug 再回来的东西。
**坑七:让长对话越滚越脏。**为什么会踩——一路排查下来,对话里堆了大量被否定的假设和废弃的改法,模型会拿这些当有效上下文继续推理,越到后面越跑偏。怎么避——归因方向一变就开新会话,把已确认的事实(最小复现、验证结论、当前代码状态)重新整理一遍带过去,别指望它自己忘掉错的。
**坑八:把「它说修好了」当成结果。**为什么会踩——模型的表述通常很笃定,读起来比你自己的判断更有底气。怎么避——只认可执行的证据:测试从红到绿、复现脚本从错到对、回滚后 bug 重现。语言不是证据。
收尾:一份可以贴在显示器上的自检清单
调试这件事上,AI 提升的是每一步的执行速度,不是取消步骤。你把顺序守住,它是很好的助手;顺序一乱,它就是个加速你走错路的工具。
修 bug 之前对着过一遍:
- 我能用一条命令稳定复现吗?不能就先做复现。
- 这属于确定性、条件性、环境性还是认知性问题?
- 我对成因的判断,做过什么验证动作?验证结果是什么?
- 这次改动允许碰哪些文件?
git diff --stat和预期一致吗? - 验收怎么做?回滚一次补丁,bug 会重现吗?
- 回归测试补了吗?
- 已经改了几轮?对成因的认识有没有比上一轮更具体?
- 如果现在必须停手,回滚命令是什么?
最后一条最容易被忽略,也最救命。