核心摘要

推理时计算是在回答请求期间投入的计算资源,可用于延长推理、生成候选、验证或搜索。额外计算只有在产生有用候选且能选对结果时才有价值。工程上应统一核算生成与验证预算,评测最终答案准确率,并把预算耗尽和证据不足设计成明确的返回状态。

目录

推理时计算改变了什么

推理时计算(Test-Time Compute,TTC,也称测试时计算)改变模型回答一次请求时的计算量及其分配方式,不要求更新模型权重。让一条推理轨迹更长、采样五个答案、用验证器搜索不同分支,是三种不同的计算分配。

普通 大语言模型 的自回归生成本来就包含反复解码:先处理输入,再逐个生成 Token,通常通过 KV Cache 复用历史状态。因此,“普通推理只做一次前向计算”并不准确,也掩盖了输出长度对成本的影响。

理解 TTC 时应分清三个层次:

层次 改变的对象 例子
模型训练 参数与学到的推理行为 训练模型解决多步骤问题
推理策略 一次请求中的计算过程 延长生成或采样多个方案
应用控制器 何时调用、接受与停止 测试候选,拒绝后再修订

思维链 提示可以诱导中间推理,但不保证正确。推理 API 即使提供 effort 参数,也不意味着公开了内部搜索实现。解释更长、出现“让我重新检查”之类语句,都不能单独证明系统完成了有效验证。

Snell 等人的 测试时计算研究 分析了自适应提案策略和验证器引导搜索。对工程最有用的结论是:合理的计算分配依赖题目难度。论文中的效率收益受模型、任务和预算设置约束,不能直接写成任何业务都能获得的固定倍数。

根据选择依据确定方法

选用哪种 TTC 方法,首先取决于系统凭什么判断答案更好。算法更复杂,并不意味着实际准确率更高。

方法 额外计算花在哪里 怎样选择答案 主要失败方式
延长单条轨迹 一次生成内部 模型输出最终答案 重复原来的错误思路
自一致性投票 多条完整推理路径 聚合等价的最终答案 共同错误获得多数票
Best-of-N 多个完整候选及评分 选择满足条件的高分候选 评分器偏爱错误答案
序列修订 反馈与候选修改 每次修改后重新检查 修复一处却破坏另一处
思维树 中间思路的分支探索 评估、搜索与回溯 错误剪枝排除有用路径
MCTS 选择、扩展、评估与回传 明确定义终态选择规则 噪声估值吸引更多搜索

Self-Consistency 原始论文 的核心是采样推理路径并聚合答案。不同采样仍共享模型和输入,错误未必独立。还要先约定答案归一化:42、42.0 和“42 米”是否等价,必须由任务中的数值与单位规则决定。

Best-of-N 对完整候选打分;束搜索(Beam Search)则反复扩展局部序列或状态,保留有限数量的分支。生成五个完整答案再选一个,不能直接称作束搜索。

思维树(Tree-of-Thought) 显式探索中间思路。采用这类方法前,需要说明状态包含什么、允许怎样转换、何时算终态。如果节点只是“一段看起来合理的话”,验证往往很难落到实处。

以排班为例,状态可以包含已分配任务、剩余任务和容量约束。验证器发现容量冲突,就能停止扩展该分支。写文章时未必存在同样清晰的状态和判定规则,此时经过评测的评分量表与修订流程可能更合适。

flowchart TD A["请求与验收契约"] --> B["预留本次计算预算"] B --> C["生成或扩展候选"] C --> D["解析并验证"] D --> E{"证据是否充分"} E -->|是| F["返回所选结果与证据"] E -->|否| G{"是否还有预算"} G -->|是| B G -->|否| H["返回未解决或升级处理"]

核算整个请求的成本

TTC 预算必须覆盖生成、选择、验证和重试。只统计最后一条回复,会持续低估系统实际消耗。

对于按 Token 收费的调用,可以先使用这个核算框架:

总费用 = 各次调用的输入用量 × 对应输入单价 + 输出用量 × 对应输出单价,再加工具费用

缓存输入及其他计费类别,应按服务商实际规则拆分。这个式子是记账结构,不是价格表;费用记录还应保留使用的模型和价格版本,便于后续对账。

OpenAI 推理指南 明确说明:reasoning_tokens 已包含在 output_tokens 内,不能再加一次。也不应把 output_tokens - reasoning_tokens 直接当作可见文本的精确 Token 数,因为输出还可能包含其他不可见的格式 Token。

max_output_tokens 同时限制推理和其他输出 Token。模型可能在预算耗尽时还没有给出可见答案。effort 参数调节支持该参数的模型的推理行为,不是硬性的金额上限;参数和用量字段都要以所用模型、接口的契约为准。

不同限制应分别设置:

限制 约束什么 不保证什么
最大尝试次数 发起多少次调用 每次调用的 Token 数
单次输出上限 一次调用的输出额度 输入或工具费用
总预留输出额度 已承诺的输出预算 不同模型费率下的总金额
截止时间 客户端等待时间 服务端计算一定及时取消
最大树深度与节点数 搜索扩展范围 每个节点的评估成本
工具权限与配额 外部操作及工具使用量 模型答案质量

