ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI修Bug越修越错?如何用最小化修改守住代码行为边界

AI修Bug越修越错?如何用最小化修改守住代码行为边界 1. 一次成功的修复如何变成一场灾难翻车全过程复盘1.1 一个很典型的连带事故现场我最近又经历了一次典型的AI修Bug翻车。背景是订单模块的商品价格精度问题金额在小数点后第三位偶尔出现误差定位后确认是某个工具函数里的浮点数截断逻辑惹的祸。我把问题丢给AI助手它花了不到十秒就定位到了根源给出的修复方案也很标准——引入十进制运算替换原来的round逻辑看起来天衣无缝。编译通过功能验证也过了。一周后同事跑来找我说统计报表里连续几个月的金额对不上。追根溯源最后还是找到了这次修改头上AI在修复精度问题时顺手把工具函数里某个默认参数从strictTrue改成了strictFalse。它给出的理由在局部看无懈可击——这样更灵活、兼容性更好。但调用方里有一半模块恰恰依赖strictTrue时会主动抛异常的行为一旦异常被静默吞掉这些模块在异常数据面前就会返回一个看似正常、实际错误的结果。统计报表只是第一个暴露问题的下游。这种翻车模式我见得越来越多。AI写代码失败的常见形态从来不是找不到Bug而是它修着修着把原本没毛病的代码也一并改了。找到问题是一种能力安全地把问题解决是另一种能力这两者在真实的工程环境下会被明显拉开差距。1.2 最小改动的两个不同定义事后复盘这次事故我发现一个关键的分歧点人和AI对最小改动的理解完全不是一回事。人在代码评审里说最小改动指的是对系统行为的影响面最小——宁可改三十行只要这些改动全部局限在一个即时生效的局部逻辑里那也算安全的小改动。AI理解的最小改动则更接近文本层面的差异最小——在Token级别少动几行代码它就会觉得自己做了一件克制的事。于是AI倾向于用最省钱的方式修改改一个公共函数的签名、替换一个默认参数、调整一行返回值。这样的改动在diff里可能只有一两行但行为影响可能横跨几十个调用方。你把问题描述得越模糊它就越容易选择这种看起来局部、实际上波及很广的改法。另一个隐患是AI在长上下文任务里的记忆漂移。修一个复杂Bug往往要连续处理多处关联逻辑AI在改到第N处的时候未必还记得第1处改动的初衷。为了保持自洽它会把原本正确的旁支逻辑也调整一番好让整段代码看起来顺理成章。这部分被顺手调整的代码就是回归事故的温床。1.3 这类翻车的三个共同特征总结了手头几个典型的翻车案例之后我发现它们都有三个共同特征帮助你在复盘时快速定位问题特征具体表现排查难度跨越代码自然边界修改范围超出了目标函数动到了同名模块的公共逻辑高需要理解模块边界存在与Bug无关的顺手优化变量改名、抽取公共方法、风格统一看似优雅但无关中diff里一眼可见改变了异常处理约定把显式报错改成静默兜底或反过来调用方行为被颠覆极高通常要到下游才暴露尤其是最后一条又隐蔽又致命。AI非常倾向把显式抛错改成静默兜底因为它觉得这样更健壮。但在真实系统里这个模块必须报错本身就是一种被依赖的契约一旦契约被悄悄改掉问题不会出现在当前模块而是会潜伏到下一个依赖它的调用方里。这也是为什么修好A、搞坏B的事故总是比直接的失败更难发现——A确实被修好了而B的异常表现往往要过好几天才有人注意到。2. 为什么AI会越修越错三个藏在模型机制里的盲区2.1 上下文窗口之外全是隐形契约工程代码里的大量约束并不写在代码本身里。接口的调用约定在接口文档里状态流转的先后顺序在时序图里某些业务参数不能同时为真的规则在需求文档里甚至只存在于一位老同事的脑子里。AI能看到的只是上下文窗口里那几段代码对全项目的契约关系缺乏感知。打个比方这就像你请了一位装修师傅来改造客厅但他只拿到客厅的设计图客厅之外全是盲区。他按照自己理解的合理布局把客厅改得很漂亮但他拆掉的那面墙实际上是走廊的承重墙。代码里的承重墙就是那些没有写在当前文件里的依赖关系、调用约束、初始化顺序。AI不知道它们的存在拆墙对它来说只是一次完全符合局部逻辑的改动。2.2 Token级理解不等于系统级理解大模型的本质是预测下一个Token。它在写代码时做的其实是续写——根据前面已有的代码推断后面应该出现什么。它判断一段修改是否合理的依据是这段代码像不像一段能通过评审的代码而不是这段代码在运行时对系统有没有副作用。这点差异在日常的Bug定位场景里特别明显。AI修复并发问题时会加一把锁锁的位置在局部看没问题但它不会真正执行程序、跟踪状态变化所以看不出这个锁的粒度和范围其实覆盖了另一个不该阻塞的调用路径。AI修复性能问题时可能用全表扫描替代索引查询逻辑正确但数据量一大就崩。它给出这些方案的时候完全是局部自洽的——因为Token层面的推理只看得到文本连续性看不到真实的运行时因果。这也是为什么有些问题你让AI分析它能头头是道地说出一套完整归因甚至给出看起来非常合理的修复方案但落地之后反而把原本稳定的行为弄乱。它理解的是代码怎么写才像对的而不是系统怎么跑才是对的。2.3 更安全的写法正在悄悄改掉行为契约AI还有一个我在实际项目中反复碰到的倾向过度防御式编程。给它一个裸的属性访问它可能改成带默认值的get操作给它一个可能抛异常的调用它可能包上一层try-catch兜底。单独看任何一处都像在提高代码的健壮性但如果把这些改动叠在一起就会显著改变原有分支的行为。举个例子有一段代码def get_retry_count(self): return self._retry_count # 依赖 AttributeError 来表达配置未加载AI修复时可能改成def get_retry_count(self): return getattr(self, _retry_count, 0) # 静默处理属性缺失单独看第二段确实更安全——不会因为属性缺失而崩溃。但原本调用方依赖那个AttributeError来判断配置模块还没初始化这个状态一旦异常被吞掉配置漏加载的问题就被静默化了后面出错的时候排查方向会完全走偏。AI认为自己在兜底实际上是在破坏一处精心设计的失败语义。大量此类更安全的修改加在一起就是行为回归的源头。它修一个Bug却把原本依赖错误状态做决策的上游和下游全部牵连了。这比修出一个明显错误要难防得多因为代码本身看上去完全合理甚至更规范。3. 哪些Bug最容易被AI修着修着改错高风险场景速查3.1 时序与状态依赖隐性的执行顺序约束AI最普遍的认知盲区就是时序和状态。初始化顺序、事件注册顺序、缓存加载时机、异步回调的执行顺序这些约束从来不会写成明确的注释它们只体现在代码的执行顺序里。AI修改某一处初始化逻辑时不太会意识到另一处代码正在依赖这个对象此刻还没被创建这个隐含条件。我曾经让AI修复一个数据加载的竞态问题它为了确保数据已就绪就把初始化逻辑前移了。结果是初始化动作跑在了一个必要的订阅动作之前原本的订阅逻辑在事后执行时已经错过了初始化发布的事件。这类Bug的可怕之处在于修改本身完全符合让数据更早就绪的直觉测试用例也未必覆盖得到只有上线后在某些特定的加载时序下才会偶发出现。3.2 魔法值与隐式约定没有注释的业务规则业务代码里到处是魔法值和隐性业务规则。某个数字0.1在这个模块里代表最大重试次数某个字符串空值在这里表示尚未设置负数值的意思可能是忽略此项。AI由于不了解业务看到数字就按常规理解看到分支就按通用逻辑判断这个分支可以合并掉。我见过最典型的案例是AI修复超时时间设置逻辑时把代码里的0.1秒超时理解成了一个过小的值优化成了一秒。单看代码一秒比0.1秒更合理但在真实业务里0.1秒是产品经理精心计算过的交互上限超时时间一改整个功能的行为就变了。这种错误的根子是AI在给代码强加自己认为合理的语义它不是恶意地改错而是不知道业务约定的存在。3.3 跨模块耦合公共接口上的无声雷区当一个函数同时被多个模块调用时AI修改它的风险会急剧上升。改公共函数签名、改枚举值、改配置项名称这些改动在单一文件内部完全自洽但一旦和下游调用方一碰撞问题就来了。AI在修改公共代码时往往只会盯着当前报错的调用点它不会主动展开全局调用列表去逐个确认。一个我在项目里反复踩到的场景某个工具函数被几十个模块共用。AI修好了当前模块的诉求但在修改过程中调整了返回的数据结构导致其余十几个调用方的解构逻辑全部错位。更麻烦的是这些调用方的错误不会马上暴露只有用户点进对应页面才会触发。如果你没有完整的自动化回归测试这类问题会被拖到上线之后才被发现。3.4 性能敏感代码修对逻辑带崩性能AI在修复逻辑正确性问题时有个显著偏好——用昂贵的确定替代便宜的不确定。比如用一个遍历去替代一个索引查找用同步循环去替代一次批量查询用全量重启资源来替代精确的清理动作。从正确性角度它修好了从性能角度它在制造新的灾难。我碰到过一次AI修Bug为了让某段重试逻辑绝对可靠在循环里加了内部的完整状态扫描复杂度从O(1)变成了O(n)但每次循环都触发一次扫描整体变成O(n^2)。在小规模测试环境里一切正常一上生产数据量就卡死。这类修复的问题在于测试阶段完全通过性能问题只有到了生产环境才会被监控系统抓出来。所以涉及性能敏感路径的BugAI的修复方案一定要重点审查尤其是新增循环、嵌套逻辑和重复计算的改动。4. 让AI只修Bug、不碰其他约束式提问与最小化修改的实战方法4.1 先让AI输出方案再允许它写代码我调整工作流之后最重要的改变是顺序写代码前强制AI先输出一个根因分析修复方案影响范围的报告。理由很简单——AI如果在方案阶段就理解错了你可以提前拦住它但如果直接让它动手那就得等它改完代码之后才能发现方向错了这时还要多浪费一轮来回。实际操作中我会这么问这个Bug在[场景]下出现输入条件是[具体输入]期望行为是[期望结果]实际行为是[实际结果]。请先不要修改任何代码先分析根因并给出修复方案。方案需要包含三部分1你认为需要改动的文件和函数2改动前后行为差异说明3这次改动可能影响到的调用方或依赖方列表。这套提示词的核心目的是逼AI把它想做什么提前暴露出来。很多AI修Bug翻车的场景它在动手之前就没想清楚要改哪些范围只是一个局部一个局部地顺藤摸瓜摸到哪儿改哪儿。还有过程中你会经常发现AI分析问题时会给出和实际不匹配的自洽解释而这一步能在动手前把它暴露掉。我个人的经验花在方案确认上的这三五分钟通常能省下后面一两个小时的返工非常值得。4.2 三份可以直接抄的AI编程提示词模板针对不同的场景我整理了三份模板可以直接套用。这些模板的思路是一致的把允许做什么、不允许做什么讲清楚把边界画好AI的行为才会被约束在你想要的安全区域内。模板一限定文件与函数修复[文件/模块名]中[函数名]的Bug。 现象当[输入条件]时期望[期望行为]实际[实际行为]。 要求 1. 只修改该函数内部实现。 2. 不得修改函数签名、默认参数、返回值类型。 3. 不得改动任何与本Bug无关的代码。 4. 禁止顺手重构。 5. 完成后输出修改后的diff并列出影响说明。模板二限定行为契约修复过程中必须保持以下既有行为不变 1[场景A] 必须继续抛异常。 2[参数B]为空时的默认值保持为[值]。 3调用方现有的错误处理路径不变。 在修改过程中如果任何改动与上述约束冲突先停下来说明冲突不要自行裁决。模板三先分析后动手只分析不修改。 请阅读这个函数的当前实现定位造成[具体问题]的根因输出建议的修复方案并单独列出一份本次修改可能影响的调用方列表。 在我明确确认方案之前不要修改任何代码。这三个模板各有用处。第一份用于常规Bug第二份用于涉及边界行为和异常处理的Bug第三份用在你不确定AI理解是否准确、需要先做一轮摸底的情况。4.3 只收Diff不收整文件要求AI用git diff的格式输出修改结果而不是直接给整个文件的新版本这是我用下来最有效的最小化修改强制手段。理由是diff格式天然暴露了AI动了哪些行任何无关改动都藏不住。当你要求它输出diff时它自己也会被迫思考我到底改了哪些地方这能显著减少顺手优化。我在实践中遇到过很多次AI在回复里说我只改了一处但diff一贴出来多出了五六行无关重命名和格式调整。这不是AI在故意说谎而是它做的时候没意识到自己顺手做了这些事。diff一摆出来立刻就能发现并让它撤销。在AI说改完了之后你也可以要求它先跑一遍静态诊断插件或者用你自己的代码诊断工具扫一遍改动区域降低低级错误的漏网概率。4.4 一次只修一个Bug拆解边界明确的小任务我最后想强调的一点是永远不要在一个请求里让AI同时修多个Bug。把列表页时间显示不对和顺便看看详情页的加载抖动放在同一个问题上AI的行为自由度会急剧膨胀。每多一个目标它就越有权衡哪个改动方案更适合全局而更适合全局的判断恰恰是它最不擅长的。正确的做法是把大任务拆成单点任务按依赖顺序排列修完一个、验证一个、提交一个。每次只给它一个清晰、边界明确的问题告诉它只允许动这里然后等它确认。小步提交、及时回滚比指望AI一次性完美地处理好所有问题要可靠得多。这套工作流的本质不是限制AI而是把它的发挥框在安全边界内。就像你让一个能力很强的实习生干活你不会给他十个模糊任务而是会一个一个交代清楚。AI其实也一样。5. 修完之后才是真正的考验Diff审查、回归护栏与AI互相监督5.1 拿到Diff之后先做改动纯度检查很多人让AI修完Bug之后就直接看结果测试跑通就收工。但我现在养成的习惯是无论AI说改完了什么我都要亲自把diff完完整整过一遍然后问自己三个问题这行改动和本次Bug有关吗如果删掉这行改动Bug还能修好吗这行改动是否改变了某种被下游依赖的行为契约凡是与本次Bug无关的改动一律回滚哪怕它看起来更优雅。这里需要顶住一个诱惑AI改出来的代码往往比原来的更规范、更简洁你不自觉地就接受了。但你要清楚每一次额外改动都是一次新的风险暴露点没有充分理由就不要接受。5.2 把回归护栏建在AI动代码之前更理想的做法是让AI动手之前测试就已经摆在那里了。我现在倾向于先把能复现Bug的测试用例准备好再让AI去改。这条用例在修改前应该能复现问题验证它确实锁定了Bug修改之后应该能通过。同时把之前通过的核心用例集跑一遍确保没有回归。如果项目自动化测试覆盖不足至少要跑一遍编译和静态代码诊断再手动验证关键路径。前端场景可以用打断点的方式辅助验证——在修复逻辑的出口和关键分支上分别断点确认执行流没有偏离预期。很多AI改完后的貌似正常其实是因为你只看了最终结果没看中间过程打断点是最直接的方式。代码诊断插件在这个环节也很有用它能捕获AI容易忽略的未使用变量类型不匹配潜在空指针等静态问题。AI再聪明也不会每个改动都主动做一遍静态扫描但工具会。5.3 AI审AI用新上下文的模型实例做反向审查一个很实用的技巧是把第一个AI输出的diff贴给另一个AI会话或者同一模型的新上下文要求它只做Code Review不提出修改建议只列出每个改动可能影响的行为点。这样做有几个好处第二个实例没有第一个实例的先入为主——它没有参与原修改所以不会被这看起来是对的这种惯性影响更容易发现原本正确但现在被改变的行为。我试过几次之后效果非常好经常能揪出第一个AI做了但没写进说明的改动。另一个操作是反过来让第一个AI为自己的diff写一份修改说明清单写明它改了什么、为什么改、影响了什么。然后你拿着这份说明和实际diff逐条对照凡是没有出现在说明里的改动就是需要你特别警惕的部分。5.4 我现在固定使用的AI修Bug安全SOP综合上面的经验我目前固定使用的修Bug流程是这套供你参考写清楚Bug的输入条件、期望行为、实际行为。划定允许修改的文件和函数边界。让AI输出根因分析、修复方案、影响范围先不写代码。方案确认无误后让AI以diff格式输出具体修改。人工审查diff剔除一切无关改动。编译、静态诊断、跑相关测试。前端再补一遍断点验证。提交前把AI的修改说明与实际diff再对照一遍。确认稳定后才进入下一个Bug的修复。这套流程看似比简单地把问题丢给AI多花了几步但它解决的核心问题正是修着修着把对的又改错了——所有步骤都指向同一个目标让每一次AI的修改都被你看见、被你理解、被你认可而不是被它自洽地悄悄带进去。最后再分享一点个人体会。我现在的心态已经变了不指望AI修Bug零失误而是确保它一旦失误我能尽快发现并回滚。给AI画边界、让Diff说话、用旧测试当守卫、让第二个AI来挑刺——这套组合拳用下来我让AI修Bug的成功率确实高了不少。下次你也可以先从最小的一件事开始试让AI动手之前先把允许它改动的文件边界写清楚。就这一个动作就能帮你挡住大半的越修越错。
返回列表