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

先把被支配的配置划掉(又贵、又慢、还不更好),剩下的连成帕累托前沿,再按预算和延迟门槛在前沿上挑。但前沿是用估计值画的,质量的区间有好几个百分点宽,前沿本身也会跟着抖。

所有数据都是合成的:7 个配置在同一批 200 条 BugHunt-Bench 任务上各跑一次,生成过程是设定好的,所以知道每个配置的真实质量,可以检验"按点估计挑"到底挑得对不对。

1三根轴,各用什么数

场景:测试 Agent 在被测 Web 应用里找注入的 Bug。每条任务 = 一个应用快照 + 一个注入的 Bug,找到算成功。7 个配置是"模型档位(小 / 中 / 大)× 推理强度(低 / 高)",外加一个"大·高·关缓存"。BugHunt-Bench 的计划只注入 20~30 个 Bug;这里假设有 200 个这样的"快照 + Bug"组合,好让区间窄到能看出结构,后面会看到 200 条也常常不够。

轴报什么为什么
质量找到率 k / n,加 Wilson 区间200 条任务的找到率本身就有好几个百分点的抽样误差
成本每条任务的平均花费;另报"每找到一个 Bug 的花费"预算是加起来的:总花费 = 任务数 × 均值,所以成本用均值正合适
延迟p50 和 p95,不报均值延迟分布右偏、有长尾,门槛(SLO)管的是慢的那一头

为什么延迟不用均值

  1. 分布右偏。在这个合成设定下,7 个配置的均值都比 p50 高 7%~14%。均值描述不了"典型的一次",也描述不了"最慢的那些"。
  2. 没找到 Bug 的运行更慢。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 章原文:We generally prefer to work with percentiles rather than the mean (arithmetic average) of a set of values.

分位数的算法也要写清楚:本页和 Python 代码都用线性插值(numpy.percentile 的默认算法)。200 个数的 p95 落在排序后第 190 和第 191 个之间,基本由最慢的 10 次决定,所以 p95 自己也有不小的误差,下面会给它也算 bootstrap 区间。

2成本:先把 token 口径对齐

一次运行的花费按三类 token 分别计价(价格是示例价格(合成),单位美元 / 百万 token,只为说明计算方法,不要当作任何真实价格;2026-10-01 对照过 Claude、OpenAI、Google 的公开价格表,这几个数没有和任何一档相同):

$$\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

上面公式里的 input 包含缓存命中的部分,这是 Codex 的 TokenUsage 的口径:它的 non_cached_input() 就是 input_tokens - cached_input_tokens(codex-rs/protocol/src/protocol.rs:2426-2437);它的 input_tokens 原样取自 Responses API 返回的 input_tokens,cached_input_tokens 是其中的细分(codex-rs/codex-api/src/sse/responses.rs:128-134)。Claude Messages API 的 usage.input_tokens 则是没走缓存的剩余部分,总输入 = input_tokens + cache_creation_input_tokens + cache_read_input_tokens。字段名一样、口径不同:一个是总输入,一个是未走缓存的剩余部分。把两边的日志接进同一张成本报表时,口径弄混一次,成本就差好几倍。

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

Agent 的成本还有一层:每轮都重发全部历史(第 2 周「Agent 到底是什么?一个 while 循环」里的平方增长),所以轮数多的运行贵得多。没找到 Bug 的运行轮数多,"每找到一个 Bug 的花费" = 总花费 / 找到数,比"每条任务的平均花费"更能反映性价比。

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

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

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

没有被任何配置支配的那些点,就是帕累托前沿。前沿上的点之间没有"谁更好",只有"多花多少钱、换多少质量"。延迟通常当成门槛:p95 超过 600 秒的配置先排除(不可行),在剩下的里面画前沿。三项都当目标也行,定义一样,只是多一个不等号。

在这份合成数据上(固定种子 7):

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

差了两个:C 的真实质量(63.8%)比 B(58.4%)高,这次观测却一样都是 121/200,于是被更便宜的 B 支配掉了;E 的真实质量(70.5%)比 D(72.1%)低、总体成本还高约 98%,这次观测却是 75.0% 对 71.0%,挤上了前沿。点估计前沿上的点,不一定在真实前沿上;被划掉的点,也不一定该划。

4给前沿也加上误差

每个质量估计先带上区间

