跳到主要内容

采样参数继承:当 0.7 的温度从规划器泄露到验证器时

· 阅读需 12 分钟
Tian Pan
Software Engineer

一个在 8% 的情况下会推翻自己答案的验证器(verifier)并不是一个表现不稳定的模型。这是一个由于框架默认采用继承机制而进入生产环境的采样配置 Bug。规划器(planner)需要 temperature=0.7 来头脑风暴子任务的分解。而验证器 —— 其全部工作就是针对答案是否符合评分标准给出低方差的“是”或“否” —— 却是通过同一个 harness 调用实例化的,并默默地沿用了相同的温度设置。没有人故意这么设置。甚至根本没有人去设置它。

这是你的技术栈中最昂贵却无人认领的参数。它在调用树中不断累积:验证器上方的总结器、下方的结构化输出提取器,以及包裹整个流程的重试循环,都像使用全局变量一样沿用着规划器的“保持创意”旋钮。这笔账会同时体现在三个地方:评估的不稳定性、Token 支出,以及资深工程师花半天时间对一个结果发现根本不是退化的“性能退化”进行二分法排查。

那些看似合理实则不然的继承模式

大多数智能体(agent)框架将模型配置作为构造函数参数暴露给单个客户端对象,然后将该客户端在调用树中传递。规划器智能体、验证器智能体和结构化输出提取器各自获得对同一个客户端的引用,这意味着它们都获得了相同的 temperaturetop_p 和种子(seed)默认值。这种模式很方便 —— 你只需在应用程序的边缘配置一次模型 —— 而方便正是它在你不再需要它之后仍能长期存在的原因。

问题在于,“模型”并不是一个单一的角色。一个现代智能体至少包含四个不同的“发言者”,每个角色都有不同的采样需求:

  • 规划器(Planner):提议备选分解方案的规划器受益于多样性。0.6–0.9 范围的温度能产生多样性,让验证器可以从多个候选方案中选出优胜者。
  • 验证器(Verifier):根据标准对候选方案进行评分的验证器必须接近确定性。最近的研究表明,LLM 作为裁判(LLM-as-judge)的自洽性在 temperature=0.1 左右达到峰值,而不是 0.0,但任何超过 0.3 的设置都会引入翻转率(flip-rates),从而破坏验证步骤的价值。
  • 总结器(Summarizer):为下一步压缩工具输出的总结器需要一条中间路线:既要有足够的确定性以保证可重复性,又要有足够的灵活性来处理不寻常的措辞,而不至于截断为固定模板。
  • 结构化输出提取器(Structured-output extractor):为下游代码生成 JSON 对象的提取器需要 temperature=0 以及语法受限解码。除此之外的任何设置都会导致解析失败,重试循环会将 1% 的错误率转化为 1% 的尾部延迟峰值。

当继承成为默认行为时,所有四个角色都会获取规划器的设置。测试框架实现者的心理模型 —— “我配置了模型” —— 与运行时的现实并不相符。验证器并没有针对验证任务进行配置;它被配置成了最近一个调用者所需的模式,而最近的调用者通常需要的是创意。

为什么验证器的 Temperature 0 也不是标准答案

在第一次事故之后,人的本能是将验证器覆盖为 temperature=0 并认为 Bug 已修复。这只对了一半。温度为零是正确的“意图”,但最近关于 LLM 裁判的研究从两个具体方面使实现变得复杂。

首先,在现代推理系统中,temperature=0 并不能产生完全确定性的输出。服务端的批处理合成(Batch composition)改变了浮点运算的顺序,进而改变了 logit 值,即使在贪婪采样时也可能导致概率最高的 Token 发生翻转。“1000 次运行,1000 个不同的答案”效应已被详尽记录,而最近关于批处理不变内核(batch-invariant kernels)的研究表明,解决此问题的代价约占吞吐量的 60%。对于大规模运行的验证器来说,这就是你能达到的 SLO 与无法达到的 SLO 之间的区别。

