报告里写『通过率 92%』,等于只报了一半:把 AI 测试数字连置信区间一起交出去
同一套回归用例,周一早上跑完是 88%,你不放心又跑了一次,变成 94%。周报你贴了 94%——贴 88% 那次,评审会上要被人追问『怎么又掉了』;贴 94% 这次,没人记得它统共就跑了十七八条。数字没撒谎,你只是从一摞结果里挑了最顺眼的那一颗珠子。
AI 应用的通过率、稳定性,天生是一个分布,不是一个点。模型输出的随机性、用例里那一两条边界抖动,都会让『同一次改动』每次跑出来的通过率在一个带子里晃。报告只交一个点,是系统性地把『我其实没那么确定』这件事抹平了。正确做法,是把每个关键数字连它的不确定度一起交出去。
一、把区间交出去:通过率 X%,95%CI 也要一起写
先说最小动作。你手上有一组通过记录——每条用例跑过记 1、没过记 0,或者你把多次运行的通过率攒成一列。别只算平均,用 bootstrap 从这组记录里有放回地重采样几千次,每次算一个通过率,把这一堆通过率排序,取中间 95% 的两个端点,就是置信区间。报告里那行字,于是从『通过率 92%』变成『通过率 92%,95%CI [86%, 95%],n=210』。读者第一次能看见这个数字到底有多『实』。
| 呈现口径 | 读者会得出什么结论 | 什么时候会误导 |
|---|---|---|
| 裸点估计『通过率 92%』 | 系统稳定在 92%,可以直接跟 95% 的放行线比 | n 很小或多次波动时:一个数盖住了它其实只跑了十几条 |
| 点 + 区间『92%,95%CI [86%, 95%],n=210』 | 真实值大概率落在这个带里,够不够格卡线一眼能判 | 几乎不误导;只需提醒一句:区间说的是『当期真值』,不是『下次跑分落点』 |
| 两版区间重叠比较 | 两次到底是不是『真的不一样』 | 只报两个点就下『显著提升 / 明显退化』:两个区间其实大面积重叠 |
bootstrap 是公开统计方法,不用连任何外部服务,纯标准库就能跑——这也正是它适合长在流水线里的原因。
还有一层用 bootstrap 而不是背公式的理由:通过/失败是 0/1 数据,当真值偏向九成以上时,抽样分布明显左偏,用『±1.96×标准误』那套正态近似算出来的区间,在小样本下会越过 100%、甚至越过 0,覆盖率一塌糊涂。bootstrap 不假设正态,它把样本当成总体本身,重采样几千次,区间长成什么样完全跟着数据走——样本越小,它就越宽,这个『宽』恰恰是报告里最该被看见的诚实。
二、区间宽度本身就是一条质量结论
把那套十七八条的用例塞进脚本,跑出来是这个样子:周一那次『通过率 88%,95%CI [71%, 100%]』,重跑那次『通过率 94%,95%CI [82%, 100%]』。6 个点的差距,背后只是两条用例翻了个面——因为 n=17,一条用例的值就差不多占 6 个百分点,点估计当然站不稳。区间宽到 29 个点,等于报告在喊:这次样本太少,不足以单独支撑放行。这是一条过去被『92%』这种漂亮数字盖住的结论。
再换一套:同一套逻辑扩到 210 条,通过率还是 91%,可区间收紧到 [88%, 95%],宽度只剩 7 个点。同样是『九成通过』,一个宽到不敢签字、一个够到能拿去和 90% 的放行线比。宽度不是噪音,宽度是你本该写进报告的第二条结论。
顺带堵一个常被误读的地方:置信区间说的是『这批改动真实通过率大概率藏在哪』,不是『下一次再跑,通过率会落在哪』。前者是关于真值、关于『我有多确定』,后者是关于单次跑分的波动。报告里把区间按真值的意思写,它才能拿去当放行依据;一旦读者把它错当成下次跑分的预言,那句『95%CI [88%, 95%]』就会被理解成『下次跑一定在 88 到 95 之间』——那就不是同一件事了,卡线的锅也说不清。
【配图1插入位置:本小节之后】
三、比较两个数字,先看区间重不重叠
『新版本通过率 93%,旧版本 90%,提升 3 个点,建议升级』——这种话在报告里出现一次,就该被拦一次。把两次各 n=100 的结果丢进脚本:旧版 90% 落在 [84%, 95%],新版 93% 落在 [88%, 97%],两个区间重叠了将近八成。重叠到这个份上,3 个点的差完全在样本波动能解释的范围内,写『显著提升』就是把运气写成了能力。规矩很简单:新旧两个区间大幅重叠,就老实写『本次差异落在噪声范围内』;只有区间基本分离,才配得上『显著』这两个字。
反过来,当两个区间确实基本分离——比如新版下压到了旧版上界之上——那才敢理直气壮写『显著提升』,而且能顺带写清是怎么判的:一句『两次 95% 区间不重叠』,就能挡住所有『你这是不是运气』的口头争执。『显著』『明显』这类词,在报告里永远不该脱离动作单独出现;出现了,就得能回答『拿什么判的显著』。
四、报告里这几个字段,得并排站
区间不是孤立的一列,它得和几个字段捆在一起才读得懂。单看一个 [88%, 95%] 没意义,得知道它是 210 条跑出来的、得知道分母口径是同一批用例、得知道是哪一次运行。
| 字段 | 演示值 | 为什么必须并排出现 |
|---|---|---|
| 通过率(点估计) | 91% | 给个体感,但单独看会骗人 |
| 样本量 n | 210 | 决定区间宽度的根,没了它区间无从算起 |
| 95% 置信区间 | [88%, 95%] | 点估计的可信带,能不能卡线看它 |
| 口径 | 同一用例集、同一配置 | 防拿不同分母、不同配置的数字横比 |
| 运行时间 | 2026-09-24 01:12 · #4471 | 防拿旧一次的数冒充这一次 |
口径和运行时间这两栏看着像摆设,其实卡的是最常见的翻车:把上次大样本的区间宽度,套到这次小样本的通过率上,或者拿三个月前那次的漂亮数字给今天背书。
五、跑一遍:纯标准库把区间算出来
下面这段只用标准库,喂进去的就是本文演示构造的通过记录,开箱即跑。接自家工程时,把 records 换成你 pytest 结果里每条用例的 0/1,或多次运行攒出的通过率列表即可。
# -*- coding: utf-8 -*-
"""pass_rate_interval.py —— 通过率连置信区间一起交(纯标准库,开箱即跑)
用法:python pass_rate_interval.py
输入一组 0/1 通过记录,bootstrap 重采样算 95% 置信区间,
输出报告用文案『通过率 X%,95%CI [a%, b%],n=Y』。数据均为演示构造。
"""
import random
def point_estimate(records):
return sum(records) / len(records)
def bootstrap_ci(records, iters=10000, level=0.95, seed=20260924):
"""对 0/1 记录做 bootstrap:有放回重采样 n 次、每次算一个通过率,
把通过率分布排序后取分位数当置信区间(percentile 法)。"""
rng = random.Random(seed)
n = len(records)
means = sorted(sum(rng.choices(records, k=n)) / n for _ in range(iters))
lo = int((1 - level) / 2 * iters)
hi = int((1 + level) / 2 * iters) - 1
return means[lo], means[hi]
def fmt_pct(x):
return f"{x * 100:.0f}%"
def report_line(records, iters=10000):
p = point_estimate(records)
lo, hi = bootstrap_ci(records, iters=iters)
return f"通过率 {fmt_pct(p)},95%CI [{fmt_pct(lo)}, {fmt_pct(hi)}],n={len(records)}", hi - lo
def overlap(a, b):
"""重叠宽度占较窄区间的比例:>0.5 视为『分不出胜负』,别写显著提升。"""
inter = max(0.0, min(a[1], b[1]) - max(a[0], b[0]))
narrowest = min(a[1] - a[0], b[1] - b[0])
return inter / narrowest if narrowest else 1.0
def gen_demo_passes(n_true, n_total, seed):
rng = random.Random(seed)
recs = [1] * n_true + [0] * (n_total - n_true)
rng.shuffle(recs)
return recs
def main():
print("== 场景A:同一套用例,样本很小,点估计站不稳 ==")
for lab, rec in [("周一那次", gen_demo_passes(15, 17, seed=1)),
("手快重跑那次", gen_demo_passes(16, 17, seed=2))]:
line, width = report_line(rec)
print(f" {lab}:{line}(区间宽度 {fmt_pct(width)})")
print("\n== 场景B:同一套逻辑扩到上百条,通过率没动,区间却收窄 ==")
line2, width2 = report_line(gen_demo_passes(192, 210, seed=3))
print(f" {line2}(区间宽度 {fmt_pct(width2)})")
print("\n== 场景C:新旧两版通过率,先过区间重叠关 ==")
old, new = gen_demo_passes(90, 100, seed=4), gen_demo_passes(93, 100, seed=5)
lo_old, hi_old = bootstrap_ci(old)
lo_new, hi_new = bootstrap_ci(new)
print(f" 旧:{report_line(old)[0]}")
print(f" 新:{report_line(new)[0]}")
ov = overlap((lo_old, hi_old), (lo_new, hi_new))
verdict = "两区间大幅重叠,别写『显著提升』" if ov > 0.5 else "区间基本分离,可写『显著提升』"
print(f" 重叠比例 {ov * 100:.0f}% → {verdict}")
if __name__ == "__main__":
main()
两个刻意的处理值得点名。其一,bootstrap 固定了随机种子(seed=20260924),这样这份报告里的区间下次重算还是同一个——用随机方法去算一份要进报告的确定性数字,得先把重采样这层随机按住在报告里,不然『区间』本身又成了新的一颗珠子。其二,overlap 判的是『重叠宽度占较窄区间的比例』,不是简单看有没有交集;只要沾一点边就算分开太松,会把噪声差判成显著。脚本跑出来的三组数,就是前面几节引用的全部数字:n=17 两条宽到 [71%, 100%]、[82%, 100%],n=210 收到 [88%, 95%],新旧对比重叠 78% 判成『别写显著提升』。
六、挂进流水线:报告正文一行带区间
区间化这件事最好长在流水线里,否则又变成『想起来才做』。pytest 出结果之后加一步统计,直接写进 step summary:
- run: pytest --junitxml=report.xml
- run: python pass_rate_interval.py >> $GITHUB_STEP_SUMMARY
从此报告里那句通过率,天生带着它的区间和 n。谁想只贴那颗最顺眼的珠子,脚本第一行就把区间甩在他脸上。
写这行报告文案时,给个小的语序建议:先点估计给体感,再区间给幅度,最后 n 给底气——『通过率 91%(95%CI 88%–95%,n=210)』。别把区间放在最前面,读者抓不到锚;也别把 n 省在文末脚注里,忙的人根本不会往下翻,而 n 恰恰是那个能一票否决整句结论的数字。
七、回到那场评审会
下次有人拿你周报里那个漂亮的百分比对上『为什么又掉了』,答案不再是运气:那个数一直是带,不是点;宽的时候本就不该拿去签字,窄的时候才有资格和放行线比。把区间交出去,不是把报告写复杂,是终于把『我有多确定』这件一直在心里、却从没写下来的事,写下来了。
诚实的质量报告,报的不是一个通过率,是一整个『这个数大概落在哪』——只交点估计,等于把确定性的成本悄悄转嫁给了读报告的人。
- 点赞
- 收藏
- 关注作者
评论(0)