第 1 周「30 次挂了 2 次,失败率在什么范围?」讲过:只报 k/n 不报区间,等于没交代这个数字有多可信。200 条任务、找到率 70% 左右时,Wilson 95% 区间宽约 12 个百分点(D:142/200 = 71.0%,[64.4%, 76.8%])。7 个区间里,F 的 [82.2%, 91.4%] 没罩住它的真实值 79.3%:95% 区间本来就有约 5% 的时候会漏,同时看 7 个,漏一个不稀奇。

两个配置比,用配对

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×

G 和 F 的真实质量完全相同(只差缓存),这次观测却差了 6.5 个点。E 和 D 的真实差是 D 高 1.58 个点,两个配置在约 31.7% 的任务上结果不一致;按第 1 周「要跑多少次才够?」里的配对样本量公式,要以 80% 的功效检出这 1.58 个点,大约需要 9,922 条任务。BugHunt-Bench 计划注入 20~30 个 Bug,离这个量差两个数量级:在几百条任务的规模下,质量相近的配置多半就是分不出来。

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

按任务配对重抽 2000 次,每次用重抽出的质量、平均成本、p95 重新判断可行性、重新画前沿,数每个配置落在前沿上的比例(下面演示里的"前沿概率"列)。200 条任务时:A 100%、B 99.0%、C 46.0%、D 99.4%、E 83.0%,F、G 因为 p95 超标始终不可行。只用前 50 条任务时 B 62.6%、C 72.8%、E 15.6%,F 的 p95 有 23.4% 的重抽里达标。

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

挑点时把不确定性算进去

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

  1. 先定业务能接受的质量下降界值 δ(比如 5 个百分点)和延迟门槛,写在跑评测之前。
  2. 从最便宜的可行配置往上看,第一个对"观测最好的那个"满足非劣效(配对差 95% 区间下界 > −δ)的,就是候选。这是第 1 周换便宜模型那一节的做法。
  3. 按字面执行,A、B、C、D 都过不了非劣效,第一个满足的就是 E 自己(和自己比,差恒为 0):规则在这份数据上给不出更便宜的选项。D 只能作为"质量证据不够、但愿意承担风险换成本"的候选单独讨论:D − E 的下界是 −11.5 个点,不满足 δ = 5,证据不够。要么加任务,要么明确写下"选 D,接受质量可能差到 11.5 个点的风险,换来省下 47% 的成本"。不要把"不显著"说成"一样好"。

把整个实验重做 1000 次

合成数据的好处是可以重来:每次重新抽任务、重新跑,看"按点估计"有多可靠。

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

真实最优的可行配置是 D(每条任务平均 $0.1810)。在这个合成设定下,选错的代价主要在钱上:选中 E 时真实质量只低 1.58 个点,总体成本却高约 98%($0.3580 对 $0.1810);n = 200 时平均多花约 34%。任务数翻 4 倍,选错的比例也只从 35% 降到 20%,因为 D 和 E 本来就离得近。

▶互动演示 1:成本–质量散点与前沿

数据是合成的:7 个配置在同一批任务上各跑一次(固定种子 7)。横轴是每条任务的平均成本(示例价格(合成),对数刻度),竖轴是找到率,竖线是 Wilson 95% 区间。实心点是 p95 达标的配置,空心点不达标;蓝色折线连起点估计前沿。bootstrap 按任务配对重抽 2000 次,所有数字都在页面里实时算。

任务数
显示
点估计前沿 p95 超过门槛(不可行)
配置找到率Wilson 95%平均成本每找到 1 个p95p95 的 95% 区间前沿概率达标概率

配对比较两个配置

减
观测质量差
配对 bootstrap 95% 区间
重抽里前者更好的比例
分歧(前者过后者挂 : 前者挂后者过)
McNemar 精确 p(双侧)
成本比

▶互动演示 2:延迟分布,均值、p50、p95

同一份合成数据。直方图按 20 秒分箱,绿色是找到了 Bug 的运行,灰色是没找到的。可以把这个配置的最后 5 次运行换成 1800 秒超时,看三个数各动多少。

配置
故障
找到 Bug 没找到 均值 p50 p95
均值
p50
p95
找到的运行 p50
没找到的运行 p50
均值 / p50

✎练习

