Skip to content
Published on

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

分享
Authors
AI Papers: Test-Time Scaling 核心论文梳理

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

这个实现有三点值得注意。

  1. 异步并行生成:N 个请求并发发送而非顺序发送,从而降低总体延迟。
  2. 多 scorer 合成:不依赖单一评分函数,而是汇总多个视角的 scorer。
  3. 异常容忍:即使部分候选生成失败,也能用其余候选继续进行。

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_rateSelf-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
测验
  1. Test-time scaling 与 train-time scaling 的根本区别是什么? 答案:||Train-time scaling 会改变模型权重(增加参数/数据/训练量),而 test-time scaling 保持已训练模型不变,通过增加推理阶段的计算量来提升性能。||

  2. Self-Consistency 与 Best-of-N 相比,核心差异是什么? 答案:||Best-of-N 会对每个候选逐一评估质量、选出得分最高者;Self-Consistency 则按最终答案的多数表决(出现频率)来选择。它不需要额外的 scorer,但只在正确答案能被明确定义的任务上有效。||

  3. 在 pass@1 为 30% 的模型上,pass@10 的理论上限是多少? 答案:||1 - (1-0.3)^10 = 1 - 0.7^10 ≈ 97.2%。不过,这是在能够从 10 个候选中完美挑出正确答案(oracle selection)这一前提下的上限。||

  4. Tree-of-Thought 比单纯的 Best-of-N 更有优势的问题类型是什么? 答案:||中间步骤的分支判断很重要的搜索问题(24 点游戏、计划制定、多步决策)。相比整体生成多个完整答案,在中途剪枝在预算上更划算。||

  5. TTS 中的 compute-optimal scaling 是指什么? 答案:||不是给所有问题分配相同的预算,而是根据问题难度差异化分配 —— 简单问题用较少样本(1-2 个),困难问题用较多样本的策略。Snell 等(2024)对此做了系统性分析。||

  6. 将 Verifier 与 Self-Consistency 结合会带来什么好处? 答案:||同时看 Self-Consistency 的多数表决结果和 verifier 打分,verifier 能捕捉到模型的 systematic bias(系统性错误偏向),而多数表决又能弥补 verifier 个别误判,两者的弱点可以相互补足。||

  7. 为什么代码生成中的 test-based reranking 比其他领域更有效? 答案:||因为存在"是否通过测试"这一二元(binary)且确定的验证信号。而在大多数其他领域中,正确答案的验证要么模糊,要么成本很高。||

  8. 引入 TTS 时最先要确保拿到的 baseline 数值是什么? 答案:||以单次采样(single pass,temperature=0)为基准的准确率、延迟、成本。没有这个 baseline,就无法定量衡量 TTS 实际带来的改进效果。||