同一个 bug 让 AI 改了五轮还没好:怎么识别它在原地打转并强行换轨

2026-07-28

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

**改到第五轮还没好,多数人第一反应是模型不行、该换个更强的模型,这几乎总是归错了因。**真正卡住的地方通常在你这一侧:你和它共享了同一个错误前提,而且这几轮里没有任何新的可观测事实进来。模型每轮都在同一份(错的)上下文上重新演绎,输出自然收敛到同一族答案。换个模型只是换了演绎风格,前提没动,第六轮照样不好。

本篇只解决一件事:多轮之后,你怎么在三五分钟内判断它已经在原地打转,然后强行换轨。重试与退避策略本身在 [Agent 执行失败后怎么恢复](/learn/agent-shibai-chongshi/) 里讲过,模型凭空编造接口与参数的识别方法在 [大模型为什么会「胡说八道」?幻觉是什么、怎么](/learn/damoxing-huanjue/) 里讲过,这里都不重复;本篇的位置在它们之后——重试也做了、幻觉也排了,问题还在,那就该谈止损和换轨。

一、死循环的定义:不看轮数,看信息增量

「改了五轮」这个说法本身没有判别力。有的问题第七轮才好,过程一直在推进;有的问题第二轮就已经死了,只是双方都还在客气地来回。判据不是轮数,而是这一轮有没有产生新的可观测事实

什么算新事实:一条此前没见过的报错、一个打印出来的实际值、一次从失败翻绿的最小复现、一个在仓库里真实存在且被你确认过的符号名、一段两边环境的差异对照。什么不算:更长的解释、更自信的语气、一个「应该是因为……」的猜测、把同一处代码换一种写法。

三个红灯,出现任意一个就要停下来重新分因:

  • diff 互为逆操作。这一轮把 A 改成 B,下一轮又把 B 改回 A,或者在两种写法之间来回。还没提交的那层用 git diff 看,已经提交的几轮用 git log -p -6 -- <出问题的文件> 看,一眼就能看出来(--oneline 只给标题,看不到改动内容,别指望它)。这说明双方都没有判定成败的标准,只能猜。
  • 报错文本一字不变。改了代码、跑了、错还长得一模一样。这时第一嫌疑不该是逻辑没修对,而是你改的代码根本没被执行——先证伪这一条,再谈逻辑。
  • 解释越来越长,验证步骤越来越少。第一轮它说「加个日志看看」,第五轮它直接给你一段重构方案。这是上下文被自己前几轮的猜测污染后的典型表现:越往后,它引用的「事实」里自己编的成分越多。

第三点值得单独说一句。多轮对话里,模型上一轮的猜测会作为「已知」进入下一轮,几轮之后,整段上下文里混着大量未经验证的推断,而它无法区分哪些是你给的、哪些是它自己说的。这是机制层面的问题,不是态度问题,靠追问「你确定吗」解决不了。要解决只能截断——开新会话,只带经过验证的事实进去。上下文该怎么裁、留什么,可以看 [Agent 的上下文怎么管](/learn/agent-shangxiawen-guanli/)。

二、五分钟分因:从现象反推成因

在动手改之前,先把问题落到某一层。同一个「跑不通」,可能落在五层里的任意一层,而不同层的处置动作完全不同:

  1. 需求层:你要的行为和你描述的行为不是一回事。
  2. 代码层:真的是逻辑或边界写错了。
  3. 构建与执行层:改动没生效——缓存、构建产物、进程没重启、改错了目录或环境。
  4. 环境与网络层:环境变量、时区、路径大小写敏感、证书链、代理、超时。
  5. 账号与配额层:密钥、权限、限流。

按我自己带团队排查的手感,多轮修不好的案例里,落在第 2 层(真代码 bug)的并不占多数——这不是统计结论,只是提醒你别默认「肯定是代码写错了」,第 3 层(改动没生效)和第 5 层(账号配额)被漏掉的次数远比想象中多。下面这张表是我实际用的分因表,先对现象,再按「怎么验证」那一列做一次,不要跳过验证直接执行处置动作。

