最近在对比推理大模型(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 本文实战场景

我设计的示例场景如下:

  1. 有一个自建推理服务,接口兼容 OpenAI API 格式。
  2. 需要评测模型在高斯数学题上的准确率。
  3. 每次生成不只跑 1 条答案,而是先采样 N 条候选,再用多数投票得出最终答案。
  4. 评测脚本需要记录温度、种子、模型版本、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 评估流程的组成

一次完整的评测流程,通常包含:

  1. 数据准备:测试用例的加载与清洗。
  2. 推理调用:调用自建 API,设置 Inference Regimes。
  3. 答案解析:从模型输出中提取最终答案。
  4. 指标计算:根据自定义标准评分。
  5. 结果记录:保存原始输出、参数和指标到本地文件。

在这套流程里,数据、参数、代码、运行环境四者耦合在一起,哪一环没记录,复现都会出问题。

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。
  • 如何自定义评估标准,比如多数投票和答案包含判断。
  • 如何提升评测结果的可复现性。

如果你正在做推理大模型的效果评测,下一步可以优先做这几件事:

  1. 把现有的评测脚本改成“数据集 + Prompt 模板 + 推理配置 + 评估脚本”分离的结构。
  2. 引入 promptfoo 或 DeepEval 中的任意一个,先把自建 API 的冒烟评测跑通。
  3. 在固定 seed 和参数记录的前提下,对比 n=1 和 n=5 的多数投票效果差异。
  4. 尝试用更复杂的数据集,比如 GSM8K 或 MATH 的子集,观察 Test-Time Scaling 在不同难度上的表现差异。

评测这件事,投入产出比很高。把评估流程标准化之后,后面每一次模型迭代、参数调整,都能得到清晰可信的结论。希望这篇内容对你正在做的推理模型评测项目有帮助。

更多推荐