题 1(口径):给财务报"每条任务的成本",该报均值、中位数还是 p95?
成本要回答"跑 N 条任务一共花多少",只有均值乘回去等于总数。中位数会漏掉那些跑满轮数、特别贵的运行。延迟不同:用户体验和门槛管的是单次的慢,所以看分位数。
题 2(算):示例价格(合成)中档:输入 1.15、缓存命中 0.115、输出 5.75 美元 / 百万 token(只为说明计算方法)。一次运行 input_tokens = 203,500(含缓存命中),cached_input_tokens = 172,500,output_tokens = 10,556。花费多少美元?
$
未命中 203,500 − 172,500 = 31,000 × 1.15 = 35,650;命中 172,500 × 0.115 = 19,837.5;输出 10,556 × 5.75 = 60,697;合计 116,184.5 ÷ 106 ≈ $0.1162。如果把 203,500 全部按全价算,会得到 $0.2947。
题 3(算):D 在 200 条任务里找到了 142 个 Bug。Wilson 95% 区间的下界是多少?
%
p̂ = 0.71,z²/n = 3.8415/200 = 0.0192;中心 (0.71 + 0.0096)/1.0192 ≈ 0.7061;半宽 1.96/1.0192 × √(0.71×0.29/200 + 3.8415/160000) ≈ 0.0624;区间 ≈ [64.4%, 76.8%],宽约 12 个点。
题 4(挑配置):E 观测 75.0%,D 观测 71.0%,两者 p95 都达标,E 贵 89%。配对 bootstrap 给出 E − D 的 95% 区间 [−3.5, +11.5],McNemar p = 0.358。事先定的非劣效界值是 5 个点。怎么说最准确?
A 把点估计当成了真值(在这份合成数据里,E 的真实质量反而低 1.58 个点)。B 把"不显著"说成了"一样"。C 把不确定性和成本都摆上桌,让决策的人知道自己在赌什么。
题 5(延迟):把 D 的 5 次运行换成 1800 秒超时后,均值 275 → 312 秒,p50 244 → 245 秒,p95 449 → 471 秒。哪种解读对?
均值把"少数卡死"和"整体变慢"混在一起;p50 只看典型情况,看不见尾巴。p50 和 p95 一起报,才分得清是哪一种。

5面试要点与代码

一句话讲清楚

换模型或换配置,在同一批任务上把每个候选都跑一遍,量三样:找到率带 Wilson 区间,成本报每条任务的均值和每找到一个 Bug 的花费,延迟报 p50 和 p95。先按延迟门槛排除不可行的,再划掉被支配的,剩下的帕累托前沿上按预算挑。前沿是用估计值画的,所以两个配置之间要做配对比较,挑便宜的那个时要过非劣效,证据不够就加任务或者把风险写明白。

追问准备

  1. 为什么延迟不报均值?分布右偏有长尾,几次超时就能把均值拉走;门槛和用户体验看的是 p95 这样的尾部分位数。p50 看典型情况,p95 看最慢的 5%。
  2. p95 自己可信吗?200 次运行的 p95 基本由最慢的 10 次决定,要给它也算区间(按任务 bootstrap)。在这份合成数据里 F 的 p95 是 707 秒,区间 [644, 779] 秒;只用 50 条任务时,F 在 23.4% 的重抽里会"达标"。
  3. 两个配置的区间重叠,能说一样吗?不能。同一批任务是配对数据,用配对 bootstrap 或 McNemar;"不显著"也不等于"一样",要用非劣效。
  4. 成本怎么从 token 算?按未命中输入、缓存命中输入、输出三类分别计价。注意口径:Codex 的 TokenUsage.input_tokens 是总输入(含缓存命中),Claude API 的 usage.input_tokens 是未走缓存的剩余部分。

常见错误说法

❌ "E 的找到率最高,选 E":观测差 4 个点,配对区间 [−3.5, +11.5],还贵 89%。
❌ "平均延迟 275 秒,满足 10 分钟的要求":门槛管的是 p95,不是均值。
❌ "C 和 B 都是 60.5%,C 贵,所以 C 被支配":两个点估计相等不代表真实质量相等,C 在 46.0% 的重抽里仍在前沿上。
❌ "差异不显著,所以便宜的配置一样好":要看差值区间的下界,过不了非劣效就是证据不够。
❌ "日志里 input_tokens 乘输入单价就是输入成本":先确认这个字段是总输入还是未走缓存的剩余部分。

核心代码(Python,无第三方依赖)

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


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}

完整代码在学习目录 week07_回放评测与模型对比/code/cost_latency_quality/:synthetic.py 生成合成数据,run_experiment.py 打印本页所有数字并导出 data.json,test_cost_latency_quality.py 是 18 个单元测试。本页的 bootstrap 用和 Python 逐位一致的 mulberry32 随机数,所以默认设置下页面和 Python 输出的数字相同。

公式显示依赖 KaTeX(CDN),断网时公式会显示成原始 LaTeX 源码,交互部分不受影响。