现象大概率成因怎么验证处置动作
每轮都在同几行反复横跳,A 改 B、B 又改回 A没有判定成败的标准,双方都在猜git log -p -6 -- <文件> 看几次改动是否互为逆操作;未提交的用 git diff停止改业务代码,先补一个能红能绿的最小复现
报错文本一字不变改动没被执行:缓存、构建产物、进程未重启、改错目录或环境在报错路径前插一行显式输出,确认它真的打印出来先证明代码生效,再谈逻辑对错
每轮报错都变,一直换新错一次改了多处,范围太大,错误互相掩盖回到基线,只保留一处改动重跑缩范围,单变量推进
解释合理,但引用的函数或参数你在仓库里搜不到补全了不存在的接口仓库内 git grep -n "符号名";依赖目录通常被 .gitignore 挡着,git grep 搜不到,得用 git grep -n --no-index "符号名"rg -n --no-ignore "符号名"要求先给出来源文件路径与行号,搜不到就不采纳
只在 CI 或同事机器上失败,本地一切正常环境差异:环境变量、时区、路径大小写敏感、依赖版本两边都跑 env | sort 和版本命令,逐行比对把差异固化成配置或启动检查,别去改业务逻辑
返回 429,或 401/403大概率不在业务逻辑,而在配额、密钥、权限、网关层绕开程序,用 curl -i 带同一份密钥单独打一次同一个接口,看状态码是否复现curl 也复现:转账号侧(密钥、配额、权限),业务逻辑一行别动;curl 正常而程序失败:问题在请求构造(读错了哪个环境变量、少了哪个头),也不在业务逻辑里
提示证书链校验失败,或 ETIMEDOUT / ECONNRESET网络与中间设备:自签证书、代理、连接被重置curl -v 看握手过程,换一条网络再试交给网络侧处理,业务代码不动
返回 500,但换个时间点又好了服务端不稳定或下游依赖抖动,不在你这边同一请求隔几分钟重放三次,记录成功率补超时与退避重试(前提是这个请求幂等,非幂等的按 [Agent 执行失败后怎么恢复](/learn/agent-shibai-chongshi/) 的口径先做去重再谈重试),不要改逻辑去「适配」偶发失败

表里 429 和证书/超时这两行是最容易白折腾的类型。看到 429 就让 AI 改代码,它大概会给你一套看起来很专业的重试与队列改造,而真正的问题可能只是密钥用错了环境。各家的配额规则与限流口径不同、也会调整,具体以官方最新说明为准,但排查顺序是通用的:先用 curl 单打确认是账号侧还是代码侧,再决定谁来处理。

三、四个换轨动作,按顺序做

确认在打转之后,别再「换一种说法再问一遍」。按下面四步走,每一步都是硬动作,做完才进下一步。

动作一:换假设——把当前假设写成一句可否证的话

打转的本质是双方共用一个没被写下来的假设。所以第一步是把它写出来,写成一句能被证伪的话,例如「失败是因为配置在运行时没被加载」。写成这样的句子之后,验证方法自然就有了:打印加载到的配置值。

写不出这样一句话,说明你还不知道自己在修什么,这时候任何一轮改动都是抽奖。写出来了,就立刻主动列第二假设和第三假设,哪怕它们看起来不太可能。只有一个假设的排查等于没有排查——它没有分叉,只会越挖越深。

动作二:缩范围——回到基线,单变量推进

五轮之后,工作区往往已经堆了一层层互相叠加的改动,谁在起作用、谁在捣乱都分不清了。这时候最省时间的操作是回到干净基线:

git stash push -u -m "5轮乱改先存着"
git status            # 确认工作区是干净的

先确认基线上问题确实存在(如果基线上问题不存在,那恭喜,问题是这几轮改出来的,直接放弃这堆改动)。然后一次只加一处改动,跑一次,记一次结果。

如果问题是「以前好的,现在坏了」,别猜,直接二分:

git bisect start
git bisect bad                 # 当前这个提交是坏的
git bisect good <已知好的提>
# 每次它切到一个提交,你跑一次复现,然后
git bisect good   # 或 git bisect bad
git bisect reset               # 结束后务必重置

