别被“已修复”骗了:AI 修 bug 最该追问这一句

dev-skills(仓库:Jason-chen-coder/dev-skills) 是我在 AI 编程实践里沉淀的一套工作流规则集。
它想解决的不是“AI 会不会写代码”,而是“AI 写代码、修 bug、做验证、准备提交时,能不能按更稳的工程步骤往前走”:需求先对齐,方案先讲清,代码先被测试约束,修 bug 先找根因,完成前必须拿出验证证据,commit 前再 review 一遍 diff。
这篇文章展开的,是 dev-skills 里专门处理 bug 修复的工作流:dev-fix。
如果说普通的 AI 修 bug 是“你给现象,它给补丁”,那 dev-fix 更像一份失败契约:先把问题稳定复现,再列出可证伪的假设,沿着调用链和数据流反向追到 root cause,只改确认过的根因路径,最后用 red → green → red 的回归验证证明测试真的能抓住这个 bug。
它不是为了让 AI 修 bug 变慢,而是为了阻止 AI 过早说“修好了”。

AI 修 bug 最大的问题,不是它不会改代码。
恰恰相反,它太会改了。
你贴一段报错,它马上补一个空判断。
你说页面偶尔白屏,它马上加一个默认值。
你说接口有时候超时,它马上包一层 try-catch。
几秒钟后,它告诉你:
“已修复。”
这四个字听起来很让人安心。
但在真实工程里,它也可能是最危险的幻觉。
因为很多时候,AI 所谓的“修好了”,只是把最吵的症状按下去了。
报错不再弹。
页面不再白。
日志不再红。
测试表面变绿。
但真正的 root cause 还在那里。
下一次,它会换一条路径回来。
这也是我做 dev-fix 时最想解决的问题:别再让 AI 用一个补丁把你糊弄过去。修 bug 不是让错误消失,而是找到错误为什么会发生。
一、AI 最擅长的,是把症状修得像根因
我们先看一个最常见的场景。
页面报错:
Cannot read properties of undefined
你让 AI 修。
它很快给出一行:
if (!user) return null;
报错没了。
这当然可能是正确修复。
但也可能完全不是。
真正的问题可能是接口少返回了字段,可能是缓存 key 错了,可能是某个异步状态被提前消费了,可能是权限判断让用户数据在某条路径下变成了半截对象。
如果你只看报错消失,很容易觉得“修好了”。
但这个补丁只回答了一个问题:
“怎样让这里不炸?”
它没有回答真正重要的问题:
“为什么这里会拿到一个不该出现的坏状态?”
这就是 AI 修 bug 最容易出问题的地方。
它倾向于在失败出现的地方动刀。
报错在这里,就守这里。
空值在这里,就兜这里。
异常在这里,就 catch 这里。
可工程里的报错点,往往只是坏状态最终爆炸的地方,不是坏状态第一次被引入的地方。
一个靠谱的修复,不应该停在“最后炸掉的那一行”。
它要沿着调用链往回追,追到那个坏状态第一次出现的位置。
在那里修,才叫修根因。

二、dev-fix 的第一条铁律:没有复现,就没有修复
很多人让 AI 修 bug 的第一句话是:
“这个问题帮我修一下。”
dev-fix 的第一反应不是修。
而是问:
这个问题怎么稳定出现?
正常应该是什么样?
用户给的触发动作是什么?
最近什么时候开始出现?
严重程度有多高?
这些问题看起来慢,但它们是在把“一个模糊事故”变成“一个可执行的失败”。
因为没有稳定复现,你根本不知道后面的修复有没有用。
AI 加了一行代码,问题暂时没出现,是因为修对了?
还是因为你这次没触发到?
还是因为时序问题刚好没撞上?
还是因为测试写得太宽,永远都会绿?
dev-fix 要求先把 bug 编码成一个会失败的自动测试。
能用单测就不用浏览器。
能用最小输入就不用完整环境。
如果是时序问题,就不能写一个随缘的 sleep(500)。
要等真实条件成立,而不是赌 500 毫秒以后世界会变好。
更硬的一点是:失败测试必须稳定失败。
如果它一会儿红、一会儿绿,这不是可以继续糊弄的信号。
这是 race、时序、跨系统状态不稳定的信号。
这时候就该升级到更深的排查,而不是兴奋地开始补丁。
没有复现的修复,本质上是在猜。
而猜对一次,不等于建立了工程可信度。