其次,即使采样在数学上是贪婪的,单次样本评估也是脆弱的。每次调用仅给出一个“是/否”结果的裁判捕捉到了分布的众数,但没有告诉你任何关于分布离散度(spread)的信息。那些严肃对待这一问题的论文 —— 即那些在多个温度下运行相同比较并计算翻转次数的论文 —— 发现裁判的最佳温度更接近 0.1 而非 0。因为轻微带噪的采样能暴露模型信念确实处于 51/49 的情况,而这些情况应该被标记出来,而不是默默地倾向于其中一方。

正确的验证器配置不是“温度为零”。而是“低温度、多次采样,并在生产环境中观察翻转率指标”。如果翻转率超过阈值(例如 5%),验证器会升级到更强大的模型或人工审核。这将采样参数从一个你设置一次就了事的旋钮,变成了一个你可以观察的信号。

将按角色划分的采样配置作为框架原语

架构层面的修复方案是停止将采样参数视为模型客户端配置,而开始将其视为角色配置。一个角色配置集(Role Profile)捆绑了 temperaturetop_ppresence_penaltyfrequency_penaltymax_tokens、可选的 seed,以及与结构化输出配套的语法或 schema 约束。当调用方调用 planner(规划器)时,它向框架请求的是 planner 配置集,而不是模型客户端。

框架边界的准则是 默认拒绝继承。除非调用方显式选择加入(opt-in),否则子智能体(subagent)的调用不会传播采样设置。如果 planner 生成了一个 verifier(验证器)子智能体却忘记指定 verifier 配置集,调用应该显式报错(fail loudly)——而不是静默地回退到 planner 的设置。这与安全领域的最小权限原则一致:默认应该是安全的选项,而任何覆盖操作都应该是可见的。

在实践中,这表现为一个命名配置集的注册表——包含 plannerverifiersummarizerextractortool_use 等——以及一个要求在每次调用时都必须提供配置集名称的模型客户端包装器。模型客户端本身变成了一个没有人直接调用的底层原语。应用程序代码按角色请求配置集,而框架负责强制执行每个角色的参数。这样做的好处是,“周二下午 4 点时验证器的 temperature 是多少”变成了一个配置查询问题,而不是一个堆栈跟踪的“考古”问题。

第二个层面的收益体现在评估框架(eval harness)中。如果评估套件通过与生产环境相同的配置注册表来运行验证器,它就不会意外地在生产环境从未出现过的 temperature 下测试验证器。这与提示词版本问题引发的失败模式相同,只是应用在了采样上:如果评估套件运行的配置与生产环境不同,那么你测量的是一个完全不同的系统。

能捕获继承 Bug 的评估方法

捕获这些 Bug 需要一种通常没人会写的评估:重复调用下的验证器不一致率。给定一个固定的输入——一个候选答案和一个 rubric(评分标准)——对验证器运行十次。如果验证器自身不一致的比例超过 5%,那么 Bug 出在采样配置上,而不是模型本身。这个数字是可以调节的,但原则是普适的:一个在相同输入上摇摆不定的验证器并没有在验证任何东西,它只是在进行随机采样。

这种评估实现成本很低,而且在发现继承 Bug 方面效果惊人,因为它既能捕获机械层面的情况(例如 temperature 被继承为 0.7),也能捕获更微妙的情况(例如 temperature 设置为 0.1 但 seed 未固定,且底层的推理系统进行了非确定性的批处理)。当不一致率激增时,原因几乎总是以下三种之一:配置继承 Bug、改变了底层分布的模型版本变更,或者是提示词的更改将验证器的判断推向了接近 50/50 的摇摆区。

