Learning AI Quality 返回 KuthorX Blog II博客首页

第 33 章

第 7 周:七个配置选哪个?成本、延迟、质量的帕累托前沿

7 个模型配置在同一批 BugHunt-Bench 任务上各跑一次:质量报找到率加 Wilson 区间,成本按 token 口径算均值,延迟报 p50 / p95 而不是均值;先排除延迟不达标的、划掉被支配的,得到帕累托前沿;再用配对 bootstrap 算前沿概率、用非劣效挑点,并把整个实验重做 1000 次,看按点估计挑配置有多不可靠。全部用合成数据在本地跑。一段讲解视频,两个互动演示。

面试题"能不能把模型换成便宜的?“背后,其实是一张三根轴的图:质量、成本、延迟。手上有 7 个候选配置(模型档位 × 推理强度,再加一个关掉缓存的),在同一批任务上各跑一遍:质量最高的 F(大·高)找到的 Bug 最多;最便宜的 A 延迟只有它的四分之一左右、成本便宜二十多倍。选哪个?第一步是把"又贵、又慢、还不更好"的配置划掉,剩下的连起来就是帕累托前沿。但这条前沿是用估计值画出来的:200 条任务的找到率,自身就有十几个百分点宽的区间,前沿上的点会因为抽样误差混进来,也会因为抽样误差被挤掉。这一章把第 1 周的 Wilson 区间和配对比较用到"挑配置"上。

本章数据都是合成数据:不调任何模型 API,7 个配置的"能力”、轮数、token、延迟都按设定好的规则生成,所以知道每个配置的真实质量,可以检验"按点估计挑"到底挑得对不对。价格全部是示例价格(合成),只为说明计算方法,不要当作任何真实价格(2026-10-01 对照过 Claude、OpenAI、Google 的公开价格表,这几个数没有和任何一档相同)。所有结论都只在这个合成设定下成立。

讲解视频

互动演示

两个演示:成本–质量散点图,可以切换任务数、拖延迟门槛、叠加真实值,表格里实时算 Wilson 区间、p95 的 bootstrap 区间和每个配置的前沿概率,下面还能任选两个配置做配对比较和非劣效判断;延迟直方图,可以把 5 次运行换成 1800 秒超时,看均值、p50、p95 各动多少。页面里的 bootstrap 用和 Python 逐位一致的随机数,默认设置下的数字和正文相同。页面底部有自动判分的练习。

互动演示:成本、延迟、质量与帕累托前沿 在新标签页打开

合成实验:7 个配置 × 200 条任务

场景沿用 BugHunt-Bench:测试 Agent 在被测 Web 应用里找注入的 Bug。每条任务 = 一个应用快照 + 一个注入的 Bug,找到算成功。BugHunt-Bench 的计划只注入 20~30 个 Bug;这里假设有 200 个这样的「快照 + Bug」组合,是为了让区间窄到能看出结构,后面会看到,就算 200 条也常常不够。

配置模型档位推理强度说明
A 小·低小低最便宜、最快
B 小·高小高轮数和每轮输出都更多
C 中·低中低
D 中·高中高
E 大·低大低真实质量比 D 略低,但更贵
F 大·高大高真实质量最高,也最慢
G 大·高·关缓存大高和 F 完全一样,只是不用缓存

生成规则(synthetic.py):

  • 找到没找到:任务难度 \(d \sim N(0, 1.3^2)\),配置能力为 \(a\),找到的概率是 \(\sigma(a - d)\)。同一条任务对所有配置难度相同,所以结果是配对的:难的任务大家都容易挂。真实质量 = \(E_d[\sigma(a-d)]\),用数值积分算,单元测试里用 40 万次蒙特卡洛核对过。
  • 轮数:找到 Bug 的运行轮数少;没找到的会一直探索,直到轮数上限。
  • token:每轮重发全部历史,第 \(k\) 轮输入 \(6000 + 2500(k-1)\) 个 token(第 2 周「Agent 到底是什么?一个 while 循环」的平方增长);开缓存时,第 \(k \ge 2\) 轮命中上一轮的整段前缀。
  • 延迟:每轮 = 首 token 延迟 + 输出时间 + 一次浏览器操作(对数正态),每轮 2% 概率碰上 30 秒卡顿,每次运行 3% 概率碰上 180 秒的页面挂起。另外每次运行加上未命中缓存的输入的处理时间(按每秒 2 万 token 算),所以关缓存的 G 比 F 慢。

