当一个大语言模型在某个 benchmark 上取得更高分时,我们很容易把结果直接解释成“模型更聪明了”。但一个分数可能同时受到训练数据污染、题目熟悉度、提示词格式、答案先验、评测脚本以及工具调用能力的影响。BenchMIRT 这个主题真正值得关注的地方,是把问题从“谁的分数更高”推进到“这个分数究竟测量了什么”。
分数不等于单一能力
一个 benchmark 通常把复杂能力压缩成一个数字。例如,问答题的正确率可能同时反映:
- 模型是否见过相同或相似题目;
- 是否掌握相关事实;
- 是否能够进行多步推理;
- 是否理解题目格式和选项分布;
- 是否能遵守输出格式;
- 评测器是否正确解析了答案。
因此,模型在某项测试上的提升,不一定意味着目标能力本身提升。它可能只是更适应题型、更熟悉评测格式,或者更善于利用数据中稳定存在的线索。
这也是 benchmark 设计中的构念效度问题:测试名称声称测量“推理”“知识”或“代码能力”,但实际测量结果可能是多个因素的混合物。严谨的比较需要说明测试的目标构念,以及哪些因素可能干扰这个构念。
需要拆开的几个变量
训练数据污染
如果测试集中的题目、答案或高度相似的改写出现在训练数据里,评测结果就可能更接近记忆检索,而不是泛化能力。污染并不只意味着逐字出现;题目模板、解析文本和社区讨论也可能提供足够强的提示。
提示词和格式敏感性
同一道题使用不同的系统提示、选项顺序、答案格式或上下文长度,结果可能明显变化。一个模型如果只在特定 prompt 模板下表现良好,分数就不能简单概括为稳定能力。
题目难度与样本构成
平均分会隐藏样本分布。模型可能在简单题和极难题上的表现相近,却在中等难度题上产生差异;也可能只是受益于某一类领域样本比例更高。报告总体准确率时,最好同时查看按主题、难度、题型和语言划分的结果。
评测器与答案解析
生成式模型的输出往往不是一个干净的选项字符。评测脚本如果只接受 A,就可能把“答案是 A,因为……”判错;如果使用另一个 LLM 作为裁判,又会引入裁判模型的偏差。评测流程本身也是测量系统的一部分。
可以这样做一个最小稳健性检查
下面的示例假设你已经有一个本地模型接口,接口接收 prompt 并返回文本。代码会用多种题目格式测试同一批选择题,并比较答案是否稳定。它不是完整的 benchmark 框架,但适合在正式评测前快速发现 prompt 敏感性。
运行前需要把 call_model 替换成你的模型调用逻辑。示例中的 mock_answers 只是为了让脚本可以直接运行;接入真实模型时删除它即可。
from collections import Counter
from typing import Callable, Dict, List
QUESTIONS = [
{
"question": "Which number is prime?",
"options": ["A. 21", "B. 29", "C. 35", "D. 39"],
"gold": "B",
},
{
"question": "What is 3 * 4?",
"options": ["A. 7", "B. 10", "C. 12", "D. 14"],
"gold": "C",
},
]
PROMPTS = {
"plain": lambda q: f'{q["question"]}\n' + "\n".join(q["options"]),
"instruction": lambda q: (
"Answer the multiple-choice question. Return only the option letter.\n"
f'{q["question"]}\n' + "\n".join(q["options"])
),
"json": lambda q: (
"Answer the question and return JSON such as {\"answer\": \"A\"}.\n"
f'{q["question"]}\n' + "\n".join(q["options"])
),
}
def mock_answers(prompt: str) -> str:
"""Replace this function with an SDK or HTTP model call."""
if "prime" in prompt:
return '{"answer": "B"}' if "JSON" in prompt else "B"
return '{"answer": "C"}' if "JSON" in prompt else "C"
def extract_letter(text: str) -> str:
for letter in "ABCD":
if letter in text.upper():
return letter
return "?"
def evaluate(call_model: Callable[[str], str]) -> None:
results: Dict[str, List[str]] = {}
for name, make_prompt in PROMPTS.items():
predictions = []
for question in QUESTIONS:
raw = call_model(make_prompt(question))
predictions.append(extract_letter(raw))
results[name] = predictions
for name, predictions in results.items():
correct = sum(
prediction == question["gold"]
for prediction, question in zip(predictions, QUESTIONS)
)
print(f"{name:12} accuracy={correct / len(QUESTIONS):.2f} answers={predictions}")
per_question = list(zip(*results.values()))
for index, answers in enumerate(per_question, start=1):
print(f"question_{index}: {answers}, mode={Counter(answers).most_common(1)[0][0]}")
if __name__ == "__main__":
evaluate(mock_answers)
这个实验至少能回答两个问题:不同提示格式是否改变准确率,以及模型是否对同一题给出稳定答案。如果结果波动很大,报告单一 prompt 下的分数就需要更加谨慎。进一步实践时,可以增加选项重排、问题改写、无答案解释与强制答案格式等条件,并为每个条件报告置信区间。
从“排行榜”转向“测量报告”
更有价值的 benchmark 报告,不应只给出一个总分。建议至少记录以下信息:
| 维度 | 应记录的内容 |
|---|---|
| 数据 | 数据集版本、题目来源、去重和污染检查方法 |
| 推理 | 是否允许思维链、工具、外部检索和多次采样 |
| 提示 | 完整 system prompt、user prompt 和示例 |
| 解析 | 输出规范、异常答案处理和裁判规则 |
| 统计 | 样本数、置信区间、分组分数和随机种子 |
| 成本 | 延迟、token 消耗、调用次数和硬件环境 |
如果目标是研究推理能力,可以加入模型没有见过的新题、结构保持但内容变化的题目,以及对选项顺序不敏感的测试。如果目标是衡量真实业务价值,则应保留生产环境中的上下文、工具和失败成本,而不是只追求一个干净的离线分数。
采用前的检查清单
- 明确 benchmark 声称测量的能力,以及可能混入的替代指标。
- 检查训练数据污染,至少关注逐字重复和高相似度样本。
- 在多个 prompt、选项顺序和输出格式下重复测试。
- 将总体分数拆成领域、难度、题型和语言等子集。
- 固定并公开评测器、解析器、采样参数和版本。
- 把准确率与成本、延迟、稳定性和真实任务成功率一起看。
BenchMIRT 所代表的问题意识并不是否定 benchmark,而是要求我们把 benchmark 当成测量仪器来校准。一个分数只有在测量目标、干扰因素和实验条件都足够清楚时,才适合支撑模型能力比较。排行榜可以告诉你谁在某套规则下表现更好;可靠的测量报告则要进一步解释,为什么更好,以及这种优势能否迁移到题目之外。