Agent 工程学习站 做棵大树 出品 · beatree.cn 免费 · 无需登录 · 进度存本机
I 版 · 大模型原理 第 04 章 Sampling and Determinism
I · 大模型原理 04 / 22 入门

采样、温度与确定性

Sampling and Determinism
预计阅读 20 分钟
难度 入门
关键词 Temperature · Top-P
本机状态 未读

同一个问题问两遍得到不同答案,这不是缺陷,是解码策略的必然结果。但在 Agent 里,这种不确定性会以最讨厌的方式暴露出来:工具参数填错、JSON 少一个大括号、同一个任务两次跑出不同的步骤数。这一章讲清每个采样参数到底在动什么,以及哪些地方必须确定、哪些地方必须不确定。

编辑部注 · 读完请务必记住一条工程纪律:需要结构化输出的地方,temperature 应该是 0(或极低),而不是「调低一点」。

模型输出不是一个答案,而是一个概率分布。每一步它给出词表上每个 token 的概率,然后由解码策略从中挑一个出来。所谓「温度」「top-p」,都是在改这个挑选过程。理解这一点,你就能判断哪些不确定性是可修的、哪些是不可修的。

一、模型实际吐出的是什么

在最后一层之后,模型给出的是 logits——词表大小(通常 10 万量级)的一个向量,每个位置一个分数。经过 softmax 变成概率分布。

一个必须建立的直觉:模型从来没有「想好了一个答案」,它只是不断地在每一步给出一个分布,然后从里面抽一个。 所谓「生成」,是抽样的累加。

解码参数:各自在动什么
参数作用位置在做什么调高 / 调低的后果
temperaturesoftmax 之前给 logits 除以 T:T<1 让分布更尖锐,T>1 让分布更平坦调低 → 更确定、更保守、也更容易陷入重复;调高 → 更发散、更有创意,也更容易跑偏
top_p(核采样)softmax 之后按概率从高到低累加,只保留累计到 p 的那批候选,其余丢弃调低 → 候选集变小,输出收敛;调高 → 保留更多长尾可能
top_ksoftmax 之后固定只保留概率最高的 k 个候选与 top_p 作用类似;但 k 固定,遇到分布极尖或极平时表现不一致
repetition_penaltylogits 修正对已出现过的 token 降低其分数轻微使用可缓解复读;过强会破坏专有名词与代码标识符的重复出现
seed随机数源固定采样用的随机序列同一 seed + 同一请求 + 同一后端版本 → 可复现;换后端可能失效

二、temperature 与 top_p 的区别

这两个参数经常被混着用,但它们动的是分布的两个不同阶段。

温度改「形状」,top-p 改「范围」 原始 logits 经 softmax(T=1) 候选 token(按概率降序)→ T = 0.2 分布更尖 几乎总选第一个 → 确定性高,但可能复读 T = 1.5 分布更平 长尾也被抬起来 → 发散,但容易胡言 top_p = 0.9 截断尾部 保留:累计概率到 0.9 为止 丢弃 10% ← 这些低概率 token 不再可能被选中
图 1 温度在 softmax 之前缩放 logits,改变整条分布曲线的陡峭程度;top-p 在 softmax 之后按累计概率切一刀,改变的是可被选中的候选范围。两者可以叠加使用,但叠加时要注意:高温 + 高 top-p 会让输出非常不稳。

三、Agent 场景下的确定性纪律

这是本章最有工程价值的一节。把 Agent 里所有调用模型的地方按「要不要确定性」分两类:

调用类型推荐设置原因
工具调用 / 参数生成temperature 0,top_p 尽可能低参数写错就执行失败,没有「创意」的余地
结构化输出(JSON / YAML)temperature 0 + 强制 schema格式错误直接导致解析异常,会引发重试与成本翻倍
意图分类 / 路由temperature 0同一句话应该稳定路由到同一条链路,便于复现问题
代码生成temperature 0 ~ 0.2可验证(能跑通),不需要多样性;低温度减少无意义的变体
方案探索 / 头脑风暴temperature 0.7 ~ 1.0此时多样性本身就是产出,需要多个不同的候选
文案润色temperature 0.5 左右既要稳定风格,又要避免套话
temperature = 0 并不等于完全确定

