最近几周在折腾推理大模型的测试时扩展(Test-Time Scaling),一个很直观的感受是:模型本身的能力只是起点,推理阶段的“算力分配方式”对最终效果的影响比想象中更大。同一个模型,采用不同的推理模式,在数学推理、逻辑问答这类任务上可能拉开 10 到 20 个百分点的差距。更麻烦的是,这些推理模式的评估结果还不容易复现,换一个采样温度、换一个评估脚本,结论可能就变了。
这篇文章打算系统地拆解一下 Reasoning LLMs 中的 Test-Time Scaling:它到底在优化什么、有哪些典型的推理模式(Inference Regimes)、如何设计可复现的评估流程,以及第三方评估工具如何接入自己写的 API 和自定义指标。内容偏向工程落地,会给出可以直接跑的 Python 示例,适合正在做 LLM 应用评测、推理优化或模型选型的同学参考。文中使用的代码和配置都以常见开源环境为例,实际使用时需要根据自己的模型部署方式调整。
1. 背景与核心概念
1.1 什么是 Test-Time Scaling
Test-Time Scaling 指的是在模型推理阶段,通过增加计算量来提升输出质量的做法。传统机器学习中,我们更熟悉的是训练阶段的扩展,也就是通过增加参数量、训练数据量和训练算力来提升模型能力,这一类规律对应的是经典的 Scaling Laws。
但到了 Reasoning LLM 时代,情况发生了一个明显变化:模型在推理阶段也可以通过“多想一会”“多采样几次”“多搜索几步”来获得更好的答案。这就是测试时扩展的含义。它并不修改模型权重,而是在同样的模型参数下,优化推理时的计算预算分配方式。
一个直观的例子是数学应用题。直接让模型输出答案,可能只能得到 60% 的准确率;但如果让模型生成多个候选答案,再通过投票或奖励模型筛选,准确率可能提升到 80% 以上。这个过程消耗的推理计算量增加了,但没有重新训练模型。
1.2 为什么 Test-Time Scaling 备受关注
Reasoning LLMs 的核心能力在于“推理”,而推理本身是可以逐步展开的。像 o1 系列、DeepSeek-R1 这类模型,在内部会生成一段较长的思考过程,再给出最终答案。这种“长思考”本身就是一种测试时扩展。
对于应用开发者来说,Test-Time Scaling 的价值主要体现在三个场景:
- 高难度推理任务:数学竞赛题、代码竞赛题、复杂逻辑推理,需要多次尝试和验证。
- 缺少明确参考答案的场景:只要答案有可验证的评分方式,就可以通过多次采样来提高上限。
- 算力充足但模型参数量受限的情况:用小模型配合多轮采样,有时可以逼近大模型单次推理的效果。
1.3 推理模式(Inference Regimes)的通俗理解
推理模式可以理解为“在给定测试时计算预算下,模型以什么策略来生成答案”。不同的模式决定了计算资源的分布方式。
举几个典型的模式:
- 单次贪心解码:让模型直接以最高概率路径生成答案,计算开销最小,但容易陷入局部最优。
- 多次采样 + 多数投票:生成多个答案,按答案出现频率投票,适合答案可比较的任务。
- 多次采样 + 奖励模型排序:生成多个答案,用奖励模型打分,取最高分答案,适合答案难以直接比较的任务。
- 搜索式推理:在推理过程中引入树搜索或 beam search,让模型在候选推理路径中探索,适合需要多步验证的任务。
1.4 核心关键词速览
| 关键词 | 含义 | 在本文中的作用 |
|---|---|---|
| Test-Time Scaling | 测试时扩展 | 文章主题 |
| Reasoning LLMs | 推理大模型 | 被优化的对象 |
| Inference Regimes | 推理模式 | 计算预算分配策略 |
| Evaluation | 评估 | 验证扩展是否有效 |
| Reproducibility | 可复现性 | 保证结论稳定可靠 |
2. 环境准备与版本说明
2.1 运行环境
本文的示例代码基于 Python 3.9 以上版本,推荐使用 Python 3.10 或 3.11。操作系统不限,Windows、Linux、macOS 都可以运行,但涉及模型部署的部分建议使用 Linux 服务器。
在开始之前,建议先创建一个独立的虚拟环境,避免依赖冲突:
python -m venv venv source venv/bin/activateWindows 环境下激活命令为:
venv\Scripts\activate2.2 需要安装的依赖
示例代码的核心依赖如下:
openai>=1.0.0 pandas>=1.5.0 numpy>=1.24.0 pyyaml>=6.0 requests>=2.28.0 python-dotenv>=1.0.0安装命令:
pip install -r requirements.txt如果你的模型服务是通过 vLLM、TGI 或 Ollama 部署的,这些工具大多提供 OpenAI 兼容接口,本文的代码可以直接使用。只需要修改base_url指向你的本地服务地址即可。
2.3 模型服务部署方式
本文不重点讲解模型部署细节,但为了方便下面的代码演示,建议准备一个 OpenAI 兼容的推理服务。
以 vLLM 为例,部署一个开源模型的命令大致如下:
python -m vllm.entrypoints.openai.api_server \ --model Qwen/QwQ-32B \ --port 8000这里需要说明:示例中的模型名称、端口号仅作演示,具体以你实际使用的模型和服务地址为准。如果你使用的是云端 API,也可以直接配置OPENAI_API_KEY和base_url。
2.4 环境变量配置
在项目根目录创建一个.env文件,用于保存 API 相关的配置:
OPENAI_API_KEY=your-api-key OPENAI_BASE_URL=http://localhost:8000/v1 MODEL_NAME=your-model-name然后在 Python 中加载:
from dotenv import load_dotenv import os load_dotenv() api_key = os.getenv("OPENAI_API_KEY") base_url = os.getenv("OPENAI_BASE_URL") model_name = os.getenv("MODEL_NAME")3. 核心机制拆解
3.1 推理模式的分类
在 Test-Time Scaling 研究中,推理模式通常可以按“生成方式”和“选择方式”两个维度来分类。
生成方式决定了模型如何产生候选答案:
- Greedy Decoding:每次只选概率最高的 token,输出唯一答案。
- Temperature Sampling:通过温度参数控制随机性,生成多个候选答案。
- Top-p Sampling:在概率累积到一定阈值的小集合内采样,兼顾多样性。
- Beam Search:保留多个候选序列,逐步扩展,适合生成任务。
选择方式决定了如何从多个候选中确定最终答案:
- Majority Voting:按答案内容聚类,取出现最多的答案。
- Weighted Voting:按置信度加权,例如使用模型对每个答案的自评估概率。
- Reward Model Ranking:训练或使用一个奖励模型,对候选答案打分排序。
- Verification:让模型反向验证每个候选答案的正确性,例如要求模型检查推理步骤。
实际项目中常常组合使用。例如,先采样 N 个答案,再用一个轻量级验证器挑选最优答案。
3.2 计算预算分配策略
Test-Time Scaling 的核心问题是在固定推理成本下,如何分配计算预算。
先看一个简单的对比:
| 策略 | 采样数 | 单次推理长度 | 总成本 | 适用任务 |
|---|---|---|---|---|
| 单次贪心 | 1 | 短 | 低 | 简单问题 |
| 自一致性采样 | 8 | 中 | 中 | 答案可比较的任务 |
| Best-of-N | 16 | 中 | 高 | 需要奖励模型筛选的任务 |
| 搜索扩展 | 8 | 长 | 很高 | 需要多步验证的任务 |
这里的“总成本”并不是简单相乘,因为有些任务需要先将生成的候选答案缓存下来,再做后续筛选。你可以在实际项目中先跑一个小规模实验,统计不同策略的准确率和成本曲线,再决定生产环境使用哪种模式。
3.3 自一致性(Self-Consistency)详解
自一致性是最容易落地的一种 Test-Time Scaling 方法。它的思路很简单:让同一个模型在相同提示下采样多个答案,然后统计答案出现的频率,选择出现次数最多的答案。
这个方法的有效性来自一个观察:对于推理任务,正确答案往往在采样分布中占据较高概率质量,虽然单次采样可能因为随机性跑偏,但多次采样后,正确答案会更容易“重复出现”。
下面是一个伪代码逻辑:
def self_consistency(samples): # samples 是模型生成的多个答案,通常是字符串列表 counter = {} for answer in samples: normalized = normalize_answer(answer) counter[normalized] = counter.get(normalized, 0) + 1 best_answer = max(counter, key=counter.get) return best_answer, counter需要注意的是,自一致性要求答案能够被规范化比较。对于数学题,可以比较最终数值;对于选择题,可以比较选项字母;但对于开放式问答,直接比较字符串可能不合适,这种情况更适合用奖励模型排序。
3.4 奖励模型与排序策略
当答案难以直接比较时,可以引入一个奖励模型(Reward Model)或验证器。这个模型接收“问题 + 候选答案”,输出一个分数,代表答案的质量。
在测试时扩展流程中,典型的做法是:
- 用生成模型采样 N 个候选答案。
- 对每个候选答案,调用奖励模型打分。
- 选择分数最高的答案作为最终输出。
这里的奖励模型可以是训练好的专用模型,也可以是一个通过 API 调用的评分服务,甚至可以是一个带有评分 prompt 的大模型。关键是这个评分器要尽量稳定,不能在多次评估之间产生较大波动,否则会破坏可复现性。
4. 完整实战案例
4.1 场景定义
为了演示 Test-Time Scaling 的完整流程,我们使用一个简单的数学推理场景。假设需要回答下面这类问题:
一个果园有 48 棵苹果树,每棵树平均结 126 个苹果。如果每 12 个苹果装一箱,一共需要多少个箱子?这类问题答案明确,适合用自一致性方法评估。下面的代码会实现三种推理模式:
- 单次贪心解码。
- 多次采样 + 多数投票。
- 多次采样 + 简易排序器。
4.2 项目结构
test_time_scaling_demo/ ├── .env ├── requirements.txt ├── llm_client.py # 封装 OpenAI 兼容的模型调用 ├── inference_regimes.py # 实现不同推理模式 ├── evaluation.py # 自定义评估脚本 └── run_demo.py # 主入口4.3 模型调用客户端
llm_client.py的作用是统一管理模型 API 的调用方式。这里使用 OpenAI Python SDK,但通过base_url指向本地或第三方服务。
# 文件路径:test_time_scaling_demo/llm_client.py import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("OPENAI_API_KEY", "EMPTY"), base_url=os.getenv("OPENAI_BASE_URL", "http://localhost:8000/v1"), ) MODEL_NAME = os.getenv("MODEL_NAME", "your-model-name") def generate(prompt: str, temperature: float = 0.0, max_tokens: int = 1024) -> str: """ 调用模型生成文本。 temperature=0.0 时接近贪心解码,temperature>0 时采样会更有随机性。 """ response = client.chat.completions.create( model=MODEL_NAME, messages=[ {"role": "system", "content": "你是一个严谨的数学解题助手。"}, {"role": "user", "content": prompt}, ], temperature=temperature, max_tokens=max_tokens, ) return response.choices[0].message.content.strip()4.4 实现不同推理模式
inference_regimes.py中实现了三种推理模式。
# 文件路径:test_time_scaling_demo/inference_regimes.py import re from collections import Counter from llm_client import generate def greedy_decoding(prompt: str) -> str: """模式一:单次贪心解码,temperature=0。""" return generate(prompt, temperature=0.0) def majority_voting(prompt: str, n_samples: int = 8, temperature: float = 0.7) -> str: """ 模式二:多次采样 + 多数投票。 关键点: 1. temperature 要大于 0,否则多次采样结果完全一样。 2. 答案需要统一提取为“最终数值”之类可比较的字符串。 """ samples = [] for _ in range(n_samples): sample = generate(prompt, temperature=temperature) samples.append(extract_answer(sample)) counter = Counter(samples) best_answer = counter.most_common(1)[0][0] return best_answer def best_of_n_with_ranker(prompt: str, n_samples: int = 8, temperature: float = 0.7) -> str: """ 模式三:多次采样 + 简易排序器。 这里用一个启发式排序规则代替真正的奖励模型: 优先选择包含“因此”“所以”“最终答案”等推理标记,且答案行较完整的输出。 生产环境可以替换为训练好的奖励模型或独立的评分 API。 """ samples = [] for _ in range(n_samples): sample = generate(prompt, temperature=temperature) score = heuristic_score(sample) samples.append((sample, score)) samples.sort(key=lambda x: x[1], reverse=True) return samples[0][0] def extract_answer(text: str) -> str: """ 从模型输出中提取最终答案。 这里使用一个最简单的规则:提取所有数字。 你可以根据自己的任务类型,替换为更严谨的解析逻辑。 """ matches = re.findall(r"\d+", text) if not matches: return "NO_ANSWER" # 如果有多个数字,取最后一个,因为最终答案通常出现在结尾 return matches[-1] def heuristic_score(text: str) -> float: """ 简易排序器:根据文本特征给答案打分。 注意:这只是一个演示用的启发式方法,不代表真实奖励模型的性能。 """ score = 0.0 if "因此" in text or "所以" in text: score += 1.0 if "最终答案" in text: score += 1.0 # 更长的解答往往包含更完整的推理过程,但也可能包含废话,需要按实际任务调整 score += min(len(text) / 500.0, 1.0) return score4.5 主入口脚本
run_demo.py会依次运行三种推理模式,并输出对比结果。
# 文件路径:test_time_scaling_demo/run_demo.py from inference_regimes import greedy_decoding, majority_voting, best_of_n_with_ranker PROMPT = """ 一个果园有48棵苹果树,每棵树平均结126个苹果。 如果每12个苹果装一箱,一共需要多少个箱子? 请给出你的推理过程,并最终输出数字答案。 """ if __name__ == "__main__": print("=== 模式一:贪心解码 ===") result1 = greedy_decoding(PROMPT) print(result1) print() print("=== 模式二:多数投票 ===") result2 = majority_voting(PROMPT, n_samples=8, temperature=0.7) print(result2) print() print("=== 模式三:Best-of-N + 排序器 ===") result3 = best_of_n_with_ranker(PROMPT, n_samples=8, temperature=0.7) print(result3)运行命令:
python run_demo.py4.6 预期结果与说明
对于上面这个数学题,正确结果是504(48 × 126 ÷ 12 = 504)。
- 贪心解码可能直接输出正确答案,也可能在中间步骤出错后得到错误答案,取决于模型本身能力。
- 多数投票会对 8 次采样结果做数字频率统计,通常会提高正确答案的占比。
- Best-of-N 排序器因为只是启发式打分,效果不一定比多数投票更好,它的优势主要体现在答案无法直接比较的开放式任务中。
这里需要强调一个容易误解的点:增加采样数并不总是带来提升。如果模型本身能力较弱,多次采样只会产生更多错误答案,多数投票可能把高频错误答案当成最终结果。因此在实践前,建议先做一个小样本实验。
5. 评估与可复现性
5.1 为什么评估 Test-Time Scaling 很困难
评估测试时扩展的效果,比评估普通模型生成质量更复杂。原因主要有三个:
- 答案多样性:不同推理模式产生的答案风格差异很大,简单字符串匹配无法衡量质量。
- 指标敏感性:准确率、F1、BLEU 等指标对答案格式非常敏感,同一个正确答案,描述方式不同就可能被判定为错误。
- 采样随机性:Temperature Sampling 本身带有随机性,如果不对随机种子和采样参数做固定,实验结果很难复现。
因此,设计评估流程时,必须把“评估指标”和“评估过程”都固定下来。
5.2 第三方评估工具的选型思路
如果你正在寻找一个第三方评估工具,要求是“可以调用自己写的 API,并根据自己定义的评估标准来评分”,目前的常见做法有两种。
第一种是使用开源评估框架。例如EleutherAI/lm-evaluation-harness可以注册自定义模型后端和自定义任务,你在任务中定义评估指标和评分逻辑。它支持接入 OpenAI 兼容的模型服务,也支持自定义指标函数。
第二种是自建轻量评估服务。通过一个 HTTP API 将待评估的模型输出发送给评分器,评分器内部可以调用大模型、规则脚本或人工审核接口,最终返回一个可解析的分数。这种方式的优点是灵活,适合团队内部已经有评分中台的场景。
5.3 调用自定义 API 的评估实现
下面是一个轻量评估脚本的示例。它会把“问题、模型输出、参考答案”打包成 JSON,发送到你自己的评估 API,然后解析返回的分数。
# 文件路径:test_time_scaling_demo/evaluation.py import json import requests def evaluate_with_custom_api( question: str, model_output: str, reference_answer: str, eval_api_url: str, ) -> dict: """ 调用自定义评估 API 评分。 请求体设计为通用格式: { "question": 原始问题, "model_output": 模型输出, "reference_answer": 参考答案 } 评分 API 应返回 JSON,至少包含 score 字段,例如: {"score": 0.85, "passed": true, "comment": "答案正确"} """ payload = { "question": question, "model_output": model_output, "reference_answer": reference_answer, } try: response = requests.post(eval_api_url, json=payload, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.Timeout: return {"score": 0.0, "passed": False, "comment": "评估超时"} except requests.exceptions.RequestException as e: return {"score": 0.0, "passed": False, "comment": f"请求失败: {e}"} if __name__ == "__main__": result = evaluate_with_custom_api( question="一个果园有48棵苹果树,每棵树平均结126个苹果,每12个苹果装一箱,需要多少个箱子?", model_output="504", reference_answer="504", eval_api_url="http://localhost:9000/evaluate", ) print(json.dumps(result, ensure_ascii=False, indent=2))你只需在本地启动一个评分服务,并在/evaluate接口里实现自己的评分逻辑,就可以用这套流程评估任意模型的输出。
5.4 可复现性清单
为了保证 Test-Time Scaling 实验结果可复现,建议记录以下信息:
| 项目 | 说明 |
|---|---|
| 模型名称与版本 | 记录模型权重版本、部署方式 |
| 采样参数 | temperature、top_p、max_tokens |
| 随机种子 | 如果框架支持,固定全局 seed |
| 采样次数 | N 的数量 |
| 提示词模板 | 包括 system prompt 和 user prompt |
| 答案提取规则 | 例如提取最后一个数字 |
| 评估 API 版本 | 自定义评分服务的版本号 |
| 运行时间与批次 | 记录实验批次,方便回溯 |
将这些信息写入一个experiment_config.yaml文件会是一个不错的选择。
# 文件路径:test_time_scaling_demo/experiment_config.yaml model: name: your-model-name base_url: http://localhost:8000/v1 version: "2025-06-01" sampling: temperature: 0.7 top_p: 0.9 max_tokens: 1024 n_samples: 8 seed: 42 evaluation: api_url: http://localhost:9000/evaluate metric: custom_score后续复现实验时,只要读取这个配置文件,就能保证大部分关键参数一致。
6. 常见问题与排查思路
6.1 典型问题列表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 多次采样结果完全相同 | temperature 设置为 0 或模型忽略了采样参数 | 将 temperature 调到 0.7 以上 |
| 多数投票结果反而变差 | 模型本身错误率高,错误答案重复出现 | 减少采样数,或改用奖励模型排序 |
| 评估 API 请求超时 | 自定义评分服务处理速度慢,并发过高 | 增加超时时间,或对评估请求做批量排队 |
| 答案提取错误 | 正则规则过于简单,无法覆盖所有输出格式 | 根据任务定制解析规则,或使用大模型抽取答案 |
| 实验结果无法复现 | 没有固定随机种子、模型版本或提示词 | 记录配置清单,固定所有关键参数 |
| 成本骤增 | N 设置过大,推理长度过长 | 先画准确率-成本曲线,再选拐点 |
6.2 排查流程建议
如果你发现 Test-Time Scaling 没有带来明显提升,建议按以下顺序排查:
- 检查任务类型:答案是否存在唯一标准?如果答案本身有歧义,多数投票可能不适用。
- 检查采样多样性:输出是否过于重复?可以打印 8 次采样的原始文本,观察差异性。
- 检查答案提取规则:是否把正确答案解析成了错误格式?
- 检查评估指标:如果评估 API 的评分标准不稳定,实验结果自然不稳定。
- 检查成本预算:如果 N 太大但收益很小,需要换一个更高效的推理模式。
7. 最佳实践与工程建议
7.1 结合任务选择推理模式
不要一上来就堆采样数量。先明确任务的答案是否可比较、验证成本高不高、延迟要求多严格。
- 答案可比较的任务:优先用多数投票。
- 答案不可比较、有验证器:优先用 Best-of-N + 奖励模型。
- 多步推理、需要探索的任务:考虑搜索式推理,但成本和延迟会明显增加。
7.2 设置自适应预算
可以在代码中加入一个简单的自适应逻辑:先用低采样数生成回答,如果置信度不足,再增加采样数。例如,通过模型输出多个候选答案,若多数投票的一致性比例较高,就直接返回结果;如果一致性较低,再扩大采样规模。
def adaptive_majority_voting(prompt, min_samples=4, max_samples=16, threshold=0.6): samples = [] for i in range(max_samples): samples.append(extract_answer(generate(prompt, temperature=0.7))) if i + 1 >= min_samples: counter = Counter(samples) top_count = counter.most_common(1)[0][1] if top_count / (i + 1) >= threshold: break return counter.most_common(1)[0][0]这种策略可以显著减少平均推理成本。
7.3 做好缓存与去重
在多次采样中,经常会出现完全相同的输出。可以在客户端缓存历史请求的 response,对相同的 prompt + 采样参数组合直接复用结果。同时,在答案提取后做去重,减少后续排序的候选数量。
7.4 日志与版本管理
每次实验都需要保存原始输出、解析结果、评估结果和配置信息。建议使用统一的 JSON 格式存储,文件名包含实验批次和模型名称。
{ "experiment_id": "exp-001", "model": "your-model-name", "inference_regime": "majority_voting", "n_samples": 8, "temperature": 0.7, "question": "...", "raw_outputs": ["...", "..."], "final_answer": "504", "evaluation_score": 1.0 }这样可以随时回溯“某条答案是怎么来的、对应的参数是什么”。
7.5 生产环境的注意事项
- 控制最大采样数和 max_tokens,避免单次请求超时。
- 对上游模型 API 做限流和重试,防止批量评估时触发限流。
- 评估服务与模型生成服务最好隔离部署,避免互相影响性能。
- 在正式上线前,先用小规模数据集跑通全流程,再逐步扩大评估集。
8. 总结与学习路线
在本文中,我们拆解了 Test-Time Scaling 的核心概念,梳理了推理模式分类,并用一个数学推理场景实现了贪心解码、多数投票和 Best-of-N 排序三种模式。然后,我们讨论了评估和可复现性的关键问题,给出了调用自定义评估 API 的脚本示例,并整理了常见问题和工程建议。
如果你正在落地推理模型的测试时扩展,我建议按下面的顺序继续深入:
- 先固定一个任务集和评估 API,保证实验结果可信。
- 在 100 条样本上对比单次贪心、多数投票、Best-of-N 的效果和成本。
- 找到适合自己任务的最优推理模式后,再考虑奖励模型或搜索式推理。
最后给一个实用的小建议:不要追求“把 N 调到最大”。Test-Time Scaling 的核心是“以最低的计算成本获得最大的效果提升”,这更像是一个工程优化问题,而不是一个简单的参数调节问题。每次实验前写下配置、每次实验后记录结果,你会发现可复现性带来的收益,远比你多跑几组参数更大。