最近在对比推理大模型(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 promptfoo2.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.htmlpromptfoo 的断言机制非常灵活,包括:
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 shareDeepEval 也支持与 pytest 配合,直接作为测试套件运行。
8. 总结与下一步学习路线
本文从 Test-Time Scaling 的背景出发,梳理了推理阶段扩展的核心思路,重点解决了三个工程问题:
- 如何用第三方评估工具接入自建推理 API。
- 如何自定义评估标准,比如多数投票和答案包含判断。
- 如何提升评测结果的可复现性。
如果你正在做推理大模型的效果评测,下一步可以优先做这几件事:
- 把现有的评测脚本改成“数据集 + Prompt 模板 + 推理配置 + 评估脚本”分离的结构。
- 引入 promptfoo 或 DeepEval 中的任意一个,先把自建 API 的冒烟评测跑通。
- 在固定 seed 和参数记录的前提下,对比 n=1 和 n=5 的多数投票效果差异。
- 尝试用更复杂的数据集,比如 GSM8K 或 MATH 的子集,观察 Test-Time Scaling 在不同难度上的表现差异。
评测这件事,投入产出比很高。把评估流程标准化之后,后面每一次模型迭代、参数调整,都能得到清晰可信的结论。希望这篇内容对你正在做的推理模型评测项目有帮助。