🏠 总目录📚 本教程 06c · 解码参数怎么调 ← →
📑 本页目录(点开跳转)

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 直接硬禁 最粗暴,也最容易误伤

⚠️ 它们最大的问题:不区分「该重复」和「不该重复」。

操作步骤

代码生成:for / i / = / ( / ) / return 本来就该反复出现
开 frequency_penalty = 模型被逼着换写法 = 语法错误
结构化输出:JSON 的 { } " : 和字段名必须重复
惩罚会把 JSON 打坏,而且是【间歇性】打坏,最难查
中文:的、是、了、在 天生就是高频字
惩罚一大,句子开始变得别扭
⭐ 结论:代码 / JSON / 工具调用场景,这三个参数一律留 0。

🔑 更根本的一条:复读常常是"没东西可说"的症状,不是解码的病。 上下文里信息不够时,模型会退回到最安全的模式反复念。 这时候加惩罚只是把症状换个样子——该做的是查提示词和检索(第 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

⭐ 三条规则,比上面这张表更值钱:

  1. temperature 和 top_p 只调一个。 两个一起动,出了问题你分不清是谁干的。常见做法是把 top_p 钉死在 0.9,只调 T。
  2. 先固定随机性,再调提示词。 T=0 时你改一句提示词、输出变了,那就是提示词的功劳;T=0.9 时你根本不知道是提示词起效了还是抽签换了个结果。这条是提示词工程能不能做下去的前提。
  3. ⚠️ 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

📦 深挖的话要学什么

需要的前置:知道模型最后一层输出 logits(第 2 章)+ decode 是逐 token 的(第 6 章)。不需要新数学,本章唯一的公式就是 softmax。


⚖️ 要不要深挖

你的情况 建议
只调 API 做应用 ✅ 这一章就是给你写的,全读,而且是本导论里当天就能用上的少数几章之一
做结构化输出 / 工具调用 / 代码生成 ✅ 重点看 penalty 那一节和调参表——这里最容易埋雷
自建推理服务 ✅ 再往下补约束解码和批量采样
只关心成本和吞吐 🟡 看完「四个动作」和最后那三条规则就够

🔑 我的判断:这一章的投入产出比是整本导论里最高的—— 二十分钟,换掉的是"参数瞎试、出了问题不知道该动哪个"的状态。 而且它不需要 GPU、不需要新数学、不需要重新部署,改一个数字就能验证。


✅ 检查点

  1. temperature 在数学上改变了什么、没改变什么?logits 差 2.0 时,T=0.5 和 T=2 的概率比各是多少?
  2. 复读机是怎么形成的?frequency_penalty 和 presence_penalty 的区别?哪些场景这几个参数必须留 0?
  3. 调参的三条规则是什么?为什么"先固定随机性再调提示词"不只是习惯问题?
  4. T=0 就一定能逐位复现吗?这件事和推理优化的哪个手段有关?
👀 答案
  1. 改变的是差距,没改变排序(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 倍。
  2. 自我强化回路:模型刚写出的模式进了上下文,"出现过"本身抬高了它再次出现的概率,greedy 和低温尤其容易掉进去。frequency_penalty 按出现次数累加地罚(次数越多越狠);presence_penalty 只罚"出现过"这个事实,作用是推动换话题;repetition_penalty 是乘性的,出现 1 次和 100 次罚得一样。代码 / JSON / 工具调用场景一律留 0——那些 token 本来就该重复。更根本的是:复读常常是"没东西可说"的症状,不是解码的病。
  3. ①temperature 和 top_p 只调一个(常见做法是把 top_p 钉死在 0.9 只调 T),两个一起动就分不清是谁干的;②先固定随机性再调提示词;③T=0 也不保证逐位复现。第二条不只是习惯问题:T=0.9 时你根本不知道输出变化是提示词起效了还是抽签换了个结果——没有归因,提示词工程就无法迭代。
  4. 不保证。 GPU 浮点加法不满足结合律,归约顺序会随 batch 组成变化,同样输入拼进不同 batch 可能算出不同 logits。而 batch 组成动态变化正是06 章武器二连续批处理造成的——吞吐优化和逐位复现是一对天然矛盾,要严格复现只能牺牲吞吐。

🛑 可以停在这里

读完这一页,你手上有一张可以直接用的排查表:输出飘 → 先看 T 和 top_p;复读 → 先问是不是没东西可说,再考虑 penalty;JSON 崩 → penalty 是不是没留 0;结果不可复现 → 那不是你的 bug。

⚠️ 什么时候回来:这些都调过了输出还是不行,那问题多半不在解码,在提示词或检索 —— 去 08 系列。

⚡ 走神救援

先记住这几件事

下一节 👉 07-本地部署与开源生态.md

打卡记录保存在你的浏览器里,首页能看到总进度