跳到主要内容

确定性预算:将随机性视为按层面的分配,而非全局开关

· 阅读需 12 分钟
Tian Pan
Software Engineer

Temperature 之争是 AI 工程中最具宗教色彩的争论,也是最没效率的争论之一。每个团队都会形成两个阵营:决定论者希望将所有地方的 Temperature 都固定为 0,因为他们无法调试不稳定的系统;而创意论者则希望调高它,因为这样输出结果感觉更“有灵性”。两者都错了,因为他们都在错误的层面上回答这个问题。Temperature 不是一个全局设置。它是一项预算——就像任何预算一样,它应该被分配,而不是被宣告。

高效的框架很简单:系统中每个模型调用都有其目的,随机性要么在那个层面(surface)发挥作用,要么就不该存在。决定下一个调用哪个工具的规划器(planner)无法从变化中获益;选错一个工具就是调试噩梦,而且没有任何创意上的好处。如果一万个用户看到的摘要措辞都一模一样,那么为他们总结搜索结果的响应合成层很快就会显得呆板——SEO 团队最终会标记这些样板内容。一个让模型提出备选方案供人类选择的头脑风暴层,在 Temperature 为 0 时表现反而 更糟;多样性本身就是其核心功能。

如果你无法清晰地说明随机性在特定调用位置的作用,你就不应该为此付费。

层面图谱:随机性在何处发挥价值

第一个实际行动是停止将你的系统视为“对 LLM 的调用”,而是将其视为一系列截然不同的“层面(surfaces)”,每个层面都有其特定的随机性目的。一个有用的分类法如下:

  • 路由(Routing)。 模型从 N 个选项中选择一个——一个工具、一个子智能体或工作流中的一个分支。Temperature 应该是 0(或尽可能接近提供商允许的最小值)。这里的差异性纯粹是成本:一个不稳定的路由器会产生跨越三个日志和四个工程师的调试追踪。
  • 解析(Parsing)。 模型被要求提取结构化字段、规范化数值或输出 JSON。Temperature 应该是 0。结构化输出调用中的任何非零设置都是一个潜在的错误,其唯一的表现形式就是两个月后出现不稳定的解析器。
  • 合成(Synthesis)。 模型根据检索到的上下文进行总结、改写或撰写响应。Temperature 0.2–0.5 通常是合适的。这个范围既能保证对源内容的忠实度,又能避免在成千上万个相同查询中出现机器人般的复制粘贴感。
  • 生成(Generation)。 模型生成完整的创意输出——营销文案、文章大纲、代码骨架。Temperature 0.5–0.8。其任务是实现合理的差异化;纯粹的贪婪解码(greedy decoding)会让作品变得平庸乏味。
  • 探索(Exploration)。 模型提出备选方案供人类选择,运行 N 个样本以实现自洽性(self-consistency),或为下游选择器提供素材。Temperature 0.7+ 并采用有意的采样。多样性就是全部意义;如果像对待路由调用那样对待这些调用,就会让“AI 头脑风暴”功能感觉像是一个套着聊天机器人外壳的同义词词典。

请注意,“创意写作”并未作为一个单独的类别出现在此列表中。营销文案和头脑风暴层面从外部看似乎相同,但具有不同的随机性目的——前者希望在不同用户之间产生合理的差异化输出,后者则希望在单个用户的会话中产生有意的多样化备选方案。将两者混为一谈是最常见的偏差。

解析层面:非零 Temperature 即 Bug

对于解析层面,规则是明确的:如果响应将由代码解析,Temperature 应该为零。从业者对此争论不休,因为他们听过一些违反直觉的结论,即对于某些复杂的、包含多个部分的 schema,少量的 Temperature 实际上能提高字段完成率——严格的贪婪解码器可能会在所有字段输出之前就提前终止,尤其是当 Prompt 包含许多部分时。