调用发出前先预留上限,拿到可信用量后再结算。超时后若实际用量未知,应保留预留额度,待后续对账;把未知用量当成零,会使重试突破预算。并发采样需要原子化的共享额度,不能给每个工作线程复制一份完整预算。

验证器调用同样挤占候选生成预算。The Art of Scaling Test-Time Compute 在固定总预算下研究了这一权衡,并报告了自一致性比生成式奖励模型更高效的实验设置。它支持“把验证成本计入比较”,不支持“验证器在所有任务里都没有价值”。

用 Go 实现有预算的自一致性控制器

下面的 Go 1.23 示例展示输出额度预留、最终答案解析、失败返回和提前停止。它使用固定响应,便于在没有 API 凭证的情况下复现控制逻辑;不调用真实模型,不测量模型准确率,也不限制整个请求的金额。

任务只接受能装入 int64 的十进制带符号整数。截断响应不计票;选中答案必须独占最高票,并达到最低票数。只有剩余所有尝试都投给第二名仍无法追平时,才提前结束。

将代码保存为 main.go,运行 go run main.go。

go
package main

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"
)

type Sample struct {
	Answer     string
	Output     int
	UsageKnown bool
	Complete   bool
}

type Generate func(context.Context, int) (Sample, error)

type Config struct {
	MaxAttempts int
	PerCall     int
	TotalOutput int
	MinVotes    int
}

type Result struct {
	Answer   string
	Selected bool
	Attempts int
	Valid    int
	Charged  int
	Stop     string
}

func leader(votes map[string]int) (string, int, int) {
	answer, first, second := "", 0, 0
	for value, count := range votes {
		if count > first {
			answer, second, first = value, first, count
		} else if count > second {
			second = count
		}
	}
	return answer, first, second
}

func solve(ctx context.Context, cfg Config, generate Generate) (Result, error) {
	var result Result
	if cfg.MaxAttempts < 1 || cfg.PerCall < 1 || cfg.TotalOutput < 1 ||
		cfg.MinVotes < 1 || cfg.MinVotes > cfg.MaxAttempts || generate == nil {
		return result, fmt.Errorf("invalid configuration")
	}
	votes := make(map[string]int)
	for result.Attempts < cfg.MaxAttempts {
		if err := ctx.Err(); err != nil {
			return result, err
		}
		remaining := cfg.TotalOutput - result.Charged
		if remaining == 0 {
			result.Stop = "output_budget"
			break
		}
		cap := min(cfg.PerCall, remaining)
		result.Charged += cap
		result.Attempts++
		sample, err := generate(ctx, cap)
		if sample.UsageKnown {
			if sample.Output < 0 || sample.Output > cap {
				return result, fmt.Errorf("usage outside reserved cap")
			}
			result.Charged -= cap - sample.Output
		}
		if err != nil {
			return result, fmt.Errorf("generation failed: %w", err)
		}
		if err := ctx.Err(); err != nil {
			return result, err
		}
		if !sample.Complete {
			continue
		}
		value, err := strconv.ParseInt(strings.TrimSpace(sample.Answer), 10, 64)
		if err != nil {
			continue
		}
		votes[strconv.FormatInt(value, 10)]++
		result.Valid++
		_, first, second := leader(votes)
		if first >= cfg.MinVotes &&
			first > second+cfg.MaxAttempts-result.Attempts {
			result.Stop = "locked_plurality"
			break
		}
	}
	if result.Stop == "" {
		result.Stop = "attempt_limit"
	}
	answer, first, second := leader(votes)
	if first >= cfg.MinVotes && first > second {
		result.Answer, result.Selected = answer, true
	}
	return result, nil
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	replies := []string{"42", "41", "42", "42", "41"}
	index := 0
	generate := func(ctx context.Context, cap int) (Sample, error) {
		if err := ctx.Err(); err != nil {
			return Sample{}, err
		}
		if cap < 12 || index >= len(replies) {
			return Sample{}, fmt.Errorf("scripted response unavailable")
		}
		reply := replies[index]
		index++
		return Sample{reply, 12, true, true}, nil
	}
	result, err := solve(ctx, Config{5, 32, 160, 3}, generate)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Printf("answer=%s selected=%t attempts=%d valid=%d charged=%d stop=%s\n",
		result.Answer, result.Selected, result.Attempts, result.Valid,
		result.Charged, result.Stop)
}

预期输出:

text
answer=42 selected=true attempts=4 valid=4 charged=48 stop=locked_plurality

第四次调用后,领先答案获得三票,另一答案一票,只剩一次尝试,因此不可能追平。selected=true 仅表示通过了声明的选择规则,不表示外部验证器已经证明 42 正确。

