Learning AI Quality 返回 KuthorX Blog II

第 05 章

第 1 周:跑了 300 次,就是 300 个样本吗?

独立性假设与有效样本量。同一个用例重跑多次,失败会扎堆,区间要按用例来算。一段讲解视频,一个可以调相关程度的模拟器。

30 个用例,每个跑 10 次,一共 300 次,挂了 30 次。按 300 个独立样本算,失败率的 95% 区间是 [7.1%, 13.9%]。可如果这 30 次失败有一大半集中在 3 个难用例上,这 300 次还算 300 个样本吗?这一章讲前面所有公式共同的前提:独立性。

讲解视频

互动演示

调用例数、每个用例的重跑次数和簇内相关 ρ,看失败怎么扎堆、按独立算的区间实际能罩住真实值几次。页面底部有自动判分的练习。

互动演示:独立性假设 在新标签页打开

前面的公式都有同一个前提

前面每个公式都写着"每次试验相互独立、失败率相同",统计上叫独立同分布(i.i.d.):

概念用到独立性的地方
rule of three\( P(\text{全过}) = (1-p)^n \):n 次全过的概率等于单次通过概率连乘
Wilson 区间标准误 \( \sqrt{p(1-p)/n} \):n 个独立样本平均后,波动按 \( 1/\sqrt{n} \) 缩小
pass^k\( p^k \):k 次都成功的概率等于单次成功概率连乘

公式里只有 n 这个数字。n 到底该是多少,公式不会替你检查。

两种跑法,回答两个问题

同一个用例跑 30 次30 个不同用例各跑 1 次
估的是什么这一个用例的不稳定率(flaky 率)Agent 在这类任务上的平均失败率
随机性来自模型采样、环境抖动用例本身的难度差异,加上采样
30 次全过能说明这个用例的失败率 ≤ 9.5%这类任务整体失败率 ≤ 9.5%(前提是用例随机抽取)
说明不了换一个用例行不行某个具体用例稳不稳

两种跑法没有好坏,关键是结论要和跑法对得上。拿"同一个用例跑 30 次全过"去说"Agent 失败率 ≤ 10%",就是把一个用例的结论推广到了所有用例。

实际评测一般是两者结合:C 个用例,每个跑 m 次,一共 \( n = C \times m \) 次。麻烦在于同一个用例的 m 次结果彼此相关,难的用例总是挂,简单的总是过。

独立性是怎么被破坏的

来源表现后果
同一用例重跑用例难度不同,失败集中在少数难用例上有效样本接近用例数,而不是运行次数
temperature = 0 / 缓存同样的输入给出几乎一样的输出,甚至命中缓存30 次运行其实是 1 次运行复制了 30 份
共享环境状态上一次留下的数据库记录、浏览器 cookie、文件前一次失败连带后一次失败,或者掩盖失败
LLM Judge 系统性误判Judge 对某种输出格式总是判过或总是判错误判不会被平均掉,所有样本一起偏
同一时段 API 限流或故障某 10 分钟内的运行一起超时失败在时间上扎堆,和 Agent 的能力无关

共同点:多次运行共享了同一个随机因素,可能是用例、种子、环境、Judge 或者时间段。共享得越多,它们越像同一次运行。

簇内相关系数和设计效应

把同一个用例的 m 次运行看成一簇。簇内相关系数(ICC,记作 ρ)衡量同一簇里两次运行的结果有多像:

$$ \rho = \frac{\operatorname{Var}(p_i)}{\mu(1-\mu)} $$

\( p_i \) 是第 i 个用例自己的失败率,\( \mu \) 是所有用例的平均失败率。分子是用例之间的难度差异,分母是总波动。

  • ρ = 0:所有用例一样难,每次重跑都是一个全新的独立样本。
  • ρ = 1:每个用例要么永远挂、要么永远过,重跑没有任何新信息。

有了 ρ,就能算出 n 次运行相当于多少个独立样本:

$$ n_{\text{eff}} = \frac{n}{1 + (m-1)\rho} $$

分母 \( 1 + (m-1)\rho \) 叫设计效应(design effect)。区间宽度按 \( 1/\sqrt{n} \) 变化,所以区间会变宽约 \( \sqrt{\text{设计效应}} \) 倍。

