最近在看斯坦福的 CS329A《自我改进 AI 智能体》,第二讲把“测试时计算”这个主题讲得很透。它的核心主张很直接:不更新模型权重,也能让 AI 在具体任务上表现更好。做法是在推理阶段投入更多计算,多生成几个候选结果,再用验证机制挑出更可靠的那个。这和“堆数据、做微调”的传统路线是两条完全不同的思路。如果你现在更关心 prompt 调到头了怎么继续提升,或者做一个 Agent 时发现模型经常在最后一步选错工具、漏信息、给错格式,那测试时计算正好切中这些问题。最值得关注的点是:它把“让 AI 更强”这件事从训练阶段搬到了推理阶段,见效更快,约束也更清楚。
1. 先搞清楚“测试时计算”到底在解决什么问题
1.1 常规推理:一次前向,一次答案
先看默认情况。绝大多数应用是把输入丢给模型,做一次前向计算,拿到一个输出就结束。这个流程快、便宜、容易理解,但有一个隐藏问题:模型的输出是概率采样出来的结果之一。如果任务本身有歧义、有多个可行解,或者模型对 prompt 的理解不够稳定,一次输出很可能不是最优答案。业务里常见的现象是:同一个问题多问几次,答案会不一样,有时还差得很多。这不是模型“坏”,而是推理方式没有给模型留出选择空间。
测试时计算(Test-Time Compute)解决的就是这个问题:不改权重,不重新训练,而是在推理时让模型探索多个可能答案,再用某条标准选出、合并或验证出更可靠的结果。这条路线在最近的大模型研究中也被称为 inference-time scaling,意思是把“更多算力”花在推理阶段,而不是训练阶段。
1.2 测试时计算的三个基本动作
拆开看,测试时计算通常由三个动作构成:
- 并行采样或路径搜索:让模型在多个随机种子、多组参数或多条推理路径下生成候选。
- 验证或评分:对每个候选做质量判断,判断依据可以是规则、外部工具结果、模型自身或者奖励模型。
- 选择或聚合:选出得分最高的候选,或者把多个候选合并成最终答案,也可以让 Agent 根据验证结果继续尝试。
这三个动作可以单独出现,也能组合使用。最轻量的例子是让模型生成 5 个回答,然后用少数服从多数选出最终答案。最重的例子是让 Agent 对同一任务规划多条路径,逐步执行、逐步验证、失败后回溯。它们的共同点是不动模型参数。
1.3 为什么“不需要训练”也能变强
这里需要把概念说清楚。测试时计算不是说模型能力凭空变强,而是说模型权重里已经有足够的知识和能力,只是单次生成时没有被稳定地调用出来。
一个常见类比是:面试者能力本来不差,但只有一次机会,紧张了或理解偏了就直接失败。如果让他多答几遍、再请评审检查,得到满意答案的概率会明显提高。测试时计算做的就是“多答几遍 + 评审检查”,而不是重新上课。
这个性质决定了它的边界:
- 模型本来就会的任务,测试时计算能提升稳定性和成功率。
- 模型完全不会的任务,测试时计算基本没用,再多样本也是围绕错误知识采样。
- 输出可以被验证的任务,收益最明显。
- 输出无法验证的任务,多采样可能会放大幻觉。
所以测试时计算从原理上就离不开验证,课程把它放在核心位置是有原因的。后面会专门展开。
1.4 训练时计算 vs 测试时计算,差别在哪
可以把两条路线放到一张表里看。
| 对比维度 | 训练时计算(Fine-tuning) | 测试时计算(Test-Time Compute) |
|---|---|---|
| 修改对象 | 模型权重 | 推理过程、候选结果、验证策略 |
| 前置条件 | 标注数据、GPU 训练环境、评估集 | 推理环境、验证规则或评判模型 |
| 见效速度 | 慢,通常要小时到天 | 快,配置好即可跑 |
| 部署影响 | 需要替换模型权重 | 不改权重,仍用原模型 |
| 典型成本 | 训练算力 + 数据成本 | 推理算力成倍增加 |
| 风险 | 过拟合、灾难性遗忘、数据偏见 | 延迟变高、成本上升、验证不可靠 |
| 适用场景 | 模型不会任务、领域术语差异大 | 模型会但偶尔不稳定、任务可验证 |
我自己的经验是:测试时计算和微调不是二选一,更像先后关系。先用测试时计算把现有模型的潜力利用起来,再判断到底缺的是推理策略还是模型内化能力。如果采样 20 次仍然没有正确答案,这时候才值得考虑训练侧的事情。
2. 并行采样:让模型在同一时刻给出多种思路
2.1 贪心解码为什么不够
模型生成答案时,最保守的方式是贪心解码,也就是每一步选概率最高的 token,一路推到结束。这种方式输出稳定,速度快,代价是几乎不会出现“惊喜”。碰到需要重新组织思路、换一个角度解题的任务,贪心输出往往停留在大众化路径上。
另一个问题是:很多真实任务没有唯一正确答案。比如让 Agent 写查询条件、设计排查方案、重构一段代码,能用的方案不止一个。贪心解码只能输出一种,即使模型本身知道多种思路,它也没有机会展示。
所以并行采样要做的事情是:在相同输入下,用不同的随机性生成多个候选,让模型有机会探索不同的推理路径。它不是模型层面的并行,而是多次前向推理的组合。
2.2 采样相关参数怎么理解
做并行采样时,四个参数最常用:
- n / num_return_sequences:希望生成多少个候选结果。
- temperature:控制概率分布的平滑程度。温度越低越接近贪心,越高越容易出现低概率词汇和结构。
- top_p:只从累计概率达到 p 的 token 里采样,用来截断不合理的候选。
- top_k:只从概率最高的 k 个 token 里采样,作用类似。
不同任务的设置思路不一样。我做这类实验时,一般先固定 n=5,温度从 0.7 到 1.0 之间试。如果是代码生成、工具调用这种格式要求高的任务,温度会调低一点,避免生成过多语法噪声。如果是开放性写作、方案设计,温度可以高一点,让候选之间差异更明显。
需要提醒的是:参数调的是“多样性”,不是“质量”。温度调高之后,候选差异变大,废稿也会变多。所以采样数量、温度和验证成本要一起看。
2.3 最小可运行的并行采样流程
下面是一个偏伪代码的流程,描述“生成多个候选”这一步。不同框架的 API 名称会不一样,但结构类似:
candidates = [] for i in range(n): response = model.generate( prompt=task_prompt, temperature=0.8, top_p=0.9, max_tokens=max_tokens, seed=base_seed + i, # 不同种子保证多样性 ) candidates.append(response) # 到这里先不急着返回,后续交给验证环节这段代码不复杂,但能说明关键点:并行采样不是把 prompt 改复杂,而是多次调用。真正要注意的是每次调用之间的独立性。如果 API 底层会自动做缓存,或者你固定了相同 seed,得到的候选可能是重复的,那采样就白做了。
2.4 并行采样的成本和边界
并行采样的成本非常好估算:采样 n 次,推理计算量大约变成原来的 n 倍,延迟视具体部署方式而定。如果是本地 GPU 推理,可以把多个候选拼成一个 batch 一起跑,吞吐通常会更高,但显存占用也会涨。
低配置环境不要一上来就 n=20。先把 n 降到 3 到 5,看输出多样性和成功率有没有变化,再逐步往上加。我见过不少项目在 n=5 到 n=10 之间就达到收益瓶颈,继续加候选只是在增加成本。
使用边界也要清楚:
- 任务简单、模型已经稳定,并行采样收益很低。
- 任务太难、模型本身不会,并行采样只会让失败模式多样化。
- 任务存在多种可行方案,但验证标准模糊,并行采样会让结果更难收敛。
- 任务成本敏感、实时性要求高,并行采样需要做取舍,不是无脑使用。
3. 验证:真正决定“采样多有没有用”
3.1 候选越多,选错的风险越大
很多人第一次跑并行采样,最大的困惑是:模型确实生成了 5 个甚至 10 个结果,但最后不知道哪个是对的。如果随便选一个,效果可能比单次输出还差,因为候选里面混入了更多低质量结果。
所以测试时计算里,验证不是可选步骤,而是核心步骤。没有可靠验证,并行采样只是把错误答案的数量放大。这也解释了为什么课程把验证和并行采样放在同一讲:两者必须配合使用。
验证的本质是给每个候选打一个“可靠度”标签,然后按标签决定返回哪个答案。验证越准确,候选数量增加带来的收益就越明显。验证不准确,候选越多噪声越大。
3.2 四类常见的验证方式
我习惯把验证方式分成四类:
- 规则验证:用代码检查输出是否符合格式、是否包含必要字段、是否通过单元测试。最可靠,成本低,但要能写出来。很适合 JSON 输出、SQL 语句、代码函数、工具调用参数。
- 外部工具验证:调用真实工具或环境,比如执行生成的代码、查询数据库、跑测试用例,看是否成功。这类验证最接近真实结果,但要注意工具本身的安全和副作用。
- 模型自验证:让同一个模型或者更强大的模型判断候选是否正确,也就是常说的 LLM-as-judge。灵活,但可能偏袒某种风格,需要调 prompt。
- 奖励模型或排序器:用一个专门训练过的模型给候选打分。效果好,但需要训练数据,已经不属于严格的“无需训练”路线,可以在后续阶段引入。
对 Agent 场景来说,我建议优先做前两类。因为 Agent 的任务往往有明确的执行结果,比如文件有没有生成、接口返回是不是 200、数据库有没有查到记录、代码能不能跑通。规则和工具验证比“模型说它好”要可信得多。
3.3 验证提示词怎么写
如果只能依赖模型自验证,提示词的质量直接影响选答效果。写验证 prompt 时,我一般会注意几点:
- 给验证模型看清楚原始任务,不要只给它一个候选答案。
- 让验证模型把判断理由先写出来,再给分数或结论,避免直接给分导致解释缺失。
- 判断标准要具体。比如“候选是否包含所有步骤”“是否满足输出 JSON 格式”“是否解决了用户提到的全部约束”。
- 不要用“好不好”这种模糊词,换成可检查的条件。
一个示例结构:
你的任务是判断下面的候选答案是否满足要求。 任务要求: {task_requirements} 候选答案: {candidate} 请按以下步骤判断: 1. 列出任务要求中所有必须满足的条件。 2. 逐个说明候选答案是否满足,引用候选中的原文作为依据。 3. 最后输出 PASS 或 FAIL,并给出一个 0 到 10 的置信分。 只输出 JSON。这种提示词比“请判断这个答案对不对”稳定得多。原因很简单:它把验证拆成了可执行的子步骤,减少了模型凭感觉做判断的空间。
3.4 过程验证 vs 结果验证
在 Agent 场景里,只验证最终结果往往不够。
比如智能体需要调用三个工具,最后返回一个总结。即使最终总结看起来正确,中间也可能选错了一个工具、漏了一次清洗、跳过了必要检查。结果正确有时是运气。过程验证检查的是每一步是否合理:调用的工具是否匹配意图、参数是否完整、顺序是否合理、失败分支有没有处理。
测试时计算放在智能体语境里,会更偏向过程验证。原因是 Agent 任务通常有状态、有工具、有执行顺序,单看最终输出很难判断执行轨迹是否健康。
实操时我会同时保留两类验证:
- 结果验证:判断最终输出是否满足任务要求。
- 过程验证:判断执行轨迹是否合理、是否有冗余或错误步骤。
过程验证如果不能自动做,至少要留日志,方便事后回看。
4. 落到智能体场景:从“生成答案”变成“完成任务”
4.1 Agent 里的测试时计算长什么样
在普通问答场景,测试时计算的单位是“候选答案”。在 Agent 场景里,候选的单位变成了“执行轨迹”或“计划”。一个 Agent 可能先生成 3 条行动计划,逐条尝试,执行过程中每完成一步就检查结果,失败就回到上一步重新规划。
这和单独采样有几个重要区别:
- 状态会变化。第一次尝试可能已经调用了工具、创建了文件、发了请求,需要处理回滚和副作用。
- 验证是多阶段的。每一步都可能有验证点,而不是最后统一判断。
- 失败会产生新输入。Agent 可以把验证结果反馈给模型,生成下一轮尝试,这是“自我改进”的雏形。
所以测试时计算在 Agent 里不是简单的 n 次生成,而是“计划-执行-验证-重试”的循环。
4.2 单任务最小流程
我自己跑 Agent 任务时,会先把单任务流程固定下来,再考虑批量和并发。最小流程可以拆成 6 步:
- 任务解析:把用户目标转换成 Agent 可执行的步骤。
- 计划采样:生成多个候选计划,数量用 plan_n,一般 2 到 5。
- 计划筛选:用规则或轻量验证挑出最合理的计划,先执行一个。
- 逐步执行:调用工具、更新状态、记录日志。
- 步骤验证:每一步检查是否成功,失败则重试或回退。
- 最终验证:对最终结果做一轮验证,不通过再回到步骤 2,最多尝试 round_limit 次。
伪代码示意:
for attempt in range(max_attempts): plans = model.generate(prompt, n=plan_n) for plan in plans: state = initialize_state() for step in plan: result = execute_step(step, state) if not verify_step(step, result, state): break else: final = verify_final(state) if final: return state return best_failed_state # 记录失败原因,不要静默返回这段流程体现了一个关键点:验证失败不是直接返回报错,而是交换到下一个候选计划。这样可以最大程度利用并行采样得到的多样性。
4.3 批量任务怎么组织
单任务跑通后,接下来要处理的是批量任务。批量场景里常见的问题不是模型能力,而是工程控制。
我一般会按下面几项检查:
- 输入列表是否完整:任务描述、依赖文件路径、预期输出目录、超时时间。
- 输出命名是否唯一:给每次任务加任务 ID,避免结果互相覆盖。
- 并发数:先设为 1,跑通 3 个样例后再逐步增加。
- 失败重试:区分“可重试错误”和“不可重试错误”。比如网络超时可重试,参数错误重试也没用。
- 日志:每个任务一个日志文件,记录计划、执行步骤、验证结果、最终输出。
这里最容易踩的坑是:单任务都正常,一上批量就出现资源耗尽、输出目录混乱、失败任务没有重试。解决办法不是改模型参数,而是先把任务队列、日志和失败策略做好。
4.4 接口化设计与参数暴露
如果要把测试时计算能力提供给其他模块调用,建议把关键参数暴露成接口字段,而不是写死在代码里:
| 参数名 | 含义 | 建议默认值 |
|---|---|---|
| n | 采样候选数 | 5 |
| temperature | 采样温度 | 0.7 |
| max_steps | Agent 最大执行步数 | 10 |
| max_attempts | 最大尝试轮数 | 3 |
| timeout | 单次工具调用超时 | 30 秒 |
| retries | 可重试错误的最大重试次数 | 2 |
| verification_level | none / result / process | process |
| output_format | 最终输出格式 | json |
接口返回时,除了最终结果,尽量返回候选数量、验证分数、尝试次数和执行日志。这样出了问题能快速定位是采样问题、执行问题还是验证问题。
5. 资源占用、延迟与稳定性:先想清楚再上并发
5.1 延迟从哪儿来
引入测试时计算后,延迟不再是单次推理的延迟。一次完整请求的耗时大致是:
总延迟 ≈ 采样耗时 + 执行耗时 + 验证耗时 + 重试耗时
如果 n=5,模型侧延迟可能接近原来的 5 倍。如果有 Agent 工具调用,还要加上网络请求、文件读写、外部服务响应的耗时。验证模型若用另一个大模型,延迟也要单独计算。
我一般会在方案设计阶段先估算上界:n × max_steps ×(单步推理 + 工具调用)× max_attempts。如果这个上界用户不能接受,就先砍参数,而不是等上线后被打爆。
5.2 并行采样不是免费加速
并行采样在多卡或单卡 batch 推理时可以摊薄部分延迟,但显存占用会上升。假设单条推理占 6GB 显存,一次性 batch 5 条可能不是 30GB,而是 25GB 左右,具体取决于推理框架的显存分配方式。如果显存不够,框架会自动排队,延迟反而更高。
低配置环境的使用建议:
- 采样数先控制在 3 以内。
- 尽量用规则验证替代大模型验证。
- Agent 任务把 max_steps 压缩到能完成任务的边界。
- 用异步任务替代同步等待,用户不需要一直等。
- 只对困难任务开启 n>1,简单任务直接单次推理。
不需要追求“所有任务都用测试时计算”。更好的做法是做一个难度判断:按照任务类型、历史成功率、验证可行性决定是否启用多候选。
5.3 怎么判断收益是不是值得
看测试时计算有没有效果,不能只看一两个例子。我建议记录这几个指标:
- 单任务成功率:最终验证通过的任务比例。
- 平均尝试次数:成功任务平均试了几次。次数太高说明计划采样质量差。
- 平均耗时:包括采样、执行、验证时间。
- 验证准确率:人工抽查时,验证器给的结论是否可靠。
- 输出一致性:同一任务跑多次,最终结果是否稳定。
把这些指标放到一张表里,和单次推理基线对比。只要成功率上升明显、耗时增加可接受,测试时计算就是值得的。如果成功率没有变化,先把验证器质量修好,再谈增加采样数。
5.4 稳定性问题:随机性要可控
测试时计算天然依赖随机性,但随机性不能变成不可复现。项目里需要注意:
- 每次运行记录 seed、模型版本、prompt 版本、采样参数。
- 输出文件按任务 ID 和时间戳命名。
- 验证结论要留证据,比如引用了候选里的哪句话、执行了什么工具。
- 修改 prompt 后,旧的验证结论和日志不要覆盖,方便回溯。
稳定性不是指每次结果一模一样,而是指每次结果都能追溯、都能解释。这个习惯在单任务时看不出价值,批量跑之后会很省事。
6. 常见问题和排查顺序
6.1 采样很多候选,成功率却没提升
这是最常遇到的问题。按下面的顺序排查:
- 先看候选之间的差异。把 n 个候选打印出来,如果内容几乎一样,说明温度太低或 seed 设置有问题。
- 再看模型本身会不会。把某个候选拿给更强模型判断,或者人工判断,确认候选里确实存在正确答案。
- 如果候选里有正确答案但验证没选出来,问题在验证器。
- 如果候选里根本没有正确答案,问题在生成阶段,加大采样数大概率也没用。
有一种情况容易被忽略:任务本身没有明确定义,不同候选各有侧重,谈不上谁对谁错。这时候先收敛任务定义,再谈测试时计算。
6.2 验证器把好答案筛掉了
验证器太严格、理解偏差、提示词不清晰,都会把好答案判错。处理方法:
- 把任务要求和候选答案放到同一上下文里,避免信息缺失。
- 让验证模型先给理由,再给结论。
- 增加多数投票:多个验证候选或多次验证,取多数结论。
- 对关键任务做人工抽查,统计验证准确率。
如果验证采用规则方式,要小心规则写得过死。比如检查 JSON 字段时,允许 key 顺序变化、允许额外字段存在;检查代码时,不要只看函数名,还要看行为。
6.3 Agent 卡住、重复执行同一动作
Agent 场景特有的问题:模型反复调用同一个工具、反复返回同一段失败信息。可能的原因:
- 失败反馈没有传给模型,模型不知道上一步失败了。
- max_steps 太大,模型在长轨迹里忘记了之前的尝试。
- 计划采样没有真正多样化,每个候选都是同一模式。
- 验证条件太宽松,模型以为已经成功。
处理顺序是先看日志里每一步的状态和工具返回,再检查是否在循环,然后检查 max_attempts 和回溯条件。必要时要加“重复动作检测”:如果连续 N 步动作和参数都相同,强制中断并换计划。
6.4 加了测试时计算后,速度变慢到不可接受
速度变慢是正常现象,重点看变慢多少。先做拆分:
- 采样阶段慢:减少 n、降低 max_tokens、使用 batch 推理。
- 工具调用慢:检查网络、外部服务、超时配置。
- 验证阶段慢:用规则验证替代模型验证,或者换成更小更快模型。
- 重试太多:提高计划采样质量,减少无效尝试。
不要把目标定成“既快又好”。更合理的思路是:给不同任务设置不同的计算预算。高价值、难验证的任务多花时间;简单、低风险任务保持快速。
6.5 排查顺序总表
| 现象 | 第一步检查 | 第二步检查 | 第三步检查 |
|---|---|---|---|
| 候选结果不理想 | 候选多样性 | 模型能力上限 | 任务定义 |
| 有好候选但最终结果差 | 验证 prompt | 验证方式是否符合任务 | 候选生成是否覆盖正确答案 |
| Agent 卡住 | 日志里的循环动作 | max_steps / timeout | 失败反馈是否进入上下文 |
| 延迟过高 | 采样数 n | 验证模型大小 | 工具调用耗时 |
| 批量任务失败 | 输出命名冲突 | 并发数是否过高 | 失败重试策略 |
排查的首要原则是:先把“生成阶段”和“验证阶段”分开。很多人一看到结果不好就调采样参数,其实问题是验证器选错答案;一看到任务失败就调 prompt,其实问题是执行环境异常。分开看,才能少走弯路。
7. 什么时候值得用测试时计算:我的判断
7.1 适合优先尝试的场景
如果满足下面几个条件,可以优先尝试测试时计算:
- 模型在大部分时候能完成任务,但成功率不够稳定。
- 任务有明确的验证方式,比如代码测试、数据库查询、JSON 格式检查、接口返回判断。
- 用户能容忍一定延迟,或者任务可以异步执行。
- 没有足够资源、时间做微调。
这几个条件里,最重要的是验证方式明确。验证越明确,采样数量增加越有意义。
7.2 不建议一上来就用的场景
反过来,下面这些场景不建议马上引入:
- 实时对话、低延迟要求极高,多一次采样都会影响体验。
- 任务本身没有清晰成功标准,验证无从谈起。
- 模型根本不会任务,候选里全是相似错误。
- 每次调用成本很高,而成功率提升不明显。
遇到这些情况,先解决基础能力问题,再考虑推理侧优化。
7.3 不同阶段的配置建议
| 阶段 | 采样数 n | 验证方式 | 最大尝试次数 | 备注 |
|---|---|---|---|---|
| 学习验证 | 3 | 规则 + 自验证 | 1 | 跑通流程,不做并发 |
| 小规模试用 | 5 | 规则 + LLM 自验证 | 2 | 记录指标,人工抽查验证准确率 |
| 生产环境 | 按任务动态 | 规则优先,模型验证兜底 | 2 到 3 | 加日志、超时、任务队列、监控 |
生产环境不建议固定所有任务都用 n=10。我倾向于把任务分成简单、普通、困难三档,简单任务 n=1,普通任务 n=3 到 5,困难任务才用更高的采样数。
7.4 测试时计算和微调不是替代关系
最后说一个容易误判的问题。测试时计算能提升的是模型在推理阶段对已有能力的利用效率,它不会让模型学到新知识,也不会改变模型对陌生领域的理解。
如果你的问题本质是“模型不知道这个领域长什么样”,比如特殊的行业术语、私有数据格式、新出的接口规范,那测试时计算帮助有限。这时候更该考虑 RAG、工具调用,或者最终做微调。
反过来,如果模型完全知道任务怎么做,只是偶尔不稳定,那微调不一定能解决问题,还可能引入数据偏差。先用测试时计算,通常更划算。
第二讲给到我的核心收获就是:自我改进的起点不是一直改权重,而是先给模型足够的尝试空间,再配上可靠的验证器。把这个思路理解透了,再去设计 Agent 的规划、执行和反馈循环,会顺手很多。
我自己落地时,仍然坚持一个顺序:先跑通单任务,再调参数;先记录指标,再上并发;先把验证做准,再增加采样数。这样踩坑最少,也最容易判断测试时计算到底有没有用。