从两个反例说起

同一个模型,greedy 在数学题上是好选择,在写故事上是灾难
先看一个真实工程里常见的对比。把同一个模型分别在数学题和开放写作上分别用贪心解码(每步取概率最高的 token)和随机采样(temperature 1.0)跑一遍,会发现截然相反的结论:数学题上贪心更准,故事生成上贪心不可用。这不是玄学,而是两类任务的输出分布形状不同。数学题存在唯一的中间推导路径,模型对正确 token 的置信度会随推导深入而迅速集中;开放写作的最优解分布在一片高熵区域,贪心解码每次都走概率最高的一条窄路,很快陷入重复。
为什么贪心在数学题上更准?因为 pass@1 这种「一次生成必须正确」的指标衡量的不是创造力,而是模式的最优路径。Chen 等人在 Codex 论文(arXiv 2107.03374)里给出了具体数字:用 temperature 0.2 到 0.8 的采样比贪心解码在 HumanEval pass@1 上高出约 10 个百分点,但对 pass@100 这种覆盖指标,低温反而占优。这说明温度和解码方式的选择本质上是在「利用模型已有能力」与「探索解空间」之间做权衡,没有任何一组参数通吃所有任务。
似然陷阱:概率高不等于文本好
所有搜索式解码都建立在同一个假设上:对数概率越高的文本质量越好。这个假设在机器翻译的评测(BLEU 参考答案唯一性较强)里大体成立,但在开放式生成里被反复证伪。Holtzman 等人 2020 年的论文《The Curious Case of Neural Text Degeneration》系统性地展示了这一点:把 beam 宽度从 1 加到 32,GPT-2 生成人类续写的保真度(用 MAUVE 指标衡量,人工评测交叉验证)不升反降,beam 越大文本越呆板,重复率越高;而 top-p 0.9 的核采样拿到了最高的 MAUVE 分数。论文给出的解释是:语言模型的高概率区往往对应「安全的、陈词滥调的」续写,人类偏好恰好落在中等概率区域。这个现象后来被称为似然陷阱(likelihood trap),它是否定 beam search 在开放式任务上价值的核心证据。
理解这一点之后再看下面的三类高级解码策略,评价标准就清晰了:它们各自解决不同的问题——beam search 解决「解空间中的搜索」问题,contrastive search 解决「退化循环」问题,约束解码解决「输出合法性」问题——而不是「更好的文本质量」这一个问题。
Beam Search:更大搜索空间为什么反而更差
算法机制与计算代价
Beam search 是贪心的直接推广:每一步不保留单一最优序列,而是保留 k 条累计对数概率最高的候选(k 称为 beam 宽度),下一步对这 k 条候选分别取 top v 个 token 扩展,再从 k×v 条新序列里剪枝回前 k 条,直到所有候选都产生结束符。它的正确性来自一个贪心不可及的性质:累计概率是逐步相乘的,早期看似概率略低的分支可能因为后续 token 的爆发而反超,beam 保留了这种「先抑后扬」的可能。
代价是计算量与 beam 宽度线性挂钩。以 KV cache 命中的推理为例,每步的注意力计算量正比于当前 beam 里的序列数,beam=8 意味着显存里的 KV cache 是单条序列的 8 倍,吞吐量大致掉到 1/8。工程上还有更隐蔽的成本:padding。不同 beam 的长度可能不同,batch 处理时需要对齐,短序列的无效计算进一步稀释有效吞吐。这就是为什么 HuggingFace transformers 里 num_beams 默认是 1,而 vLLM 这类生产级引擎干脆不提供 beam search——对一个吞吐优先的服务系统来说,把一张卡算 8 份换来的文本质量提升是负数。
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
inputs = tokenizer("北京是一座", return_tensors="pt")
# beam=8,四行参数决定了搜索的宽度和行为
out = model.generate(
**inputs,
num_beams=8, # beam 宽度
num_return_sequences=1, # 最终返回几条
length_penalty=0.6, # 长度归一化指数,见下节
no_repeat_ngram_size=3, # 禁止 3-gram 重复
max_new_tokens=200,
)
长度惩罚:beam 的隐形开关
Beam search 有个必须理解的细节:累计对数概率天然偏爱短文本。每多生成一个 token 就要多乘一个小于 1 的概率项,对数概率不断变小,于是长度不归一化时,beam 倾向于提前用结束符收尾,产出「戛然而止」的短答案。解决方案是长度惩罚:比较候选时改用 (累计对数概率) / 长度^α,α 取 0 是不归一化(偏爱短),取 1 是平均到每个 token(偏爱长),α 在 0.6 到 0.8 是 Google NMT 时代(Wu 等人,2016)调出来的经验甜点区,至今仍被 transformers 的默认参数沿用。
这个细节的工程含义常被忽略:你调 length_penalty 调出来的「效果提升」,很可能不是模型变好了,而是答案长度分布变了。评测指标如果是 ROUGE 或按答案长度打分的指标,长度惩罚直接操纵分数。任何 beam search 的 A/B 实验都必须同时监控平均输出长度,否则优化结论不可信。
开放式生成里的实证:beam 越宽,文本越平
把 Holtzman 等人的结论翻译成工程语言:beam 宽度是「文本呆板度」的旋钮。论文中 GPT-2 在 WebText 续写任务上,greedy 已经能观察到明显的重复循环(比如连续十几次输出同样的从句),beam=32 时重复循环虽然被部分抑制,但整体文本变成了「新闻联播式的平均句」——每个词都是模型认为最安全的词。人工评估中 beam=32 的得分显著低于 top-p 0.9 的随机采样。结论是明确的:beam search 只应该用在有唯一参考答案的任务上——机器翻译、摘要、多选题的选项评估、代码修复;凡是「好答案不止一个」的任务,beam 都是错误的工具。
flowchart TD
A[当前 beam 集合 k 条候选] --> B[每条候选取 top v 个 token]
B --> C[合并为 k 乘 v 条新序列]
C --> D[计算累计对数概率]
D --> E[除以长度惩罚做归一]
E --> F[排序并剪枝回前 k 条]
F --> G{全部 beam 已产出结束符}
G -- 否 --> A
G -- 是 --> H[返回归一化得分最高的序列]
Contrastive Search:用模型自己惩罚退化
退化惩罚的数学形式
开放生成里的另一个失败模式是自强化循环:模型一旦输出一段重复模式,这段模式就进入了上下文,进一步提高了重复输出的概率,循环自我加强。Xu 等人 2022 年的论文《Learning to Break the Loop》(NeurIPS 2022)分析了这个现象,并提出退化惩罚(degeneration penalty):候选 token 与已生成文本的最大余弦相似度作为惩罚项,从解码分数里扣掉。形式化地,第 t 步的打分从 log p(x_t | 上下文) 变成 (1-α)·log p(x_t|·) - α·max_{j<t} cos(h_t, h_j),其中 h 是 token 的隐状态表示,α 是惩罚强度。
这个设计的巧妙之处在于「用模型的内部表示衡量重复」而不是表面字符串匹配:no_repeat_ngram_size 这类规则只能在 n-gram 字面完全一致时拦截,而余弦相似度能在语义重复(换个说法说同一件事)发生前就给出惩罚。代价是每步要对所有历史 token 算一遍隐状态余弦,历史越长代价越高,且依赖 representation 层的输出,一些推理引擎(如早期 vLLM)不支持暴露中间隐状态,这是它没进生产引擎选项列表的直接原因。
Contrastive Search 的组合拳与实测边界
Su 等人在 NeurIPS 2022 的论文《A Contrastive Framework for Neural Text Generation》把退化惩罚推进一步,提出对比搜索:每步先按模型概率选 top k 个候选,再在「模型置信度」与「与已有前缀的差异性」之间加权选择,公式即上面的 (1-α)·p - α·相似度惩罚,推荐参数 α=0.6、k=8。论文报告在 GPT-2 与 OPT 上,WikiText-103 与 WCSC 中文任务里,对比搜索相对贪心和核采样同时提升了 MAUVE(流畅与多样性)与事实一致性指标,重复率显著下降。
但适用边界同样清晰:这篇方法在摘要任务上是负收益。摘要是提取式倾向的任务,与原文高相似恰恰是期望行为,惩罚相似度等于惩罚正确答案。Su 等人的论文也如实报告了这一点。所以对比搜索的正确用法是:知识型/创意型长文本生成开 α=0.6;摘要、改写、风格迁移任务把它关掉。它的价值是补齐了「采样质量好但偶发退化循环」这个洞,而不是替代采样。
# transformers 的对比搜索接口:需要模型支持返回隐状态
out = model.generate(
**inputs,
penalty_alpha=0.6, # 对比搜索的 α
top_k=8, # 候选宽度 k
max_new_tokens=300,
return_dict_in_generate=True,
output_scores=True,
)
约束解码:让模型写不出非法 JSON
有限状态机如何在每步屏蔽非法 token
前两类策略优化的是「文本质量」,约束解码(constrained decoding / grammar-guided generation)解决的是另一个维度的问题:输出合法性。思路是把期望的输出形式(JSON Schema、正则表达式、上下文无关文法、某种 DSL)编译成一个状态机,解码的每一步根据当前状态计算「哪些 token 的开头能让状态机合法转移」,把非法 token 的 logit 直接置为 -inf,再在剩余合法 token 里正常采样。模型没有任何机会输出语法错误的结果——不是事后修复,而是生成过程本身在合法空间内游走。
这件事的价值在实践中远超预期。让 7B 的小模型写严格合法的 JSON,无约束时有效输出率可能只有七八成(漏个引号、多个逗号都是常事),加上 Schema 约束后是 100%,而且由于非法分支被剪掉,模型的「注意力」被集中到合法选择上,小模型在复杂 Schema 下的事实质量反而常常提升——它不再浪费概率质量在「纠结格式」上。
整个循环可以概括为五步:编译期把 Schema 或正则转成状态机;解码每一步先查当前状态、算出合法 token 集合;非法 token 的 logit 置为负无穷;在合法集合内正常采样;采样结果推进状态机,直到到达接受状态输出最终文本。合法性由此从概率问题变成了确定性问题。
Outlines 与 XGrammar 的工程实现
理论方法到生产可用之间隔着三个工程鸿沟:编译速度、状态转移的每步开销、以及 token 与字符的错位。第三个最致命——状态机工作在字符层,而模型采样在 token 层,一个 token 可能对应多个字符,「部分匹配」的 token 必须能被允许并记录中间状态。Willard 与 Louf 2023 年的 Outlines(arXiv 2307.09702)给出了系统性方案:把正则/文法编译成字符层 FSM,再通过 FSM 状态与词表的笛卡尔积预计算 token 层的转移表,把每步掩码构造摊销成近似 O(1) 的查表。
但「FSM 状态数 × 词表大小」的预计算表在复杂 Schema 下会内存爆炸。2024 年底腾讯发布的 XGrammar(arXiv 2411.15100)给出了当前最快的实现:不预计算全表,而是把文法先做下推自动机分解 + 词表 trie 压缩,运行时增量计算每步掩码,配合推理引擎的异步掩码生成掩码开销被基本隐藏。论文自称在常见配置下比 vLLM 内置的 guided decoding 快约 50 倍,被 SGLang 与 vLLM 相继集成。今天要用约束解码,默认选 XGrammar 系实现即可;llama.cpp 里对应的本地方案是 GBNF 文法,OpenAI API 的 structured outputs 也已在服务端做了同样的事。
# llama.cpp 的 GBNF 约束:强制输出只能是 yes 或 no
./llama-cli -m model.gguf -p "Q: 这条评论是正面情绪吗? A:" \
--grammar 'root ::= " yes" | " no"'
约束解码的局限
最后必须诚实面对约束解码的边界。第一,它约束形式不约束内容:模型可以产出 100% 合法但事实错误的 JSON。第二,复杂 Schema 会导致退化:约束空间过窄时(比如正则里写死长枚举),模型被迫在低概率 token 上连续游走,生成质量可能劣化为乱码。第三,掩码有概率质量截断效应——被屏蔽的合法路径里如果包含模型强烈倾向的内容,会引发分布失配。对这些问题的实用缓解是「软约束 + 校验重试」的混合:宽松 Schema + 代码级校验 + 失败重试,在多数场景比硬约束更稳。
怎么选:一张决策清单
把三类策略的适用边界收敛成可执行的规则:有唯一参考答案的任务(翻译、摘要、代码修复)用 beam search 加长度惩罚 0.6 到 0.8;开放式生成一律采样(temperature 或 top-p 已有专文),偶发重复循环再加 contrastive search(α=0.6、k=8),摘要类任务禁用;机器可读输出(JSON/SQL/DSL)上约束解码,Schema 从简,复杂校验放代码层做;生产推理引擎优先选 XGrammar 系实现,本地小模型用 GBNF。所有解码策略实验都必须控制输出长度分布再比较质量指标,否则结论会被长度惩罚之类的次级变量污染。
小结
解码策略的本质是对「概率分布到最终文本」这最后一公里的不同干预。Beam search 是搜索干预:适合答案唯一性强的任务,开放式任务上它放大似然陷阱,越宽越差;Contrastive search 是表示干预:用隐状态余弦相似度惩罚语义重复,修复采样在长文本上的退化循环,但在提取式任务上是负收益;约束解码是语法干预:FSM 在每步屏蔽非法 token,把「输出合法性」从概率问题变成确定性问题,XGrammar 等实现已把它推进生产标配。三者解决的问题维度互不重叠,工程上的正确姿势是按任务类型组合,而不是寻找万能默认值。
读者留言
COMMENTS 暂无还没有留言,来说第一句?