三、第二条铁律:先列假设,不许直接动手
AI 的默认姿势太积极。
它看到错误,就想改。
但真正的调试不是“看到哪里可疑就改哪里”,而是先建立几条能被证伪的假设。
dev-fix 会逼 AI 写出类似这样的东西:
H1:后端接口没有返回 user.email
预测观察:接口响应里 user 存在,但 email 字段缺失
证据收集:查看接口响应和序列化逻辑
H2:前端缓存 key 错误,拿到了另一个页面的旧 user
预测观察:同一请求没有发出,但缓存命中异常
证据收集:检查 query key 和缓存日志
H3:权限路径下 user 被裁剪成部分字段
预测观察:只有低权限账号复现,高权限账号正常
证据收集:用两类账号跑同一个测试
这里最关键的不是“列了几个 H”。
而是每个假设都必须能预测观察结果。
如果 H1 成立,应该看到什么?
如果 H2 成立,应该看到什么?
如果 H3 成立,应该看到什么?
不能预测观察的假设,基本就是一句“我感觉”。
而 AI 最容易把“我感觉”包装成“我判断”。
默认模式下,dev-fix 至少会让它列出一到两个假设。
遇到偶发、race、跨系统、生产事故,就进入 --deep:强制列三到五个不同维度的假设,比如代码逻辑、数据问题、并发时序、环境差异、甚至“是不是我们误解了需求”。
这件事很重要。
因为调试最怕单一路径思维。
你以为是前端按钮没禁用。
结果根因是后端写操作没有幂等。
你以为是缓存没清。
结果根因是接口协议漂移。
你以为是某个对象为空。
结果根因是上游状态机允许了一个不该存在的中间态。
好调试不是更快下注。
好调试是让每个下注都有证据成本。

四、第三条铁律:不要在第一个可疑帧打补丁
dev-fix 里有一个很硬的动作:反向追溯。
当某个假设看起来成立时,AI 不能马上在眼前那一行改。
它要沿着调用栈、数据流、状态流往回问:
这个坏值是在这一层第一次出现的吗?
还是从上一层传进来的?
如果是上一层传进来的,那上一层为什么会给它?
再上一层呢?
一直追到坏状态第一次被引入的位置。
举个例子。
你看到 user.email 是 undefined。
最差的修法是在使用处写:
if (!user?.email) return;
稍好一点,是在组件入口做兜底。
但真正的 root cause 可能在 getUserById():DB 查询时根本没有 select email 字段。
那你就应该修查询,而不是在每个消费 email 的地方贴创可贴。
再看一个更典型的 race。
用户连点两次下单按钮,系统创建了两个订单,还扣了两次款。
AI 很容易先修前端:
按钮点击后 disabled。
这当然有价值。
但它不是根因。
因为只要后端写操作没有幂等,用户可以刷新、重试、网络抖动、移动端重复提交、脚本请求,照样绕过这个按钮。
真正的根因在后端:创建订单这个写操作没有 idempotency key,没有唯一约束,没有并发保护。
前端禁用按钮只是缓解症状。
后端幂等才是 root cause fix。
这就是 dev-fix 和“随手修一下”的分水岭。
AI 不许在第一个可疑帧停下。
它必须继续往回追,直到找到坏状态最早出现的位置。

五、第四条铁律:只改 root cause 路径,不许夹带重构
AI 还有另一个很迷人的坏习惯:
一旦开始修,它会顺手把旁边的代码也“优化”了。
变量名顺一下。
helper 抽一下。
日志格式改一下。
错误处理换一套。
测试框架顺手重排。
这些改动单独看都可能有道理。
但放在 bug fix 里,它们会污染判断。
你本来只想知道:这次 root cause 是否被修掉。
结果 diff 里混进了十几个“顺手优化”。
如果测试绿了,究竟是哪一行让它绿的?
如果线上又出问题,究竟是修复没修对,还是新重构引入了风险?
dev-fix 对这点很克制:只改 confirmed root cause 直接相关的路径。
发现旁边也有问题,可以列 Follow-up。
发现同类模式在别处也存在,可以做 Pattern analysis。
但不要在这次 fix 里一起修。
这不是保守。
这是为了让修复可解释。
bug fix 最重要的不是“代码看起来更好”,而是“因果链更清楚”。
这次失败为什么发生。
这次改动为什么能阻止它再次发生。
这次测试为什么能抓住它。
讲不清这三件事,再漂亮的 diff 都不值得放心。