每个配置在同一批 200 条任务上各跑一次(种子 7)。run_experiment.py 的输出:

配置真实质量观测找到率Wilson 95%平均成本每找到 1 个 Bug延迟 p50延迟 p95
A 小·低48.1%101/200 = 50.5%[43.6%, 57.4%]$0.0240$0.047692 秒168 秒
B 小·高58.4%121/200 = 60.5%[53.6%, 67.0%]$0.0468$0.0774175 秒319 秒
C 中·低63.8%121/200 = 60.5%[53.6%, 67.0%]$0.0937$0.1548107 秒210 秒
D 中·高72.1%142/200 = 71.0%[64.4%, 76.8%]$0.1813$0.2553244 秒449 秒
E 大·低70.5%150/200 = 75.0%[68.6%, 80.5%]$0.3431$0.4575148 秒290 秒
F 大·高79.3%175/200 = 87.5%[82.2%, 91.4%]$0.6416$0.7333386 秒707 秒
G 大·高·关缓存79.3%162/200 = 81.0%[75.0%, 85.8%]$2.1007$2.5935412 秒786 秒

“真实质量"一列只有合成数据才有。真实做评测时,你手上只有后面几列。

三根轴,各用什么数

质量:找到率,带区间

只报 142/200 = 71.0% 不够。第 1 周「30 次挂了 2 次,失败率在什么范围?」讲过:只报点估计、不报区间,等于没交代这个数字有多可信。200 条任务、找到率 70% 左右时,Wilson 95% 区间宽约 12 个百分点(D 是 [64.4%, 76.8%])。

上表 7 个区间里,F 的 [82.2%, 91.4%] 没罩住它的真实值 79.3%。95% 区间本来就有约 5% 的时候会漏,同时看 7 个,漏一个不稀奇;这也提醒我们,“观测最好"的那个配置,往往是运气也最好的那个。

成本:均值,以及"每找到一个 Bug 的花费”

成本报均值。理由很简单:预算是加起来的,跑 N 条任务的总花费 = N × 均值,中位数乘回去对不上总账。中位数还会漏掉那些跑满轮数、特别贵的运行。

另外报一个每找到一个 Bug 的花费 = 总花费 / 找到数。没找到 Bug 的运行轮数多、反而更贵,这个数把质量和成本合在一起:D 每找到一个 Bug 花 $0.2553,E 花 $0.4575。

延迟:p50 和 p95,不报均值

延迟反过来,要报分位数:p50 看典型的一次,p95 看最慢的 5%。不用均值有四个原因:

  1. 分布右偏。在这个合成设定下,7 个配置的均值都比 p50 高 7%~14%。均值既不是"典型的一次”,也不是"最慢的那些"。
  2. 两群混在一起。D 找到 Bug 的运行 p50 是 216 秒,没找到的是 386 秒:没找到的往往一直探索到轮数上限。均值把两群平均成一个不对应任何一次运行的数。
  3. 几次超时就能把均值拉走。把 D 的 200 次运行里 5 次换成 1800 秒超时:均值 275 → 312 秒,p50 244 → 245 秒,p95 449 → 471 秒。均值涨了 13%,但看均值分不出是"整体变慢"还是"几次卡死";p50 不动、p95 上升,一眼看出是尾巴变长了。
  4. 门槛写在尾巴上。“95% 的任务 10 分钟内跑完"就是 p95 ≤ 600 秒,和用户体验、超时设置直接对应。Google 的 SRE 书第 4 章 Service Level Objectives 也是这个态度,原文:We generally prefer to work with percentiles rather than the mean (arithmetic average) of a set of values.