面对这种情况,正确的做法是修复 schema 或 Prompt,而不是调高 Temperature。非零的解析器 Temperature 只是解决了表象,却制造了另一个问题:现在你的结构化输出是非确定性的,这意味着你的下游解析器必须处理更广泛的输入,从而导致测试不稳定、间歇性回归,以及一类只在没有工程师盯着的生产环境中才会出现的 Bug。用采样来解决“模型截断字段”的问题,是把一个确定性问题变成了概率性问题。

当确实无法修复 schema 或 Prompt 时——例如由于供应商 JSON 模式、缺乏约束解码、复杂的多个部分输出——正确的做法是将调用封装在“带有验证的重试(retry-with-validation)”中,而不是调高 Temperature。重试产生的是一个收敛于确定性结果的确定性调用;而调高 Temperature 产生的是一个无处收敛的概率性调用。

鲜有人提及的成本视角

更高的 Temperature 与更长的输出相关联,因为模型会变得漫无边际。一个在 0.9 Temperature 下运行的“创意”合成层,比在 0.3 下运行的相同层消耗的输出 Token 要多得多,而且在留存评估(held-out eval)中测得的质量并没有提升。这是从业者在成本模型中很少体现的成本视角,因为它跨越了两个团队的边界:Prompt 团队负责质量,平台团队负责 Token,而 Temperature 与输出长度之间的关联恰恰处于这个真空地带。

让这种关联变得可见。绘制每个层面的输出长度与 Temperature 的关系图。你会发现至少有一个层面模型处于漫游状态——通常是头脑风暴或合成调用,有人为了解决质量问题曾调高过 Temperature,但在重写 Prompt 后却从未将其重置。将该层面的 Temperature 从 0.9 降至 0.5,通常可以在不产生可衡量质量下降的情况下,削减 20–30% 的输出 Token。每月数百万次的调用叠加起来,仅仅通过一个配置更改就能省下真金白银。

相反的一面,更廉价的操作则更有趣。在路由调用中调高 Temperature 在即时 Prompt 成本上是“免费”的——相同的输出长度,相同的输入长度——但成本会体现在下游:当调用了错误的工具且智能体不得不回退时产生的重试流量。昂贵的随机性,是那种在调用处毫无成本,但在系统层面代价巨大的随机性。

采样方差污染了你的评估集

评估准则(eval discipline)是大多数团队迷失方向的地方。常见的模式是:生产环境以 0.7 的温度运行,评估集也以 0.7 的温度运行,因为“我们想评估生产环境的实际表现”,然后模型升级发布,评估得分波动了两个点。这是回归(regression),还是采样噪声?

你无法判断。正因为你无法判断,你就无法决定是否回滚。采样噪声吞噬了你的回归信号。

解决方法是将问题拆分。在温度为 0 时运行回归评估——即使生产环境以非零温度运行——这样回归信号就不会被采样所掩盖。另外,在生产环境温度下运行“方差预算”(variance budget)评估,每个问题进行 N 次采样,以确认生产时的方差在历史范围内。两个评估,两个问题:“模型变差了吗”和“模型是否在乱掷骰子”。最近关于 LLM 评估噪声源的研究证实,在较高温度下,采样的预测噪声可能占据主导地位,而统计效能来自于样本平均值——而不是在生产温度下运行一次并对着结果揣摩。

一个更隐蔽的陷阱:如果你的评估裁判本身也是一个 LLM,那么裁判的温度也很重要。温度为 0.7 的裁判在不同运行中对同一个答案的评分会有所不同。如果评估流水线确定性地运行生产模型,却随机地运行裁判,那是两头不讨好——你消除了可以分析的噪声,却保留了无法分析的噪声。除非你有特定理由需要变动,否则请将裁判的温度固定为 0。

温度 0 并不等同于确定性

