同一个问题问两遍得到不同答案,这不是缺陷,是解码策略的必然结果。但在 Agent 里,这种不确定性会以最讨厌的方式暴露出来:工具参数填错、JSON 少一个大括号、同一个任务两次跑出不同的步骤数。这一章讲清每个采样参数到底在动什么,以及哪些地方必须确定、哪些地方必须不确定。
编辑部注 · 读完请务必记住一条工程纪律:需要结构化输出的地方,temperature 应该是 0(或极低),而不是「调低一点」。
模型输出不是一个答案,而是一个概率分布。每一步它给出词表上每个 token 的概率,然后由解码策略从中挑一个出来。所谓「温度」「top-p」,都是在改这个挑选过程。理解这一点,你就能判断哪些不确定性是可修的、哪些是不可修的。
一、模型实际吐出的是什么
在最后一层之后,模型给出的是 logits——词表大小(通常 10 万量级)的一个向量,每个位置一个分数。经过 softmax 变成概率分布。
一个必须建立的直觉:模型从来没有「想好了一个答案」,它只是不断地在每一步给出一个分布,然后从里面抽一个。 所谓「生成」,是抽样的累加。
下面这张表把解码参数逐个摊开:它作用在 softmax 的哪一侧、动的是分布的哪个性质、调高调低各自要付出什么代价。其中 top-p(Nucleus Sampling) 这类「按累计概率截断」的做法,最容易被和温度混为一谈,值得先分清。
| 参数 | 作用位置 | 在做什么 | 调高 / 调低的后果 |
|---|---|---|---|
temperature | softmax 之前 | 给 logits 除以 T:T<1 让分布更尖锐,T>1 让分布更平坦 | 调低 → 更确定、更保守、也更容易陷入重复;调高 → 更发散、更有创意,也更容易跑偏 |
top_p(核采样) | softmax 之后 | 按概率从高到低累加,只保留累计到 p 的那批候选,其余丢弃 | 调低 → 候选集变小,输出收敛;调高 → 保留更多长尾可能 |
top_k | softmax 之后 | 固定只保留概率最高的 k 个候选 | 与 top_p 作用类似;但 k 固定,遇到分布极尖或极平时表现不一致 |
repetition_penalty | logits 修正 | 对已出现过的 token 降低其分数 | 轻微使用可缓解复读;过强会破坏专有名词与代码标识符的重复出现 |
seed | 随机数源 | 固定采样用的随机序列 | 同一 seed + 同一请求 + 同一后端版本 → 可复现;换后端可能失效 |
二、temperature 与 top_p 的区别
这两个参数经常被混着用,但它们动的是分布的两个不同阶段。
三、Agent 场景下的确定性纪律
这是本章最有工程价值的一节。把 Agent 里所有调用模型的地方按「要不要确定性」分两类——这本身就是选解码策略(Decoding Strategy)。
| 调用类型 | 推荐设置 | 原因 |
|---|---|---|
| 工具调用 / 参数生成 | temperature 0,top_p 尽可能低 | 参数写错就执行失败,没有「创意」的余地 |
| 结构化输出(JSON / YAML) | temperature 0 + 强制 schema | 格式错误直接导致解析异常,会引发重试与成本翻倍 |
| 意图分类 / 路由 | temperature 0 | 同一句话应该稳定路由到同一条链路,便于复现问题 |
| 代码生成 | temperature 0 ~ 0.2 | 可验证(能跑通),不需要多样性;低温度减少无意义的变体 |
| 方案探索 / 头脑风暴 | temperature 0.7 ~ 1.0 | 此时多样性本身就是产出,需要多个不同的候选 |
| 文案润色 | temperature 0.5 左右 | 既要稳定风格,又要避免套话 |
即使温度设为 0(即每步都取概率最高的 token,greedy 解码),同一请求在不同条件下仍可能得到不同结果:
- 浮点非确定性:批处理中不同 token 与其他请求拼在同一次前向里,累加顺序变化会导致极小的数值差异,在分布接近的两个 token 之间翻转;
- 后端版本变化:推理框架升级、量化版本更换,都会改变 logits 的微小差异;
- 前缀缓存命中与否:不同计算路径同样可能带来微小数值差异。
所以正确的表述是:temperature=0 让结果「几乎确定」,但工程上必须假设它可能变化——不能把正确性建立在「它一定一样」上面。
温度越低,模型越「贪」,越容易挑概率最高那批 token,反复说同一段话——Agent 里尤其明显(长任务里模型会把同一句模板车轱辘式重复)。对症的旋钮是重复惩罚:在 logits 阶段对「已经出现过的 token」施加折扣,压住复读冲动。
但它是旋钮不是开关:过强会误伤本来就该重复的东西——变量名、API 名、专有名词反复出现是正常的,惩罚太重会把它们也压低,输出反而变形。常见起点是 1.1~1.2 左右压复读,一旦发现代码标识符或专名被误伤就调回接近 1.0。它和温度是配合关系:低温本就易复读,适当加一点重复惩罚,往往比单纯再降温度更有效。
这里的重复惩罚(Repetition Penalty)是采样阶段唯一一个「直接动 logits」的旋钮,下一节讲的约束手段都在解码之后、甚至解码之外起作用——记住这个分层,面试时被追问「重复惩罚到底改了什么」才答得准。
四、让输出可控的三种手段
既然模型输出本质是概率性的,工程上就要用「约束」而不是「祈祷」。按强度从弱到强:
第一层:低温度(Temperature)。 最简单,但只是降低概率,不提供任何保证。与 top-p 相对,还有 top-k 采样(Top-k Sampling):固定只保留概率最高的 k 个候选,与 top-p 作用类似但 k 固定。
第二层:JSON Schema / 结构化输出约束,即 约束解码(Constrained Decoding)。 在解码时限制每个位置只能取合法 token(例如进入字符串状态就只能取字符串字符)。这是真正意义上的强制,失败率极低。大多数现代 API 都支持 response_format 或 tool_calls 这类约束通道。
第三层:后置校验 + 有限重试。 无论前面怎么做,都要在拿到结果后做一次校验。最极端是 贪心解码(Greedy Decoding)(每步取最高概率),但它仍非绝对确定;要可复现就固定 随机种子(Seed),而 浮点非确定性(Floating-point Non-determinism) 与后端版本差异仍可能让结果翻转——所以校验不能省。
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之外还必须做校验?
五、常见误区与追问
5.1 误区:temperature = 0 就等于完全确定
错在哪:把「每步取最高概率」理解成「结果可复现」。为什么自然:直觉上贪心解码是确定性的。正确做法:即使温度归零,浮点非确定性、后端版本、批内累加顺序、缓存命中路径都会带来微小数值差异,在分布接近的两个 token 之间翻转。判据:工程上必须假设结果可能变化,把正确性建立在「校验 + 幂等」而非「它一定一样」上;要复现就固定 seed + 同后端同版本,且仍保留校验。
5.2 误区:温度调高调低只是「风格」不同,随手拧
错在哪:把温度当无后果的美学旋钮。为什么自然:很多文档把温度描述成「保守 ↔ 有创意」。正确做法:温度决定的是「错了会不会执行」——工具参数、JSON、路由这类会被执行或被解析的调用,温度必须 0;只有方案探索、头脑风暴这类以多样性为产出的调用才允许高温。判据:先问「这一步的输出会被执行还是被解析?」是 → 0;否(多样性是产出)→ 0.7~1.0。
5.3 误区:temperature 和 top-p 是一回事,用哪个都行
错在哪:认为两者可互换。为什么自然:都「限制随机性」,名字又常并列。正确做法:温度在 softmax 之前缩放 logits,改的是分布形状(高低悬殊);top-p 在 softmax 之后按累计概率切一刀,改的是候选范围。两者叠加时,高温会把原本低概率的 token 抬进 top-p 的保留范围,所以「高温 + 高 top-p」是最不稳的组合。判据:被问「哪个改形状、哪个改范围」必须答清阶段顺序。
5.4 误区:固定 seed 就能跨系统稳定复现
错在哪:认为 seed 是全局复现开关。为什么自然:seed 字面意思就是「随机种子」,容易以为一设就稳。正确做法:seed 只在同一后端、同一版本、同一调用路径下有效;换推理框架、换量化版本、换批处理拼法,logits 的微小差异都会让结果漂移。判据:复现需求要落到「同后端同版本 + 校验」,不能只靠 seed;跨系统一致性必须靠后处理保证。
5.5 误区:重复惩罚开大点就能彻底防复读
错在哪:把 repetition_penalty 当万能去重。为什么自然:复读烦人,惩罚强一点看似更干净。正确做法:过强的重复惩罚会压低所有已出现 token,包括专有名词、代码标识符、必须重复的字段名,反而制造新错误。判据:轻微使用(如 1.1~1.2)即可;真正防复读要靠约束解码与结构控制,而不是把惩罚拉满。
5.6 误区:结构化输出只要 temperature = 0 就稳了
错在哪:以为低温天然保证格式正确。为什么自然:0 温度减少变体,看起来格式更稳。正确做法:低温只降低概率,不保证「每个位置都是合法 token」。JSON 少一个大括号、字段拼错仍是高频故障。判据:结构化输出必须三层齐备——temperature=0 + response_format/工具调用约束(约束解码)+ 后置 schema 校验;少一层都可能线上解析失败。
六、自测
七、小结
- 模型输出的是分布,不是答案;解码策略决定怎么从分布里挑。
- 温度改「多平」,top-p 改「多宽」;两者叠加时要注意极端组合。
- Agent 里凡是需要结构化结果的地方,一律低温 + 强约束 + 校验 + 带反馈的有限重试。
- temperature=0 只是「几乎确定」,工程上不能依赖「一定一致」。
不确定性本身不是问题,把不确定性当成确定性来用才是问题。本刊编辑部
到这里,模型原理部分还差最后一块拼图:既然算力与显存都这么贵,工业界到底用什么手段把成本打下来?
八、参考与延伸
本章机制部分只讲到「够用」为止。想往下深挖,下面按「先看图、再看代码、最后读论文」的顺序排好了。全站不做原文转载,这里只登记链接与「为什么值得读」。
先看图(建立直觉)
- Lilian Weng · Controllable Neural Text Generation —— 把 temperature、top-k、nucleus(top-p)、beam search 逐个推导,是少数把「采样」讲成公式而不是调参笔记的材料。读本章第二节卡住时来这儿:它正好解释了为什么按累计概率截断(top-p)比固定 top-k 更稳。
再看代码(动手实现)
- 动手学深度学习(中文,zh.d2l.ai) —— 配套可跑代码,把 softmax、采样、约束生成都实现了一遍。把本章的 constrained_tool_call 照着敲一遍,比看十遍都记得牢。
- OpenAI Cookbook —— 大量关于「稳定结构化输出、降低不确定性」的可运行示例。重点看它怎么把 response_format 与重试组合起来。
最后读论文(对齐一手定义)
- The Curious Case of Neural Text Degeneration(arXiv:1904.09751,top-p / nucleus sampling 一手) —— nucleus sampling 的源头论文。重点看它怎么论证「固定 top-k 不如按累计概率截断」——这正是 top-p 优于 top-k 的理论依据。
- Chip Huyen · How to build an LLM application(blog) —— 从工程角度讲 Agent 里的确定性纪律与成本控制。想理解「为什么结构化输出必须低温 + 约束 + 校验」,看她的实战总结。
◇ 面试官会怎么问
共 5 条 · 其中 1 条高频 · 先自己答一遍,再展开对照
Temperature 和 top_p 分别在调什么?Agent 场景你会怎么设?
Agent 场景的经验是一条原则:凡是输出要被程序解析的地方(工具调用、结构化参数、JSON),尽量低温甚至贪婪解码;需要发散的地方(头脑风暴、候选方案生成)才提高温度。
要注意:即使 temperature = 0,由于并行归约的浮点非确定性、批处理组合变化等原因,输出也不保证逐字一致——所以兜底必须是校验和重试,而不是"假设它稳定"。
追问链
- 那参数校验失败了你怎么办?
- 为什么 temperature=0 仍然可能两次结果不同?
同一个问题问两次答案不同,这在 Agent 里会变成什么问题?
对应的工程手段是:结构化输出 + Schema 校验、解码参数按环节分级设置、把关键决策点做成确定性代码而不是交给模型自由发挥、评测时固定随机种子并多次重复取分布。
top_k 和 top_p 能同时用吗?会有什么问题?
更实用的建议是:固定一个主参数(OpenAI 系通常调 top_p,部分开源模型偏 top_k),另一个设为默认值,避免两个旋钮互相干扰导致调参不可解释。另外,把"温度"和"惩罚项"(presence / frequency penalty)混着调最容易出现"越调越怪"的情况。
用了约束解码(constrained decoding / response_format)之后,还需要再做 schema 校验吗?为什么? 高频
约束解码保证「语法合法」——每个位置只能取合法 token,所以几乎不会出现少括号、字段名拼错这类解析错误。
但它不保证「语义正确」:字段值可以是错的、枚举选错、数字超范围、必填项给空串,以及上下文被截断时 JSON 是否完整(取决于实现是否强收尾)。
判据:凡输出会被程序执行的调用,三层必须齐备——低温(概率上收敛)+ 约束解码(语法上强制)+ 本地 schema 与业务校验(语义上兜底)。少任何一层,线上都会以解析失败或错误执行的形式暴露。
追问链
- 约束解码会不会拖慢生成?
- 校验失败之后,你的重试策略是什么?
为了让评测可复现,固定 seed 够吗?还需要固定什么?
必须一起固定的还有:
- 推理框架与模型版本的精确标识(升级、换量化档位都会改变 logits 的微小差异);
- 批处理的拼法(同一批里别人多一个请求,你的结果就可能翻转);
- 采样参数与停止条件;
- 评测集本身与判分规则(规则也要版本化)。
判据:报告指标时给出重复次数与波动区间;跌幅小于波动区间的不叫回归。
追问链
- 你怎么给「采样噪声」和「真实回归」划线?
- 哪些指标天生不适合用来判定回归?
结论先行(一句话给出取舍)→ 机制(为什么是这样,涉及哪条链路)→ 代价或边界(这么做放弃了什么)→ 你的实践或数字(真实场景里怎么落地)。 只讲机制不讲代价,是背题;只讲取舍没有数字,是空谈。