两个细节:

  • 写清楚分位数的算法。本章 Python 和网页都用线性插值(numpy.percentile 的默认算法)。不同工具默认算法不同,样本少时会差出可见的量。
  • p95 自己也有误差。200 个数的 p95 落在排序后第 190 和第 191 个之间,基本由最慢的 10 次决定。按任务 bootstrap:D 的 p95 区间是 [423, 470] 秒(相对宽约 10%),F 是 [644, 779] 秒(约 19%):越是被几次卡顿决定的配置,p95 越不稳。作为对照,F 的平均成本区间 [$0.6091, $0.6765] 相对宽约 10%,D 是 [$0.1712, $0.1919],约 11%。

成本:先把 token 口径对齐

一次运行的花费按三类 token 分别计价:

$$ \text{cost} = \frac{(\text{input} - \text{cached})\cdot p_\text{in} + \text{cached}\cdot p_\text{cache} + \text{output}\cdot p_\text{out}}{10^6} $$
档位输入 \(p_\text{in}\)缓存命中 \(p_\text{cache}\)输出 \(p_\text{out}\)
小0.270.0271.35
中1.150.1155.75
大4.700.4723.50

单位美元 / 百万 token,全部是示例价格(合成):只为说明计算方法,不要当作任何真实价格。

公式里的 input 包含缓存命中的部分,这是 Codex 记录 token 用量的口径。 codex-rs/protocol/src/protocol.rs:2240-2260 定义的 TokenUsage(省略了序列化注解和一个内部的预算计数字段 codex_rollout_budget_units):

pub struct TokenUsage {
    // ...
    pub input_tokens: i64,
    // ...
    pub cached_input_tokens: i64,
    // ...
    pub cache_write_input_tokens: i64,
    // ...
    pub output_tokens: i64,
    // ...
    pub reasoning_output_tokens: i64,
    // ...
    pub total_tokens: i64,
    // ...
}

protocol.rs:2426-2437 的三个方法说明了口径:

    pub fn cached_input(&self) -> i64 {
        self.cached_input_tokens.max(0)
    }

    pub fn non_cached_input(&self) -> i64 {
        (self.input_tokens - self.cached_input()).max(0)
    }

    /// Primary count for display as a single absolute value: non-cached input + output.
    pub fn blended_total(&self) -> i64 {
        (self.non_cached_input() + self.output_tokens.max(0)).max(0)
    }

逐行看:i64 是 64 位整数;.max(0) 把负数截成 0,防止上游报来的脏数据算出负的 token。non_cached_input = input_tokens - cached_input_tokens,说明 input_tokens 里已经包含了命中缓存的部分。blended_total 是 Codex 拿来显示的"一个总数”:未命中缓存的输入加输出,不算缓存命中。这个结构体里只有 token 计数和一个预算单位计数,没有金额字段,钱要自己按价格表算。

Codex 的 input_tokens 是总输入: codex-rs/codex-api/src/sse/responses.rs:128-134 把 Responses API 返回的 input_tokens 原样填进来,cached_input_tokens 取自 input_tokens_details.cached_tokens,是总输入里的一个细分。Claude Messages API 的 usage.input_tokens 则是没走缓存的剩余部分,总输入 = input_tokens + cache_creation_input_tokens + cache_read_input_tokens(第 2 周的最小实现里也提醒过要把后两项加上)。字段名一样、口径不同:一个是总输入,一个是未走缓存的剩余部分。把两边的日志接进同一张成本报表时,口径弄混一次,成本就差好几倍:

合成数据里 D 的第 1 条任务:11 轮,input_tokens = 203,500,其中 cached_input_tokens = 172,500,output_tokens = 10,556。按中档示例价格(合成)是 $0.1162;如果把含缓存的 203,500 全部当成未命中、按全价算,会算成 $0.2947,是正确值的 2.5 倍。

本章的成本模型做了简化:没有算写入缓存的加价。Claude API 对写缓存另外计价(5 分钟 TTL 是基础输入价的 1.25 倍),Codex 的 TokenUsage 也单独记了 cache_write_input_tokens。真实报表要按各家的价格表把这一项也算上。

帕累托前沿:先划掉被支配的

配置 \(j\) 支配配置 \(i\):\(j\) 不比 \(i\) 贵,质量不比 \(i\) 低,并且至少一项严格更好:

$$ \text{cost}_j \le \text{cost}_i,\qquad q_j \ge q_i,\qquad \text{且至少一个不等号严格} $$

没有被任何配置支配的点,构成帕累托前沿。前沿上的点之间没有"谁更好",只有"多花多少钱、换多少质量";被支配的点,怎么挑都轮不到它。

延迟一般当门槛用:先把 p95 超过 600 秒的配置排除(不可行),在剩下的里面画成本–质量前沿。F(p95 707 秒)和 G(786 秒)因此出局。三项都当目标也可以,定义一样,只是多一个不等号;但实际业务里延迟多半是"达标就行",当门槛更贴切。

在这份数据上:

前沿上的配置
点估计前沿A 小·低、B 小·高、D 中·高、E 大·低
真实前沿(用总体值算)A 小·低、B 小·高、C 中·低、D 中·高

差了两个,都来自抽样误差:

  • C 被误杀。C 的真实质量 63.8%,比 B 的 58.4% 高;这次观测两者都是 121/200,C 更贵,于是被 B 支配掉了。
  • E 混了进来。E 的真实质量 70.5%,比 D 的 72.1% 低,还更贵,本该被 D 支配;这次观测是 75.0% 对 71.0%,挤上了前沿。

另一组更直观:G 和 F 是同一个配置,只差缓存,真实质量完全相同,这次观测却差了 6.5 个点(81.0% 对 87.5%)。

给前沿也加上误差

两个配置比,用配对

7 个配置跑的是同一批任务,比较两个配置是配对数据,做法和第 1 周「新 prompt 从 82% 涨到 86%,是真的变好了吗?」一样:按任务整条重抽(配对 bootstrap),或者对结果不一致的任务做 McNemar 精确检验。不要拿两个 Wilson 区间看重不重叠,那是不配对的做法,扔掉了"同一批任务"这个信息。

比较观测差配对 bootstrap 95% 区间McNemar 精确 p成本比
E 大·低 − D 中·高+4.0 个点[−3.5, +11.5]0.358(分歧 33 : 25)1.89×
C 中·低 − B 小·高+0.0 个点[−8.5, +8.5]1.000(38 : 38)2.00×
G 关缓存 − F 大·高−6.5 个点[−13.0, +0.0]0.079(17 : 30)3.27×

bootstrap 2000 次,按任务配对重抽。三组都不显著,区间都包含 0(G − F 的上界恰好是 0.0)。

能不能多跑点任务分出 D 和 E?它们的真实差是 D 高 1.58 个点,两者在约 31.7% 的任务上结果不一致。用第 1 周「要跑多少次才够?」里的配对样本量公式(Connor, 1987),双侧 α = 5%、功效 80%,大约需要 9,922 条任务。BugHunt-Bench 的计划是注入 20~30 个 Bug,差了两个数量级。在几百条任务的规模下,质量相近的配置多半就是分不出来,这时决定选哪个的,应该是成本和延迟。

前沿概率:每次重抽都重画一次前沿

单看两两比较还不够,前沿是 7 个配置一起决定的。做法(本文的做法):按任务配对重抽 2000 次,每次用重抽出的找到率、平均成本、p95 重新判断能不能达标、重新画前沿,数每个配置落在前沿上的比例。

配置200 条任务只用前 50 条
A 小·低100.0%100.0%
B 小·高99.0%62.6%
C 中·低46.0%72.8%
D 中·高99.4%97.0%
E 大·低83.0%15.6%
F 大·高0.0%(p95 始终超标)22.9%(p95 在 23.4% 的重抽里达标)
G 大·高·关缓存0.0%1.6%

读法:A、B、D 在 200 条任务时几乎每次都在前沿上,可以放心;C 和 E 一半一半或者大多数时候在,这份数据还定不下来它们在不在前沿上。只用 50 条任务时更乱,连 F 的 p95 都有 23.4% 的重抽里"达标":p95 由最慢的两三次运行决定,样本少时同样不可靠。

要说清楚一点:前沿概率是重抽结果里的频率,用来看点估计前沿稳不稳,不是"E 在真实前沿上的概率是 83%"。真实前沿是固定的,E 其实不在上面。83% 的意思是:重抽里有 83% 的时候 E 的找到率仍高于 D,所以 E 还在前沿上;但这不足以排除"其实 D 更好"。