现在说一个令人不悦的事实:将所有设置固定为 0 并不能让你得到一个确定性的系统。它让你得到一个概率质量集中在每一步最可能 token 上的系统——这通常是但并不总是同一个 token,因为实际推理运行在浮点运算上,而浮点运算不满足结合律。两个 logits 非常接近的候选 token 可能会根据矩阵归约(matrix reduction)中中间和的顺序而交换顺序,一旦一个 token 改变,后续的每一步都会运行在不同的前缀上,这种分歧会不断复合。

还有批处理一致性(batch-invariance)问题:生产推理服务器会将你的请求与同时到达的其他请求进行批处理,而你的请求结果可能取决于该批次的组成。相同的 Prompt,相同的温度,相同的模型——却得到不同的输出,仅仅是因为另一组用户恰好在同一毫秒内到达。

实际影响:

  • 不要在合同或测试中承诺字节级一致的输出。 你的“带有校验的重试”和回归评估即使在温度为 0 时也需要容忍一定的表面层级变化。
  • 不要把“我们在温度 0 下运行了两次却得到了不同的答案”当成 bug 去追查。 这是底层介质的属性,而不是你的 Prompt 有问题。
  • 将可复现性视为系统的属性,而不是单词调用的属性。 如果你需要回放来进行故障复盘,请记录完整的请求、响应和模型指纹——不要假设你可以重新运行调用并得到相同的结果。

能够站得住脚的思想模型是:温度 0 是“意图声明”(purpose declaration),而不是保证。你在告诉系统“我想要这里最可能的 token”——系统会努力给你这个结果,但底层的浮点运算和批处理层并不受你控制。你实际拥有的契约是“低方差”,而不是“零方差”,围绕调用的系统必须基于此进行构建。

组织失效模式与取而代之的纪律

组织失效模式很简单:某个人(通常是在项目早期)从教程中复制了一个温度值——通常是 0.7,LangChain 的默认示例值——并将其放入共享配置中。平台上的每个构建团队都会继承这个值。每个为新用例克隆的 Prompt 也会克隆这个温度。六个月后,解析层在 0.7 运行,路由层在 0.7 运行,合成层也在 0.7 运行,而唯一真正适合 0.7 的只有那个还没上线的头脑风暴功能。

取而代之的纪律虽小但可强制执行:

  • 为每个模型调用标注用途。 routing(路由)、parsing(解析)、synthesis(合成)、generation(生成)、exploration(探索)。五个标签,每次调用选一个。标注存在于调用代码旁边,而不是文档里。
  • 按用途设置采样参数,而不是按供应商设置。 无论路由调用是发给 Claude、GPT 还是本地模型,都应该具有相同的温度。供应商特定的配置层将用途转化为供应商参数;调用处永远不要直接命名温度。
  • 对映射进行契约测试(Contract-test)。 编写一个测试,当 routing 调用或 parsing 调用的温度 > 0 时报错。这种测试成本低、速度快,能在 PR 阶段而不是生产环境中捕捉到复制粘贴 bug。
  • 定期审计。 每季度根据用途标签列出生产环境中实际使用的温度值。偏移(Drift)会显现出来。总会有人为了“修复”一个不稳定的输出而将解析器温度提高到 0.3,然后忘了调回来。抓住它。

这只是一小步流程,但在实践中,这决定了一个系统是在该灵动的地方充满活力、在该确定的地方表现稳健,还是在没人预料到的地方随机崩溃。

架构层面的认知

随机性是一个可调参数,应该针对每个交互界面、结合明确的目的进行设计,而不是直接继承框架的默认设置。做得出色的团队对待采样参数的方式,就像对待数据库隔离级别一样——虽然枯燥,但影响深远,值得投入少量的前期设计。而表现欠佳的团队对待采样参数的方式,则像对待日志级别一样——直接复制粘贴,从未经过评审,偶尔会引发没人能追溯到真实原因的故障。

一句话核心结论:如果你无法说出某次调用中随机性的 具体作用,那么你就在承担一项未列入预算的成本。有目的地使用它,否则就停止消耗。

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