二分是这类问题的性价比之王,几步就能把范围从几十个提交收到一个。它也顺手补上了对话补不上的那块信息:光读你贴过去的代码片段,谁也推不出哪次提交引入了行为变化——这个结论只能靠真的跑一遍二分拿到。如果你的 AI 工具有终端执行权限,让它代跑这几条命令没问题,但每一步「这次算好还是算坏」的判定得你给,因为判定依据是你眼里的复现结果,不是它的推断。

动作三:要证据——先给来源,再改代码

换轨之后,把交付物从「改好的代码」换成「支撑判断的证据」。具体做法:让它先输出三样东西——错误发生的确切位置(文件与行)、它认为的成因、以及能验证这个成因的最小观测(打印哪个值、跑哪条命令)。这三样你看过、认可了,才允许它动代码。

顺序反过来,你就永远在验收猜测。同一件事在计划态下做效果更好:先出一份可读的排查计划,你改掉其中不成立的假设,再执行。这类先计划后执行的用法可以参考 [Claude Code Plan 模式](/learn/claude-code-plan-mode/)。

补一个能一次性给足证据的土办法:把复现压成一条可粘贴的命令。Python 生态里通常是这样一行,能同时暴露版本、路径和实际值:

python -c "import sys,json,os;print(sys.version);print(sys.executable);print(json.dumps({k:v for k,v in os.environ.items() if k.startswith('APP_')},ensure_ascii=False))"

拿到这类输出之后再讨论,比来回描述十轮都管用。

动作四:人接手——你自己读那 20 行

到这一步,请你亲手做一件 AI 替不了的事:把最可疑的那 20 行代码从头读一遍,边读边问「这一行的实际输入是什么」。多轮打转的问题,最后往往败在一个双方都以为不用验证的事实上——某个变量在这里其实是空字符串、某个分支根本没进、某个配置读的是另一个文件。

这不是复古情怀。人读代码时会怀疑前提,模型在给定上下文里倾向于接受前提,这是两种不同的行为模式。哪些环节必须留人、怎么设卡,[Agent 里的人工介入怎么设计](/learn/agent-human-in-loop/) 讲得更细。

四、什么情况下别再折腾

止损这件事需要事先约好,不然在情绪里没人下得了决心。我用三条硬线。

第一条:时间盒。 单个 bug 的 AI 协作排查给一个固定时长,比如两个番茄钟。到点无论进展如何都停下来,做一次分因复盘:现在落在哪一层,有没有新事实。停下来的意义不是放弃,而是强制从「继续试」切回「重新想」。

第二条:无信息增量两轮即停。 连续两轮没有新的可观测事实,立刻停止让 AI 改代码。这条比时间盒更严格,也更管用。判断标准就是第一节那三个红灯。

第三条:回滚优先于修复。 如果这个 bug 挡着上线或挡着别人干活,先回滚到已知可用的状态,再慢慢查。回滚点要提前准备好,别等出事才找:

git switch -c fix/issue-xxx            # 排查另开分支,主干保持可用
git revert <引入问题的提>             # 已推送的提交用 revert,不要改写历史

已经推送到共享分支的提交,用 revert 生成一个反向提交,不要 reset --hard 之后强推——那会把同事的本地历史搅乱,制造出比原 bug 更难查的问题。

什么时候该「换条路」而不是继续修:

  • 这个 bug 出在一个你并不需要的能力上。删掉这个能力比修它便宜,那就删。
  • 修它要动的那块代码,本身已经没人看得懂了。这时候正确的动作是重写那一小块,而不是继续往上打补丁。多轮打转往往是技术债的报警器:一处代码反复出问题、每次修都牵连别处,说明它的边界已经烂了。
  • 问题落在第 4、5 层(环境、配额、网络)却一直在第 2 层修。这时候换轨不是换方法,是换人:交给管账号或管网络的人,你继续修代码只会浪费两边的时间。
  • 有一条绕开它的实现路径,代价可接受。工程上绕开一个坑常常比填平它更理性,前提是把这个决定和原因记下来,别让它变成三个月后的新谜题。

五、避坑清单

