📑 本页目录(点开跳转)
06c · 解码参数怎么调,出问题怎么查
⏱ 30 分钟 | ⭐ 一张排查表:输出飘 / 复读 / JSON 崩 / 结果不可复现
🎯 一句话
上一页讲了那几个旋钮各自在做什么。这一页只回答两个操作性问题:
该配成多少,以及出了问题该怀疑谁。
⚠️ 最反直觉的一条先说在前面:temperature=0 也不保证两次运行逐位相同。
🌡️ temperature 到底在做什么(数学)
$$p_i = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}}$$
🔗 这就是 第 15 章数学地基里
softmax(x/T)那一小节的下游用法——那里讲了式子,这里讲它落到 API 参数上是什么后果。
⭐ 最值得记住的一条:温度不改变排序,只改变差距。
任意两个 token 的概率比是
$$\frac{p_i}{p_j} = e^{(z_i - z_j)/T}$$
argmax 永远是同一个词,变的是"第二名有多大机会翻盘":
| logits 差 2.0 时 | 概率比 | 直觉 |
|---|---|---|
| T = 0.5 | e⁴ ≈ 54.6 倍 | 第二名基本没戏 → 输出收敛、保守 |
| T = 1(模型原始分布) | e² ≈ 7.4 倍 | 训练时学到的那个分布 |
| T = 2 | e ≈ 2.7 倍 | 长尾被抬起来 → 输出发散、容易崩 |
🔗 这件事你在别处见过:
temperature就是探索与利用的生成版—— T=0 是纯利用(每次都选当前最优),T 大是探索。 强化学习 07 讲的是同一个权衡在决策问题上的样子。
🔁 复读机问题与三个 penalty
复读是怎么来的:自回归有一个自我强化回路——模型刚写出"非常重要",这个模式就进了上下文;而"上下文里出现过"这件事本身抬高了它下一次出现的概率;于是越读越读。greedy 和低温最容易掉进去(因为它们永远选那个已经被抬高的词)。
三个参数长得像,算法完全不同,面试最爱在这里追问:
| 参数 | 怎么算 | 特点 |
|---|---|---|
repetition_penalty |
出现过的 token,正 logit 除以 λ,负 logit 乘以 λ | 乘性,只看"出没出现过"——出现 1 次和 100 次罚得一样重 |
frequency_penalty |
logit 减去 α × 已出现次数 | 加性,次数越多罚得越狠 |
presence_penalty |
logit 减去 β × [出现过 = 1] | 加性,只罚"出现过"这个事实 → 推动模型换新话题 |
no_repeat_ngram_size = n |
已出现过的 n-gram 直接硬禁 | 最粗暴,也最容易误伤 |
⚠️ 它们最大的问题:不区分「该重复」和「不该重复」。
操作步骤
🔑 更根本的一条:复读常常是"没东西可说"的症状,不是解码的病。 上下文里信息不够时,模型会退回到最安全的模式反复念。 这时候加惩罚只是把症状换个样子——该做的是查提示词和检索(第 8 章)。
🎛️ 几个旋钮怎么配合调
| 场景 | temperature | top_p | penalty |
|---|---|---|---|
| 抽取 / 分类 / 工具调用 / JSON | 0 | — | 全 0 ⭐ |
| 代码生成 | 0 ~ 0.2 | 0.95 | 全 0 ⭐ 绝不要开 |
| RAG 问答 / 客服 | 0 ~ 0.3 | 0.9 | 0 |
| 通用对话 | 0.7 | 0.9 | presence 0 ~ 0.3 |
| 创意写作 / 头脑风暴 | 0.9 ~ 1.1 | 0.95 | presence 0.3 ~ 0.6 |
| 要 n 个不同的候选 | 0.8 ~ 1.0,采样 n 次 | 0.95 | ❌ 别用 beam search |
⭐ 三条规则,比上面这张表更值钱:
temperature和top_p只调一个。 两个一起动,出了问题你分不清是谁干的。常见做法是把top_p钉死在 0.9,只调 T。- 先固定随机性,再调提示词。 T=0 时你改一句提示词、输出变了,那就是提示词的功劳;T=0.9 时你根本不知道是提示词起效了还是抽签换了个结果。这条是提示词工程能不能做下去的前提。
- ⚠️ T=0 也不保证 100% 可复现。 GPU 上浮点加法不满足结合律,而每一步的归约顺序会随 batch 的组成变化——同样的输入,和不同的请求拼在一个 batch 里,可能算出不同的 logits,恰好卡在两个候选分数极接近时就会输出不同。 🔗 而 batch 组成是动态的,正是第 6 章的武器二 · 连续批处理造成的:吞吐优化和逐位复现是一对天然矛盾。要严格复现只能牺牲吞吐(固定 batch、关掉连续批处理)。
🔨 手写一遍:温度 + top-p 采样
import numpy as np
def sample_next(logits, temperature=0.8, top_p=0.9, rng=None):
"""从一步 logits 里采一个 token id。顺序:温度 → top-p → 归一化 → 抽签"""
rng = rng or np.random.default_rng(0)
z = np.asarray(logits, dtype=np.float64)
if temperature <= 0: # ⭐ T=0 就是 greedy,不抽签
return int(np.argmax(z))
z = z / temperature # ⭐ ① 温度先改分布形状
z = z - z.max() # 数值稳定:防 exp 溢出
p = np.exp(z); p /= p.sum()
order = np.argsort(-p) # 概率从大到小
cum = np.cumsum(p[order])
keep = order[:np.searchsorted(cum, top_p) + 1] # ⭐ ② 累计刚过 p 就停手
q = p[keep] / p[keep].sum() # ⭐ ③ 只在核里重新归一化
return int(rng.choice(keep, p=q))
logits = [3.0, 2.3, 1.75, 1.35, 1.05, 0.85, 0.35, 0.35] # 就是 06b 那张图里的 8 个
print(sample_next(logits, temperature=1.0, top_p=0.9)) # 核里 6 个候选
print(sample_next(logits, temperature=0.3, top_p=0.9)) # 核里只剩 2 个 ⭐
💡 十行代码把 06b 那张图跑了一遍:同一组 logits,只把 T 从 1.0 降到 0.3,核就从 6 个缩到 2 个——温度和截断是串起来的,不是两个独立开关。
🔗 想再深一层,去哪一套
| 去哪 | 为什么 |
|---|---|
| 06 · 推理优化 | ⭐ 「T=0 也不保证复现」的根源在那一章:连续批处理让 batch 组成动态变化,GPU 浮点归约顺序跟着变 —— 吞吐优化和逐位复现是一对天然矛盾 |
| 智能体工程教程 16c · 接进真实产品 | 结构化输出为什么会崩、护栏该怎么设计 —— penalty 留 0 只是其中最便宜的一条 |
| 数据这一关 13 · 评测集也是数据 | 「先固定随机性再调提示词」的下一步:没有固定的评测集,你连「改好了没有」都判断不了 |
| 扩散模型 06 · 条件控制与 CFG | ⭐ 同一类旋钮的另一副面孔:图像那边的 guidance scale s 和这一页的 temperature 都在「多听话 vs 多自由」之间取舍,都没有安全默认值。⚠️ 但机制不同 —— s>1 是外推,而温度只是缩放 logits |
📦 深挖的话要学什么
- 约束解码:JSON 模式 / 正则约束是怎么用 logits mask 实现的、它和 Function Calling 的关系、约束会不会伤害质量
- 更多采样方法:
min_p、typical sampling、对比解码(contrastive decoding)、镜像采样 - beam search 的细节:长度惩罚 α 怎么定、覆盖惩罚、beam 内去重
- 批量采样:一次生成 n 个候选时的 KV 块共享,以及 Self-Consistency 为什么必须配采样而不能用 greedy
- 和评测的关系:pass@k 为什么必须配温度、温度选错会让评测结论整个反过来
需要的前置:知道模型最后一层输出 logits(第 2 章)+ decode 是逐 token 的(第 6 章)。不需要新数学,本章唯一的公式就是 softmax。
⚖️ 要不要深挖
| 你的情况 | 建议 |
|---|---|
| 只调 API 做应用 | ✅ 这一章就是给你写的,全读,而且是本导论里当天就能用上的少数几章之一 |
| 做结构化输出 / 工具调用 / 代码生成 | ✅ 重点看 penalty 那一节和调参表——这里最容易埋雷 |
| 自建推理服务 | ✅ 再往下补约束解码和批量采样 |
| 只关心成本和吞吐 | 🟡 看完「四个动作」和最后那三条规则就够 |
🔑 我的判断:这一章的投入产出比是整本导论里最高的—— 二十分钟,换掉的是"参数瞎试、出了问题不知道该动哪个"的状态。 而且它不需要 GPU、不需要新数学、不需要重新部署,改一个数字就能验证。
✅ 检查点
temperature在数学上改变了什么、没改变什么?logits 差 2.0 时,T=0.5 和 T=2 的概率比各是多少?- 复读机是怎么形成的?
frequency_penalty和presence_penalty的区别?哪些场景这几个参数必须留 0? - 调参的三条规则是什么?为什么"先固定随机性再调提示词"不只是习惯问题?
- T=0 就一定能逐位复现吗?这件事和推理优化的哪个手段有关?
👀 答案
- 改变的是差距,没改变排序(
argmax永远是同一个词)。概率比 $p_i/p_j = e^{(z_i-z_j)/T}$:logits 差 2.0 时,T=0.5 → e⁴ ≈ 54.6 倍,T=1 → e² ≈ 7.4 倍,T=2 → e ≈ 2.7 倍。 - 自我强化回路:模型刚写出的模式进了上下文,"出现过"本身抬高了它再次出现的概率,greedy 和低温尤其容易掉进去。
frequency_penalty按出现次数累加地罚(次数越多越狠);presence_penalty只罚"出现过"这个事实,作用是推动换话题;repetition_penalty是乘性的,出现 1 次和 100 次罚得一样。代码 / JSON / 工具调用场景一律留 0——那些 token 本来就该重复。更根本的是:复读常常是"没东西可说"的症状,不是解码的病。 - ①
temperature和top_p只调一个(常见做法是把 top_p 钉死在 0.9 只调 T),两个一起动就分不清是谁干的;②先固定随机性再调提示词;③T=0 也不保证逐位复现。第二条不只是习惯问题:T=0.9 时你根本不知道输出变化是提示词起效了还是抽签换了个结果——没有归因,提示词工程就无法迭代。 - 不保证。 GPU 浮点加法不满足结合律,归约顺序会随 batch 组成变化,同样输入拼进不同 batch 可能算出不同 logits。而 batch 组成动态变化正是06 章武器二连续批处理造成的——吞吐优化和逐位复现是一对天然矛盾,要严格复现只能牺牲吞吐。
🛑 可以停在这里
读完这一页,你手上有一张可以直接用的排查表:输出飘 → 先看 T 和 top_p;复读 → 先问是不是没东西可说,再考虑 penalty;JSON 崩 → penalty 是不是没留 0;结果不可复现 → 那不是你的 bug。
⚠️ 什么时候回来:这些都调过了输出还是不行,那问题多半不在解码,在提示词或检索 —— 去 08 系列。
⚡ 走神救援
先记住这几件事
- 先固定其他条件,再改一个解码参数,才容易判断变化来自哪里。
- 不同重复惩罚的作用不同;代码和结构化输出中有些重复本来就必要。
- 低温不等于逐位可复现,复读也可能来自上下文或任务信息不足。
下一节 👉 07-本地部署与开源生态.md