即使温度设为 0(即每步都取概率最高的 token,greedy 解码),同一请求在不同条件下仍可能得到不同结果:

  • 浮点非确定性:批处理中不同 token 与其他请求拼在同一次前向里,累加顺序变化会导致极小的数值差异,在分布接近的两个 token 之间翻转;
  • 后端版本变化:推理框架升级、量化版本更换,都会改变 logits 的微小差异;
  • 前缀缓存命中与否:不同计算路径同样可能带来微小数值差异。

所以正确的表述是:temperature=0 让结果「几乎确定」,但工程上必须假设它可能变化——不能把正确性建立在「它一定一样」上面。

四、让输出可控的三种手段

既然模型输出本质是概率性的,工程上就要用「约束」而不是「祈祷」。按强度从弱到强:

第一层:低温度。 最简单,但只是降低概率,不提供任何保证。

第二层:JSON Schema / 结构化输出约束。 在解码时限制每个位置只能取合法 token(例如进入字符串状态就只能取字符串字符)。这是真正意义上的强制,失败率极低。大多数现代 API 都支持 response_formattool_calls 这类约束通道。

第三层:后置校验 + 有限重试。 无论前面怎么做,都要在拿到结果后做一次校验(能否解析、字段是否齐全、取值范围是否合法)。校验失败时的重试必须带「上一次错在哪」的信息,否则只是重复同样的错误。

constrained_tool_call.pypython
import json
from typing import Any
TOOL_SCHEMA = {
    "type": "object",
    "properties": {
        "city":   {"type": "string"},
        "unit":   {"type": "string", "enum": ["celsius", "fahrenheit"]},
        "days":   {"type": "integer", "minimum": 1, "maximum": 7},
    },
    "required": ["city", "unit", "days"],
    "additionalProperties": False,
}
def call_model(messages, temperature: float):
    """占位:真实调用里把 temperature 和 response_format 一起传下去"""
    raise NotImplementedError
def validated_tool_call(messages, retries: int = 2) -> dict[str, Any] | None:
    """
    三层防线的落地:
      1. temperature=0 + 结构化输出约束(由 API 保证格式)
      2. 本地 schema 校验(参数完整性、取值范围)
      3. 带错误信息的有限重试(而不是盲目重试)
    """
    local = list(messages)
    for attempt in range(retries + 1):
        raw = call_model(local, temperature=0)
        try:
            args = json.loads(raw) if isinstance(raw, str) else raw
        except json.JSONDecodeError as e:
            local.append({"role": "user",
                          "content": f"上一次输出不是合法 JSON({e}),请只输出 JSON 对象。"})
            continue
        err = validate(args, TOOL_SCHEMA)
        if err is None:
            return args
        # 关键:把具体错在哪回喂,模型才能真正修正
        local.append({"role": "user",
                      "content": f"参数校验失败:{err}。请修正后重新输出完整参数。"})
    return None      # 明确失败,交由上层决定是降级还是转人工
def validate(obj: dict, schema: dict) -> str | None:
    """极简校验:只示意逻辑,生产环境请用 pydantic / jsonschema"""
    for key in schema.get("required", []):
        if key not in obj:
            return f"缺少必填字段 {key}"
    if obj.get("unit") not in schema["properties"]["unit"]["enum"]:
        return f"unit 取值非法:{obj.get('unit')}"
    if not isinstance(obj.get("days"), int) or not 1 <= obj["days"] <= 7:
        return f"days 必须是 1-7 的整数,当前为 {obj.get('days')}"
    return None
实操任务
  • 拿一个真实的任务,把 temperature 设成 0、0.7、1.2 各跑 10 次,记录参数完全一致的次数
  • 统计「重试时把错误信息回喂」与「不带信息重试」的成功率差异——通常会差 20 个百分点以上;
  • 写一句话说明:为什么在 Agent 里,temperature=0 之外还必须做校验?

五、自测