挑点时把不确定性算进去

“在可行配置里选观测质量最高的"会选中 E:多花 89% 的钱,换一个证据不足的 4 个点,而它的真实质量反而低 1.58 个点。笔者建议的做法(不是唯一标准):

  1. 事先定好业务能接受的质量下降界值 δ(比如 5 个百分点)和延迟门槛,写在跑评测之前,不要看完结果再定。

  2. 从最便宜的可行配置往上看,第一个对"观测最好的那个"满足非劣效的就是候选:配对差 95% 区间的下界 > −δ。这是第 1 周「新 prompt 从 82% 涨到 86%,是真的变好了吗?」里换便宜模型那一节的做法。

    按字面执行这条规则,A、B、C、D 都过不了非劣效,第一个满足的就是 E 自己(它和自己比,差恒为 0)。也就是说,规则在这份数据上给不出更便宜的选项;D 只能作为"质量证据不够、但愿意承担风险换成本"的候选,单独拿出来讨论:

  3. D − E 的区间是 [−11.5, +3.5],下界 −11.5 不满足 δ = 5:证据不够。要么加任务,要么明确写下"选 D,接受质量可能差到 11.5 个点的风险,换来省下 47% 的成本”。不要把"不显著"说成"一样好"。

把整个实验重做 1000 次

合成数据可以重来:每次重新抽任务、重新跑 7 个配置,用点估计画前沿、挑配置,再和真实值比。真实最优的可行配置是 D(总体平均 $0.1810 / 条)。

任务数 n点估计前沿 = 真实前沿“选观测质量最高的可行配置"选中 E选中配置的平均成本
5028.6%34.4%$0.2337
20056.5%35.3%$0.2427
80079.1%20.4%$0.2171

三点观察(都只在这个合成设定下成立):

  • 200 条任务时,点估计画出来的前沿只有 56.5% 的时候和真实前沿完全一致。
  • 选错主要亏在钱上:选中 E 时真实质量只低 1.58 个点,但 E 的总体成本是 D 的 1.98 倍($0.3580 对 $0.1810);n = 200 时,按这个规则选中的配置平均每条任务 $0.2427,比 D 多花约 34%。
  • 任务数翻 4 倍(200 → 800),选中 E 的比例也只从 35.3% 降到 20.4%。D 和 E 离得太近,加任务的边际收益很低;这时与其继续加任务,不如承认两者分不出来、按成本选。

动手实验

代码在学习目录的 week07_回放评测与模型对比/code/cost_latency_quality/,依赖 numpy 和 scipy(只用 scipy.stats.binomtest 算 McNemar 精确 p):

文件内容
stats.pyWilson 区间、线性插值分位数、mulberry32 随机数、配对 bootstrap 下标、配对样本量
costmodel.py示例价格(合成)、按 token 口径算成本、帕累托前沿
synthetic.py生成 7 个配置 × N 条任务的合成运行数据,算真实质量
analysis.py点估计汇总、bootstrap、前沿概率、配对差
run_experiment.py打印本章所有数字,导出互动演示用的 data.json
test_cost_latency_quality.py18 个单元测试

核心两个函数可以单独拿出来跑:

def cost_usd(input_tokens, cached_input_tokens, output_tokens, p_in, p_cache, p_out):
    """input_tokens 含缓存命中部分(Codex TokenUsage 的口径);价格单位:美元 / 百万 token。"""
    if not 0 <= cached_input_tokens <= input_tokens or output_tokens < 0:
        raise ValueError("需要 0 <= cached_input_tokens <= input_tokens,且 output_tokens >= 0")
    non_cached = input_tokens - cached_input_tokens
    return (non_cached * p_in + cached_input_tokens * p_cache + output_tokens * p_out) / 1e6


