采样策略
Day 11-14 我们把 decoder 写对、验对、加速(KV cache 让逐 token 解码划算)。但模型每步输出的是一个整个词表上的概率分布——怎么从分布里挑下一个 token,就是「采样策略」,它直接决定输出的确定性 vs 多样性、稳定 vs 发散。今天 Day 15 把解码侧的旋钮系统化:greedy / temperature / top-p / top-k / structur
阶段: B2 · attention/decoder/KV + judge 校准(Day 11-20) 标签: #sampling #temperature #top-p #structured-output
今日导引(由浅入深)
Day 11-14 我们把 decoder 写对、验对、加速(KV cache 让逐 token 解码划算)。但模型每步输出的是一个整个词表上的概率分布——怎么从分布里挑下一个 token,就是「采样策略」,它直接决定输出的确定性 vs 多样性、稳定 vs 发散。今天 Day 15 把解码侧的旋钮系统化:greedy / temperature / top-p / top-k / structured output。它紧接 Day 14(有了 cache,逐 token 采样才不昂贵),并通向明天 Day 16 的「JSON-validity sweep」——量化「高温如何破坏严格语法」,从而论证「为何严格结构任务该用低温或 constrained decoding」。这也是 B2 从「模型内部机理」转向「评测/judge 校准」的过渡点。今天的「最小可判定产出」:对 30 条 prompt 在 temperature {0, 0.7, 1.2} 各采样的 transcript(T=0 最稳、T=1.2 发散)。
1. 机理精读
greedy(贪心):每步取 argmax——概率最高的 token。最确定、可复现,但易陷入重复(一旦进入高概率循环,比如「the the the」,没有随机性逃逸),且全局非最优(局部最优拼不出全局最优句)。是 T=0 的极限。
temperature(温度):在 softmax 前把 logits 除以 T,即 softmax(logits / T)。T<1 让分布变尖(放大高概率项,更确定);T>1 让分布变平(拉近各项,更随机、更有创意也更易胡说);T=1 是原始分布;T→0 退化成 greedy。温度是「确定性 ↔ 多样性」的连续旋钮。
top-p(nucleus sampling):把 token 按概率降序累加,取累积概率刚超过 p(如 0.9)的最小集合(「核」),只在这个核内重新归一化采样。好处是自适应——分布尖时核小(几乎确定),分布平时核大(保留多样性),比固定 top-k 更鲁棒。Holtzman et al.(2019)提出,用来治 greedy/beam 的退化重复。
top-k:只保留概率前 k 个 token、其余置零再归一化采样。简单但不自适应——k 固定时,分布很尖也强行留 k 个、分布很平也只留 k 个。常与 temperature/top-p 组合用。
structured output / constrained decoding:上面四个都在「自然语言自由度」里调旋钮,但当任务要求严格格式(如必须是合法 JSON)时,自由采样随时可能漏引号/逗号导致 JSON.parse 失败。结构化输出在解码时用 grammar/JSON-Schema 约束可选 token 集——每步只允许「能让最终串保持合法」的 token,从机制上保证格式合法(合法率可钉到 100%),而不是靠 prompt 央求。参考 OpenAI Structured Outputs (2024-08)。这正是 Day 16 要对照验证的主题:自由高温 vs 约束解码,谁能保证合法。
约束解码的机制再拆细一点:把目标格式编译成一个状态机/正则文法(如 JSON 的 grammar),解码每一步先算 logits,再用文法生成一个「合法 token 掩码」——不合法的 token logits 置 -∞,于是采样只可能落在合法子集里。代价是要维护文法状态机、且约束太死会牺牲一点表达自由度;收益是合法率 100% 且不靠运气。这与 Day 11 的 causal mask 是同一招数(把不该选的 token logits 置 -∞)的「格式版」——只是 mask 的依据从「位置」变成「文法状态」。
还有一类「反重复」旋钮(与上面正交):
- repetition penalty:对已出现过的 token 的 logits 打折,降低复读概率。
- frequency / presence penalty(OpenAI 风格):按某 token 已出现「次数」或「是否出现过」线性扣分,分别压「啰嗦」和「跑题外引入新词」。
- no-repeat n-gram:硬禁止重复出现过的 n-gram。
它们治的是 greedy/低温下的退化重复,是对 temperature/top-p 的补充而非替代。
温度与采样不冲突:实践中常组合——先 temperature 缩放 logits,再 top-p/top-k 截断候选集,最后在候选里按概率随机抽。T=0(greedy)时其余旋钮失效(已确定)。典型解码管线顺序:logits → /T → 反重复惩罚 → top-k 截断 → top-p 截断 → 归一化 → 多项式采样。
各旋钮的取值经验(非硬规则,随任务而变):
| 任务类型 | temperature | top-p | 理由 |
|---|---|---|---|
| 代码 / 抽取 / 分类 | 0 ~ 0.2 | 1.0(或不开) | 要确定、可复现、少幻觉 |
| 一般问答 | 0.5 ~ 0.7 | 0.9 ~ 0.95 | 平衡稳定与自然 |
| 创意写作 / 头脑风暴 | 0.9 ~ 1.2 | 0.95 ~ 1.0 | 要多样、可接受发散 |
| 严格 JSON / schema | 0(+ 约束解码) | — | 见 Day 16,自由采样保证不了合法 |
这张表也预告了 B2 后半段「评测」的一个常被忽视的点:报告模型能力时必须连采样参数一起报——同一模型 T=0 和 T=1.2 的 pass 率可以差很多,不固定采样参数的 eval 数字不可比。这与 B1 学的「数字要可复现」一脉相承。
2. 推导 / 手算 / 代码走读
手算 temperature 对分布的影响(3-token,原始 logits = [2.0, 1.0, 0.0]):
- T=1:
softmax([2,1,0]) ≈ [0.665, 0.245, 0.090]。 - T=0.5(变尖,logits/0.5 =
[4,2,0]):softmax([4,2,0]) ≈ [0.867, 0.117, 0.016]——头部更突出。 - T=2(变平,logits/2 =
[1,0.5,0]):softmax([1,0.5,0]) ≈ [0.506, 0.307, 0.187]——尾部抬头、更随机。 - T→0:分布趋于
[1,0,0]= greedy 取第一个。
可见 T 单调控制「头部集中度」。再手算 top-p / top-k 在 T=1 分布 [0.665, 0.245, 0.090] 上的截断:
- top-p=0.9:降序累加
0.665 → 0.910 ≥ 0.9,核 = 前 2 个(第 3 个被剔),在[0.665, 0.245]上重归一化为[0.731, 0.269]再采样。 - top-k=1:只留概率最高那个,等价 greedy → 必出第 1 个 token。
- top-k=2:留前 2 个,结果与本例 top-p=0.9 相同(恰好都砍掉第 3 个)——但这只是巧合;若分布是
[0.34,0.33,0.33],top-k=2 仍砍掉一个长尾,而 top-p=0.9 会保留全部 3 个(自适应),差异就显出来了。
这个对照说明 top-p 的自适应优势:分布平坦时它自动放宽候选集,分布尖锐时自动收紧;top-k 的 k 是死的,平/尖一视同仁。
代码走读:seed 计划的 scripts/sample-sweep.ts 当前仓库中不存在(Glob 核实),属 seed 标注「待跑」,不编造其函数。但本仓的 eval 调用层 scripts/run-agent-eval.ts 提供了真实的**「需 key 才跑、无 key 优雅退出」**范式,正是今天采样实战要复用的工程骨架,走读其真实行为:
- 文件头注释明确:默认 provider = deepseek(
DEEPSEEK_API_KEY),也支持--provider openrouter --model deepseek/deepseek-chat(与 seed 的 OpenRouter 路径一致)。 ENV_KEY(第 33-39 行)映射每个 provider 到环境变量名(openrouter: 'OPENROUTER_API_KEY'等)。- 关键诚实约束(文件头第 15-16 行注释):「No key set? It prints what it WOULD do and exits 0.」——无 key 时不假跑、不伪造数字,只打印将做之事并退出 0。
- 「The eval MATH … is unit-tested and needs no key」——采样统计/解析管道可离线用 fixture 跑通,真实模型采样才需 key。这正是 Day 15「待跑(需 key)」的根因。
三档温度(seed 选的 {0, 0.7, 1.2})的定性预期,作为跑出来后的对照基准:
| temperature | 输出特征 | 复现性 | 适合 |
|---|---|---|---|
| 0(greedy) | 最稳定、近确定、可能略重复 | 高(近位级一致) | 抽取/分类/代码 |
| 0.7 | 自然、略有变化、仍连贯 | 中(每次不同但合理) | 一般对话 |
| 1.2 | 发散、有创意也更易胡说/跑格式 | 低(差异大) | 创意/头脑风暴 |
seed 预期「T=0 最稳、T=1.2 发散」即此表前后两行的对比——具体 completion% 与文本差异待 key 跑出,不臆造。这张定性表也是明天 Day 16 的伏笔:T 越高,严格 JSON 的 JSON.parse 通过率越低(合法率随 T 单调下滑),届时用真实 sweep 量化。
3. 今日实战
seed:「写 scripts/sample-sweep.ts 经 OpenRouter 调 DeepSeek-V3,对 agent-evals 抽的 30 条 prompt 在 temperature {0, 0.7, 1.2} 各采样,落 transcript 到 agent-evals/sampling/。harness flow 离线可用 fixture 先跑通。」可执行步骤(待建脚本):
- 新建
scripts/sample-sweep.ts,复用run-agent-eval.ts的 provider/key 解析范式(默认 deepseek,可--provider openrouter)。 - 从 agent-evals 抽 30 条 prompt;对每条在
temperature ∈ {0, 0.7, 1.2}各调一次模型,记录 completion。 - 落 transcript 到
agent-evals/sampling/(按 temperature 分文件/分目录)。 - 无 key 时:照
run-agent-eval.ts的范式打印「将采样 30×3 条」并退出 0,或用离线 fixture 跑通解析 + completion% 统计管道。 - 有 key 时跑真实采样,肉眼对比 T=0(稳定/近确定)与 T=1.2(发散/多样)的输出差异。
离线 fixture 跑通管道的具体内容(无 key 也能交付的部分):
- 预存几条
{prompt, temperature, completion}的 fixture JSON。 - 跑
sample-sweep.ts的解析路径:读 fixture → 按 temperature 分组 → 算每档的 completion%(成功拿到非空回复的比例)→ 打印汇总表。 - 这条管道(分组 + 统计 + 落盘 + commit)完全确定、可单测,与
run-agent-eval.ts注释里「The eval MATH … needs no key」是同一原则。 - 真实模型采样(T=0/0.7/1.2 的实际 transcript 与定性差异)才需 key——保持「待跑」。
4. 今日实测 / 产出
- 状态(按 seed):seed 标注「待跑(需 OPENROUTER_API_KEY)」;可先用离线 fixture 跑通脚本出 completion% + commit。
- 诚实标注:
scripts/sample-sweep.ts尚未创建,真实三档温度采样待跑(需 key)——保持「待跑」,不升级为已完成、不臆造 completion%。 - 规划产出:三档 temperature({0, 0.7, 1.2})×30 条 prompt 的输出 transcript,落
agent-evals/sampling/,预期 T=0 最稳、T=1.2 发散——这是定性预期,具体数字待 key 跑出。 - 离线 fixture 可先把解析 + completion% + commit 管道跑通(与
run-agent-eval.ts的「math 无需 key」一致)。
5. 常见误区 / 陷阱
- 只靠 prompt「请输出 JSON」:不保证合法——高温随时漏标点。Day 16 即量化证明,正解是 constrained/structured decoding。
- 以为 T=0 就完全可复现:跨硬件/批大小的浮点归约与 tie-breaking 仍可能微抖;同环境下近似确定,但别假设位级一致。
- top-k 当万能:固定 k 不自适应,分布尖时浪费、平时砍多。top-p 通常更鲁棒,二者也可组合。
- 高温用于严格结构任务:schema 强约束任务用高温自由采样 = 自找 parse 失败。结构任务用低温或约束解码。
5.5 采样在 B 系列能力曲线里的位置
采样是「模型机理(B2 前半)」与「评测/可靠性(B2 后半起)」的接缝:
- 往前看,它建立在 Day 11-14 的解码机制上——没有 KV cache 逐 token 解码就不划算,没有 softmax 输出分布就无从采样。
- 往后看,它直接决定评测的可比性(采样参数不固定,pass 率不可比)与可靠性(高温破坏结构、低温/约束保证格式),这正是 Day 16(JSON-validity sweep)与 B2 后半 judge 校准要处理的。
- 一句话定位:采样是把「概率分布」变成「确定行为」的旋钮,旋钮怎么设,既影响质量也影响可测量性。
6. 学习资源(每条带 YYYY-MM)
- OpenAI Structured Outputs 公告与文档 (2024-08)——seed 指定来源,解码时 schema 约束保证格式合法。
- Holtzman et al.《The Curious Case of Neural Text Degeneration》(2019-04, arXiv:1904.09751)——top-p / nucleus sampling 原始论文,解释 greedy/beam 的退化重复。
- Hugging Face《How to generate text: using different decoding methods》(2020-03, 持续更新)——greedy/temperature/top-k/top-p 直观对比。
- 本仓
scripts/run-agent-eval.ts(AICAP-180, 2026-06)——「需 key 才跑、无 key 退出 0、math 离线可测」的真实工程范式(默认 provider deepseek,可--provider openrouter)。 - vLLM / SGLang structured outputs 文档(2024-2025, 持续更新)——生产侧 grammar/JSON-Schema 约束解码的工程实现参照。
SOTA检查 (2026-06 更新)
- 当前主流:temperature / top-p(nucleus)仍是标准解码旋钮。OpenAI Structured Outputs (2024-08) 仍在用。
- 是否仍 SOTA:自由采样旋钮稳定;但 2026 对严格结构任务更推 JSON-Schema constrained decoding + tool/MCP 结构化返回——把合法率从「靠运气」钉到 100%。自由高温采样不适合 schema 强约束场景。主流推理引擎(vLLM、SGLang 等)与各家 API 已内置 grammar/JSON-Schema 约束解码,生产侧「让模型输出合法 JSON」基本不再靠 prompt 而靠约束。
- 过时黑名单:避免只靠 prompt「请输出 JSON」(不保证合法,Day 16 即验证);避免把高温自由采样用于格式严格任务。
- 下次复查点:明天 Day 16 用 JSON-validity sweep 量化高温对合法率的破坏;B2 末(Day 20)回看;B12/agent 阶段以 MCP/tool 结构化返回为生产基线复验。
衔接
- 昨天:Day 14 — KV cache 原理(逐 token 解码加速,让采样在工程上划算)。
- 今天:系统化解码侧旋钮——greedy/temperature/top-p/top-k + structured output 的确定性↔多样性取舍。
- 明天:Day 16 — JSON-validity sweep(量化高温如何破坏严格语法,论证 constrained decoding 的必要性,转入 judge 校准主题)。