- Published on
AI Papers: Test-Time Scaling 核心论文梳理 — 用推理预算提升性能的方法
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Train-Time 与 Test-Time:扩展的两个轴
- 从论文脉络看 TTS 的发展史
- Best-of-N:最简单却效果惊人
- Self-Consistency:多数表决的数学依据
- 基于 Tree Search 的方法:结构化搜索
- 代码生成中的 TTS:Test-based Reranking
- 推理预算控制:防止成本爆炸
- 运营指标采集与监控
- 应用判断框架
- 常见错误与应对
- 参考资料

Train-Time 与 Test-Time:扩展的两个轴
LLM 性能提升的历史大致分为两条脉络。
Train-time scaling 是通过增加参数数量、训练数据和训练步数来让模型本身变强的方法。Kaplan 等(2020)的 Scaling Laws 论文(arxiv:2001.08361)为这一方向奠定了理论基础,GPT-4、Claude、Gemini 等最新模型都是这一策略的产物。
Test-time scaling(TTS) 保持已训练好的模型不变,在推理阶段投入更多计算来提升性能。由于不改变模型权重,因此无需微调或重新训练即可应用。它在数学、编程、多步推理这类"抵达正确答案的路径有多条、且可以验证"的任务上表现尤为出色。
用一句话概括 TTS 的核心思路:
与其只回答一次,不如生成多个候选答案,再从中选出最可信的那一个。
这个简单的策略为何有效,从概率上看很直观。假设模型单次尝试答对的概率为 p,独立尝试 N 次后,至少有一次答对的概率为 1-(1-p)^N,会迅速上升。当然,只有具备"挑出正确答案的能力",这个概率才能真正转化为实际性能。
从论文脉络看 TTS 的发展史
TTS 并非出自单一论文,而是由多项研究累积形成的领域。下面按时间顺序梳理主要里程碑。
Chain-of-Thought Prompting(Wei 等,2022,arxiv:2201.11903) 以 "Let's think step by step" 为代表的提示技巧。它证明了显式生成中间推理过程能大幅提升最终答案的准确率。虽然它本身并非直接的 TTS 技巧,但"推理路径可以被拆开来看"这一想法,成为此后所有 TTS 方法的前提。
Self-Consistency Decoding(Wang 等,2022,arxiv:2203.11171) 对 CoT 进行多次采样,再对最终答案取多数表决。其依据是一个观察 —— 即使推理路径不同,也往往会收敛到同一个正确答案。在 GSM8K 数学基准上,相比 greedy decoding,它展现出大幅的准确率提升。
Verifier-guided Decoding(Cobbe 等,2021,arxiv:2110.14168) 在数学题上训练一个 solution-level verifier,从多个候选中选出 verifier 打分最高的答案。核心在于把"生成"与"验证"分离开来。此后延伸出 OpenAI 的 process reward model(PRM)研究(Lightman 等,2023,arxiv:2305.20050),朝着评估解题过程每一步的方向发展。
Tree-of-Thought(Yao 等,2023,arxiv:2305.10601) 把推理构造成树结构,用 BFS/DFS 进行搜索。每个节点由 LLM 执行自我评估(self-evaluation)来做剪枝。在 24 点游戏、填字游戏等搜索性质较强的问题上,相比 CoT 表现出明显的性能差距。
Scaling LLM Test-Time Compute(Snell 等,2024,arxiv:2408.03314) 系统分析"该如何分配推理预算"的论文。从 compute-optimal scaling 的角度证明了,根据问题难度调节 N(采样数)与搜索深度是有效的。简单问题只需 Best-of-2 即可,预算应集中投入到困难问题上。
TTS Survey: What, How, Where, and How Well?(arxiv:2503.24235,2025) 一篇综合性综述,把 TTS 方法论整理为 what(扩展什么)、how(如何扩展)、where(在哪里扩展)、how well(效果如何)4 个轴。本文涉及的大多数方法都能在这个框架内找到自己的位置。
Best-of-N:最简单却效果惊人
Best-of-N 是 TTS 最基本的形式。提高 temperature 生成 N 个独立答案,再按某种标准选出其中最好的一个。
看起来简单,但实际效果很大。例如在代码生成中,pass@1 只有 30% 的模型,pass@10 常常能超过 60%(Chen 等,"Evaluating Large Language Models Trained on Code",arxiv:2107.03374)。关键在于"如何挑出好答案"。
下面是一个可用于生产环境的 Best-of-N 流水线实现。
from dataclasses import dataclass, field
from typing import Callable
import asyncio
import time
@dataclass
class ScoredCandidate:
text: str
score: float
latency_ms: float
token_count: int
@dataclass
class BestOfNConfig:
n_samples: int = 4
temperature: float = 0.7
max_tokens: int = 1024
timeout_seconds: float = 15.0
cost_budget_usd: float = 0.05
class BestOfNPipeline:
"""生产环境用 Best-of-N 流水线。
将候选生成、评分、预算控制整合在一个流程中管理。
"""
def __init__(
self,
generator: Callable,
scorers: list[Callable],
config: BestOfNConfig = BestOfNConfig(),
):
self.generator = generator
self.scorers = scorers
self.config = config
async def generate_candidates(self, prompt: str) -> list[ScoredCandidate]:
"""异步生成 N 个候选并打分。"""
candidates = []
tasks = [
self._generate_one(prompt)
for _ in range(self.config.n_samples)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
continue # 跳过 timeout 或 API 错误
text, latency_ms, token_count = result
score = self._compute_score(text)
candidates.append(ScoredCandidate(
text=text,
score=score,
latency_ms=latency_ms,
token_count=token_count,
))
return sorted(candidates, key=lambda c: c.score, reverse=True)
def _compute_score(self, text: str) -> float:
"""计算多个 scorer 的加权平均值。"""
if not self.scorers:
return 0.0
scores = [scorer(text) for scorer in self.scorers]
return sum(scores) / len(scores)
async def _generate_one(self, prompt: str):
"""生成单个候选。超过 timeout 时抛出 asyncio.TimeoutError。"""
start = time.monotonic()
result = await asyncio.wait_for(
self.generator(prompt, temperature=self.config.temperature),
timeout=self.config.timeout_seconds,
)
elapsed_ms = (time.monotonic() - start) * 1000
return result.text, elapsed_ms, result.token_count
async def run(self, prompt: str) -> ScoredCandidate | None:
"""返回最优候选。没有候选时返回 None。"""
candidates = await self.generate_candidates(prompt)
return candidates[0] if candidates else None
这个实现有三点值得注意。
- 异步并行生成:N 个请求并发发送而非顺序发送,从而降低总体延迟。
- 多 scorer 合成:不依赖单一评分函数,而是汇总多个视角的 scorer。
- 异常容忍:即使部分候选生成失败,也能用其余候选继续进行。
Self-Consistency:多数表决的数学依据
Self-Consistency 与 Best-of-N 类似,都会生成多个答案,但选择方式不同。它不评估单个答案的"质量",而是挑出出现次数最多的最终答案。
这个方法之所以有效,在于数学/逻辑问题的特性。推理路径可以是多样的,但正确答案只有一个,所以如果多条路径收敛到同一个答案,它是正确答案的概率就更高。相反,错误答案则会以各种不同的形式散布开来。
from collections import Counter
import re
from typing import Optional
def extract_final_answer(text: str) -> Optional[str]:
"""从 CoT 输出中提取最终答案。
依次尝试多种模式,如果全部都不匹配,
则使用最后一行作为答案。
"""
# 模式 1: "정답: X"、"Answer: X" 形式
patterns = [
r"(?:정답|답|answer|result)\s*[::=]\s*(.+)",
r"\\boxed\{(.+?)\}", # LaTeX boxed 形式
r"####\s*(.+)", # GSM8K 形式
r"(?:따라서|그러므로|therefore)\s*(.+)",
]
for pattern in patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
if matches:
return matches[-1].strip()
# 所有模式都失败时,返回最后一个非空行
lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
return lines[-1] if lines else None
def normalize_answer(answer: str) -> str:
"""用于答案比较的归一化处理。
去除空格、标点、单位表记等细微差异,
以便把相同的答案识别为同一个答案。
"""
s = answer.strip().lower()
# 去除货币/单位符号
s = re.sub(r"[$€₩\\,]", "", s)
# 去除结尾的句点/逗号
s = s.rstrip(".,;")
# 把多个空格合并为一个空格
s = re.sub(r"\s+", " ", s)
return s
def self_consistency_vote(
samples: list[str],
min_agreement: int = 2,
) -> tuple[Optional[str], int, int]:
"""执行 Self-Consistency 多数表决。
Returns:
(best_answer, vote_count, total_valid):
- best_answer: 出现次数最多的答案(提取失败时为 None)
- vote_count: 该答案获得的票数
- total_valid: 成功提取出答案的样本总数
"""
extracted = []
for sample in samples:
ans = extract_final_answer(sample)
if ans is not None:
extracted.append(normalize_answer(ans))
if not extracted:
return None, 0, 0
counter = Counter(extracted)
best_answer, vote_count = counter.most_common(1)[0]
if vote_count < min_agreement:
return None, vote_count, len(extracted)
return best_answer, vote_count, len(extracted)
Self-Consistency 的局限也很明显。在自由形式生成(文章、摘要)中,"相同答案"的定义本身就模糊,因此难以适用。此外,如果模型系统性地给出相同的错误答案(systematic bias),多数表决反而会强化这个错误答案。
基于 Tree Search 的方法:结构化搜索
Tree-of-Thought(ToT)和基于 MCTS(Monte Carlo Tree Search)的方法,会把推理过程构造成树来进行搜索。它不是单纯生成多个完整答案,而是在中间步骤上分支、评估、剪枝。
from dataclasses import dataclass, field
from typing import Optional
import heapq
@dataclass
class ThoughtNode:
"""推理树中的一个节点。"""
content: str
depth: int
score: float = 0.0
parent: Optional['ThoughtNode'] = None
children: list['ThoughtNode'] = field(default_factory=list)
def path(self) -> list[str]:
"""返回从根节点到当前节点的推理路径。"""
nodes = []
current = self
while current is not None:
nodes.append(current.content)
current = current.parent
return list(reversed(nodes))
class BeamSearchReasoner:
"""基于 Beam Search 的推理搜索器。
在每个 depth 维持 beam_width 个候选,
搜索到 max_depth 后返回得分最高的路径。
"""
def __init__(
self,
expand_fn, # (context: str) -> list[str]: 下一推理步骤的候选
evaluate_fn, # (partial_solution: str) -> float: 中间评估分数
beam_width: int = 3,
max_depth: int = 5,
):
self.expand_fn = expand_fn
self.evaluate_fn = evaluate_fn
self.beam_width = beam_width
self.max_depth = max_depth
def search(self, problem: str) -> list[str]:
"""用 beam search 寻找最优推理路径。"""
root = ThoughtNode(content=problem, depth=0)
beam = [root]
for depth in range(1, self.max_depth + 1):
all_candidates = []
for node in beam:
context = "\n".join(node.path())
expansions = self.expand_fn(context)
for thought in expansions:
child = ThoughtNode(
content=thought,
depth=depth,
parent=node,
)
partial = "\n".join(child.path())
child.score = self.evaluate_fn(partial)
node.children.append(child)
all_candidates.append(child)
# 只保留得分最高的 beam_width 个
beam = heapq.nlargest(
self.beam_width,
all_candidates,
key=lambda n: n.score,
)
if not beam:
break
# 从整个搜索结果中返回得分最高的路径
best = max(beam, key=lambda n: n.score)
return best.path()
Tree search 的成本可能呈指数级增长,因此实务中必须设置 depth 上限和 beam width 上限。推理各步骤评估函数(evaluate_fn)的质量,决定了整个系统的性能。
代码生成中的 TTS:Test-based Reranking
代码生成是 TTS 应用最自然的领域,因为"正确答案"的定义很明确 -- 通过测试就是对,失败就是错。
import subprocess
import tempfile
import textwrap
from pathlib import Path
from dataclasses import dataclass
@dataclass
class CodeEvalResult:
code: str
passed: bool
tests_passed: int
tests_total: int
error_output: str
execution_time_ms: float
def evaluate_code_candidate(
code: str,
test_code: str,
timeout_seconds: int = 30,
) -> CodeEvalResult:
"""用测试套件评估代码候选。
实际运行 pytest 来判定通过/失败,
并处理 timeout、import error 等异常情况。
"""
import time
with tempfile.TemporaryDirectory() as tmpdir:
solution_path = Path(tmpdir) / "solution.py"
test_path = Path(tmpdir) / "test_solution.py"
solution_path.write_text(code, encoding="utf-8")
# 构造测试文件,以便能从中 import solution
full_test = textwrap.dedent(f"""\
import sys
sys.path.insert(0, "{tmpdir}")
from solution import *
{test_code}
""")
test_path.write_text(full_test, encoding="utf-8")
start = time.monotonic()
try:
result = subprocess.run(
["python", "-m", "pytest", str(test_path), "-v", "--tb=short"],
capture_output=True,
text=True,
timeout=timeout_seconds,
cwd=tmpdir,
)
elapsed_ms = (time.monotonic() - start) * 1000
# 从 pytest 输出中解析 passed/failed 数量
passed = result.returncode == 0
lines = result.stdout.splitlines()
summary_line = [l for l in lines if "passed" in l or "failed" in l]
return CodeEvalResult(
code=code,
passed=passed,
tests_passed=result.stdout.count("PASSED"),
tests_total=result.stdout.count("PASSED") + result.stdout.count("FAILED"),
error_output=result.stderr if not passed else "",
execution_time_ms=elapsed_ms,
)
except subprocess.TimeoutExpired:
elapsed_ms = (time.monotonic() - start) * 1000
return CodeEvalResult(
code=code,
passed=False,
tests_passed=0,
tests_total=0,
error_output=f"Execution timed out after {timeout_seconds}s",
execution_time_ms=elapsed_ms,
)
def pick_best_code(
candidates: list[str],
test_code: str,
) -> CodeEvalResult | None:
"""在多个代码候选中选出通过测试数最多的一个。
如果有多个候选全部通过,则优先选择执行时间更短的。
"""
results = [evaluate_code_candidate(c, test_code) for c in candidates]
# 第 1 优先级:通过测试数,第 2 优先级:执行时间(越低越好)
results.sort(key=lambda r: (r.tests_passed, -r.execution_time_ms), reverse=True)
return results[0] if results else None
推理预算控制:防止成本爆炸
TTS 在实务中最大的风险是成本爆炸。如果对所有请求都套用 N=8,API 成本会接近上涨 8 倍。需要根据问题难度动态分配预算的策略。
# inference_policy.yaml
# 按问题难度分配 TTS 预算的策略
policies:
easy:
description: '简单 QA、FAQ、分类'
strategy: single_pass
samples: 1
temperature: 0.0
max_tokens: 256
estimated_cost_per_query_usd: 0.001
medium:
description: '摘要、翻译、简单推理'
strategy: best_of_n
samples: 3
temperature: 0.5
max_tokens: 512
scorer: format_and_length
estimated_cost_per_query_usd: 0.005
hard:
description: '数学、编程、多步推理'
strategy: self_consistency
samples: 8
temperature: 0.7
max_tokens: 1024
min_agreement: 3
estimated_cost_per_query_usd: 0.015
critical:
description: '代码审查、安全判断、医疗/法律'
strategy: verified_best_of_n
samples: 5
temperature: 0.6
max_tokens: 2048
verifier: domain_specific_verifier
fallback_on_low_confidence: human_review
estimated_cost_per_query_usd: 0.025
routing:
classifier: difficulty_classifier_v2
fallback_policy: medium
guardrails:
max_total_tokens_per_query: 16384
timeout_seconds: 30
daily_budget_usd: 500
alert_threshold_usd: 400
运营指标采集与监控
引入 TTS 后,需要追踪与以往单次生成不同的指标。
-- 用于 TTS 运营看板的查询
-- 一眼看清按天、按策略划分的性能/成本/延迟
SELECT
date_trunc('day', created_at) AS day,
tts_strategy,
COUNT(*) AS total_queries,
AVG(correct::int) AS accuracy,
PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY latency_ms) AS p50_latency_ms,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY latency_ms) AS p95_latency_ms,
AVG(samples_generated) AS avg_samples,
SUM(cost_usd) AS total_cost_usd,
AVG(cost_usd) AS avg_cost_per_query,
-- 效率指标:每个正确答案的成本
SUM(cost_usd) / NULLIF(SUM(correct::int), 0) AS cost_per_correct_answer
FROM reasoning_eval_runs
WHERE created_at >= CURRENT_DATE - INTERVAL '14 days'
GROUP BY 1, 2
ORDER BY 1 DESC, 2;
需要追踪的核心指标整理如下。
| 指标 | 说明 | 警戒基准示例 |
|---|---|---|
| accuracy(准确率) | 正确率或通过率 | 相比 baseline 提升不到 5% 时,需怀疑是否值回成本 |
| p95 latency | 第 95 百分位延迟 | 相比单次生成高出 3 倍以上时,需检查对 UX 的影响 |
| avg_samples | 平均样本数 | 低于目标 N 时,需检查 timeout/错误比率 |
| cost_per_correct | 每个正确答案的成本 | 对照业务价值评估 ROI |
| agreement_rate | Self-Consistency 的一致率 | 低于 40% 时需重新审视模型或问题类型 |
应用判断框架
把 TTS 用在所有地方并不高效。可按以下标准判断是否适用。
TTS 效果显著的情形:
- 正确答案的验证可以自动化的任务(运行测试、规则检查、公式验证)
- 错误答案的业务成本很高的情形(医疗、法律、金融分析)
- 延迟要求以秒为单位、较为宽松的情形(批处理、异步流水线)
- 每次输入的价值较高的情形(一次代码审查能节省数小时的调试时间)
TTS 效率低下的情形:
- 需要实时流式响应的聊天(以 100ms 为单位的 TTFT 很关键)
- 答案本身带有主观性的创意生成(文章、营销文案)
- 验证器薄弱或缺失的情形(如果 scorer 接近随机,提高 N 也没有意义)
- 输入量大而单次价值低的情形(日志分类、垃圾邮件过滤)
常见错误与应对
错误 1:只提高 N,却放任选择策略不管
即使把 N 提高到 16,如果 scorer 只是"基于长度",选出来的仍会是冗长而无用的答案。scorer 的质量决定了提高 N 所能带来收益的上限。投入的优先级应该是改进 scorer,而不是增加 N。
错误 2:对所有请求套用同一套 TTS 策略
"喂,FAQ 上居然在跑 Best-of-8" -- 这是实际会遇到的情况。应该先设置一个难度分类器(difficulty classifier),把简单问题分流到 single pass。
错误 3:只看离线基准数值就上线
即便在 GSM8K 上准确率提升了 15%,也要同时看生产环境中延迟和成本的增加。需要在 A/B 测试中确认用户满意度或业务 KPI 是否真的得到了改善。
错误 4:盲目相信 verifier
verifier 本身也可能带有偏差。如果存在对某类错误答案打高分、或对正确答案打低分的系统性错误,TTS 反而会拖累性能。verifier 的精确率/召回率需要单独追踪。
错误 5:未设计 fallback 路径
如果全部 N 个生成都 timeout 或者出现 API 错误,该怎么办?不应该只是"直接返回错误",而要有 single pass fallback,或者返回缓存中相似答案等替代方案。
参考资料
- Wei 等,"Chain-of-Thought Prompting Elicits Reasoning in Large Language Models",2022 -- arxiv:2201.11903
- Wang 等,"Self-Consistency Improves Chain of Thought Reasoning in Language Models",2022 -- arxiv:2203.11171
- Yao 等,"Tree of Thoughts: Deliberate Problem Solving with Large Language Models",2023 -- arxiv:2305.10601
- Lightman 等,"Let's Verify Step by Step",2023 -- arxiv:2305.20050
- Snell 等,"Scaling LLM Test-Time Compute Optimally can be More Effective than Scaling Model Parameters",2024 -- arxiv:2408.03314
- "What, How, Where, and How Well? A Survey on Test-Time Scaling in LLMs",2025 -- arxiv:2503.24235
- "The Art of Scaling Test-Time Compute for Large Language Models",2025 -- arxiv:2512.02008
- Cobbe 等,"Training Verifiers to Solve Math Word Problems",2021 -- arxiv:2110.14168
测验
-
Test-time scaling 与 train-time scaling 的根本区别是什么? 答案:||Train-time scaling 会改变模型权重(增加参数/数据/训练量),而 test-time scaling 保持已训练模型不变,通过增加推理阶段的计算量来提升性能。||
-
Self-Consistency 与 Best-of-N 相比,核心差异是什么? 答案:||Best-of-N 会对每个候选逐一评估质量、选出得分最高者;Self-Consistency 则按最终答案的多数表决(出现频率)来选择。它不需要额外的 scorer,但只在正确答案能被明确定义的任务上有效。||
-
在 pass@1 为 30% 的模型上,pass@10 的理论上限是多少? 答案:||1 - (1-0.3)^10 = 1 - 0.7^10 ≈ 97.2%。不过,这是在能够从 10 个候选中完美挑出正确答案(oracle selection)这一前提下的上限。||
-
Tree-of-Thought 比单纯的 Best-of-N 更有优势的问题类型是什么? 答案:||中间步骤的分支判断很重要的搜索问题(24 点游戏、计划制定、多步决策)。相比整体生成多个完整答案,在中途剪枝在预算上更划算。||
-
TTS 中的 compute-optimal scaling 是指什么? 答案:||不是给所有问题分配相同的预算,而是根据问题难度差异化分配 —— 简单问题用较少样本(1-2 个),困难问题用较多样本的策略。Snell 等(2024)对此做了系统性分析。||
-
将 Verifier 与 Self-Consistency 结合会带来什么好处? 答案:||同时看 Self-Consistency 的多数表决结果和 verifier 打分,verifier 能捕捉到模型的 systematic bias(系统性错误偏向),而多数表决又能弥补 verifier 个别误判,两者的弱点可以相互补足。||
-
为什么代码生成中的 test-based reranking 比其他领域更有效? 答案:||因为存在"是否通过测试"这一二元(binary)且确定的验证信号。而在大多数其他领域中,正确答案的验证要么模糊,要么成本很高。||
-
引入 TTS 时最先要确保拿到的 baseline 数值是什么? 答案:||以单次采样(single pass,temperature=0)为基准的准确率、延迟、成本。没有这个 baseline,就无法定量衡量 TTS 实际带来的改进效果。||