🏠 总目录📚 本教程 06b · 解码策略 ← →
📑 本页目录(点开跳转)

06b · 解码策略:模型是怎么选出下一个词的

⏱ 24 分钟 | ⭐ 输出飘、复读、JSON 偶尔崩 —— 从这一页开始查


🎯 一句话

模型每一步吐出来的是一个概率分布,不是一个词。从这个分布里挑出下一个 token 的那套规则,就是解码策略。 这一页讲挑法本身:贪心、Beam Search、top-k、top-p 各自在做什么。 ⭐ 参数具体怎么配、出了问题怎么排查,在下一页。


🧭 先接住上一章

第 6 章推理优化的三大武器(量化 / 批处理 / 投机解码)解决的是「这一步跑得多快」。 这一章解决完全不同的另一件事:「这一步选哪个词」。

两件事是正交的,但经常被混在一起。分开的办法很简单,看现象:

现象 去哪一章
慢、贵、显存不够、吞吐上不去 第 6 章
输出飘 / 复读 / 每次结果不一样 / JSON 间歇性打不开 ⭐ 这一章

它是每个调 API 的人天天在动、却最少被讲清楚的东西。 唯一的前置是知道 decode 是一个一个吐 token 的——第 6 章「两个阶段」讲过。


🎲 一步 decode 里到底发生了什么

流程图

① 模型吐出 logits —— 一个和词表一样长的实数向量
词表 10 万→10 万个分数,可以是任意实数(3.0、-8.2、0.35…)
⚠️ 它【不是概率】,还没归一化
② 温度缩放:logits ÷ T ← temperature 在这里生效
③ 截断候选集:top_k / top_p ← 把长尾砍掉
④ softmax 归一化→抽签 ← 真正「选」出那一个 token
⭐ 注意顺序:温度【先】改变分布形状,截断【再】在改过之后的分布上切。
所以调大 T 会让 top_p 放进来更多词 —— 这两个旋钮不是独立的。

⚠️ 主流实现(HuggingFace transformers、vLLM)的默认顺序是 温度 → top_k → top_p, 但不同框架不保证一致。拿不准就只动一个参数,别指望跨框架搬参数还是同一个行为。

🥇 Greedy:永远选最大的那个

每一步取 argmax,不抽签。等价于 T→0。

✅ 好处 ❌ 代价
确定性、最快(省掉抽签)——但"可复现"有个坑,见本章最后 ⚠️ 局部最优 ≠ 全局最优:第一个词选歪,整句跟着歪
抽取/分类/工具调用的默认选择 最容易掉进复读循环(下面会讲机制)

机制:不再只留一条路,而是同时留 k 条部分序列(beam)。每一步把每条 beam 的所有可能扩展都算出来,按累计对数概率排序,全局取前 k 条继续。

因果链

k = 3,第 t 步:
3 条 beam × 每条取若干候选→一堆候选序列
按 Σ log p 排序→只留前 3 条→进入第 t+1 步
⚠️ 长度偏好:每多一个词就多乘一个 <1 的概率,总分只会更低
beam search 天然偏爱【短句】
所以要配长度惩罚 length_penalty,把总分除以 长度^α

它在哪些任务上是对的:机器翻译、语音识别、OCR、结构化摘要——这些任务有一个(接近)唯一的正确输出,"找到概率最高的那条序列"就是任务目标本身。

⭐ 但开放式生成(聊天、写作、创意)不用它,原因有四条,第一条最根本:

原因 说明
高概率 ≠ 好文本 ⭐ 人写的句子并不走最高概率路径——真实人类文本的逐词概率是起伏的(时而出人意料),而 beam 挑出来的序列概率曲线平得不自然。追求"最可能的一句话",得到的是最平庸的一句话
文本退化 beam 开得越宽,输出越短、越干瘪、越容易复读。这个观察正是核采样(top-p)被提出来的直接动机
多样性塌缩 k 条 beam 常常只差一两个词。想要「给我 5 个不同的开头」,beam 给你的是 5 个几乎一样的开头
成本 k 倍 k 条 beam = k 份 KV Cache,显存和带宽都乘 k。而 decode 本来就卡在带宽上(第 6 章的第一条铁律:decode 卡在显存带宽,不是算力)

🔗 最后那条成本其实有救:vLLM 的块共享让各条 beam 共用公共前缀的 KV 块, 只有分叉之后的部分才各存一份 —— 展开见 AI基础设施 17。

