用 AI 复现缺陷:从报错到最小复现
「偶尔报错」「压测时才出现」「重启就好了」——这类缺陷不是靠读代码猜出来的,而是靠逐步缩小范围逼出来的。AI 在这条链路里的价值很具体:把它当成一个有耐心的假设生成器,让它把「可能的原因」穷举出来,但每一次验证都必须由你亲自跑出来。
第一步:先把事实固定下来
动手之前先保住现场:日志、版本、配置、请求参数、时间窗口、同期变更。喂给 AI 的材料质量,直接决定它给出的假设有没有用。
【错误】贴完整栈,不要只贴最后一行;带上错误码与关键变量的值
【环境】Go 1.22.3、PostgreSQL 16、依赖 X v1.3.2、容器 2C4G、并发 50
【复现步骤】1) …… 2) …… 3) ……;标注是否稳定复现,大约几次出现一次
【期望 vs 实际】期望产生一条退款流水,实际产生两条
【日志片段】保留请求 ID 与前后 20 行,去掉无关的中间件噪声
【最近改动】git log --oneline -10 的输出,以及可疑提交的 diff
第二步:要假设清单,不要修复方案
最容易被跳过、也最容易踩坑的一步:直接问「怎么修」,它会给你一段看起来合理的补丁,而你没有验证过任何东西,真因也还在。
下面是线上报错与相关代码。规则:
1) 先只列根因假设,每条写成一行:假设 | 支持证据(引用日志原文)| 能证伪它的最小实验 | 该实验的预期输出与失败输出,按可能性排序,最多 5 条;
2) 这一步不要给修复代码,也不要一次给多个假设的补丁;
3) 严格区分「日志里已经出现的事实」与「你的推断」,推断要标注置信度,并说明什么信息能让置信度上升;
4) 指出完成复现我还缺什么(版本、配置、数据形态、并发条件、缓存状态);
5) 最后给出一个最小复现骨架:单文件、单命令、不依赖外网与真实数据,失败时打印关键中间变量。
【报错/代码/复现步骤/环境】
拿到假设清单后逐条做实验,一次只改一个变量——同时改三处再跑,无论结果如何你都不知道是哪一处起了作用。
第三步:两种二分定位
| 手段 | 做法 | 适合 | 成本 |
|---|---|---|---|
| 输入二分 | 把触发数据或参数集合对半砍,看错误是否还在,只保留仍然失败的那一半 | 数据相关、批量相关 | 每次迭代要能重跑 |
| 提交二分 | git bisect 加自动化判据,定位引入缺陷的提交 | 曾经正常、最近才坏 | 需要一个确定性的判据 |
| 依赖二分 | 逐个回退第三方库或基础镜像版本 | 升级后才出现 | 依赖多时迭代慢 |
| 环境二分 | 对比两套配置、两台机器的差异(缓存、连接池、超时、DNS) | 只在某个环境复现 | 需要保留原始现场 |
# 提交二分:run 的退出码 0=good、非 0=bad、125=跳过(判据不稳定时用 125)
git bisect start
git bisect bad HEAD
git bisect good v1.8.0
git bisect run go test ./internal/order -run TestRefundIdempotent -count=1
git bisect reset
# 输入二分:每次砍一半,保留仍然触发错误的那一半
total=$(wc -l < data.jsonl); head -n $((total / 2)) data.jsonl > half.jsonl
go run ./cmd/repro -in half.jsonl # 仍失败 → 目标在前半段;否则在后半段
git bisect run 的前提是判据确定:偶发用例会让它一路给出错误答案,此时先用退出码 125 跳过不稳定提交,或把判据换成「日志里是否出现某个错误码」。
第四步:什么算最小复现
最小复现(Minimal Reproduction)的验收标准是可检查的,不是「感觉差不多了」:
| 检查项 | 合格标准 |
|---|---|
| 复现方式 | 单文件、一条命令、30 秒内跑完 |
| 外部依赖 | 无外网、无私有数据、无手工前置步骤 |
| 稳定性 | 连续 20 次复现同一错误;偶发问题要标出出现概率 |
| 最小性 | 删掉其中任何一行,错误就不再出现 |
| 断言精度 | 断言具体错误类型/消息/错误码,而不是「报错了」 |
| 可回归 | 已固化为测试,并验证过「修复前红、修复后绿」 |
第五步:固化成回归测试
复现的价值一半在「找到原因」,另一半在「不让它回来」。
把上面的最小复现固化为回归测试:
1) 测试名包含 issue 编号与行为描述,例如 TestRefundTwice_OnlyOneRefund_Issue2143;
2) 断言具体的错误/状态,不要断言「返回了错误」;
3) 内联最小数据,不依赖外部库与执行顺序;
4) 先在当前代码上运行,确认它确实是红的;如果需要改动被测代码才能跑起来,先说明改了什么、为什么;
5) 给出验证步骤的命令:删掉修复代码应当变红,恢复后应变绿(命令请写全)。
常见坑
| 坑 | 后果 | 规避 |
|---|---|---|
| 直接问「怎么修」 | 拿到未经验证的补丁,真因仍在 | 先要假设清单,再逐条实验 |
| 一次改多处 | 无法归因,问题反复 | 一次只改一个变量 |
| 把 AI 的推断当事实 | 修错地方,浪费一天 | 要求标注事实与推断,推断必须实验证伪 |
| 没保现场就重启服务 | 日志与内存状态丢失,只能等下次复现 | 先留证据再操作 |
| 断言「有错误」就算复现 | 换成别的错误也算通过 | 断言具体类型、消息或错误码 |
| 拿偶发问题做二分 | bisect 结论错误 | 判据不确定时用退出码 125 跳过 |
| 复现依赖线上数据 | 本地无法重跑,无法回归 | 脱敏后内联成最小样本 |
小结:从报错到最小复现是一条收敛链路——固定事实、要假设清单、一次验证一个假设、用输入二分或提交二分缩小范围,最后用「删掉任何一行就不再复现」验收最小性,并固化成一个修复前必红的回归测试;AI 负责穷举假设与生成骨架,而「哪个假设成立」只能由运行结果回答。