不稳定测试让AI排查,为什么它总给错答案:根因与四步修法
把 flaky test 丢给 AI 让它找原因,它往往给出「可能是时序问题」这种空话。本文解释为什么会这样,并给出一套让 AI 真正定位不稳定根因的排查流程。
团队里最常见的场景:CI 上某个测试十次里挂一次,重跑就过。你把报错日志复制给 AI,它回答「这可能是异步时序问题,建议增加等待时间」。
你照着加了 sleep(2),测试确实不挂了。三个月后它换个姿势又挂——而且挂在了别的测试上。
这不是 AI 不行,是你给它的输入不包含定位根因所需的信息。flaky test 的根因几乎从不写在报错里。
为什么 AI 在 flaky test 上必然失效
三个结构性原因:
一、报错位置不是原因位置。 一个并发写入测试的失败,报错通常出现在断言处,而真正的问题在几百毫秒前的另一个线程。AI 只能看到断言失败的那一行。
二、缺失「通过时」的对照。 诊断的本质是对比。AI 手里只有失败案例,没有成功案例的日志、时间线和状态快照,就像侦探只有一份现场照片。
三、候选原因太多且概率相近。 时序、共享状态、随机数据、外部依赖、资源泄漏、测试顺序依赖——六类根因的现象高度重叠。信息不足时,模型倾向输出最「安全」的那一类,也就是时序。
四步修法:把 AI 变成有证据可用的侦探
第一步:先分类,再问 AI
在问之前,你自己先做一次粗分类,能砍掉一大半无效对话。我用一个简单判据:单独运行是否稳定。
- 单独跑 100 次都过、全量跑才挂 → 90% 是测试间状态污染或顺序依赖,与异步无关;
- 单独跑也会挂 → 再分:用同一份随机种子是否还挂(区别随机性根因);
- 只在 CI 挂、本地不挂 → 优先怀疑资源差异(核数、内存、时区、DNS)。
一句话就能判断的信息,不要交给 AI 猜。
第二步:给它对照证据,而不是一段报错
最有效的输入不是日志片段,而是失败与通过两次运行的完整差异。实操上最容易做的三件事:
- 在测试前后各打一行带时间戳的标记,让 AI 看到是「卡在某步」还是「跑完了但结果不对」;
- 挂一次不删容器,把断言失败时的 DOM / 数据表 / 队列快照留下来;
- 把
--repeat 50的连续运行结果给它,附上失败序列(第几次挂、间隔多久挂一次)。
有了失败序列,AI 的判断力会立刻上一个台阶:间歇性挂且间隔递增,指向资源泄漏;固定在第 N 次挂,指向缓存或连接池;随机挂,指向竞态。
第三步:让它生成排查假设,而不是方案
把提问从「怎么修」改成「列出三个可能根因,按可能性排序,并说明每个假设可以用哪条命令验证」。这一步的目的是让 AI 输出可验证的实验,而不是可执行的补丁。
一个合格的输出长这样:
| 假设 | 验证方法 |
|---|---|
| 上一个测试残留了未关闭的数据库连接 | 在 setup 里打印活跃连接数 |
| 断言时机早于 React 提交 | 在断言前加 await waitFor(() => ...) 而非 sleep |
| 测试依赖系统时区 | 用 TZ=UTC 重跑 |
拿到这张表,你去跑命令,比让 AI 直接改代码快得多。
第四步:修的是机制,不是这一次
最常见的错误修复就是加 sleep。它把概率性失败变成概率性更小的失败,代价是每次都要等,而且掩盖了真实缺陷。
正确的修复方向按优先级:
- 去掉共享状态:每个测试自建独立的数据空间(唯一邮箱前缀、独立 schema),这是收益最高的一招;
- 用条件等待替代时间等待:
waitFor(条件)而不是sleep; - 固定随机性:种子写死并记录,失败时可复现;
- 隔离外部依赖:把第三方 API 换成契约测试桩,别让网络抖动进入测试。
一个可以立刻加进流程的小习惯
在 CI 里跑 --repeat 20 作为独立的「稳定性任务」,只在主干上触发。它不阻塞开发,但能让 flaky 在变成他人事故之前暴露出来。同时把每次修复写进一份 flaky-log.md:测试名、根因类别、修复方式。三个月后你会发现,80% 的问题集中在三四类模式上,AI 的回答质量也随之上升——因为它终于有了历史证据。
小结
AI 在 flaky test 上失灵,不是模型问题,是输入问题。先自己分类,再给对照证据,让它输出假设而非补丁,最后修机制不修这一次。四步都不需要更聪明的模型,只需要更完整的信息。