本章自测答错的题请回看第三节的表格
Q1temperature 与 top-p 分别作用在什么位置?
B。顺序很重要:温度先改分布形状,top-p 再在改过的分布上切一刀。这个顺序解释了一个常见现象——高温度会让原本概率很低的 token 被抬到 top-p 的保留范围内,所以「高温 + 高 top-p」是最不稳定的组合。
Q2为什么把 temperature 设为 0 仍不能保证输出完全一致?
C。这是非常实用的一条知识:即使 greedy 解码,工程上仍必须假设结果可能变化。由此推出的纪律是——不要把系统的正确性建立在「模型每次输出一样」上,而要建立在校验与幂等之上。
Q3工具调用参数解析失败后,哪种重试方式最有效?
A。无信息重试等于重复同一件事,成功率提升极小。错误信息回喂把「一次采样」变成了「一次带反馈的修正」,这是 Agent 重试逻辑里性价比最高的改动。注意 C 是反向操作:参数生成场景应该保持低温。

六、小结

  • 模型输出的是分布,不是答案;解码策略决定怎么从分布里挑。
  • 温度改「多平」,top-p 改「多宽」;两者叠加时要注意极端组合。
  • Agent 里凡是需要结构化结果的地方,一律低温 + 强约束 + 校验 + 带反馈的有限重试。
  • temperature=0 只是「几乎确定」,工程上不能依赖「一定一致」。

不确定性本身不是问题,把不确定性当成确定性来用才是问题。本刊编辑部

到这里,模型原理部分还差最后一块拼图:既然算力与显存都这么贵,工业界到底用什么手段把成本打下来?

面试官会怎么问

共 3 条 · 先自己答一遍,再展开对照

Temperature 和 top_p 分别在调什么?Agent 场景你会怎么设?
两者都作用于下一个 token 的概率分布,但手法不同:temperature 在 softmax 前对 logits 做缩放,调的是分布的"陡峭程度"——温度越低分布越尖,越倾向于选最高概率的词;top_p(核采样)先按概率从高到低累加,只保留累计概率达到 p 的那一截候选,把长尾直接砍掉。
Agent 场景的经验是一条原则:凡是输出要被程序解析的地方(工具调用、结构化参数、JSON),尽量低温甚至贪婪解码;需要发散的地方(头脑风暴、候选方案生成)才提高温度。
要注意:即使 temperature = 0,由于并行归约的浮点非确定性、批处理组合变化等原因,输出也不保证逐字一致——所以兜底必须是校验和重试,而不是"假设它稳定"。

追问链

  1. 那参数校验失败了你怎么办?
  2. 为什么 temperature=0 仍然可能两次结果不同?
同一个问题问两次答案不同,这在 Agent 里会变成什么问题?
在聊天场景里这只是"有创造力",在 Agent 里它会直接变成三类故障:① 工具参数漂移(同一次意图生成出不同的参数,导致查询结果不一致);② 流程分支抖动(该调工具时直接凭记忆回答,或者反过来);③ 评测不可复现(你无法判断指标变化是改动带来的还是采样噪声)。
对应的工程手段是:结构化输出 + Schema 校验、解码参数按环节分级设置、把关键决策点做成确定性代码而不是交给模型自由发挥、评测时固定随机种子并多次重复取分布。
加分点:把"采样不确定性"翻译成具体的故障清单,比讨论参数含义更能体现工程视角。
top_k 和 top_p 能同时用吗?会有什么问题?
可以同时用,但一般是"有一个主约束"更清晰。两者叠加的实际效果是取交集性质——先按 top_k 砍掉长尾,再按 top_p 收敛,最终候选集会比单独用任何一个都小,可能过度限制多样性。
更实用的建议是:固定一个主参数(OpenAI 系通常调 top_p,部分开源模型偏 top_k),另一个设为默认值,避免两个旋钮互相干扰导致调参不可解释。另外,把"温度"和"惩罚项"(presence / frequency penalty)混着调最容易出现"越调越怪"的情况。
答这类题的通用结构

结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。

本章收尾 · Wrap up
掌握度自评
点一下给自己打分;低于 3 分建议加入复习队列
个人笔记 · Notes