⚠️ 一个常见的搬运事故:把翻译服务里调好的 num_beams=5 原样搬到"生成营销文案"上, 结果每次生成的十条文案开头几乎一模一样。参数没错,任务变了——从"有唯一正确答案"变成了"要多样性"。

✂️ top-k:固定砍到 k 个

只保留概率最高的 k 个候选,重新归一化再抽签。

它的毛病只有一条:k 是固定数量,但分布形状是变的。

关键信息

分布很尖("中华人民共和" 后面几乎必然是 "国")
k=50 会把 49 个荒唐候选硬塞进候选集
分布很平(一个故事的开头,几百个词都合理)
k=50 又把大量合理选项砍掉了

🎯 top-p 核采样(nucleus sampling):候选集自适应

按概率从大到小累加,累计一超过 p 就停手,只在这个"核"里抽签。

⭐ 关键优点:候选集大小自己会变。 分布尖 → 核可能只有 1–2 个;分布平 → 核可以有几百个。这正好补上了 top-k 的毛病,所以它是现在的默认选择(典型值 0.9 / 0.95)。

下一个词的概率分布(真实词表 10 万个,这里只画概率最高的 8 个) 0.42 0.21 0.12 0.08 0.06 0.05 0.03 0.03 天气 心情 计划 咖啡 头发 钥匙 砖头 蟑螂 ① greedy (等价 T→0) 1 个 ② top_k = 3 (固定数量) 3 个 ③ top_p = 0.9 (T = 1,自适应) 6 个 ④ 先降温 T=0.3 再 top_p = 0.9 2 个
同一个分布,四种解法留下的候选集完全不同。top_k 是固定数量(不看分布形状),top_p 是自适应的。而温度先改分布、截断再切——降温到 T=0.3 后,同样一个 top_p=0.9 只剩 2 个候选(因为「天气」的概率被拉到了 0.89)。

⚠️ top_p = 1.0 不是"安全值"——它表示完全不截断,长尾里那 0.001% 的怪词有真实机会被抽中。想要"别乱来"应该设 0.9,不是 1.0。

🔗 想再深一层,去哪一套

去哪 为什么
06 · 推理优化 同样是 decode 那一步,那一章管的是怎么让它更快,这一章管选哪个词。两件事,两套参数
AI全栈 05 · 流式输出 流式输出里一个字一个字往外吐 —— 每一个字都走了一遍这一页的四个动作
强化学习基础 12 · RLHF全流程 为什么「高概率 ≠ 好文本」:对齐那一步改的正是模型对「什么算好」的分布

✅ 检查点

  1. 一步 decode 里的四个动作按什么顺序发生?为什么说 temperature 和 top_p 不是独立的两个旋钮?
  2. Beam Search 适合什么任务?开放式生成为什么不用它(至少说出两条)?
  3. top-p 比 top-k 好在哪?top_p = 1.0 意味着什么?
👀 答案
  1. ① 吐 logits(不是概率)→ ② 温度缩放 → ③ top_k/top_p 截断 → ④ softmax 归一化后抽签。不独立是因为温度先改分布形状、截断再在改过的分布上切:降温到 0.3 后,同样的 top_p=0.9 从 6 个候选缩到 2 个。
  2. 适合有唯一正确输出的任务:机器翻译、语音识别、OCR、结构化摘要。开放式生成不用它的四条:①高概率 ≠ 好文本(人类文本的逐词概率是起伏的,追求"最可能的一句话"得到的是最平庸的一句话)②文本退化(beam 越宽越短越干瘪,这正是核采样被提出来的动机)③多样性塌缩(k 条 beam 只差一两个词)④成本 k 倍 KV Cache。
  3. top-k 是固定数量,不看分布形状——分布尖时塞进 49 个荒唐候选,分布平时又砍掉合理选项;top-p 的候选集大小自适应。top_p = 1.0 表示完全不截断,长尾里那 0.001% 的怪词有真实机会被抽中——想"别乱来"该设 0.9 不是 1.0。

🛑 可以停在这里

读到这里,你已经知道那几个参数各自在分布上做了什么手脚 —— 这足够让你看懂 API 文档里的 temperature / top_p / top_k 分别是什么。

⚠️ 什么时候看下一页:你已经知道它们是什么,但不知道该配成多少;或者线上出现了复读、JSON 偶尔打不开、同样输入两次结果不一样。

⚡ 走神救援

先记住这几件事

下一节 👉 06c-解码参数怎么调.md

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