1. 为什么“让AI顺手把bug修了”往往修不干净很多人第一次用大模型改代码都是同一个姿势把报错日志、相关文件、需求描述一股脑塞进对话框然后说一句“帮我修一下”。结果要么改错地方要么改对了地方却引入新问题要么干脆给你一段看起来对、跑起来崩的代码。于是有人得出结论AI修bug不靠谱。但真正在一线用AI改代码的人会发现问题往往不在模型能力而在任务结构。“定位”和“修复”是两种性质完全不同的认知活动把它们压进一次对话等于让一个人同时当侦探和外科医生——侦探还没锁定病灶外科医生已经动刀了。我拿一个真实场景举例。某次线上服务偶发空指针日志只抛了一行NullPointerException堆栈指向一个工具类的第87行。如果直接让AI“修这个bug”它大概率会做一件事在第87行加一个if (obj ! null)判空。代码能跑测试能过但根因可能是上游某个接口在特定条件下返回了null而那个条件才是真正要处理的。判空只是把症状盖住了。这就是定位与修复混在一起时最典型的失败模式AI会优先选择“局部最小改动”来让报错消失而不是找到真正的因果链。因为在一个混合任务里模型同时承受着“解释问题”和“产出可运行代码”两种压力而后者往往在训练信号里权重更高——它更倾向于给你一个能交差的补丁。所以“分成两份任务”不是流程洁癖而是对模型工作方式的尊重。定位任务的目标是收敛假设空间从一堆可能的原因里逐步排除、验证最终锁定一个或几个高置信度的根因。修复任务的目标是在已知根因的前提下生成最小且正确的变更。两者的输入、输出、成功标准都不一样。维度定位任务修复任务输入报错信息、日志、代码上下文、复现步骤已确认的根因描述、相关代码片段、约束条件输出根因假设列表 验证方法具体代码变更 影响范围说明成功标准假设能被验证或排除变更后测试通过且不引入回归主要风险过早收敛到错误假设过度修改、破坏原有逻辑模型行为倾向需要被引导去“怀疑”需要被约束去“最小化”这张表是我自己在多个项目里反复验证后总结的。你会发现定位任务里模型最需要的是发散和怀疑而修复任务里最需要的是收敛和克制。这两种倾向在同一个提示里会互相干扰。还有一个更隐蔽的问题当定位和修复混在一起时你很难判断AI到底哪一步错了。它给的补丁没生效是因为根因找错了还是因为修复方式不对你没法归因也就没法迭代。拆成两步之后每一步都有独立的验证点整个调试过程从“黑盒抽奖”变成了“可观测流水线”。2. 定位任务到底在做什么从报错到根因的收敛过程2.1 定位不是“猜”而是构造可验证的假设很多人以为定位就是让AI“看看哪里有问题”。这是最大的误解。定位的本质是在有限信息下构造一组可验证的假设然后设计最小成本的验证手段逐个排除。我通常会把定位任务拆成三个子步骤交给AI但注意这三个子步骤是在同一个定位任务里完成的不要和修复混在一起信息整理把报错、日志、相关代码、最近变更、复现条件结构化。这一步AI很擅长它能从一堆杂乱信息里提取出关键字段。假设生成基于整理后的信息列出所有可能的原因按可能性排序。这里要明确要求AI“不要给修复方案”只给原因和验证方法。验证设计对每个高可能性假设给出一个具体的验证动作比如“在X处加日志”“用Y输入复现”“检查Z配置”。举个例子。之前有个Python服务在并发下偶发KeyError日志只显示某个字典取值失败。如果直接修AI会加.get()默认值。但我让它先做定位它给出的假设列表是这样的假设A该字典在某个初始化路径下未被填充概率高验证方法是检查初始化顺序。假设B多线程同时读写导致中间状态被读到概率中验证方法是加锁后压测。假设C上游传入的key本身拼写不一致概率低验证方法是打印所有key。然后我按假设A去查发现确实是一个懒加载的配置字典在并发首次访问时被两个线程同时初始化其中一个线程读到了空字典。根因是初始化竞态不是取值方式。如果我一开始就让AI“修”它加.get()只会让问题从崩溃变成静默错误更难排查。注意定位任务里一定要明确告诉AI“先不要改代码”。否则它会在假设列表后面顺手附上补丁而那个补丁会锚定你的思路让你不自觉地跳过验证。2.2 定位任务的输出应该长什么样一个合格的定位输出不是一段散文式的分析而是一张假设-证据-验证表。我一般要求AI按这个格式给假设编号根因描述支持证据反对证据验证方法置信度H1配置懒加载竞态偶发于首次并发请求单线程无法复现加日志观察初始化时序高H2上游key拼写不一致无日志中key格式统一打印所有传入key低这张表的价值在于它把“我觉得”变成了“可检验”。你可以拿着验证方法去实际操作然后根据结果更新置信度。整个过程是可追溯的而不是“AI说啥就是啥”。还有一个关键点定位任务要允许AI说“信息不足”。很多人在提示里写“你必须找出原因”这会逼着模型编一个看起来合理的根因。正确的做法是明确告诉它“如果现有信息不足以区分假设请指出还需要哪些信息。”这样它才会诚实地告诉你“需要看XX模块的代码”或“需要复现步骤”。2.3 为什么定位阶段最怕“顺手修一下”我踩过最深的坑就是在定位阶段让AI“顺便给个修复建议”。那次是一个前端状态更新不触发渲染的问题。AI在分析原因时提到“可能是依赖数组缺少某个值”然后顺手把依赖补上了。我看代码确实合理就直接用了。结果渲染是触发了但陷入了无限循环——因为那个值每次渲染都会变。问题出在哪修复建议一旦出现它就成了一个“锚”。你的大脑会不自觉地围绕这个补丁去构建解释而不是独立地验证根因。AI也一样如果它在同一次输出里既定位又修复它的定位部分会不自觉地偏向那个修复方案形成自我一致性偏差。所以我的硬性规则是定位任务的提示词里必须包含“只输出根因假设和验证方法不要输出任何代码变更”。这不是形式主义是在保护你自己的判断力。3. 修复任务的独立价值为什么它需要另一套提示词3.1 修复的约束条件比定位多得多定位阶段你可以天马行空假设越多越好。但修复阶段约束是硬性的不能破坏现有接口、不能引入新依赖、要兼容旧数据、要符合代码风格、要考虑性能影响。这些约束如果和定位混在一起模型根本顾不过来。我通常会在修复任务的提示里明确列出这些约束比如只允许修改service/order.py中的calculate_total方法不允许新增第三方库必须保持函数签名不变变更后需通过现有单元测试如果根因涉及上游只做防御性处理并标注TODO有了这些约束AI的输出会收敛很多。它不会再给你“重构整个模块”这种大动作而是老老实实在指定范围内做最小变更。3.2 修复任务需要“变更说明”而不是“解释原因”定位任务的输出是“为什么”修复任务的输出应该是“改了什么、影响什么、怎么验证”。我一般要求修复输出包含三部分变更内容具体改了哪几行改成什么。变更理由一句话说明这个改动如何消除根因注意是“消除根因”不是“绕过症状”。影响评估这个改动可能影响哪些调用方、哪些边界条件、是否需要额外测试。这三部分里影响评估是最容易被忽略但最重要的。因为修复的本质是引入变更而任何变更都有副作用。AI在独立修复任务里因为没有定位阶段的“发散惯性”反而更容易冷静地评估影响。3.3 一个反直觉的发现修复任务里AI更容易“过度修改”你可能觉得定位和修复分开后修复应该更精准。但实测下来有个反直觉的现象在独立修复任务里如果根因描述不够具体AI反而更容易过度修改。原因很简单定位阶段它已经“理解”了问题修复时它想“既然都改了不如把相关的地方也优化一下”。比如你告诉它“根因是空值未处理”它可能不仅加判空还会顺手把整个方法的错误处理重构一遍。所以修复任务的提示词里根因描述必须具体到可操作。不要写“空值问题”要写“当user.profile为 null 时第42行的profile.age取值会抛异常”。根因越具体修复越收敛。根因描述方式AI典型行为风险“空值问题”加判空 重构错误处理 加日志变更范围失控“第42行 profile 为 null 时取值异常”在第42行前加判空或默认值变更可控“上游可能返回 null需防御”在调用处加多层防御可能掩盖上游问题这张表是我在code review里反复观察到的模式。你会发现根因描述的粒度直接决定了修复的粒度。而根因描述的质量又取决于定位任务做得好不好。这就是为什么两步必须分开定位的输出质量直接决定修复的输入质量。4. 两步之间怎么衔接定位输出到修复输入的转换4.1 不是所有定位结果都值得修定位任务可能会给出多个假设但修复任务只能针对已确认的根因。所以两步之间有一个关键动作验证与筛选。这个动作目前还得人来做或者至少人要在环里。我的做法是定位输出假设表后我按置信度和验证成本排序先验证高置信度、低成本的假设。验证通过后把那个假设的根因描述重新写一遍写成修复任务能直接用的输入。注意是重新写不是复制粘贴。因为定位阶段的描述是面向“理解”的修复阶段需要面向“操作”。比如定位输出是“H1配置懒加载竞态两个线程同时初始化导致读到空字典。”修复输入应该改写成“在ConfigLoader.get()方法中_instance为 null 时存在竞态。需保证初始化只执行一次且其他线程等待初始化完成。只修改该方法不改变外部调用方式。”你看修复输入里包含了位置、条件、目标、约束而定位输出里只有原因和证据。这个转换过程本身就是一种质量把关。4.2 修复任务要带上定位阶段的“排除记录”还有一个细节修复任务的提示里最好附上定位阶段排除了哪些假设。比如“已排除上游key拼写问题日志确认key格式统一”。这样AI不会在修复时又绕回去处理那些已经被排除的可能性。这就像医生做手术前护士会告诉他“过敏史已确认无、术前禁食已完成”。这些信息不直接参与手术但能防止医生做出错误决策。修复任务里的“排除记录”起的就是这个作用。4.3 如果定位错了修复再漂亮也没用这是最残酷的现实。如果定位阶段收敛到了错误根因修复任务会非常“成功”地修一个不存在的问题而真正的问题还在。更糟的是它可能引入新问题让排查变得更复杂。所以我的经验是定位阶段的验证成本永远比修复阶段的返工成本低。宁可多花时间在定位上做验证也不要急着进入修复。一个被验证过的根因哪怕修复方案笨一点也是安全的一个未验证的根因哪怕修复方案再优雅也是危险的。我一般会给自己定一个规则定位阶段至少排除两个假设才能进入修复。如果只找到一个假设就直接修大概率会翻车。5. 实战拆解一个并发bug的两步处理全过程5.1 问题背景与初始信息某订单服务在压测时偶发ConcurrentModificationException堆栈指向一个遍历订单列表的方法。单线程测试无法复现QPS超过200时概率出现。最近一次变更是在该方法里加了一个“过滤无效订单”的逻辑。如果直接让AI修它大概率会把for循环改成Iterator或者用CopyOnWriteArrayList。但这些都是症状级修复因为真正的问题可能是过滤逻辑在遍历时修改了原列表。5.2 定位任务的实际提示与输出我给AI的定位提示是这样的你是一个代码调试助手。现在只做定位不要给任何修复代码。 背景订单服务压测时偶发 ConcurrentModificationException堆栈在 OrderService.filterValidOrders 的遍历处。 单线程不复现QPS200 时出现。最近变更是该方法内新增了过滤逻辑。 请输出 1. 所有可能的根因假设按可能性排序 2. 每个假设的支持证据和反对证据 3. 每个假设的最小验证方法 4. 如果信息不足指出还需要什么AI的输出里高置信度假设是“过滤逻辑在遍历过程中直接调用了list.remove()导致迭代器失效。”验证方法是“检查过滤逻辑是否在遍历同一列表时做了删除操作。”我一看代码果然如此。根因确认。5.3 修复任务的提示与输出确认根因后我重新写修复提示根因已确认filterValidOrders 方法在 for-each 遍历 orderList 时调用了 orderList.remove()。 已排除并发写入问题该方法内无其他线程写操作。 要求只修改 filterValidOrders 方法保持方法签名和返回类型不变。 不允许新增依赖。变更后需保证遍历安全且过滤逻辑不变。 请输出变更后的代码 变更说明 影响评估。AI给出的修复是用Iterator.remove()替代list.remove()并附上说明“在迭代器遍历时使用迭代器自身的remove方法避免并发修改异常。”影响评估指出“该方法为私有方法仅被processOrders调用无外部影响。”这个修复干净、最小、可验证。如果我一开始就让AI“修这个并发异常”它很可能给我换成CopyOnWriteArrayList那会改变整个列表的语义影响其他读取方。5.4 这个案例里两步分开带来的实际收益对比项混合任务假设两步任务实际修复方案换线程安全列表或加锁改用 Iterator.remove变更范围可能影响列表所有使用方仅一个私有方法性能影响写时复制有内存开销无额外开销回归风险中高低根因是否消除可能只是绕过是这个对比不是理论推演是我实际对比过的。同一个bug混合提示下AI给了三个方案两个都有副作用两步提示下AI给了一个方案直接命中。6. 把两步任务固化成可复用的提示词模板6.1 定位任务模板你是一个代码调试助手当前只做问题定位禁止输出任何修复代码或代码变更建议。 【背景信息】 - 报错/异常 - 复现条件 - 相关代码位置 - 最近变更 - 已尝试的排查 【任务要求】 1. 列出所有可能的根因假设按可能性从高到低排序 2. 每个假设给出支持证据和反对证据 3. 每个假设给出最小验证方法具体到操作 4. 如果现有信息不足以区分假设明确指出还需要哪些信息 5. 输出格式为表格假设编号 | 根因描述 | 支持证据 | 反对证据 | 验证方法 | 置信度 再次强调不要输出任何修复方案。这个模板的关键在于最后那句重复强调。我试过不写AI有30%左右的概率会附上修复建议。写了之后基本能压住。6.2 修复任务模板你是一个代码修复助手。根因已确认当前只做修复不做额外重构或优化。 【已确认根因】 具体到位置、条件、触发路径 【已排除假设】 列出定位阶段排除的可能性防止绕回 【修复约束】 - 允许修改的文件/方法 - 不允许修改的内容 - 不允许新增的依赖 - 必须保持的接口/签名 - 必须通过的测试 【任务要求】 1. 输出变更后的代码 2. 输出变更说明改了什么为什么这样改能消除根因 3. 输出影响评估可能影响哪些调用方、边界条件、需要补充的测试这个模板里“已排除假设”是我后来加上的效果很明显。不加的时候AI有概率在修复时又去处理那些已经被排除的问题导致变更范围扩大。6.3 两步之间的检查清单在从定位切换到修复之前我会过一遍这个清单[ ] 根因是否被至少一个验证方法确认过[ ] 是否至少排除了两个其他假设[ ] 根因描述是否具体到位置和条件[ ] 修复约束是否明确列出[ ] 是否已把定位输出重新改写成修复输入这个清单看起来简单但每次跳过其中一项后面都会付出代价。尤其是第三项根因描述不够具体时修复几乎必然过度。7. 常见误区与踩坑记录7.1 误区一让AI“先定位再直接修”这是最常见的。很多人觉得分两步麻烦就在一个提示里写“先分析原因然后修复”。结果AI的分析部分会不自觉地服务于它想好的修复方案形成“先射箭再画靶”。你以为它在定位其实它在为修复找理由。我实测过同一个bug用“先定位再修复”的混合提示AI给出的根因是“缺少空值检查”修复是“加判空”。而用两步提示定位给出的根因是“上游接口在超时分支返回了不完整对象”修复是“在超时分支补全对象或抛明确异常”。后者才是真正的根因。7.2 误区二定位阶段信息给太多有人觉得信息越多越好把整个项目代码都塞进去。结果AI的假设列表里全是低置信度的噪音反而淹没了真正的高概率原因。定位阶段的信息要精准不是全面。报错堆栈、相关方法、最近变更、复现条件这四样够了。其他代码按需追加。7.3 误区三修复阶段不写约束不写约束的修复任务等于让AI自由发挥。它可能给你重构、可能给你换库、可能给你加一堆防御性代码。这些在它看来都是“合理”的因为你的提示里没有说“不”。所以约束必须显式写出来哪怕你觉得是常识。7.4 误区四定位输出直接当修复输入定位输出是给人看的修复输入是给AI操作的。两者格式和粒度都不一样。直接复制粘贴定位输出到修复提示里AI会缺少操作所需的精确位置和约束。中间那个“重写”步骤不能省。7.5 一个我至今还在用的土办法如果时间紧来不及走完整的两步流程我会做一个简化版先让AI只输出“根因假设列表”我扫一眼挑一个最可能的然后新开一个对话把那个假设作为已知条件让AI修复。新开对话很重要因为同一个对话里AI会记住之前的发散过程修复时容易带惯性。新对话等于给它一个干净的上下文。这个土办法在紧急线上问题处理时救过我几次。虽然不如完整两步严谨但比混合提示强得多。8. 这套方法在哪些场景下收益最大不是所有bug都值得走两步流程。改个拼写错误、调个样式直接修就行。但以下几类场景两步分开的收益非常明显并发/时序问题根因往往不在报错位置混合提示容易修错地方。偶发/难复现问题需要假设-验证循环混合提示会过早收敛。涉及多模块调用链的问题根因可能在上下游混合提示容易在本地打补丁。性能问题根因和症状距离远需要独立定位。数据一致性问题修复方案影响面大需要独立评估。反过来以下场景可以不走两步编译错误、语法错误根因明确直接修。单行逻辑错误位置和原因都清楚。样式/文案调整不涉及因果链。我自己的判断标准是如果报错位置和根因位置可能不是同一个地方就走两步。这个标准简单但有效。9. 关于AI辅助调试的一点个人体会用了这么久AI改bug我最大的体会是AI在定位阶段的价值被严重低估在修复阶段的价值被高估。很多人把AI当“自动修理工”指望它直接给补丁。但实际上AI最擅长的是从杂乱信息里提取模式、生成假设、设计验证方法——这些恰恰是定位阶段的核心工作。而修复阶段因为约束多、影响面大反而更需要人的判断。把定位和修复分成两份任务本质上是在把AI放在它最擅长的位置上。定位时让它发散、怀疑、构造假设修复时让它收敛、克制、最小变更。两个任务各自有独立的提示词、独立的输出格式、独立的验证标准。这样你得到的不是一个“黑盒补丁”而是一条可追溯、可验证、可复现的调试流水线。最后分享一个小技巧每次修完bug把定位阶段的假设表和修复阶段的变更说明一起存档。下次遇到类似报错先翻存档往往能直接命中根因连AI都不用问。这个习惯我坚持了半年现在处理同类问题的速度比之前快了一倍不止。