def pareto_front(costs, quals, feasible=None):
    """成本越低越好、质量越高越好。返回每个点是否在(可行点里的)帕累托前沿上。"""
    n = len(costs)
    ok = list(feasible) if feasible is not None else [True] * n
    front = []
    for i in range(n):
        if not ok[i]:
            front.append(False)
            continue
        dominated = any(
            ok[j] and j != i and costs[j] <= costs[i] and quals[j] >= quals[i]
            and (costs[j] < costs[i] or quals[j] > quals[i])
            for j in range(n)
        )
        front.append(not dominated)
    return front


# 示例价格(合成),中档:输入 1.15、缓存命中 0.115、输出 5.75(只为说明计算方法)
print(round(cost_usd(203_500, 172_500, 10_556, 1.15, 0.115, 5.75), 4))
# 本章合成数据的点估计:A~G 的平均成本、找到率、p95 是否 <= 600 秒
costs = [0.0240, 0.0468, 0.0937, 0.1813, 0.3431, 0.6416, 2.1007]
quals = [0.505, 0.605, 0.605, 0.710, 0.750, 0.875, 0.810]
feas = [True, True, True, True, True, False, False]
print([k for k, on in zip("ABCDEFG", pareto_front(costs, quals, feas)) if on])

输出:

0.1162
['A', 'B', 'D', 'E']

前沿概率就是在每次重抽上调一遍 pareto_front:

def front_probability(boot, keys, slo):
    """boot[k] 里是按任务配对重抽得到的 q / cost / p95 数组。每次重抽都重新判断可行性、重画前沿。"""
    n_boot = len(boot[keys[0]]["q"])
    on = {k: 0 for k in keys}
    for b in range(n_boot):
        costs = [boot[k]["cost"][b] for k in keys]
        quals = [boot[k]["q"][b] for k in keys]
        feas = [boot[k]["p95"][b] <= slo for k in keys]
        for k, o in zip(keys, pareto_front(costs, quals, feas)):
            on[k] += o
    return {k: on[k] / n_boot for k in keys}

关键是"配对”:所有配置共用同一组重抽下标,抽中第 17 条任务,就同时带上 7 个配置在这条任务上的结果、成本和延迟。

python3 run_experiment.py(Python 3.9、numpy 1.22、scipy 1.8,约 25 秒,大部分时间花在重做 1000 次实验上)的完整输出:

== 1. 合成数据:200 条任务 × 7 个配置(种子 7)
示例价格(合成),美元 / 百万 token:小 输入 0.27 / 缓存命中 0.027 / 输出 1.35;中 输入 1.15 / 缓存命中 0.115 / 输出 5.75;大 输入 4.7 / 缓存命中 0.47 / 输出 23.5
A 小·低      真实 48.1%  观测 101/200 = 50.5%  Wilson [43.6%, 57.4%]  平均成本 $0.0240  每找到一个 Bug $0.0476  延迟 均值 98s  p50 92s  p95 168s  (总体 p95 170s,总体成本 $0.0241)
B 小·高      真实 58.4%  观测 121/200 = 60.5%  Wilson [53.6%, 67.0%]  平均成本 $0.0468  每找到一个 Bug $0.0774  延迟 均值 192s  p50 175s  p95 319s  (总体 p95 323s,总体成本 $0.0474)
C 中·低      真实 63.8%  观测 121/200 = 60.5%  Wilson [53.6%, 67.0%]  平均成本 $0.0937  每找到一个 Bug $0.1548  延迟 均值 119s  p50 107s  p95 210s  (总体 p95 207s,总体成本 $0.0921)
D 中·高      真实 72.1%  观测 142/200 = 71.0%  Wilson [64.4%, 76.8%]  平均成本 $0.1813  每找到一个 Bug $0.2553  延迟 均值 275s  p50 244s  p95 449s  (总体 p95 463s,总体成本 $0.1810)
E 大·低      真实 70.5%  观测 150/200 = 75.0%  Wilson [68.6%, 80.5%]  平均成本 $0.3431  每找到一个 Bug $0.4575  延迟 均值 168s  p50 148s  p95 290s  (总体 p95 290s,总体成本 $0.3580)
F 大·高      真实 79.3%  观测 175/200 = 87.5%  Wilson [82.2%, 91.4%]  平均成本 $0.6416  每找到一个 Bug $0.7333  延迟 均值 423s  p50 386s  p95 707s  (总体 p95 772s,总体成本 $0.6948)
G 大·高·关缓存  真实 79.3%  观测 162/200 = 81.0%  Wilson [75.0%, 85.8%]  平均成本 $2.1007  每找到一个 Bug $2.5935  延迟 均值 454s  p50 412s  p95 786s  (总体 p95 815s,总体成本 $2.1852)

