核心摘要
推理时计算是在回答请求期间投入的计算资源,可用于延长推理、生成候选、验证或搜索。额外计算只有在产生有用候选且能选对结果时才有价值。工程上应统一核算生成与验证预算,评测最终答案准确率,并把预算耗尽和证据不足设计成明确的返回状态。
目录
推理时计算改变了什么
推理时计算(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) 显式探索中间思路。采用这类方法前,需要说明状态包含什么、允许怎样转换、何时算终态。如果节点只是“一段看起来合理的话”,验证往往很难落到实处。
以排班为例,状态可以包含已分配任务、剩余任务和容量约束。验证器发现容量冲突,就能停止扩展该分支。写文章时未必存在同样清晰的状态和判定规则,此时经过评测的评分量表与修订流程可能更合适。
核算整个请求的成本
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。
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)
}
预期输出:
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、时间、树深度、节点数等预算耗尽时停止。证据不足时返回未解决状态或升级处理,不能因为搜索分数高就认定答案正确。
增加推理计算能替代更大的模型吗?
在部分任务和预算下可以,但取决于候选质量、题目难度与选择器准确性。应在同一任务集上比较完整系统的费用和延迟,不能把特定论文实验中的收益当作普遍替代关系。
来源与延伸阅读
- Snell 等:Scaling LLM Test-Time Compute Optimally:特定实验条件下的自适应推理计算分配。
- Wang 等:Self-Consistency Improves Chain of Thought Reasoning:多路径采样与答案聚合。
- Yao 等:Tree of Thoughts:中间思路的探索与评估。
- The Art of Scaling Test-Time Compute:总预算约束下生成与验证的成本关系。
- OpenAI 推理指南:推理 effort、输出上限与用量统计。