同样的评估模板也适用于结构化输出的 extractor(提取器),此时指标变为“schema 验证失败率”。如果你的提取器在 99.2% 的时间内生成有效的 JSON,而有 0.8% 的时间失败,那么问题在于这些失败是否与批次大小、时间段或模型负载相关——这些都是继承了非确定性的信号,而语法约束解码(grammar-constrained decoding)本可以消除这些信号。这 0.8% 的失败率同样涉及 Token 开销:每一次解析失败都会触发重试,而重试的输入 Token 成本比首试成功更高,因为它们包含了之前失败的输出作为上下文。

账单中而非日志中发现的成本惊喜

对于那些不看追踪仪表盘的人来说,采样继承 Bug 会在账单中变得显而易见。高 Temperature 的子智能体产生的输出更长、更散乱,因为采样分布奖励了那些低概率的 Token。一旦选中了这些 Token,就会将生成过程拖入响应空间的“长尾”地带。一个在 temperature=0.7 下运行的验证器不仅会翻转其答案,它还会长篇大论地解释其(现在已错误的)答案,因为使它选择一个罕见 Token 的同等 Temperature,也会让它不断选择下一个罕见的 Token。

Token 成本的追踪轨迹通常是这样的:一个本应输出 30 个 Token 的验证器(“是的,答案符合评分标准,因为……”)输出了 180 个 Token 啰嗦且模棱两可的话。6 倍的输出增量直接推高了单次调用的成本。由不一致性触发的重试循环又使调用次数翻倍或翻三倍。上层的 summarizer(总结器)由于接收了验证器散乱的输出,其运行时间也会变长,因为输入变长了。通过调用图追踪,一个设置错误的采样参数导致的总成本放大倍数很少低于 4 倍,且经常超过 10 倍——而且这会先出现在月度账单中,然后才出现在延迟仪表盘上,因为每次单独的调用仍然能在不到一秒的时间内完成。

修复方法不是搞一个成本仪表盘。修复方法是认识到,采样参数具有一种通过智能体图(agent graph)复合的成本效应,而且在模型客户端设置的“合理默认值”不可能对下游的每个角色都合理。一个为了创造力而多花 50% 输出 Token 的 planner 是在尽职尽责;而一个多花 50% 输出 Token 的 verifier 则是失职地干了 planner 的活。

合理的默认设置是什么样的

框架设计的经验在于,采样默认值应该是基于角色的、显式的,而不是全局通用且隐式的。一个合理的初始注册表如下:

  • 规划器 (Planner)temperature=0.7top_p=0.95,不固定种子。输出的多样性是关键。
  • 验证器 (Verifier)temperature=0.1top_p=0.5,在关键环节采样三次并进行多数投票。在评估时固定种子以保证可复现性;除非你的推理栈是批处理无关的 (batch-invariant),否则不要在生产环境中固定。
  • 摘要生成器 (Summarizer)temperature=0.3top_p=0.8。取中间路径。
  • 提取器 (Extractor)temperature=0top_p=1.0,使用语法约束解码为 JSON Schema。仅对已知可信的前缀绕过 Schema。
  • 工具调用生成器 (Tool-call generator)temperature=0,对工具名称和参数进行结构化输出。自由形式的推理发生在一个单独的、温度更高的调用中。

这些数值只是起点,而非终点。重点在于数值因角色而异,框架在调用处强制执行这些数值,并且没有人在模型客户端层 (model-client layer) 设置一个会静默传播到各处的默认值。在你的智能体栈 (agent stack) 中,最重要的参数是决定参数来源的那一个——而正确的答案是:“始终基于角色配置,严禁继承,故障即关闭 (fail closed)。”

下次当验证器的翻转率 (flip-rate) 飙升或 JSON 解析失败率攀升时,第一个问题不应该是“模型变了吗?”,而应该是“调用处知道它请求的是哪个配置文件吗?”如果答案是“模型客户端直接从构造函数中获取默认值”,那么 Bug 在于框架,而非模型。

References:Let's stay in touch and Follow me for more thoughts and updates