测试时扩展与推理大模型评测:可复现性工程实践
最近在对比推理大模型(reasoning LLM)的不同评测方式时,被一个很现实的问题卡住:模型效果波动不小,同一个 prompt 跑两次,答案可能完全不同;换一个推理采样配置,分数能差好几个点。如果评测工具、推理参数、随机种子都没固定下来,最后很难说清楚模型能力提升究竟是模型变了,还是只是测试方式变了。
这篇文章围绕 Test-Time Scaling(测试时扩展)这一主题,结合 Reasoning LLMs 的推理行为、Inference Regimes(推理配置)、第三方评估工具接入,以及可复现性工程实践,梳理一套完整可落地的评测思路。内容偏实操,代码都是可以直接在本地跑的最小示例,适合正在做 LLM 效果评测、想接入第三方评估工具,或者研究采样策略对模型分数影响的读者。
1. 背景与核心概念
1.1 什么是 Test-Time Scaling
传统的大模型能力提升,主要靠训练阶段:更多数据、更大模型、更长时间的训练。但对于推理任务,比如数学题、逻辑判断、代码调试,模型在训练完成之后,仍然可以通过“多算一会儿、多想几步”来获得更高质量的回答。
这种在推理阶段增加计算量、改善生成质量的做法,就是 Test-Time Scaling。
一个最简单的例子是数学题:
问题:一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?
不做扩展时,模型可能直接输出: 3 * 2 + 2 * 4 = 14。
如果做测试时扩展,模型可能会先自言自语:
鸡有 2 条腿,3 只鸡是 6 条腿。
狗有 4 条腿,2 只狗是 8 条腿。
总腿数是 6 + 8 = 14。
当推理过程变长、中间检查变多,错误概率会下降。更进一步的扩展,可以让模型生成多条候选答案,再用投票或验证器选出最可靠的结果。
这种现象在 OpenAI 的 o1 / o3 系列模型中比较典型,也让“测试时计算(Test-Time Compute)”这个概念成为研究热点。与之相似的还有 self-consistency(自洽性)、best-of-n 采样、多数投票、轻量级验证器筛选等方法。
1.2 为什么 Inference Regimes 会影响评测结果
Inference Regimes,可以理解为“模型推理阶段的运行配置组合”。它决定了模型在给定同一个 prompt 时,到底如何生成内容。
常见维度包括:
| 配置项 | 影响 |
|---|---|
| temperature | 控制采样随机性,越高越随机 |
| top_p / top_k | 控制候选 token 范围 |
| max_tokens / max_completion_tokens | 限制输出长度 |
| 采样次数 n | 生成多少条候选答案 |
| 多数投票轮数 | 对多条答案如何聚合 |
| 是否使用验证器 | 是否用额外模型排序候选答案 |
| prompt 模板 | 是否加入思考引导 |
| 种子 seed | 固定随机数生成器状态 |
同一个模型,temperature=0 和 temperature=0.7,n=1 和 n=10,最终评测分数可能差异不小。
因此,如果你在论文或项目报告中写“本模型准确率达到 85%”,却不描述推理阶段参数,那这个结果就很难复现。Inference Regimes,本质上是评测报告里必须交代清楚的实验条件。
1.3 评测与可复现性的关系
评测(Evaluation)回答的是“模型效果到底怎么样”。可复现性(Reproducibility)回答的是“别人能不能按同样的设置得到相同结果”。
评测结果可信的前提,是可复现。但在真实项目中,数据集版本会在更新、评估脚本会被调整、模型服务可能做了量化或 batch 推理,这些都会影响最终指标。
常见的评测完整性问题包括:
- 只记录准确率,不记录 prompt 模板和模型版本。
- 为了效果对比,临时把 temperature 调低,却没写在报告里。
- 评测数据集做完清洗后,没有保存清洗脚本。
- 多个评测工具混用,指标口径不一致。
- 随机性没有固定,实验重复两次结果相差很大。
所以,评测工程中需要引入稳定的第三方评估工具,并且让工具能够调用自建的推理 API,使用自定义评估标准。这也是本文后半部分的实战重点。
2. 环境准备与第三方评估工具选型
2.1 基础环境
本文示例以 Python 3.10+ 为基准,需要准备:
- Python 3.10 或更高版本
- pip 包管理工具
- OpenAI SDK(用于调用兼容接口)
- FastAPI + uvicorn(用于模拟自建推理服务)
- promptfoo 或 DeepEval(第三方评估工具)
如果你已经有可用的推理 API,就不用搭 mock 服务,直接把 base_url 和 API Key 换成真实服务即可。
版本说明:不同工具的配置格式差异较大,本文以常见版本为主。实际使用时,请以官方文档和 pip show 输出的版本为准。
安装依赖的命令:
pip install openai fastapi uvicorn promptfoo deep-eval
如果你只需要其中某一个工具,可以分开安装。例如:
pip install deep-eval
# npm 安装 promptfoo
npm install -g promptfoo
2.2 主流第三方评估工具
目前社区常用评估方案大致分两类。
一类是离线评估框架,适合批量跑 benchmark,比如:
- lm-evaluation-harness(EleutherAI 出品,支持大量公开数据集)
- OpenCompass(上海人工智能实验室开源,支持中英文评估)
另一类是面向业务的自定义评估工具,适合对接自建 API、编写自定义指标,比如:
- promptfoo(类似单元测试的评测工具,用 YAML 配置测试用例)
- DeepEval(Pytest 风格的 LLM 评估框架,可以自定义 metrics)
- OpenAI Evals(OpenAI 开源的评估框架,可注册自定义 eval)
如果你需要“调用自己写的 API,根据自己定义的评价标准”来做评估,promptfoo 和 DeepEval 是更灵活的选择。
2.3 本文实战场景
我设计的示例场景如下:
- 有一个自建推理服务,接口兼容 OpenAI API 格式。
- 需要评测模型在高斯数学题上的准确率。
- 每次生成不只跑 1 条答案,而是先采样 N 条候选,再用多数投票得出最终答案。
- 评测脚本需要记录温度、种子、模型版本、prompt 版本,保证实验可复现。
在代码实现上,我会先用 FastAPI 写一个 mock 推理服务,再分别演示 promptfoo 和 DeepEval 的接入方式。这样你不需要真实模型也能跑通流程。
3. 核心概念拆解
3.1 Test-Time Scaling 的常见策略
Test-Time Scaling 并不单指“让模型生成更多内容”,而是包含多个层面的扩展手段。
3.1.1 多数投票 / Self-Consistency
对同一个问题生成 N 个答案,然后统计答案中出现次数最多的那个作为最终输出。
这是最直观的扩展方式,效果稳定,实现简单。
import collections
answers = ["14", "14", "14", "15", "14", "13"]
final_answer = collections.Counter(answers).most_common(1)[0][0]
print(final_answer)
输出:
14
对于有唯一正确答案的数学题,多数投票能显著提升准确率,但代价是多次调用模型,推理成本变为原来的 N 倍。
3.1.2 Best-of-N 采样 + 验证器
生成 N 个候选答案,再使用一个验证器(reward model / verifier)对候选答案排序,选出得分最高的。
这种方式适合“答案没有单一标准,但可以判断好坏”的任务,比如代码、开放式问答。
3.1.3 长思维链 / 隐式搜索
像 o1 这类模型,会在内部生成较长的推理轨迹,再输出最终答案。这种方式不需要显式采样多轮,而是把更多计算放在单次生成内。
3.2 Inference Regimes 的关键参数
| 参数 | 说明 | 对评测的影响 |
|---|---|---|
| temperature | 采样温度,默认为 1 | 温度越低,输出越确定;温度为 0 时,多数采样可能失效 |
| n | 每个 prompt 生成的候选数 | 影响多数投票、Best-of-N 的基数 |
| max_tokens | 最大生成 token 数 | 太短会导致推理过程被截断 |
| seed | 随机种子 | 控制可复现性 |
| stop | 停止符 | 可能影响结构化输出 |
| response_format | 输出格式约束 | 影响解析难度 |
| reasoning_effort | 推理难度 | 部分模型支持 low / medium / high |
需要注意的是, temperature=0 在技术上并不是完全确定性,不同框架、不同硬件下仍有微弱差异。要保证严格可复现,最好固定 seed,并保留完整参数快照。
3.3 评估流程的组成
一次完整的评测流程,通常包含:
- 数据准备:测试用例的加载与清洗。
- 推理调用:调用自建 API,设置 Inference Regimes。
- 答案解析:从模型输出中提取最终答案。
- 指标计算:根据自定义标准评分。
- 结果记录:保存原始输出、参数和指标到本地文件。
在这套流程里,数据、参数、代码、运行环境四者耦合在一起,哪一环没记录,复现都会出问题。
4. 实战:用第三方评估工具评测自定义 API
下面进入代码实战部分。我会给出一个可以直接复制的完整流程。
4.1 准备一个兼容 OpenAI 的自建推理服务
先写一个 FastAPI 服务。为了演示方便,这个服务不会真的加载大模型,而是返回一个模拟推理结果:从几个预置答案中随机抽一个。
# 文件路径:server.py
import random
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ChatRequest(BaseModel):
model: str = "mock-reasoner"
messages: list
temperature: float = 0.7
max_tokens: int = 128
seed: int | None = None
class Choice(BaseModel):
index: int
message: dict
finish_reason: str = "stop"
class ChatResponse(BaseModel):
id: str
object: str = "chat.completion"
choices: list[Choice]
usage: dict
ANSWERS = [
"The answer is 14.",
"Final answer: 14.",
"14",
"Let me think carefully. 3 chickens have 6 legs. 2 dogs have 8 legs. Total is 14.",
]
@app.post("/v1/chat/completions", response_model=ChatResponse)
def chat_completion(req: ChatRequest):
if req.seed is not None:
random.seed(req.seed)
content = random.choice(ANSWERS)
return ChatResponse(
id="chatcmpl-mock",
choices=[
Choice(
index=0,
message={"role": "assistant", "content": content},
finish_reason="stop",
)
],
usage={"prompt_tokens": 20, "completion_tokens": len(content), "total_tokens": 20 + len(content)},
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
启动服务:
python server.py
注意:这只是一个 mock 服务,用来验证评估流程。真实项目中,你需要把 /v1/chat/completions 转发到自己的推理引擎,比如 vLLM、TGI,或者你内网部署的模型服务。
4.2 使用 promptfoo 进行评测
promptfoo 是一个命令行评测工具,可以用 YAML 定义 prompt 和测试用例,也支持自定义 API 地址。
4.2.1 初始化配置文件
在项目目录下创建一个 promptfooconfig.yaml :
# 文件路径:promptfooconfig.yaml
prompts:
- |
请回答下面的数学题,只输出最终答案数字。
题目:一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?
providers:
- id: openai:chat:mock-reasoner
config:
apiBaseUrl: http://localhost:8000/v1
apiKey: dummy-key
temperature: 0.7
max_tokens: 128
tests:
- vars:
question: "一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?"
assert:
- type: contains
value: "14"
这里 apiBaseUrl 指向自建服务, apiKey 填任意非空字符串即可, openai:chat:mock-reasoner 表示调用 OpenAI 兼容接口。
4.2.2 运行评测
promptfoo eval
如果希望输出完整报告:
promptfoo eval -o report.html
promptfoo 的断言机制非常灵活,包括:
contains:输出包含某个文本。equals:输出完全等于某个文本。javascript:执行自定义 JS 判断。python:调用自定义 Python 脚本。model-graded:用另一个 LLM 打分。
这种方式非常适合“自己定义评价标准”。
4.3 使用 DeepEval 进行评测
DeepEval 是 Python 生态的评估库,风格接近 pytest,适合在代码里精细控制评估流程。
4.3.1 编写自定义评估用例
# 文件路径:test_eval.py
import os
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric
def call_my_api(question: str, seed: int = 42) -> str:
# 这里直接调用自建 API
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy-key")
response = client.chat.completions.create(
model="mock-reasoner",
messages=[
{"role": "user", "content": question}
],
temperature=0.7,
max_tokens=128,
seed=seed,
)
return response.choices[0].message.content
def test_math_answer():
question = "一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?"
output = call_my_api(question)
# 自定义判断:答案里必须包含 14
assert "14" in output, f"模型输出不包含 14,实际输出:{output}"
test_case = LLMTestCase(
input=question,
actual_output=output,
expected_output="14",
)
metric = AnswerRelevancyMetric(
threshold=0.5,
model="gpt-4o-mini"
)
assert_test(test_case, [metric])
这里有一个关键点: AnswerRelevancyMetric 本身通常需要调用 OpenAI 模型来判断相关性。如果不想引入另一个模型,可以只用简单的 assert 做规则判断,或者自定义一个 Metric 类。
DeepEval 的自定义 Metric 示例:
# 文件路径:custom_metric.py
from deepeval.metrics import BaseMetric
from deepeval.scorer import Scorer
class ContainsAnswerMetric(BaseMetric):
def __init__(self, expected: str):
self.expected = expected
self.threshold = 1.0
def measure(self, test_case) -> float:
if self.expected in test_case.actual_output:
self.success = True
return 1.0
self.success = False
return 0.0
def is_successful(self) -> bool:
return self.success
@property
def __name__(self):
return "ContainsAnswerMetric"
然后用这个自定义指标跑测试:
# 文件路径:test_with_custom_metric.py
from deepeval.test_case import LLMTestCase
from custom_metric import ContainsAnswerMetric
test_case = LLMTestCase(
input="一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?",
actual_output="The answer is 14.",
)
metric = ContainsAnswerMetric(expected="14")
score = metric.measure(test_case)
print(f"Score: {score}, Success: {metric.is_successful()}")
这种方式的好处是:评估标准完全由你定义,不依赖其他模型判断。
4.4 实现多数投票评估
要评估 Test-Time Scaling 的效果,需要对比不同采样数量 n 下的准确率。
下面是一个完整的 Python 脚本。它会调用自建 API N 次,提取答案并投票。
# 文件路径:majority_vote_eval.py
import collections
import random
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy-key")
QUESTIONS = [
"一个农场里有 3 只鸡和 2 只狗,总共有多少条腿?",
"小明有 5 个苹果,给了小红 2 个,还剩几个?",
"12 + 7 等于多少?",
]
CORRECT_ANSWERS = ["14", "3", "19"]
def extract_answer(text: str) -> str:
# 简单提取数字,真实项目需要更严谨的解析
import re
numbers = re.findall(r"\d+", text)
if not numbers:
return ""
return numbers[-1]
def generate_with_api(question: str, temperature: float, seed: int) -> str:
response = client.chat.completions.create(
model="mock-reasoner",
messages=[{"role": "user", "content": question}],
temperature=temperature,
max_tokens=128,
seed=seed,
)
return response.choices[0].message.content
def majority_vote(question: str, n: int, temperature: float, seed: int) -> str:
answers = []
for i in range(n):
raw = generate_with_api(question, temperature, seed + i)
answer = extract_answer(raw)
answers.append(answer)
counter = collections.Counter(answers)
final_answer, _ = counter.most_common(1)[0]
return final_answer, answers
def evaluate(temperature: float = 0.7, n: int = 5):
correct = 0
for i, question in enumerate(QUESTIONS):
final_answer, all_answers = majority_vote(question, n=n, temperature=temperature, seed=42 + i)
is_correct = final_answer == CORRECT_ANSWERS[i]
correct += int(is_correct)
print(f"Question {i + 1}: final={final_answer}, expected={CORRECT_ANSWERS[i]}, correct={is_correct}")
print(f" candidates: {all_answers}")
accuracy = correct / len(QUESTIONS)
print(f"Accuracy: {accuracy:.2%}")
if __name__ == "__main__":
evaluate(temperature=0.7, n=5)
运行效果:
Question 1: final=14, expected=14, correct=True
candidates: ['14', '14', '14', '14', '14']
Question 2: final=3, expected=3, correct=True
candidates: ['3', '3', '3', '3', '3']
Question 3: final=19, expected=19, correct=True
candidates: ['19', '19', '19', '19', '19']
Accuracy: 100.00%
当前 mock 服务只返回固定答案,所以准确率为 100%。真实场景中,候选答案会有波动,投票机制才会发挥明显作用。
5. 可复现性实践
5.1 固定随机种子与参数
大多数推理框架都支持 seed 参数。评测脚本里,建议对每次请求传入不同的 seed 组合,比如:
seed = 1000 + question_index * 10 + sample_index
这样即使多轮运行,同一位置的采样结果不会变化。
同时,把所有推理参数记录到 JSON 文件中:
# 文件路径:save_config.py
import json
config = {
"model": "mock-reasoner",
"temperature": 0.7,
"top_p": 1.0,
"max_tokens": 128,
"seed": 42,
"n_answers": 5,
"voting": "majority",
"prompt_version": "v1.0",
}
with open("inference_config.json", "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
5.2 保存完整实验记录
推荐每个实验一个目录:
experiments/
run_20250101_1200/
inference_config.json
dataset.csv
raw_outputs.jsonl
metrics.json
prompt_template.txt
eval_script.py
保留原始输出到 JSONL 文件,可以随时复盘:
# 文件路径:save_raw_outputs.py
import json
with open("raw_outputs.jsonl", "a", encoding="utf-8") as f:
record = {
"question": question,
"candidates": all_answers,
"final_answer": final_answer,
"expected": CORRECT_ANSWERS[i],
"config": config,
}
f.write(json.dumps(record, ensure_ascii=False) + "\n")
5.3 版本锁定
不管是用 lm-evaluation-harness、promptfoo 还是 DeepEval,都要锁定工具版本。
pip freeze > requirements.txt
如果使用 npm 安装的 promptfoo,则提交 package-lock.json 到仓库。
模型版本也很关键。如果你的模型服务支持多个版本,评测时要显式指定,避免默认版本悄悄变化。
5.4 随机性与不确定性的处理
即使固定 seed,在不同硬件或不同 batch 大小下,结果也可能有差异。可复现性实践并不是追求“绝对相同”,而是追求“在合理误差范围内一致”。
更严谨的做法是:多次运行评测,报告平均值和标准差,而不是单次结果。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| promptfoo 连不上自建 API | apiBaseUrl 配置错误,或服务未启动 | 先 curl 测试接口,再检查 YAML 中的 URL 和路径 |
| 报错 OpenAI API Key 无效 | 自建服务不校验 key,但 SDK 要求非空 | 设置任意非空字符串,如 dummy-key |
| 评测结果不稳定 | 温度较高或没有固定 seed | 固定 seed、降低温度、多次运行取平均 |
| 多数投票效果不明显 | mock 服务输出单一,或数据量太少 | 更换真实模型,增大 N,或使用更难题库 |
| DeepEval 调用外部模型时慢 | AnswerRelevancyMetric 依赖另一个 LLM |
改用自定义规则 metric,或只在部分样本上使用模型打分 |
| 输出答案带推理过程,解析不到数字 | 正则提取规则太简单 | 根据模型输出格式设计更健壮的解析器 |
| 实验记录不完整 | 依赖版本或参数未保存 | 按实验目录保存 config、dataset、raw outputs 和脚本 |
| 换了评估工具后分数差异大 | 指标口径或 prompt 模板不一致 | 统一 prompt 模板和答案解析逻辑,先在小数据集上对齐两个工具 |
7. 最佳实践与工程建议
7.1 评测数据集与 prompt 分离
不要用临时写在脚本里的字符串作为测试集。把测试数据和 prompt 模板分开管理,推荐使用文件组织:
data/questions.jsonl:只存题目和标准答案。prompts/math_cot.txt:存 prompt 模板,包含变量占位符。
这样修改 prompt 时不需要改动代码,也方便对比不同 prompt 版本。
7.2 评估 Metrics 尽量可解释
在 Test-Time Scaling 实验中,单纯看准确率有时会掩盖问题。建议同时观察:
- 平均生成 token 数。
- 单次推理耗时。
- 候选答案多样性。
- 投票后正确率 vs 单次正确率。
这些指标能帮你判断扩展策略是否真的有效。
7.3 区分模型能力与采样策略收益
如果你发现 n=10 的多数投票比 n=1 准确率高,这不代表模型更强,而是采样策略带来增益。报告里最好分开描述:
- 基础准确率:n=1 时的表现。
- 扩展后准确率:n=10 时的表现。
- 增益量:扩展带来的提升幅度。
这样能避免“刷分式评测”带来的误导。
7.4 安全与授权提醒
如果你要评估的是线上模型,注意:
- 评测请求量不要超过服务配额。
- 不要在评测脚本里明文存储生产环境 API Key,建议使用环境变量。
- 如果要跑大量 Prompt,先在小范围测试,避免触发服务端限流或安全策略。
7.5 自动化与 CI 集成
对业务关键指标,可以把评测流程接入 CI。比如每次模型版本更新后,自动运行 100 条冒烟测试用例,失败则阻止发布。
promptfoo 支持直接作为命令行工具集成到 CI:
promptfoo eval --max-concurrency 4
promptfoo share
DeepEval 也支持与 pytest 配合,直接作为测试套件运行。
8. 总结与下一步学习路线
本文从 Test-Time Scaling 的背景出发,梳理了推理阶段扩展的核心思路,重点解决了三个工程问题:
- 如何用第三方评估工具接入自建推理 API。
- 如何自定义评估标准,比如多数投票和答案包含判断。
- 如何提升评测结果的可复现性。
如果你正在做推理大模型的效果评测,下一步可以优先做这几件事:
- 把现有的评测脚本改成“数据集 + Prompt 模板 + 推理配置 + 评估脚本”分离的结构。
- 引入 promptfoo 或 DeepEval 中的任意一个,先把自建 API 的冒烟评测跑通。
- 在固定 seed 和参数记录的前提下,对比 n=1 和 n=5 的多数投票效果差异。
- 尝试用更复杂的数据集,比如 GSM8K 或 MATH 的子集,观察 Test-Time Scaling 在不同难度上的表现差异。
评测这件事,投入产出比很高。把评估流程标准化之后,后面每一次模型迭代、参数调整,都能得到清晰可信的结论。希望这篇内容对你正在做的推理模型评测项目有帮助。
更多推荐
所有评论(0)