换更强的模型当作首选动作。 为什么会踩:模型能力是唯一能一键切换的变量,人在受挫时会抓最容易的那个。怎么避:把「换模型」放到分因之后。前提错的时候,换模型不改变结论。真要换,也要在一个干净会话里、只带经过验证的事实重问一遍——你换掉的其实是被污染的上下文,不是模型。

在同一个会话里一直追问。 为什么会踩:对话看起来连续,你舍不得丢掉「它已经了解了背景」这个错觉。实际上它了解的背景里混着自己前几轮的猜测。怎么避:一旦出现红灯就开新会话,手工写三到五条经过验证的事实带进去,其余全部不带。

让它一次改多个文件。 为什么会踩:一次多改看着效率高,模型也乐意给整套方案。但多处改动会互相掩盖,报错一直变、你却无法归因。怎么避:约定单变量,一次一处;用 git diff --stat 检查这一轮的改动面,超过预期就退回去。

接受它引用的接口不做核实。 为什么会踩:它给出的函数名、参数名、配置项名往往在命名习惯上完全合理,读起来毫无破绽。怎么避:任何你没亲眼见过的符号,一律搜一次再采纳——仓库内 git grep -n "符号名",装在 node_modules、site-packages 这类被忽略目录里的依赖源码得改用 git grep -n --no-indexrg -n --no-ignore(默认的 git grep 只看已跟踪文件,搜不到就给你一个假的「不存在」)。两种都搜不到,就当这个符号不存在。

把偶发失败当稳定 bug 修。 为什么会踩:一次失败就开始归因,是排查里最常见的抢跑。怎么避:先重放三次记录成功率。成功率不是 0 也不是 1,说明这是稳定性问题(超时、并发、下游抖动),处置方式是补超时和退避重试,不是改逻辑。要重试就先确认这次调用重复执行不会造成二次扣款、二次下单这类后果,不幂等的先补幂等键——这条口径和重试细节在 [Agent 执行失败后怎么恢复](/learn/agent-shibai-chongshi/) 里,本篇不重复。

没有最小复现就开始改。 为什么会踩:写复现要花十几分钟,感觉是「没在解决问题」。怎么避:把它当成投资——有了能红能绿的复现,判定每轮改动是否有效的成本降到零,后面每一轮都在省时间。没有复现,你和 AI 都只是在换写法。

用中转服务但不把它当变量。 为什么会踩:部分海外厂商的编程工具与模型对中国大陆的可用性有区域限制,各家口径不同也会调整,以官方最新说明为准;实践中不少人经由第三方中转访问(这里不做背书,也不提供任何具体渠道或方法)。只要链路上多了一层中转,它就有自己的失败模式:连接被截断、超时、返回的模型和你以为的不是同一个。怎么避:排查时把中转层显式列为可疑变量,先确认实际生效的模型标识与你的预期一致——如果接口会回传模型名,就把它打印出来看,别凭配置文件里写的那一行下结论。

把冲突标记直接提交上去。 为什么会踩:多轮反复中夹着分支切换和合并,<<<<<<< 这类标记很容易被顺手带进提交,之后出现的错误千奇百怪、指向完全错误的方向。怎么避:提交前扫一次暂存区,git diff --cached --check 会把残留的冲突标记按文件和行号列出来(它同时报空白字符问题,有输出就别急着提交);把这条检查挂到 pre-commit 钩子里,别靠自觉。

六、收束

多轮修不好,本质是排查失去了信息增量:每一轮都在消耗时间,但没有一轮把不确定性变小。识别它靠三个红灯(diff 互为逆操作、报错一字不变、解释变长而验证变少),处置它靠四个动作(换假设、缩范围、要证据、人接手),兜底靠三条硬线(时间盒、无增量两轮即停、回滚优先于修复)。

下次卡住的时候,按这五条自检一遍,多数情况在第三条就能出结果:

  1. 我这一轮拿到了什么此前没有的可观测事实?说不出来就停。
  2. 我改的代码真的被执行了吗?插一行输出确认过没有?
  3. 这个问题落在哪一层:需求、代码、构建执行、环境网络、账号配额?
  4. 我的假设能写成一句可证伪的话吗?有第二假设吗?
  5. 工作区现在是不是干净的、单变量的?回滚点在哪、我确认过它可用吗?

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。