这个公式有前提:每个用例的重跑次数 m 相同,所有用例共用同一个 ρ,用例是随机抽出来的,估计的是全部运行的总体失败率。各用例的 m 不相等时,可以用平均簇大小近似,或者直接用 cluster bootstrap。下面"按有效样本算 Wilson"是把 n 和 k 按同一比例缩到 n_eff,这也是一种近似。

代入数字

30 个用例 × 10 次 = 300 次,挂了 30 次,ρ = 0.3:

按独立算(n = 300)按有效样本算(n_eff ≈ 81)
设计效应11 + 9 × 0.3 = 3.7
95% Wilson 区间[7.1%, 13.9%][5.2%, 18.4%]
区间宽度6.8 个百分点13.3 个百分点,约 1.9 倍

按独立算的那个区间,在这种设置下重复实验,只有约 71% 的时候能罩住真实失败率。名义上是 95%。

ρ00.050.10.30.50.8
按独立算的实际覆盖率96%91%86%71%61%52%

表中数据:30 个用例 × 10 次,μ = 10%,每种 ρ 做 20 万次蒙特卡洛模拟。互动演示每次只模拟 2000 次,显示的数值会和表里差 1~2 个百分点。

加重跑次数,救不回来

固定 30 个用例、ρ = 0.3,只增加每个用例的重跑次数 m:

m1251030100∞
总运行次数30601503009003000∞
n_eff304668819398100

m 趋于无穷时,\( n_{\text{eff}} \to C/\rho = 30/0.3 = 100 \)。跑 3000 次也只顶 98 个独立样本。想要更多有效样本,要加用例,不是加重跑。

对策

做法解决什么
按用例聚类分析先算每个用例自己的失败率,再在用例层面汇总;报告里写清"C 个用例 × m 次",不要只写总次数
多加用例,少加重跑有效样本的上限是 C/ρ,由用例数决定
分层抽样按任务类型、难度分层抽用例,避免某一类用例占满样本
cluster bootstrap重抽样时以用例为单位整组抽,区间自动把簇内相关算进去
隔离环境每次运行用干净的数据库、新的浏览器上下文
每次运行换种子、关缓存避免"跑 30 次其实是同一次"
记录 trace,事后检查扎堆看失败是否集中在某些用例、某个时段、某种 Judge 输出;集中就说明独立性有问题

cluster bootstrap 的写法:

import numpy as np

def cluster_bootstrap_ci(fails: np.ndarray, n_boot: int = 10_000, seed: int = 0):
    """fails: 形状 (C, m) 的 0/1 矩阵,每行一个用例。返回平均失败率的 95% 区间。"""
    if fails.ndim != 2 or fails.size == 0:
        raise ValueError("fails 需要是非空的 (用例数, 重跑次数) 矩阵")
    rng = np.random.default_rng(seed)
    per_case = fails.mean(axis=1)                  # 每个用例自己的失败率
    idx = rng.integers(0, len(per_case), size=(n_boot, len(per_case)))
    means = per_case[idx].mean(axis=1)             # 整组抽用例,再求平均
    return np.percentile(means, [2.5, 97.5])

关键是 per_case[idx]:抽中一个用例,就带上它的全部 m 次结果。如果按单次运行抽,就又回到了"假装独立"。

面试追问

  • ρ 怎么估? 用单因素方差分析的组间、组内方差估;或者绕开 ρ,直接用 cluster bootstrap 算区间。
  • 什么时候可以不管? 每个用例只跑 1 次(m = 1)时设计效应就是 1。但这时要求用例本身是从任务分布里随机抽的。
  • temperature = 0 就一定没用吗? 对"这个用例稳不稳"没用,重跑 30 次几乎是同一个结果。对"这批用例整体通过率"没影响,因为那时的样本是用例。
  • 两个版本怎么比? 让两个版本跑同一批用例,按用例配对比较,用例难度的影响会在配对里抵消。这就是后面要学的 McNemar 检验和配对 bootstrap。

常见错误说法

  • “跑了 300 次挂了 30 次,95% 区间 [7.1%, 13.9%]”:没交代几个用例 × 几次,默认成了 300 个独立样本。
  • “同一个用例跑了 30 次全过,所以 Agent 失败率 ≤ 10%”:这只说明这一个用例,换个用例不一定成立。
  • “区间太宽,每个用例再多跑 100 次”:ρ 大的时候加重跑几乎没用,要加用例。
  • “bootstrap 按单次运行重抽”:抽样单位必须是用例,否则区间和按独立算的一样窄。