== 2. 帕累托前沿(成本低、质量高;p95 <= 600s 才算可行)
点估计前沿: A小·低 B小·高 D中·高 E大·低
真实前沿:   A小·低 B小·高 C中·低 D中·高
可行配置里观测质量最高:E大·低(75.0%,真实 70.5%,平均成本 $0.3431)

== 3. 配对比较(同一批 200 条任务;bootstrap 2000 次,mulberry32 种子 2026)
E大·低 - D中·高:观测差 +4.0 个点  95% 区间 [-3.5, +11.5]  E 更好的比例 83.0%  分歧 E过D挂 33 / E挂D过 25  McNemar 精确 p = 0.358  成本比 1.89x
C中·低 - B小·高:观测差 +0.0 个点  95% 区间 [-8.5, +8.5]  C 更好的比例 46.0%  分歧 C过B挂 38 / C挂B过 38  McNemar 精确 p = 1.000  成本比 2.00x
G大·高·关缓存 - F大·高:观测差 -6.5 个点  95% 区间 [-13.0, +0.0]  G 更好的比例 2.3%  分歧 G过F挂 17 / G挂F过 30  McNemar 精确 p = 0.079  成本比 3.27x
非劣效(界值 5 个点):D - E 的 95% 区间下界 -11.5,不能说明 D 不比 E 差 5 个点以上
真实差 D - E = +1.58 个点,真实不一致率 ψ = 31.7%;要以 80% 功效检出它,配对设计约需 9,922 条任务
D中·高 p95 的 bootstrap 95% 区间:[423s, 470s]  平均成本区间:[$0.1712, $0.1919]
F大·高 p95 的 bootstrap 95% 区间:[644s, 779s]  平均成本区间:[$0.6091, $0.6765]

== 4. 前沿概率:每次重抽都重算前沿(含 p95 达标判断),2000 次
前 200 条任务:点估计前沿 ABDE;A 100.0%  B 99.0%  C 46.0%  D 99.4%  E 83.0%  F 0.0%(达标 0.0%)  G 0.0%(达标 0.0%)
前 50 条任务:点估计前沿 ABCD;A 100.0%  B 62.6%  C 72.8%  D 97.0%(达标 97.9%)  E 15.6%  F 22.9%(达标 23.4%)  G 1.6%(达标 2.3%)

== 5. 为什么不用平均延迟
A小·低:均值 98.4s  p50 92.3s  p95 168.1s  均值/p50 = 1.07
B小·高:均值 192.0s  p50 175.3s  p95 319.1s  均值/p50 = 1.10
C中·低:均值 119.1s  p50 107.2s  p95 210.2s  均值/p50 = 1.11
D中·高:均值 275.5s  p50 244.4s  p95 449.4s  均值/p50 = 1.13
E大·低:均值 168.1s  p50 147.8s  p95 289.8s  均值/p50 = 1.14
F大·高:均值 423.0s  p50 386.3s  p95 706.7s  均值/p50 = 1.10
G大·高·关缓存:均值 454.0s  p50 411.5s  p95 786.3s  均值/p50 = 1.10
D:找到 Bug 的运行 p50 216s,没找到的 p50 386s(没找到的往往跑满轮数)
把 D 的 5 次运行换成 1800s 超时:均值 275s -> 312s,p50 244s -> 245s,p95 449s -> 471s

