问题与目标
生成模型每一步输出的不是完整句子,而是词表中每个 Token 的 logits。解码策略把 logits 转换为概率并选出下一个 Token,再将它拼回上下文继续计算。相同模型和 Prompt 因参数不同,可能得到确定、发散或重复的结果。
本篇使用 NumPy 实现 Temperature、Top-k 和 Top-p 过滤,观察概率分布如何改变;同时解释贪心、Beam Search、停止条件、上下文窗口和 KV Cache。

每轮只确定一个新 Token。过滤策略决定候选集合,采样决定从候选中选择哪个,停止条件决定循环何时结束。
核心概念
从 logits 到概率
Logits 可以是任意实数,Softmax 把它们归一化为概率。Temperature 在 Softmax 前缩放 logits:
softmax(logits / temperature)
- 温度小于 1:分布更尖锐,结果更稳定。
- 温度大于 1:分布更平坦,低概率 Token 更可能被选中。
- 贪心解码直接选择最大值,不需要采样。
温度不是“创造力开关”,它只改变当前模型分布的平滑程度。模型不知道的事实不会因为温度降低就自动变正确。
Top-k 与 Top-p
Top-k 只保留概率最高的固定数量 Token。Top-p 按概率从高到低累加,保留累计概率达到阈值的最小候选集合。两者可以组合,但过滤过强可能让文本僵硬,过滤过松则可能增加跑题和错误。
Beam Search
Beam Search 同时保留若干条累计得分较高的序列,适合翻译等目标较明确的任务。它不等于随机采样,Beam 越大也不保证开放式对话越自然。
上下文窗口与 KV Cache
上下文窗口限制一次计算能容纳的输入 Token 和已生成 Token 总量。达到限制时,需要截断、压缩或重新组织上下文。
自回归生成每轮都会读取历史 Token。KV Cache 保存已有位置的 Key 和 Value,避免反复计算历史表示,从而加快后续 Token 生成;代价是缓存随序列增长占用显存。它优化计算,不扩展模型本身的上下文能力。
可运行实现
import numpy as np
TOKENS = np.array(["正常", "异常", "恢复", "继续", "<eos>"])
LOGITS = np.array([2.4, 1.7, 1.2, 0.8, 0.2], dtype=float)
def softmax(values: np.ndarray) -> np.ndarray:
shifted = values - values.max()
exp = np.exp(shifted)
return exp / exp.sum()
def filter_logits(
logits: np.ndarray,
top_k: int | None = None,
top_p: float | None = None,
) -> np.ndarray:
filtered = logits.copy()
if top_k is not None:
keep = np.argsort(filtered)[-top_k:]
mask = np.ones_like(filtered, dtype=bool)
mask[keep] = False
filtered[mask] = -np.inf
if top_p is not None:
order = np.argsort(filtered)[::-1]
ordered_probabilities = softmax(filtered[order])
cumulative = np.cumsum(ordered_probabilities)
remove = cumulative - ordered_probabilities >= top_p
filtered[order[remove]] = -np.inf
return filtered
def sample_next(
logits: np.ndarray,
temperature: float = 1.0,
top_k: int | None = None,
top_p: float | None = None,
seed: int = 7,
):
if temperature <= 0:
raise ValueError("temperature must be greater than 0")
filtered = filter_logits(logits / temperature, top_k, top_p)
probabilities = softmax(filtered)
rng = np.random.default_rng(seed)
index = rng.choice(len(TOKENS), p=probabilities)
return TOKENS[index], dict(zip(TOKENS, probabilities.round(3)))
for config in [
{"temperature": 0.5},
{"temperature": 1.2},
{"temperature": 1.0, "top_k": 2},
{"temperature": 1.0, "top_p": 0.75},
]:
token, probabilities = sample_next(LOGITS, **config)
print(config, "->", token, probabilities)
固定随机种子后结果可重复。修改种子可以观察采样差异;把 top_k=1 与直接取 argmax 比较,可以看到它们都只保留最高分 Token。
正式生成循环还需要:
- 把新 Token 追加到输入;
- 检查
<eos>或停止字符串; - 限制
max_new_tokens; - 记录生成原因、Token 数和耗时;
- 必要时处理重复片段和非法输出。
常见问题与排查
设置 Temperature 却没有变化
检查是否启用了采样。贪心或确定性 Beam Search 通常不会按概率随机选择,Temperature 可能不会产生预期影响。
同一个 Prompt 每次结果都不同
采样本来具有随机性。需要可重复实验时固定随机种子、模型版本、推理参数和输入模板;业务场景还应比较多次结果,而不是只保存最好的一次。
输出不断重复
检查 Prompt 是否诱导循环、上下文中是否存在重复模板,以及重复惩罚、n-gram 限制和停止条件。过强惩罚也可能破坏正常术语复现。
输入没有超过窗口,生成仍然被截断
检查限制的是总长度还是新增长度,并确认输入 Token 数。max_length 与 max_new_tokens 语义不同,优先明确“最多新增多少 Token”。
小结
大语言模型的生成是一轮轮的概率决策。理解 logits、过滤、采样、停止条件和缓存后,才能解释推理参数为什么改变结果,也能更准确地排查重复、截断、随机性和显存问题。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