接入真实服务前,应检查这些边界:

  • 两次尝试分别得到 42、41,即使最低要求只有一票,也应因为平票而返回未解决。
  • 格式错误和不完整的响应消耗预算,但不能投票。
  • 可信输出用量可退回未使用的预留额度;未知用量保留整次上限。
  • 示例遇到 API 错误立即停止,不返回已选答案。若增加重试,必须重新预留尝试次数和用量。
  • 预算耗尽时,如果独占最高票且满足最低票数,仍可以返回选择结果,同时记录耗尽原因。业务若不能接受相对多数票,就必须增加外部验证条件。

真实 API 适配器需要把输出上限传给服务商、传递取消信号、识别响应完成状态,并返回权威用量。输入限制、工具费用、准入控制、限流和持久化费用对账仍需单独实现。

让搜索与验证提供可信证据

验证器的价值在于提供生成器自身不能稳定提供的证据,并明确证据覆盖的范围。

验证依据 通过能够说明什么 仍有哪些不确定性
解析器或 Schema 符合结构契约 字段值可能是假的
算术或约束检查 编码进去的条件成立 条件可能不完整
测试集 已测试行为符合预期 未覆盖行为与测试质量
检索来源 来源包含相关证据 时效、权威性与是否支持结论
模型评审 评审模型更偏好该答案 偏见、校准与共同盲区

结果验证针对完整候选;过程验证针对中间步骤,可用于剪枝。两者没有普遍的高下关系:过早误判会删除唯一有用的分支,薄弱的最终评分器也可能选出流畅但错误的答案。

树搜索必须在扩展时执行深度限制,同时约束总节点数,把“终态有效”与“启发式分数高”分开。MCTS 的访问次数反映特定策略下的搜索分配,不是经过校准的真实性概率。返回访问最多的一段未完成推理,不等于返回已验证解。

执行生成代码时,应隔离凭证和宿主机权限。带超时的子进程不是安全沙箱;需要具有资源、文件系统和网络约束的隔离环境,并防止候选代码悄悄修改用来验收自己的测试。

如果还要根据反馈修改答案,应保存已经检查的候选,每次修改后重新验证。自纠错工程实践 进一步讨论验收、回归和停止条件。

怎样评测自适应预算

自适应策略应在明确的费用和延迟约束下,提高最终交付答案的质量。“模型说这题很难”只能作为待评估的路由特征,不能当作真实难度标签。

先建立固定单次调用基线,再比较延长轨迹、自一致性、验证器选择和修订。对照实验中,按比较目的固定模型版本、任务集、提示词、答案归一化和评分规则。

指标 解决的问题
最终答案准确率 用户真正得到的答案是否正确
候选集合成功率 采样中是否曾出现正确答案
选择差距 明明有正确候选,选择器却没有选中
回答覆盖率与已回答请求准确率 弃答是否只是隐藏难题
每请求费用与每正确答案费用 纳入失败请求和验证成本
P50/P95 延迟及超时率 暴露排队与慢分支问题
按难度、领域拆分的表现 防止总体平均掩盖退化

候选集合成功率需要正确性标签,属于离线“知道正确答案后再检查”的视角,不是可部署准确率。同时记录覆盖率和所有请求中的成功比例,避免只回答简单问题的策略被误判为全面更好。

阈值在开发集调优,再在独立留出集评估。如果反复用同一基准选择提示词、停止规则和验证阈值,这套基准就已成为开发过程的一部分。

“选择差距”很适合指导下一步投入:如果采样中正确答案更多,但最终准确率没变,可能需要改善选择器;如果所有候选都重复同一个缺乏依据的前提,更好的输入证据或模型能力可能比继续采样更有效。推理模型对比 讨论训练与评测边界,上下文工程 则讨论如何向模型提供证据。

常见问题

普通大模型推理是一次前向计算吗?

不是。自回归生成会反复运行模型,逐个产生 Token,并通常复用 KV Cache。推理时计算改变的是推理策略与计算预算,例如延长单条轨迹、生成多个候选、增加验证或执行搜索。

多数投票的占比等于答案正确率吗?

不等于。票数占比只表示候选答案的一致程度。模型可能重复同一种错误,答案解析规则也可能错误地合并结果。需要在独立评测集上校准完整的选择策略,不能把三票一致直接解释为高置信度。

推理 Token 应该怎样计费和统计?

应遵循服务商的用量定义。OpenAI Responses API 的 output_tokens 已包含 reasoning_tokens,不能重复相加。输入、输出、验证器调用和工具费用应分别核算;输出上限也不等于整个请求的金额上限。

推理搜索什么时候应该停止?

验收规则通过,或者请求次数、Token、时间、树深度、节点数等预算耗尽时停止。证据不足时返回未解决状态或升级处理,不能因为搜索分数高就认定答案正确。

增加推理计算能替代更大的模型吗?

在部分任务和预算下可以,但取决于候选质量、题目难度与选择器准确性。应在同一任务集上比较完整系统的费用和延迟,不能把特定论文实验中的收益当作普遍替代关系。

来源与延伸阅读