== 6. 把整个实验重做 1000 次(每次重新抽任务、重新跑)
n= 50:点估计前沿和真实前沿完全一致 28.6%;「选观测质量最高的可行配置」选中 A:0.1% B:2.4% C:10.0% D:52.6% E:34.4% F:0.4% G:0.1%;选中不在真实前沿上的配置 34.9%;真实质量平均比 D 低 1.70 个点,平均成本 $0.2337(真实最优 D 成本 $0.1810)
n=200:点估计前沿和真实前沿完全一致 56.5%;「选观测质量最高的可行配置」选中 B:0.1% C:0.7% D:63.9% E:35.3%;选中不在真实前沿上的配置 35.3%;真实质量平均比 D 低 0.63 个点,平均成本 $0.2427(真实最优 D 成本 $0.1810)
n=800:点估计前沿和真实前沿完全一致 79.1%;「选观测质量最高的可行配置」选中 D:79.6% E:20.4%;选中不在真实前沿上的配置 20.4%;真实质量平均比 D 低 0.32 个点,平均成本 $0.2171(真实最优 D 成本 $0.1810)

== 7. 按 Codex TokenUsage 的口径算一次运行的成本(input_tokens 含缓存命中)
D 第 1 条任务:11 轮  input_tokens=203,500  cached_input_tokens=172,500  output_tokens=10,556  -> $0.1162(和向量化结果 $0.1162 一致)
口径弄错:把含缓存的 input_tokens 全部当成未命中、按全价算 -> $0.2947,是正确值的 2.5 倍

已导出 data.json(21,513 字节)

python3 -m unittest -v test_cost_latency_quality.py 的 18 个测试全部通过,其中几个是对推导和实现的数值核对:Wilson 和 scipy 的 proportion_ci(method="wilson") 一致,并和第 1 周手算的 30 次挂 2 次对上;Wilson 在 n = 200、p = 0.72 时的覆盖率蒙特卡洛落在 93.5%~96.5%;token 累计的闭式公式和逐轮累加一致;向量化的成本和逐条按 cost_usd 算的一致;真实质量的数值积分和 40 万次蒙特卡洛一致;不一致率 ψ 的积分和 10 万条模拟一致;配对样本量公式和第 1 周的表(ψ = 10% 时 312 条)一致;mulberry32 的输出和 node 跑出来的逐位相同。

测开视角

  • 评测报告固定三列。每个候选配置:找到率和 Wilson 区间、每条任务平均成本和每找到一个 Bug 的成本、延迟 p50 / p95。只有一列的报告(“新模型找到率高了 4 个点”)没法拿来做决定。
  • 门槛和界值写在跑之前。延迟门槛、预算、非劣效界值 δ 都是业务决定,先写进评测配置,再跑。看完结果再定门槛,很容易定成"刚好让自己喜欢的那个配置通过"。
  • 成本报表要写口径。日志字段从哪个 SDK 来、input_tokens 含不含缓存、价格表是哪天的,都写在报表里。换一家模型或换一个 SDK 版本时,先用一条已知数据核对一遍成本算法。
  • 前沿概率低的点,不急着下结论。前沿概率低于 90%(笔者的经验门槛,不是标准)的配置,要么加任务,要么承认分不出来、按成本和延迟选。真正在意的那两个配置,单独做配对比较和非劣效。
  • 重跑时任务集要固定。配对比较的前提是同一批任务;任务集一变,上一轮的结果就不能直接和这一轮配对。

常见错误说法

  • “E 的找到率最高,选 E”:观测差 4 个点,配对区间 [−3.5, +11.5],还贵 89%。在这份合成数据里,E 的真实质量反而比 D 低 1.58 个点。
  • “平均延迟 275 秒,满足 10 分钟的要求”:门槛管的是 p95,不是均值;均值还会被几次超时拉走。
  • “成本也该报中位数,更稳健”:预算是加起来的,只有均值乘回去等于总花费。
  • “C 和 B 都是 60.5%,C 更贵,所以 C 不用考虑”:两个点估计相等不代表真实质量相等,C 在 46.0% 的重抽里仍在前沿上,它的真实质量其实比 B 高。
  • “差异不显著,所以便宜的配置一样好”:要看差值区间的下界,过不了非劣效就是证据不够。
  • “两个配置的 Wilson 区间重叠,所以没差别”:同一批任务是配对数据,要做配对比较;区间重叠本身也不是检验。
  • “日志里的 input_tokens 乘输入单价就是输入成本”:先确认这个字段是总输入还是未走缓存的剩余部分。Codex 的 input_tokens 是总输入(含缓存命中),Claude API 的是剩余部分。