用120行Python评估模型能力与成本
文章构建约120行、依赖较少的评测框架,按能力维度报告准确率、Bootstrap置信区间和成本。示例展示模型在算术、字符串、长上下文及多步任务上的差异,说明总平均分可能误导选型。
AI 深度解读
模型 A 的评测总分是 0.675,模型 B 是 0.713。选 B 看起来很合理,但拆开任务后,A 更擅长算术和字符串处理,B 更擅长另外两类任务。总分排出了名次,却没回答产品应该用谁。
作者给出的办法,是用约 120 行 Python 输出一张「能力画像」:每类任务的准确率、置信区间和成本一起看。这套报告方法值得借鉴,但文中的模型是模拟器,所谓「长上下文」也只是演示标签,不能当成真实模型选型证据。
总分接近,为什么选型结论可能相反
混合测试集的平均分,会把不同能力之间的差异抵消掉。算术多答对几道,文档处理多答错几道,最后仍然可能得到一个漂亮的总分。
作者列出了三个缺口:看不到能力取舍,看不到抽样带来的不确定性,也看不到成本。例如,准确率高出 5%,但难题需要消耗 3 倍 token,预算下的选择未必更好。这里的数字是问题举例,并非后文实验测出的收益。
演示给每个能力轴安排了 60 道题,结果如下。准确率和区间保留原文精度,成本单位是人为设定的相对单位。
| 能力轴 | A 准确率与 95% 区间 | B 准确率与 95% 区间 | 每次成本 A/B | acc/cost A/B |
|---|---|---|---|---|
| arithmetic | 0.93,[0.87, 0.98] | 0.67,[0.55, 0.78] | 1.0 / 1.0 | 0.933 / 0.667 |
| string_ops | 0.88,[0.80, 0.95] | 0.63,[0.52, 0.75] | 1.0 / 1.0 | 0.883 / 0.633 |
| long_context | 0.37,[0.25, 0.48] | 0.77,[0.67, 0.87] | 4.0 / 4.0 | 0.092 / 0.192 |
| multi_step | 0.52,[0.38, 0.63] | 0.78,[0.67, 0.88] | 3.0 / 3.0 | 0.172 / 0.261 |
四个轴的区间都不重叠,胜者各占两个。总分差距按未舍入结果输出为 0.037,只看总分很容易忽略这组差异。
代码中的 headline 是各能力轴准确率的等权平均。这里每轴题数相同,与全部题目的平均准确率一致;以后各轴题数不同,两种算法就会产生不同结果。
评测应该先回答业务要做什么决定,再决定怎样汇总分数。
作者建议按决策划分能力轴,例如检索、工具调用、长上下文。某个维度能改变模型路由或选型,才有单独报告的价值。
置信区间解决的是「这批题够不够」
60 道题得到的准确率,并不是模型能力的精确刻度。换一批题,分数可能变化。只给出 0.77,会让读者误以为这个数字比实际更稳定。
作者用自助法(bootstrap)估计这种不确定性:从已有结果中有放回地抽样,每次重新计算平均分,再取分位数形成区间。它不要求预先指定某种参数分布,也能处理答对为 1、答错为 0 的结果。
下面这段函数负责计算区间,依赖 numpy,对应原文件中的 import numpy as np:
def bootstrap_ci(x: np.ndarray, n_boot: int = 2000, alpha: float = 0.05,
rng: np.random.Generator | None = None) -> tuple[float, float]:
"""Percentile bootstrap confidence interval for the mean of 0/1 scores."""
rng = rng or np.random.default_rng(0)
idx = rng.integers(0, len(x), size=(n_boot, len(x)))
means = x[idx].mean(axis=1)
return float(np.quantile(means, alpha / 2)), float(np.quantile(means, 1 - alpha / 2))
要看的是 size=(n_boot, len(x)):默认重采样 2000 次,每次抽取与原样本相同数量的结果;alpha=0.05 对应取 2.5% 和 97.5% 分位数,得到 95% 区间。
作者给出的量级是:每轴 60 题,区间大约上下浮动 10 个百分点;每轴 200 题,大约上下浮动 6 个百分点。这些是粗略估计,实际宽度还与正确率有关,不能当成固定保证。
更关键的是,区间只反映已有题目上的抽样不确定性。题目偏简单、重复度高,或者偏离线上请求,增加重采样次数也无法修复。
原文用「两个 95% 区间不重叠」筛选差异,并明确说这是保守筛查。它没有替代正式检验;需要严谨比较时,应在相同题目的逐题结果上使用配对检验。区间重叠,也不能直接判定两个模型没有差异。
接真实 API 时,评分和成本都要重新定义
这套脚本定位在报告层,可以接收 lm-evaluation-harness、HELM 或 OpenAI Evals 产出的逐题结果,无须替换现有任务执行框架。
它要求模型适配器接收一个 Item,返回 (answer, cost)。evaluate 再按 axis 分组,计算准确率、区间、平均成本,以及 acc_per_cost。成本可以记录 token、秒数或货币,但单位必须一致。
下面是原文提供的聊天 API 接入示意,作者明确标注它是未经测试的伪代码:
def api_model(client, model_name):
def run(item):
resp = client.generate(model=model_name, prompt=item.prompt, max_tokens=64)
return resp.text.strip(), resp.usage.total_tokens # cost = tokens
return run
要留意 max_tokens=64 和 resp.usage.total_tokens 两处:前者限制回答长度,后者决定成本口径;具体方法名和字段需要按实际客户端调整。
默认评分使用 out.strip() == it.answer。它适合答案格式固定的题目,自由文本则需要替换:数值题可以允许误差,代码任务可以跑单元测试,开放回答可以按评分规则判定。评分器也应按能力轴配置,否则格式差异可能被误算成能力差异。
acc_per_cost 是平均准确率除以平均成本,并非先计算每题比值再取平均。若成本用货币表示,它可以辅助观察单位花费带来的正确结果,但仍不能代替业务对错误后果的判断。
作者还建议给题目和能力轴映射保存哈希,让不同批次可比较。项目反应理论(IRT)则被列为后续方向,用来共同估计题目难度和模型能力,本文没有实现。
我的判断
我认同它对报告方式的改进。相比只报平均分,逐轴列出准确率、区间和成本,更容易解释模型为什么适合某类请求;相比直接引入更复杂的评测体系,它也更容易接到已有流程后面。
但要分清「脚本输出真实」和「模型实验真实」。
文中的 simulated_model 按预设的逐轴成功概率决定是否答对,并不理解题目。long_context 只是对 5 个整数求和;multi_step 使用 ((x*3)+4)*2,成本再乘以 1.5,最终为 3.0。它们能演示报告结构,无法验证真实模型的长上下文或多步推理能力。
此外,A、B 在同一能力轴上的成本完全相同。表格展示了如何记录成本,却没有测出真实模型之间的 token 效率差异。
提供的原文代码还在 model_a = simulated_model({"arithm 处截断,缺少完整演示入口。虽然原文写着保存为 profile_eval.py 后运行 python profile_eval.py,仅凭当前内容无法复现整张输出表。
这篇提供的是值得采用的评测报告方法,真实选型仍需要业务题目和真实调用数据。
作者关于「改变实际决策」「更难被刷分」的结论,也没有附上团队对照数据,应当视为方法主张。
能拿来做什么
- 给现有 AI 编程评测增加能力轴,按真正会影响选型的任务拆分,例如代码修改和工具调用,并分别报告准确率、95% 区间与成本。
- 评估前端或 Node 代码生成时,用任务相关的测试判断正确性;需要精确文本输出的任务,再使用原文的字符串匹配方式。
- 比较两个模型时,让它们处理同一批题,保留逐题答案、评分和实际消耗;先用区间筛查差异,需要正式结论时再做配对检验。
- 把题目、评分规则和能力轴映射一起版本化,并记录哈希;预算优先补充样本不足且影响决策的能力轴,而不是只增加测试总题数。