重跑次数统计,回答不了"为什么失败"这个问题
这个系列到目前为止反复验证同一件事:结构化程度越高的场景(API 测试)LLM 越保守,需要语义理解的场景(视觉回归、UI 操作)LLM 才有真正的用处。Flaky test(不稳定测试,同一份代码不改动、同一个测试反复跑,结果却时通时不通)恰好是一个很适合拿来检验这个规律的场景,因为规划文档提出的问题非常具体:一次测试失败,到底属于三类根因里的哪一类——真的是代码里的 bug、是环境问题(网络抖动、时序竞争、并发资源冲突),还是测试用例本身设计得不够健壮? 这个判断,传统工具目前是怎么做的,LLM 又能不能替代或者辅助做这个分类。
先说结论:star 数最高的传统工具(pytest-rerunfailures,477 star;flaky,397 star)处理 flaky test 的手段,本质上都是同一句话——失败了就重跑几次,重跑通过了就当没发生过。这完全回答不了"为什么失败",只是把不稳定性用统计的方式"平滑"掉了。 真正尝试用 LLM 去做根因分类的开源实现,是一个刚发布不到两个月、只有 2 star 的项目 pytest-triage——但它做得比想象中更谨慎,谨慎到把这篇文章想讨论的"降级策略"问题,直接写进了自己的架构设计里。
pytest-rerunfailures 和 flaky:477 与 397 star,靠的是"重跑几次"
pytest-rerunfailures(477 star,pytest-dev 官方组织维护,最近提交 2026-09-24)和 Box 公司的 flaky(397 star,Apache 2.0,最近提交 2025-09-08)是这个领域事实上的行业标准插件——机制极其简单:给测试打一个标记(比如 @pytest.mark.flaky(reruns=3)),失败了自动重跑最多 N 次,只要有一次通过就算通过。
这套机制完全不做任何根因判断——它不区分"这次失败是网络超时"还是"这次失败是并发死锁"还是"这次失败真的是代码改坏了",它的假设很粗暴:真 bug 会稳定复现,重跑几次还失败就大概率是真 bug;flaky 问题重跑一次就好了。这个假设在很多场景下确实成立,也是这两个工具能撑起几百 star 的原因——便宜、零配置成本、不需要理解失败原因就能用。 但规划文档提出的问题恰好是这套机制的天花板:如果一个真 bug 恰好只在某些时序下才触发(比如一个竞态条件,大部分时候不触发,少数时候触发),重跑机制会把它当成 flaky 直接放过;如果一个测试本身设计有问题(比如依赖了执行顺序,或者用了固定的 sleep 时间去等异步操作),重跑机制只会不断地"凑巧"通过,却永远不会告诉你测试本身该重写了。 重跑次数统计能解决"要不要现在让 CI 变绿"这个短期问题,但完全不解决"这个不稳定性到底是什么原因造成的"这个长期问题——而长期问题积累下来,就是大量团队都遇到的"没人知道哪些测试是真的 flaky、为什么 flaky、什么时候该修"的技术债。
DeFlaker:不用 LLM,但确实在做根因层面的区分——用代码覆盖率交叉验证 diff
在传统阵营里,还有一个值得单独提的项目:DeFlaker(gmu-swe/deflaker,39 star,一个 Maven 扩展,来自乔治梅森大学软件工程实验室)。它比"重跑几次"聪明的地方在于:它真的在尝试回答"这次失败跟这次代码改动有没有关系"这个问题,只是用的是纯确定性的技术手段——把 git diff 出来的改动行,跟每个测试执行时收集到的代码覆盖率做交叉比对。
具体逻辑很直白:如果一个测试失败了,而它的覆盖率数据显示它确实执行过这次 diff 里改动的代码行,DeFlaker 判定这是"可能与改动相关的真实回归";如果一个测试失败了,但它压根没执行过任何改动过的代码行,DeFlaker 就把它标记为"看起来是 flaky,跟这次改动无关"。这套机制不需要理解代码语义,纯粹靠"这个测试的执行路径有没有碰到被改动的代码"这个可计算的事实做判断——这跟前面提到的重跑机制相比,已经往"根因"方向走了一步,但它能回答的问题依然局限于"跟这次改动有没有关系",回答不了"如果无关,那到底是环境问题还是测试设计问题"这个更细的三分类。
pytest-triage:2 star,但是我读过的对"AI 判断该有多大权力"想得最清楚的项目
IKrysanov/pytest-triage(2 star,Apache 2.0,2026-07-23 创建,最近提交 2026-09-21——这个系列里研究过的最新、最小的项目之一)做的事情,README 里第一句话说得很直接:"collects a machine-readable report of every failed test and — optionally — enriches each failure with an LLM verdict (regression/flaky/environment/test bug)"。它的分类枚举跟规划文档问的三类根因几乎是逐字对应——它输出的 category 字段枚举值是 regression(回归)、flaky(不稳定)、env(环境问题)、test_bug(测试本身设计问题)、unknown(判断不了),配上 confidence(置信度)、hypothesis(具体假设,比如"tests/test_shop.py::test_db_connection 里的 ConnectionError")、suggested_fix(建议修复方式)四个字段。它调用 Anthropic 或 OpenAI 的 API,用工具调用(tool use)强制模型必须返回这个结构化 schema,而不是自由文本——Anthropic provider 里的 schema 设了 "strict": True 和 "additionalProperties": False,并通过 tool_choice={"type": "tool", "name": "record_verdict"} 强制模型只能调用这一个工具,不能用自由文本回复。
真正值得记录的,是它给这套 LLM 判断加的四条"安全约束"——原文写的是"invariants"(不变式),而且明确说是用测试强制保证的,不只是文档里写的承诺:
- "AI never affects the test verdict"(AI 从不影响测试的通过/失败判定)——原文举的例子是"A provider raising, timing out, or returning garbage leaves the run byte-identical to a run without the plugin"(模型报错、超时或者返回垂圾内容,整次测试运行的结果跟完全没装这个插件时逐字节相同)。这条约束直接回答了规划文档里"根因分类准确率不足时的降级策略"这个问题——pytest-triage 给出的答案不是"模型判断错了就人工复核",而是从架构上直接切断 AI 判断和测试真实结果之间的因果链条:AI 只负责在失败之后追加一段诊断信息,永远不允许反过来决定这次运行算通过还是失败。
- 默认关闭——装上这个插件本身不改变任何现有测试套件的行为,必须显式开启才会触发 LLM 调用。
- 预算硬上限——默认
--ai-budget=10,一次运行最多调用 10 次模型,即使有 300 个测试失败,第 11 个开始直接返回unknown,不再调用;设成--ai-budget=0可以让报告功能继续工作但完全不花钱。 - 四层防御式包装——
CachingClient(同一份失败特征缓存复用,不重复计费)包住CircuitBreakerClient(连续报错或超时两次直接跳闸,后续调用直接返回unknown)包住BudgetedClient(预算耗尽直接拒绝)包住TimedOutClient(每次调用有硬性墙钟超时,在独立线程里跑,超时或崩溃都不会拖垮整个测试运行)。README 里的设计理由写得很清楚:"Cache is outermost, so a cache hit costs neither budget nor time; the breaker sits above the budget, so a tripped breaker spends nothing at all"(缓存放在最外层,命中缓存既不占预算也不占时间;断路器放在预算之上,跳闸之后完全不花钱)。
这四条约束合在一起,回答的其实是同一个问题:如果 LLM 的根因判断本身就有可能出错(模型幻觉、API 超时、返回格式不对),那这套系统的默认姿态应该是什么? pytest-triage 的答案不是"让 AI 更准",而是让 AI 判断错误的代价被严格限制在"这条诊断信息不准确"这个范围内,永远不会传导到"CI 流水线的通过/失败结果"或者"测试运行的总耗时/总花费"这些更严重的后果上。 这跟文章 11 里 AutoRestTest 把 LLM 限定在 SmartValueGenerator、文章 12 里 vlmkit 把 VLM 判断限定在"确定性数据不覆盖的最窄环节"是同一种工程纪律的第三次重现——只是这次的约束对象不是 LLM 该做什么,而是 LLM 判断错了之后,系统该怎么兜底。
三类根因、统计降级、判断范围:这篇的答案都比想象中具体
规划文档提出的三个问题,现在都有具体依据了:
LLM 从失败日志里做三类根因分类,传统重跑统计做不到的部分:重跑机制的判断依据只有"重跑之后是不是还失败"这一个信号,不看失败的具体内容;pytest-triage 的 LLM verdict 会读实际的 traceback、异常类型、stdout/stderr 尾部日志,给出一个具体假设("这是 tests/test_shop.py 里的 ConnectionError")和建议修复方式,这是重跑次数统计原则上做不到的,因为重跑统计根本不读失败内容。
跟传统基于重跑次数统计的 flaky 检测比,节省的人工排查成本:DeFlaker 已经证明"用确定性方法做部分根因区分"是可行的(diff 覆盖率交叉验证),但它给不出人类能直接读懂的诊断文字,只能告诉你"这个测试跑没跑到改动的代码";pytest-triage 的价值在于把这一步产出变成"一句人类可读的假设 + 一个可执行的修复建议",省掉的是工程师自己去翻 traceback、猜测原因这一步——但要注意,这个价值本身就建立在"分类准确"的前提上,一旦分类不准,省下的排查成本可能变成新增的误导成本。
根因分类准确率不足时的降级策略:pytest-triage 给出了这篇研究里最具体的答案——不是"人工复核 vs 直接标记 quarantine"的二选一,而是从架构上让"分类不准"这件事的影响半径被死死限制住:预算耗尽、断路器跳闸、超时,统统直接返回 unknown,不猜、不重试、不影响测试真实结果。这比"要不要相信 AI 的判断"这种态度性问题更实际——它把问题转化成了一个纯工程问题:即使 AI 判断的准确率是 0,这个系统的最坏情况是什么?pytest-triage 的答案是"最坏情况就是一堆 unknown,测试运行本身不受任何影响"。
总结
- pytest-rerunfailures(477 star)和 flaky(397 star)是这个领域的事实标准工具,机制是失败重跑 N 次、通过一次就算过,完全不做根因判断,能解决"CI 要不要变绿"的短期问题,解决不了"这个不稳定性到底是什么原因"的长期问题。
- DeFlaker(39 star)用确定性技术(git diff 与代码覆盖率交叉比对)在根因判断上往前走了一步——能区分"失败跟本次改动有没有关系",但回答不了"如果无关,是环境问题还是测试设计问题"这个更细的三分类。
- pytest-triage(2 star,2026-07 创建,是这次研究里最新最小的项目)是唯一验证到的、真正调用 LLM API 做根因分类的开源实现——输出枚举
regression/flaky/env/test_bug/unknown,配合置信度、假设和建议修复,并用工具调用强制模型输出结构化结果。 - pytest-triage 最值得记录的不是它的分类能力,而是它给 AI 判断加的四条安全约束:AI 从不影响测试真实通过/失败结果、默认关闭、预算硬上限(默认每次运行最多 10 次调用)、四层防御式包装(缓存/断路器/预算/超时)——这是对"根因分类准确率不足时怎么办"这个问题给出的具体工程答案:不是提高准确率或人工复核,而是把判断出错的影响半径限制在"这条诊断信息不准",不传导到测试运行结果本身。
- 三个项目摆在一起,呼应了这个系列反复验证的模式:高星的传统工具解决的是统计意义上的稳定性问题,不涉及语义理解;唯一做语义层面根因分类的项目 star 数最低、上线时间最短,但架构设计上对"AI 判断可能出错"这件事的克制程度,反而超过大多数成熟工具。
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页