用 AI 调试与重构:定位 bug 与梳理遗留代码
调试和重构是两个最容易「看起来做完了」的任务:报错消失了但根因还在,代码变漂亮了但行为变了。AI 在这两件事上非常有用,前提是你按证据给料、按假设推进、按测试收口。核心纪律三条:先给全上下文、先要假设后要改动、危险操作人工确认。
给足上下文:调试四件套
「帮我看看为什么报错」几乎必然得到编造的答案。一次有效的求助至少包含四样东西。
| 要素 | 要给什么 | 常见缺失 |
|---|---|---|
| 报错全文 | 完整堆栈、错误码、时间点、发生频率 | 只给最后一行 |
| 最小复现 | 能稳定触发的步骤或测试用例 | 「有时候会错」 |
| 相关代码 | 出错函数 + 调用方 + 涉及的类型定义 | 只贴一个函数 |
| 已尝试 | 你改过什么、结果如何 | 不提,导致它重复给同一建议 |
我在排查一个线上问题,先只看证据、不要给方案,等你读完再回答。
【现象】POST /api/order/submit 大约每 200 次请求出现 1 次 500,持续两天,
昨天开始变频繁。发生时间集中在上午 10 点前后的高峰时段。
【报错全文】
(这里粘贴完整堆栈,从第一行异常到最后一个业务帧,不要截断)
【最小复现】本地用脚本并发 50 个相同请求可复现,单线程请求无法复现。
【相关代码】
- service/order_submit.go 第 120~210 行(主流程)
- repository/order_repo.go 第 60~110 行(事务与库存扣减)
- 类型定义 model/order.go
【已尝试】1) 加了重试,错误率下降但没消失;2) 怀疑连接池,把最大连接数调大后无变化。
【要求】
1) 先指出你认为最可能的原因,按可能性排序,每条说明依据来自堆栈或代码的哪一行;
2) 明确区分「代码里能看到的证据」与「你的推测」;
3) 如果信息不足,列出你需要我补充什么,不要凭想象补全;
4) 现在不要改代码。
最小复现:把「有时候错」变成「每次都错」
| 手段 | 做法 | 目的 |
|---|---|---|
| 写测试复现 | 把线上输入固化成一条失败用例 | 有了确定的红,才有确定的绿 |
| 缩小数据 | 从大文件/大表里裁剪出最小数据集 | 排除数据量干扰 |
| 隔离依赖 | 用桩替换下游,固定返回 | 确认是不是依赖侧问题 |
| 对齐环境 | 比对版本、配置、时区、字符集 | 排除环境差异 |
# 把线上出错的那一条输入固化成回归用例,先确认它能稳定失败
go test ./service -run TestOrderSubmit_DuplicateStock -count=5 -v
# 并发复现:只在并发下出现的问题,单线程测试永远是绿的
go test ./service -run TestOrderSubmit -race -count=20
# 观察资源层面是否有异常(连接、文件描述符、线程)
ss -s && cat /proc/<pid>/limits | grep -i 'open files'
先要假设,再要验证
让 AI 一次给三个假设并按证据排序,比让它直接改代码有效得多。它最常见的失败模式是「猜一个原因 → 改一处代码 → 说修好了」。
基于我给的堆栈与代码,请给出这份分析:
1) 假设列表(至少 3 条,按可能性从高到低):
每条写:假设内容 | 支持它的证据(引用文件与行号或堆栈帧)| 如果成立会看到什么现象;
2) 验证方法:每条假设对应一个最小验证动作,说明预期结果与「被推翻」的判定标准;
3) 优先级建议:先验证哪一条,为什么;
4) 明确写出「我无法从现有信息判断的部分」。
规则:不要给修复方案,不要假设我没提供的代码存在;任何结论都要能被我按你给的步骤验证。
| 假设 | 支持证据 | 验证动作 | 结果 |
|---|---|---|---|
| 事务里重复扣库存 | 堆栈停在扣减处 | 并发跑 50 次并打印库存 | 成立 |
| 连接池耗尽 | 错误率随时段升高 | 打点连接池等待时间 | 被推翻 |
| 重试导致重复提交 | 加过重试 | 检查是否幂等 | 成立 |
把这张表填满,比读十页 AI 分析更有用。成立的假设才算根因,其余都只是猜测。
重构前先补测试
没有测试的重构等于赌博。顺序永远是:先固化现状,再改结构,再验证行为不变。
| 步骤 | 做什么 | 完成标志 |
|---|---|---|
| 1 | 跑通现有测试并记录覆盖率 | 有一份「改之前」的基准 |
| 2 | 给要重构的代码补特征测试 | 覆盖主要分支与边界,改坏会红 |
| 3 | 做结构性改动,一次一步 | 每步之后测试全绿 |
| 4 | 对比改动前后行为 | 输出一致,性能无明显退化 |
我要重构 legacy/pricing.py 里的 calculate_price 函数(约 300 行,无测试)。
请按顺序做,每步停下来等我确认:
第一步:读代码,用一张表列出它的输入、输出、分支条件、副作用(读写数据库/缓存/日志)
以及你发现的隐藏假设;
第二步:列出重构前必须补的测试用例清单(覆盖主要分支与边界),先只给清单;
第三步:等我确认后写出这些测试,我先跑通它们、确认是绿的;
第四步:再开始重构,每次只做一种改动(先提取函数,再消除重复),
每次改完运行测试,全绿才继续下一步。
约束:不改公开函数签名与对外行为;遇到不确定的行为,保留原样并在报告里标出。
危险操作必须人工确认
| 危险动作 | 为什么危险 | 替代做法 |
|---|---|---|
| 删除文件或目录 | 不可恢复 | 先移到备份目录,确认后再删 |
| 改数据库结构 | 影响线上、回滚成本高 | 生成变更脚本,人工评审后执行 |
| 直接改生产数据 | 可能污染业务事实 | 只生成 SQL 与影响行数,人工执行 |
| 强制推送/重写历史 | 覆盖他人工作 | 用普通提交,必要时人工操作 |
| 升级或删除依赖 | 可能带来连锁影响 | 单独提交、单独验证 |
| 批量替换代码 | 误伤面大 | 先出清单与影响文件数,小批多次 |
在任何删除、覆盖、改动数据库、强制推送之前,你必须先停下来输出:
1) 你要执行的确切命令或 SQL;
2) 影响范围:涉及哪些文件、多少行、哪些表、预计多少行数据;
3) 回滚办法:如果这一步错了,怎么恢复;
4) 风险点:你认为最可能出问题的地方。
必须等我回复「确认执行」后才能动手。没有得到确认时,宁可停下并说明卡在哪里。
常见坑
| 坑 | 后果 | 规避 |
|---|---|---|
| 只贴报错最后一行 | 得到编造的根因 | 贴完整堆栈与上下文 |
| 让它直接给修复方案 | 掩盖根因,问题复发 | 先要假设与验证步骤 |
| 没有失败用例就重构 | 行为悄悄变了没人发现 | 先补特征测试 |
| 一次重构多个模块 | 出问题无法定位 | 一次一种改动,逐步验证 |
| 相信「已修复」 | 没验证就上线 | 自己跑复现用例确认 |
| 让它直接改生产库 | 数据事故 | 只出 SQL,人工执行 |
小结:调试的关键是证据链——给全堆栈与最小复现、让 AI 先列假设和验证方法、用实验推翻或确认,成立的假设才是根因;重构的关键是安全网——先补测试固化现状,再一次一步地改,危险操作一律先出清单与回滚办法、人工确认后才执行。