六、最狠的一步:绿了以后,还要证明测试真的会红
很多流程到这里就结束了。
失败测试写了。
代码改了。
测试绿了。
AI 说:
“已完成。”
dev-fix 还不让它走。
它要求再做一个反向证明:
临时移除 fix,测试必须重新变红。
恢复 fix,测试必须重新变绿。
这一步非常朴素,也非常狠。
因为它直接打掉了一类最常见的假验证:
测试其实没测到 bug。
断言太宽。
路径没走到。
mock 写错了。
测试数据绕开了真实失败条件。
环境里刚好没触发。
如果移除 fix 以后测试还是绿,那这个测试就不是回归测试。
它只是一个看起来很努力的摆设。
dev-fix 逼 AI 做 red → green → red,本质上是在问一句:
你说 bug 被修了,证据在哪?
不是“我觉得”。
不是“理论上”。
不是“应该可以”。
而是:bug 以前能被这个测试抓住,现在被修掉;把修复拿掉,它又会被抓住。
这才叫回归证明。

七、找到同类风险,但不要一口气全修
dev-fix 还有一个很实用的要求:Pattern analysis。
当这次 root cause 被确认以后,要问一句:
这种 bug 的形状,在仓库其他地方还有没有?
比如这次问题是“写操作缺幂等”。
那就去搜其他 POST 写操作。
这次问题是“异常输入没有 schema 校验”。
那就去搜其他入口。
这次问题是“空值在边界没挡住”。
那就去搜同类边界函数。
注意,这一步不是让 AI 把所有命中都顺手改掉。
正好相反。
dev-fix 要求把它们列出来,作为 Follow-up。
为什么?
因为这次 fix 的目标,是修掉这个被复现、被定位、被验证的 bug。
Pattern analysis 的目标,是把同类隐患变成可见的后续工作。
两者不能混在一个 diff 里。
AI 很喜欢“一次性帮你都处理了”。
工程上,很多灾难就来自这句“一次性”。

八、dev-fix 真正改变的,是你和 AI 的关系
表面上看,dev-fix 是一个修 bug 的 skill。
但更准确地说,它是在改变 AI 修 bug 时的权限边界。
以前是:
你给现象。
AI 猜补丁。
AI 宣布修好。
你相信或不相信。
现在是:
你给现象。
AI 先复现。
AI 列假设。
AI 收集证据。
AI 反向追根因。
AI 只改根因路径。
AI 用 red → green → red 证明测试有效。
AI 留下 RCA 和后续风险。
这两种工作方式,差别不是流程多了几步。
差别是责任从“生成代码”转向了“证明因果”。
AI 写代码的速度当然重要。
但修 bug 时,速度不是第一优先级。
第一优先级是不要被错误的确定感骗过去。
“修好了”不是一句话。
“修好了”应该是一组证据:
我知道它怎么失败。
我知道为什么失败。
我知道改在哪里。
我知道为什么这次改动能挡住它。
我知道把改动撤掉,测试会重新失败。
我知道同类风险在哪里,但这次不乱改。
如果 AI 不能给出这些东西,它就不应该说“修好了”。
最多只能说:
“我写了一个补丁。”
这两个句子,中间隔着真实工程。

结尾
我越来越觉得,AI 编程时代最稀缺的不是“会写代码的模型”。
这样的模型会越来越多,也会越来越快。
真正稀缺的,是能约束它的工程协议。
什么时候该问。
什么时候该查。
什么时候该停。
什么时候不许改。
什么时候必须拿证据说话。
dev-fix 做的就是这件事。
它不是为了让 AI 修得更慢。
它是为了让 AI 不再用速度掩盖不确定。
下一次,当 AI 对你说“已修复”的时候,你可以先别急着信。
问它四个问题:
你怎么复现的?
你排除了哪些假设?
root cause 是哪一层第一次引入的?
把 fix 拿掉,测试会不会重新红?
如果这四个问题答不上来,那就别让它说“修好了”。
它只是暂时让错误安静了。
而真正危险的 bug,最擅长的就是安静地活着。

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)