ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI测试Flaky治理实战:用评分卡给不稳定测试建健康档案

AI测试Flaky治理实战:用评分卡给不稳定测试建健康档案 干了十几年测试最近两年集中做 AI 相关的质量保障最大的感受是Flaky Tests 已经从烦人的小问题变成了吞噬团队生产力的无底洞。传统 Web 项目里偶尔挂一条用例重跑一下就好但到了 AI 测试尤其是嵌入式 AI、智能网联整车这类场景一条用例这次过、下次挂、换个机器挂得更离谱的情况能把人逼疯。后来我学到用评分卡这套思路去管理测试稳定性才算真正把问题按住了。这篇就把我在实际项目里打磨出来的评分卡实战经验完整分享出来。它会涉及几个关键问题为什么 AI 测试里的 flaky 比传统项目难缠、评分卡的维度怎么设计、数据怎么采集、算出来的分怎么真正驱动治理动作以及在嵌入式/整车场景里有哪些特殊坑。适合正在被不稳定测试折磨的测试开发、QA 负责人以及开始搭建 AI 测试体系但还没想清楚稳定性度量怎么做的人。1. 为什么 AI 测试里的 Flaky 比传统项目难缠先别急着上评分卡得先搞明白我们的敌人长什么样。传统项目里 flaky 测试的来源业内早就有共识无非是网络超时、共享状态污染、用例顺序依赖、静态单例串数据这类问题。这些问题虽然讨厌但根因相对好追修起来也快。AI 测试完全不是这么回事。我把这两年踩过的坑归纳成三类这三类直接决定了为什么通用去 flaky 手段在 AI 场景里经常失效。1.1 传统 flaky 的常见来源这块简单过一下给后面做对比。传统场景的 flaky 通常有三个典型来源环境和资源竞争CI 机器负载高、数据库连接池打满、缓存过期时间刚好卡在用例中间。测试代码自身缺陷断言写得过严、sleep 代替等待、共享单例被前一条用例改了状态。外部依赖抖动第三方 API 时快时慢、消息队列消费延迟、定时任务和测试抢资源。这类问题的共性是什么根因基本是稳定、可复现的。你用固定顺序重跑十有八九能稳定复现修完就再也不会出现。1.2 AI 测试的三重不确定性AI 测试里第一重不确定性来自模型推理本身就不是确定性的。拿嵌入式端侧的 AI 模型举例同一个模型、同一份输入在不同批次的芯片上跑浮点累加顺序可能不一样GPU 算子在不同平台上实现也不同。模型输出在 0.71 和 0.72 之间浮动如果你断言置信度大于 0.715它就可能挂。这不是用例写错了是算法层物理性地带有随机性。第二重不确定性是数据漂移。AI 测试严重依赖测试数据集。数据管线的任何人更新了一个数据版本哪怕只是对某类样本做了重新切分整个测试的输入分布就变了。分布一变模型输出的统计特性跟着变大量用例会同时出现上轮全过、这轮全挂的现象——但这不一定代表模型回归可能只是数据变了。第三重不确定性藏在评估指标的随机性里。很多 AI 测试不是简单的断言对错而是算精度、召回率、AUC 这类统计指标。这类指标在小批量样本上本身就有置信区间样本量不够时涨跌 0.5 个点太正常了。你把阈值卡在 95% 精确率上那它就是在掷骰子。1.3 目标漂移带来的伪 Flaky这是我个人觉得最坑的一类也是很多团队吵得最凶的一类。模型更新之后模型行为会朝着新目标演进但测试代码里的预期结果往往是拿旧模型生成的。比如智能网联的感知模型新版本对夜间行人召回率提升了但某个老用例的 ground truth 却是照着旧模型行为标的。新模型跑出来的结果跟 ground truth 不一致用例挂了——可这次挂恰恰说明模型变好了。这种伪 Flaky最危险。因为它不是随机失败而是预期的逻辑在某个时间点失效了。如果不把这类失败和真正的随机失败区分开你会花大量时间修复根本没坏的东西。这三重不确定性叠在一起传统那套挂一次就查代码、重跑验证的流程根本玩不转。你查完代码发现啥也没改重跑又过了然后它又挂了。这时候需要的不是对单个用例做手术而是建立一套长期跟踪用例行为的机制——这就是评分卡出场的理由。2. 评分卡不是打分游戏稳定性画像的四维模型评分卡Scorecard这个词在金融、风控领域用得最多核心思想是把多个维度的信号合成为一个可比较的分数再根据分数区间执行不同动作。我把这套思路搬到测试稳定性上原因很简单单个用例的上次挂没挂信息量极低但它的历史行为集合信息量很高。最开始我试过只用失败率这一个指标后来发现远远不够。失败率只能告诉你这玩意儿爱挂但没法告诉你它为什么挂、该不该治理、治理后有没有效果。所以后来我设计了四个维度合成一张综合评分的画像。2.1 四维模型的来历设计的时候我给自己定了一个原则每个维度都必须对应一个可以采取的动作没有动作响应的维度不纳入否则就变成了统计报表。最终收敛下来的四个维度是维度考察内容对应动作失败频率近 N 次执行中的失败占比决定是否隔离/重试失败模式熵失败原因的集中程度决定是修根因还是当随机噪声环境敏感度在不同环境/hardware 上的表现差异决定是否限制运行环境时间窗趋势失败率随时间的走向决定是紧急处理还是纳入排期2.2 维度一历史失败率与失败模式历史失败率不是简单算一个平均失败率而是用滑动窗口。我一般取最近 20 次执行窗口太长会让问题被平均掉太短又会被单次偶发带偏。算法很简单def failure_rate(recent_results: list[bool]) - float: 最近 20 次执行中失败的次数占比 if not recent_results: return 0.0 failures sum(1 for r in recent_results if not r) return failures / len(recent_results)但光有这个数字不够会有一种很气人的情况某条用例失败率确实高但 20 次失败有 19 种不同的报错。这类用例你没法修因为根本没有稳定的根因。所以必须有第二个维度——失败模式熵。2.3 维度二失败原因的分类与熵每次用例失败的时候除了把 stdout/stderr 打出来我更在意的是给失败归一个类。我是这么分的ASSERTION断言失败模型输出和预期不一致。TIMEOUT超时用例没跑完。CRASH进程崩溃、OOM、硬件异常。DATA_ERROR数据加载失败、版本不匹配。ENV_ERR环境问题比如依赖没装、驱动掉了。把最近 20 次失败的原因汇总算一个分布种类越均匀说明越随机。我这里借用信息熵的概念统一按归一化熵处理import math from collections import Counter def failure_entropy(fail_reasons: list[str]) - float: 失败原因分布越接近均匀分布熵越高越随机 counter Counter(fail_reasons) total len(fail_reasons) if total 0: return 0.0 entropy 0.0 for count in counter.values(): p count / total entropy - p * math.log2(p) # 归一化到 [0, 1]除以最大可能熵各类别等概率 num_types len(counter) if num_types 1: return 0.0 return entropy / math.log2(num_types)熵接近 0说明失败原因高度集中值得排查根因熵接近 1说明失败原因散成一团大概率是环境随机性修根因之前先加保护。注意这里的保护指的是隔离和重试不是无条件重试这一点后面专门讲。2.4 维度三环境与执行上下文AI 测试对执行环境的敏感度比普通测试高得多。同一个用例在 x86 的 CI 机器上秒过放到嵌入式板子上就各种吐——芯片批次、内存带宽、软件栈版本都会影响推理结果。环境敏感度的计算方式我推荐做个成对对比统计同一用例在环境 A 和执行环境 B 上的失败率差值。差值越大说明环境敏感度越高。如果高敏感度又叠加硬件资源抖动这类用例就应该固定跑在某个规格的专用环境里而不是随便丢给一个空闲 runner。2.5 维度四时间窗趋势前面三个维度解决的是当前状态怎么样但没法回答正在变好还是正在变差。趋势维度我用得非常简单把最近 20 次执行按时间切成 4 段每段 5 次算每段的失败率然后看整体是上升还是下降。def trend_score(recent_results: list[bool]) - float: 4 段失败率的线性趋势正数表示变差负数表示变好 buckets 4 size len(recent_results) // buckets if size 0: return 0.0 rates [] for i in range(buckets): group recent_results[i * size:(i 1) * size] if not group: continue rates.append(sum(1 for r in group if not r) / len(group)) if len(rates) 2: return 0.0 # 简单的一阶差分只关心首尾相对变化 return rates[-1] - rates[0]这个维度最大的价值是能区别两类情况一类是一直稳定失败率 30%另一类是前两周 5%这周猛涨到 30%。前者可以排期慢慢查后者必须立刻响应。趋势为正且幅度大意味着可能有数据版本更新或者模型行为迁移在发生这种要优先处理。四维模型确定之后剩下的工作就是怎么把四个分数合起来以及定阈值。3. 从零搭建一张能用的评分卡模型讲完了来说具体落地。评分卡系统我前后迭代了三版第一版用脚本算第二版做成 CI 插件第三版才形成现在的结构。这里直接把最终版本拆给你看。3.1 数据采集测试埋点需要哪些字段没有干净的数据一切都免谈。从第一天起就要在测试框架里加埋点不要等到需要数据的时候再回填。我给每条测试用例记录的字段最小集是这样的CREATE TABLE test_execution_log ( case_id VARCHAR(128) NOT NULL, job_id VARCHAR(64) NOT NULL, env_id VARCHAR(128) NOT NULL, hardware_id VARCHAR(128), commit_sha VARCHAR(64), model_version VARCHAR(64), data_version VARCHAR(64), result ENUM(pass,fail) NOT NULL, fail_reason VARCHAR(32), duration_ms INT, started_at TIMESTAMP NOT NULL, PRIMARY KEY (job_id, case_id) );有几个字段看起来平平无奇但在 AI 测试场景里特别关键。model_version和data_version一定要记因为前面说过伪 Flaky的一大来源就是模型或数据集版本变了预期没跟上。查问题的时候如果缺这两个字段根本无法判断一条用例的失败是随机抖动还是版本更替导致的。hardware_id也别省略。嵌入式 AI 测试里同一型号的不同开发板之间都可能存在个体差异。把硬件标识记下来才能发现某个特定板子带坏的用例。3.2 画像引擎留存最近 N 次执行采集到数据之后画像引擎负责为每条用例维护一个滚动视图。不用搞复杂的流处理我用一个简单的定时任务就够了每次跑完 CI 后算一遍增量。引擎的核心逻辑就三件事对每个case_id按started_at倒序取最近 20 次记录。把 20 条记录喂给四维模型得到四个 0~1 之间的子分。按权重合成综合分落库。这里有个细节取最近 20 次执行的时候必须优先取不同 job_id 的执行记录。同一份代码在不同环境里的多次执行不要全部算进去否则环境差异会把个体行为淹没掉。我一般限制同一commit_sha只保留最早的 3 条记录参与画像计算。3.3 评分计算加权与归一化四维模型合成综合分我用的权重比例在项目里经历过几轮打磨最终稳定在下面这个组合WEIGHTS { failure_rate: 0.35, failure_entropy: 0.25, env_sensitivity: 0.20, trend: 0.20, }综合分含义是需要治理的紧急程度0 分最健康1 分最需要处理。所以子分计算时失败率和趋势本身就是越高越差直接归一化失败模式熵这一项要取反——熵越高代表越随机、越不需要修根因反而应该低分环境敏感度取失败率差值的归一化。def composite_score(case_profile: dict) - float: case_profile 包含四个输入 failure_rate 0~1 failure_entropy 0~1 env_gap 0~1 trend 0~1 weighted ( WEIGHTS[failure_rate] * case_profile[failure_rate] # 熵高说明随机性强治理优先级反而低取 (1 - entropy) WEIGHTS[failure_entropy] * (1 - case_profile[failure_entropy]) WEIGHTS[env_sensitivity] * case_profile[env_gap] WEIGHTS[trend] * max(case_profile[trend], 0) ) return round(weighted, 3)综合分出来之后所有用例按分排序分数前 20% 的用例需要进治理队列。这个阈值不是绝对的具体看你的 CI 承受能力——治理队列处理不过来就上调处理能力富余就往下压。3.4 阈值与动作矩阵评分卡不是用来看的是用来触发动作的。我们内部定了一套动作矩阵每个颜色区间对应不同的自动化处理策略综合分区间等级自动动作人工介入0 ~ 0.3绿色正常执行无需处理无0.3 ~ 0.5黄色允许自动重试 1 次并记录重试原因周会扫一眼0.5 ~ 0.75橙色自动隔离不参与关键门禁告警到 IM负责人收到通知2 天内定位 0.75红色从 CI 套餐中摘除阻断合并必须有人认领并给出处置结论动作矩阵的价值在于把可能不稳定的测试和确认不稳定的测试分开。黄色区间可能是偶发抖动给它一次重试机会能明显降低 CI 噪声橙色区间说明已经不是偶发了直接隔离避免它反复打断所有人的合并流程红色区间问题很明确不处理不行。这里必须强调评分卡本身不决定所有动作它只是给出一个治理优先级真正执行什么动作还要结合用例的类型。比如一条有明确验收标准、不该重试的测试哪怕只有 0.4 分也不能自动重试掩盖问题。评分卡是助手不是裁判。4. 基于评分卡的治理闭环隔离、重试与自动处置说完评分卡怎么算再说怎么让分数产生实际效果。这个环节如果做不好评分卡就只是一堆数字躺在看板上团队照样天天被红叉折磨。4.1 黄色区重试与标记黄色区间的用例我给它们的处置是允许重试一次但必须带标记重试。为什么强调带标记因为如果重试后通过却没有在结果里留下任何痕迹这条用例这一次通过的可靠性在将来完全无法追溯。正确做法是这样的第一次失败时记录完整的失败上下文日志、输入数据版本、硬件环境、失败原因分类。重试时也单独记录一条执行日志。等最终通过之后在结果表里标记retry_count1并保留最初的失败日志。这样做的目的只有一个下一次画像再算到这条用例时重试行为本身会贡献到失败率里而不是被悄悄抹掉。很多人犯的错是重试后直接把失败记录删除表面上是 CI 变绿了实际上是给评分卡喂了假数据后续治理全部失真。4.2 橙色区自动隔离橙色区间用例的处理我强烈推荐走自动隔离而不是自动重试。重试次数多了会把 CI 时间拉长而且掩盖问题。我的做法是在测试管理平台里给用例加一个status字段标记为stable/flaky/quarantined。评分卡定时任务跑到橙色区间时自动把它从flaky改成quarantined。隔离后这条用例不再参与合并门禁但仍然保留在夜间全量回归套餐里继续执行。这一步很多人不理解会问既然隔离了为什么还要跑答案是隔离不是为了消灭它而是为了在低风险的角色下继续收集数据。夜间的全量回归没有合并门禁压力用例跑挂了也不阻塞任何人。我们可以继续观察它看趋势维度会不会自己转好。如果转好自动释放如果持续恶化就等人工处置。这样既保护了主流程又没有放弃这条用例。4.3 红色区停止 CI 与责任认领红色区间评分卡会直接做两件事第一把用例从所有 CI 套餐里摘除第二在告警群里点名通知对应负责人并且把用例在代码仓库里的 owner 信息拉出来。这里有一个关键配置每条用例必须登记 owner。没有 owner 的用例一旦进入红色区间平台上就会显示无主用例推进起来非常费劲。我见过太多团队在这里栽跟头评分卡报警报了一堆结果没人认领最后治理就死了。所以从第一天就强制做 owner 登记宁可数组不能缺。红色区间的责任闭环我定的是48 小时内必须给出三种结论之一——已修复根因准备释放、确认为历史问题建议废弃、需要更长观察期继续隔离。没有结论就会自动升级给测试负责人。这个两日机制执行起来很硬核但确实逼着所有人认真对待红色告警。4.4 与现有 CI/CD 工具链的整合单独造一个平台的意义不大得跟现有工具链打通。我的整合经验是分三步走GitLab CI / Jenkins在 pipeline 定义里增加一个稳定性检查阶段评分卡脚本作为其中的一步。脚本输出一个文本文件内容是要阻止的红色用例清单下面的门禁步骤读取这个清单。IM 机器人评分卡脚本触发动作时把变更的用例列表和分数变化推到一个专门的告警频道。注意推送内容别太频繁只在区间跳变比如黄色变橙色、红色摘除时推送否则大家会麻木。测试平台每次 CI 执行完把执行日志都回传到平台评分卡定时任务从平台拉数据而不是直接读 CI 的裸日志。这样可以跨多套 CI 统一管理。工具链整合这块我踩过最大的坑是日志回传的时序问题。CI 的日志是跑完一批才回传如果评分卡任务刚好在回传之前跑了就会漏掉最新数据。后来我把评分卡任务改成执行事件驱动CI 跑完时的 webhook 直接触发画像重算而不是定时轮询。这样避免了时差问题也省了无谓的定时计算。5. 嵌入式与智能网联场景的 Flaky 特殊性前面四维模型听起来通用但真正把它磨出来的是嵌入式 AI 和智能网联整车测试这两个场景。这两个场景里的 flaky 来源和纯软件栈完全不一样值得单独拿出来说一下。5.1 嵌入式 AI 测试的硬件随机性嵌入式 AI 测试最大的特点是你测的不只是软件而是软硬一体甚至软硬算一体的系统。我遇到过最典型的案例是端侧目标检测模型的测试。用例逻辑很简单给板子输入一张图片断言检测出的目标置信度大于阈值。在开发用的高配板子上稳定通过一放到大批量出货板子上就开始间歇性挂掉。排查下来原因包括不同板卡之间 SoC 温度差异导致推理速度变慢、超时误判为失败同一型号两颗芯片之间算子计算结果有微小位级差异内存带宽不够时数据加载慢了一个数量级。这类问题在评分卡里怎么体现关键是环境敏感度这个维度要细分。我后来把env_id细化成三个层级dev开发机、ci_runnerCI 软环境、target_hw目标硬件。评分卡在计算环境敏感度时重点考察同一用例在target_hw上的失败率是否显著高于ci_runner。如果高说明用例本身没问题是硬件环境不满足条件。此时正确的动作不是修用例而是给这条用例指定专用硬件池或者把硬件相关的前置检查从用例里抽出来做预处理。5.2 智能网联整车测试的时间同步问题智能网联整车测试是另一个极端。它的 flaky 来源不只是硬件还有整个系统的时钟和网络时序。整车里面有好几十个 ECU电子控制单元每个都有自己的时钟。测试系统要跟这些 ECU 对时间只要毫秒级的偏差就可能导致某条场景用例的判断错位。比如测试前车切入场景车端感知的融合结果输出顺序和测试系统的预期顺序差了一帧用例就挂了。但下次跑时序刚好对上又过了。这种场景下失败原因分类里的TIMEOUT远远不够用。我专门增加了一个SYNC_ERR时间同步错误类别并且在失败日志里强制追加时间偏移快照。评分卡处理这类用例时有个特殊策略如果失败原因里SYNC_ERR占比超过一定比例那么这条用例的失败率不计入模型回归判断只进入环境健康度统计。为什么这么做因为整车的时间同步问题通常是测试台架配置问题而不是被测软件的问题。你要是不分开一条真正的回归 bug 就会淹没在时间同步噪声里等人工排查完环境才发现 bug 早该报了。5.3 用评分卡隔离硬件噪声与真实缺陷把硬件噪声和真实缺陷分开是整个评分卡系统在嵌入式/整车场景里最核心的价值。我提供一个很实用的比例参考在整车测试场景我们统计过一个季度的数据约 62% 的用例失败最终归因于环境/硬件/时序因素只有 38% 是真正的算法或软件缺陷。如果不做区分等于整个测试团队有六成精力浪费在修环境上而真正的 bug 可能混在里面被跳过。正确姿势是给评分卡加一个root_cause_classification的字段每条用例进入红色区间时责任人都必须回填根因分类——defect真实缺陷、hardware_noise硬件噪声、env_issue环境配置、data_drift数据变化、time_sync时序问题。定期回看各类占比。占比失衡的时候就该调整对应基础设施投入。这个回填动作相当于把评分卡从测试稳定性工具升级成了整个被测系统健康度仪表盘。6. 落地时最容易翻车的三个坑评分卡这套东西理论上讲多少都能讲出道理但真落地的时候我见过太团队倒在几个非常低级的坑里。提前给你打个预防针。6.1 重试策略被滥用第一个坑也是最常见的就是把重试当万能药用重试掩盖一切失败。很多团队上了评分卡之后发现黄色区间允许重试就直接给所有用例加了一次重试机会。结果 CI 立刻变绿了但大家心里都知道这个绿是假的。更糟的是重试掩盖了高频问题的真实信号评分卡因为拿到了重试后的通过失败率被低估问题永远不会暴露。我的建议是重试权限必须按用例类型开放。无状态、可重复执行的用例可以开有状态、涉及写库/写文件、且失败可能产生脏数据的用例绝对不允许自动重试。实现上就是在评分卡动作矩阵里增加一层配置retryable: true/false最终动作以这层配置为准评分只负责给优先级。6.2 评分卡变成静态排行榜第二个坑是评分卡上线之后大家看了一眼排行榜议论两句这几条真够 flaky然后就没有然后了。评分卡必须是一个活系统数字要动动作要自动发生。我要求团队每周开一次 15 分钟的稳定性评审只看三样东西本周新进入红色区间的用例列表、已经隔离两周以上还没结论的用例、以及某条用例分数连续三周没有变化的异常。如果这些数字没有在动说明评分卡没有真正被接入 CI 流程它只是个统计报表。想让数字动起来最好的办法是把评分卡结果和合并门禁强绑定。红色用例清单一出来不处理合并按钮就是灰的。这种硬性约束比任何激励都管用。当然绑定力度要逐步放开一开始可以先绑在重试次数监测上跑顺之后再上红色阻断避免一上来把团队逼到对立面。6.3 只用失败率这一个数字第三个坑是我自己最开始踩的——只用一个失败率数字运营整个评分卡。只看失败率你会碰到一个无解的问题一条用例失败率 35%但 20 次失败是 19 种不同原因你根本不知道从何下手。此时如果硬要修复大概率是在瞎猜。四维模型里failure_entropy恰恰就是用来区分有根因可查和纯随机抖动的。运营的时候我更看重熵值的变化趋势。熵从低变高说明曾经稳定的根因正在被新的随机因素覆盖这往往是环境变更的信号熵从高变低说明某个主导失败原因正在浮出水面这是一个绝佳的排查窗口期。只盯失败率会错过所有这些时机。我自己实际跑下来最大的体会是评分卡真正解决的不是消灭 flaky而是让 flaky 变成可管理的对象。你不会再因为一条用例的突然失败而手足无措因为你知道它长期的表现是怎样的也知道该用什么力度去处理它。AI 测试的不确定性短期内消不掉数据漂移、模型演进、硬件差异都是常态。与其祈祷所有用例都稳定不如给每条用例建立一份稳定的健康档案按档案办事。这套评分卡经验后续还可以往两个方向扩展一是把 score 输出直接喂给大模型做异常归因分析让 AI 来读日志总结失败原因二是把评分卡的维度模型抽出来复用到整个测试平台的全链路稳定性看板上。如果你们团队也在被 flaky 折磨建议先别急着改用例把评分卡搭起来让数据先说话。
返回列表