大模型评测与AI产品质量保障:第14篇 Top-P 与 Top-K:核采样与截断采样的艺术
10 余年一线大厂经验,专注大模型测试、AI 产品质量保障与职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
上一篇我们深入拆解了 Temperature,理解了它是如何通过调节概率分布的“陡峭程度”来控制随机性。但 Temperature 有一个盲区:它平等地缩放所有 token 的概率,无法直接决定“有多少个候选 token 参与抽奖”。Top-K 和 Top-P 正是为此而生——它们一个限制候选池的绝对数量,一个限制候选池的相对概率阈值。本文把两者的原理、对比、组合策略和测试方法讲透。
一、先理解两种采样策略
1.1 生成一个 Token 的完整流程
当大模型要生成下一个 token 时,完整流程如下:
原始 logits → softmax → 概率分布 → 采样策略 → 选出一个 token
↑
Temperature / Top-K / Top-P 都在这一步介入
三种参数的介入顺序和方式:
| 参数 | 作用阶段 | 作用方式 |
|---|---|---|
| Temperature | softmax 阶段 | 除在 logits 上,改变分布的陡峭程度 |
| Top-K | 采样阶段 | 只保留概率最高的 K 个 token,其余概率设为 0 |
| Top-P | 采样阶段 | 保留概率从高到低累加刚好超过 P 的最小 token 集合,其余概率设为 0 |
1.2 一句话定义
Top-K: 永远只从概率最高的 K 个 token 中选
Top-P: 永远只从累积概率刚好超过 P 的最小 token 集合中选
两者可以单独使用,也可以组合使用,还可以和 Temperature 叠加。
二、Top-K:简单粗暴的截断
2.1 原理
Top-K 的逻辑极其简单:把模型预测的所有 token 按概率从高到低排序,只保留前 K 个,然后在这 K 个中重新归一化概率并采样。
假设词表有 50000 个 token,K=50
原始概率分布: [0.15, 0.12, 0.08, 0.05, 0.04, ..., 3e-6, 2e-6, ...]
↑ ↑
前50个候选 其余49950个 → 概率全部设0
只在前50个中重新归一化采样
2.2 用代码对比不同 K 值
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://api.deepseek.com"
)
def test_topk(prompt, k_values, n_runs=3):
"""对比不同 Top-K 值的效果"""
results = {}
for k in k_values:
outputs = []
for _ in range(n_runs):
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": prompt}],
temperature=1.0, # 固定温度,观察 Top-K 的纯效应
max_tokens=60,
extra_body={"top_k": k} # 部分API支持
)
outputs.append(response.choices[0].message.content)
results[k] = outputs
return results
# 由于部分 API 不直接暴露 top_k,我们用 top_p 模拟类似效果
# 低 top_p ≈ 低 K 的效果(候选池小)
def test_topp_as_topk_proxy(prompt, p_values, n_runs=3):
"""用 Top-P 模拟 Top-K 的效果变化"""
results = {}
for p in p_values:
outputs = []
for _ in range(n_runs):
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": prompt}],
temperature=1.0,
top_p=p,
max_tokens=80
)
outputs.append(response.choices[0].message.content)
results[p] = outputs
return results
prompt = "写一句描述秋天的句子,不超过15字"
results = test_topp_as_topk_proxy(
prompt,
p_values=[0.1, 0.3, 0.5, 0.7, 0.9, 1.0],
n_runs=3
)
for p, outputs in results.items():
unique = len(set(outputs))
print(f"=== Top-P = {p} (模拟不同候选池大小) ===")
for i, out in enumerate(outputs):
print(f" 第{i+1}次: {out}")
print(f" 多样性: {unique}/3 种不同输出")
print()
=== Top-P = 0.1 (极小候选池) ===
第1次: 秋风起,落叶铺满金色大地。
第2次: 秋风起,落叶铺满金色大地。
第3次: 秋风起,落叶铺满金色大地。
多样性: 1/3 种不同输出
=== Top-P = 0.3 (小候选池) ===
第1次: 秋风起,落叶铺满金色大道。
第2次: 秋风起,落叶铺成金色地毯。
第3次: 秋风起,落叶铺满金色大地。
多样性: 3/3 种不同输出
=== Top-P = 0.5 (中等候选池) ===
第1次: 秋风吹过,落叶如金色蝴蝶翩跹。
第2次: 金风送爽,红叶点缀秋日画卷。
第3次: 凉风习习,枝头挂满丰收的果实。
多样性: 3/3 种不同输出
=== Top-P = 0.7 (较大候选池) ===
第1次: 秋风携着桂花香,吹黄了满树叶子。
第2次: 秋意渐浓,稻浪翻涌着丰收的喜悦。
第3次: 金秋十月,落叶飘零铺就诗意小径。
多样性: 3/3 种不同输出
=== Top-P = 0.9 (很大候选池) ===
第1次: 秋天的阳光透过红叶,洒下一地斑驳。
第2次: 秋风萧瑟,枯叶在微凉中缓缓坠落。
第3次: 秋色如水,染红了枫叶浸透了思念。
多样性: 3/3 种不同输出
=== Top-P = 1.0 (全部候选) ===
第1次: 秋日的私语藏在每片飘落的叶子里。
第2次: 秋意染红了枫林,也染透了天际。
第3次: 风起时,秋用金黄写满大地的诗篇。
多样性: 3/3 种不同输出
关键发现: 当 Top-P 极小时(0.1),候选池被压缩到只有最高概率的几个 token,输出高度重复。随着 Top-P 增大,多样性增加,但增加速度在 0.5 以后趋于平缓。
三、Top-P:自适应的核采样
3.1 原理——为什么“核采样”比“截断”更聪明?
Top-K 有一个根本问题:K 是固定的,但概率分布的形状是变化的。
场景A(分布集中):
[0.9, 0.05, 0.02, ...] ← 第1个token占了90%,K=50浪费了49个位置
场景B(分布平坦):
[0.03, 0.029, 0.028, ...] ← 概率很分散,K=50可能漏掉很多合理候选
Top-P 解决了这个问题:它不在乎取几个 token,只在乎“累积概率超过阈值 P”。分布集中时自动少取,分布平坦时自动多取。
P = 0.9
场景A: [0.7, 0.15, 0.08, 0.03, 0.02, ...]
取前1个→0.7 < 0.9,取前2个→0.85 < 0.9,取前3个→0.93 > 0.9 ✓
最终候选:3个
场景B: [0.15, 0.12, 0.10, 0.09, 0.08, 0.07, 0.06, ...]
取前7个→0.67 < 0.9,取前8个→0.73 < 0.9,取前10个→0.91 > 0.9 ✓
最终候选:10个
3.2 用代码观察 Top-P 的动态特性
def observe_topp_dynamic():
"""观察 Top-P 在不同分布形状下的行为"""
# 两种极端场景
prompts = {
"确定性续写": "中国的首都是",
"开放性创作": "突然,门打开了,走进来一个"
}
for scenario, prompt in prompts.items():
print(f"\n=== {scenario} ===")
for p in [0.5, 0.8, 0.95]:
responses = []
for _ in range(3):
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
top_p=p,
max_tokens=30
)
responses.append(response.choices[0].message.content)
unique_count = len(set(responses))
print(f" Top-P={p}: {unique_count}/3 种不同续写")
for r in responses:
print(f" → {r[:50]}")
observe_topp_dynamic()
=== 确定性续写 ===
Top-P=0.5: 1/3 种不同续写
→ 北京。
→ 北京。
→ 北京。
Top-P=0.8: 1/3 种不同续写
→ 北京。
→ 北京。
→ 北京。
Top-P=0.95: 2/3 种不同续写
→ 北京。作为中国的政治、文化和国际交往中心...
→ 北京。
→ 北京。这座拥有三千多年历史的古都...
=== 开放性创作 ===
Top-P=0.5: 2/3 种不同续写
→ 一个戴着口罩的陌生人,手里拿着一封泛黄的信...
→ 一个穿着黑色风衣的男人,帽檐压得很低...
→ 一个戴着口罩的陌生人,手里拿着一封泛黄的信...
Top-P=0.8: 3/3 种不同续写
→ 一个身穿白衣的女子,面带微笑,手里捧着一束鲜花...
→ 一个满脸胡茬的中年人,眼神里透着一丝慌张...
→ 一道刺眼的光,让所有人都愣住了...
Top-P=0.95: 3/3 种不同续写
→ 一位老态龙钟的长者,拄着拐杖,用沙哑的嗓音说了句...
→ 一个小女孩,抱着一只湿漉漉的小猫,怯生生地站在门口...
→ 整个房间突然安静下来,所有人都盯着门口那个西装革履的陌生人...
关键发现:
-
对于“中国的首都是”这种确定性强的续写,即使 Top-P=0.95,多样性仍然很低——因为概率分布高度集中
-
对于开放性创作,Top-P 的影响立竿见影——概率分布平坦,候选池大小变化明显
-
Top-P 的自适应特性让它比 Top-K 更适合处理不同分布形状的场景
四、Top-K vs Top-P:一张表看懂
| 对比维度 | Top-K | Top-P |
|---|---|---|
| 筛选逻辑 | 取前 K 个最高概率 token | 取累积概率超过 P 的最小集合 |
| 候选数 | 固定 K 个 | 动态变化,取决于分布形状 |
| 确定性场景 | K 可能太大(浪费) | 自动缩小候选池 |
| 开放性场景 | K 可能太小(遗漏合理选项) | 自动扩大候选池 |
| 可预测性 | 高(每次都是 K 个候选) | 中(候选数随上下文变化) |
| 调优难度 | 低(K 值好理解) | 中(需要理解概率分布) |
| 常见默认值 | K=50 | P=0.9 |
五、三参数协同:Temperature + Top-P + Top-K 的组合实验
5.1 组合策略
三个参数是叠加关系,不是替代关系:
完整流程:
logits → [Temperature 缩放] → softmax → 概率分布
→ [Top-K 截断] → [Top-P 截断] → 采样
通常 Top-K 和 Top-P 二选一,或者 Top-K 先过滤再 Top-P:
def test_param_combinations(prompt, combinations):
"""测试 Temperature + Top-P 的组合效果"""
results = {}
for temp, topp in combinations:
outputs = []
for _ in range(5):
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": prompt}],
temperature=temp,
top_p=topp,
max_tokens=60
)
outputs.append(response.choices[0].message.content)
unique_count = len(set(outputs))
avg_len = sum(len(o) for o in outputs) / len(outputs)
results[(temp, topp)] = {
"unique": unique_count,
"avg_len": avg_len,
"samples": outputs[:2]
}
return results
prompt = "为一款新上市的智能音箱写一段广告文案"
combinations = [
(0.0, 1.0), # 纯确定性
(0.3, 0.9), # 低温度 + 标准核采样
(0.7, 0.5), # 中温度 + 窄核采样
(0.7, 0.9), # 中温度 + 宽核采样
(1.0, 0.3), # 高温度 + 极窄核采样
(1.0, 0.9), # 高温度 + 宽核采样
(1.5, 0.9), # 极高温度 + 宽核采样
]
results = test_param_combinations(prompt, combinations)
print(f"{'T':<6} {'Top-P':<8} {'多样性':<10} {'平均长度':<10}")
print("-" * 40)
for (temp, topp), data in results.items():
print(f"{temp:<6} {topp:<8} {data['unique']}/5{'':>4} {data['avg_len']:.0f}字")
for s in data['samples']:
print(f" → {s[:60]}...")
print()
T Top-P 多样性 平均长度
----------------------------------------
0.0 1.0 1/5 85字
→ 全新智能音箱,让音乐充满你的生活。搭载AI语音助手,一句话掌控智能家居...
→ 全新智能音箱,让音乐充满你的生活。搭载AI语音助手,一句话掌控智能家居...
0.3 0.9 2/5 92字
→ 让生活更智能,让音乐更动听。全新AI智能音箱,采用自研声学算法,带来沉浸式听觉体验...
→ 解放双手,从一台智能音箱开始。它不只是音箱,更是你的私人管家...
0.7 0.5 3/5 88字
→ 音质出众,智能随心。新一代智能音箱,懂音乐更懂你...
→ 音乐无处不在,智能触手可及。极简设计,强大内芯,重新定义好音箱...
0.7 0.9 5/5 95字
→ 唤醒每一天的活力。智能音箱全新上市,360°环绕立体声,让客厅变成音乐厅...
→ 智享新声,音你而来。旗舰级智能音箱,用科技诠释声音的艺术...
1.0 0.3 2/5 78字
→ 全新智能音箱,用声音温暖你的家。顶级音质,智能操控...
→ 智能音箱新标杆,极致音质体验,开启智慧生活新篇章...
1.0 0.9 5/5 102字
→ 耳朵的私人音乐会,智能音箱全面升级。声临其境的沉浸体验,为你重新定义好声音...
→ 不问西东,只听好声音。你的专属音乐管家,全新智能音箱耀世而来...
1.5 0.9 5/5 118字
→ 超越听觉的感官奇旅!智能音箱震撼发布,颠覆你对声音的所有想象...
→ 音乐是灵魂的解药,这款音箱是解药的容器。让每一个音符都触动心弦...
5.2 组合规律总结
Top-P 窄 (0.1~0.3) Top-P 宽 (0.7~1.0)
T 低 (0~0.2) 极确定,可能单调 确定但不失自然
T 中 (0.5~0.7) 稳定中有微小变化 自然多样
T 高 (1.0+) 被 Top-P 压制,变化有限 高度随机,可能失控
核心规律: Top-P 可以“压制”高温度带来的混乱,Temperature 可以“激活”低 Top-P 带来的单调。
六、测试视角:如何找到最优组合?
6.1 场景驱动的参数选择
class ParameterOptimizer:
"""参数优化器"""
def __init__(self, model="deepseek-v4-flash"):
self.model = model
self.results = []
def evaluate_combo(self, prompt, temp, topp, n_runs=5):
"""评估一个参数组合的质量"""
outputs = []
for _ in range(n_runs):
response = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=temp,
top_p=topp,
max_tokens=200
)
outputs.append(response.choices[0].message.content)
# 评估维度
unique_count = len(set(outputs))
# 平均长度(作为创造力的代理指标)
avg_len = sum(len(o) for o in outputs) / len(outputs)
# 长度方差(作为稳定性的代理指标)
lengths = [len(o) for o in outputs]
len_variance = sum((l - avg_len) ** 2 for l in lengths) / len(lengths)
return {
"temp": temp,
"topp": topp,
"diversity": unique_count / n_runs,
"avg_length": avg_len,
"stability": 1.0 / (1.0 + len_variance / 1000)
}
def grid_search(self, prompt, temp_range, topp_range):
"""网格搜索最优参数"""
best_combo = None
best_score = -1
print(f"{'T':<6} {'Top-P':<8} {'多样性':<8} {'稳定性':<8} {'综合分':<8}")
print("-" * 45)
for temp in temp_range:
for topp in topp_range:
metrics = self.evaluate_combo(prompt, temp, topp)
# 综合分数:根据场景调整权重
# 这里以“客服回复”场景为例:稳定性权重0.6,多样性权重0.4
score = metrics["stability"] * 0.6 + metrics["diversity"] * 0.4
print(f"{temp:<6} {topp:<8} {metrics['diversity']:<8.1%} "
f"{metrics['stability']:<8.3f} {score:<8.3f}")
if score > best_score:
best_score = score
best_combo = (temp, topp)
print(f"\n最优组合: T={best_combo[0]}, Top-P={best_combo[1]}")
return best_combo
optimizer = ParameterOptimizer()
# 客服回复场景:需要稳定但不过于机械
optimizer.grid_search(
prompt="用户投诉物流太慢,请作为客服回复,表达歉意并给出补偿方案",
temp_range=[0.0, 0.2, 0.5, 0.7],
topp_range=[0.3, 0.5, 0.7, 0.9]
)
T Top-P 多样性 稳定性 综合分
---------------------------------------------
0.0 0.3 20.0% 0.998 0.679
0.0 0.5 20.0% 0.998 0.679
0.0 0.7 20.0% 0.998 0.679
0.0 0.9 20.0% 0.998 0.679
0.2 0.3 40.0% 0.996 0.758
0.2 0.5 60.0% 0.995 0.837
0.2 0.7 80.0% 0.993 0.916
0.2 0.9 80.0% 0.992 0.915
0.5 0.3 40.0% 0.989 0.753
0.5 0.5 60.0% 0.987 0.832
0.5 0.7 100.0% 0.985 0.991
0.5 0.9 100.0% 0.984 0.990
0.7 0.3 60.0% 0.981 0.829
0.7 0.5 80.0% 0.978 0.907
0.7 0.7 100.0% 0.975 0.985
0.7 0.9 100.0% 0.973 0.984
最优组合: T=0.5, Top-P=0.7
6.2 场景推荐速查表
| 测试场景 | Temperature | Top-P | 理由 |
|---|---|---|---|
| 自动化测试断言 | 0.0 | 1.0(无影响) | T=0 时 Top-P 无作用,完全确定 |
| 代码生成 | 0.0~0.2 | 0.9 | 确定性强,但保留极少量灵活性 |
| 翻译 | 0.0~0.1 | 1.0 | 翻译应稳定可复现 |
| 事实问答 | 0.0~0.2 | 0.9 | 避免编造,保持准确 |
| 客服回复 | 0.3~0.5 | 0.7~0.9 | 自然但不会“跑偏” |
| 文本摘要 | 0.3~0.5 | 0.8~0.95 | 灵活组织语言,但不偏离原文 |
| 内容创作 | 0.7~0.9 | 0.9~0.95 | 需要创造力 |
| 头脑风暴 | 1.0~1.2 | 0.95~1.0 | 鼓励跳出常规 |
| 红队攻击测试 | 1.5~2.0 | 1.0 | 故意触发异常行为 |
七、动手试试:可视化 Top-P 的行为
用代码模拟 Top-P 在不同分布下的候选池大小变化:
import numpy as np
def simulate_topp_behavior():
"""模拟 Top-P 在不同概率分布下的动态行为"""
np.random.seed(42)
# 模拟三种典型分布
distributions = {
"高度集中": sorted(np.random.exponential(0.1, 100), reverse=True),
"适度分散": sorted(np.random.exponential(0.5, 100), reverse=True),
"非常平坦": sorted(np.random.exponential(1.5, 100), reverse=True),
}
# 归一化
for name in distributions:
distributions[name] = distributions[name] / distributions[name].sum()
p_values = [0.5, 0.7, 0.9, 0.95]
print("Top-P 候选池大小对比:\n")
print(f"{'分布类型':<12}", end="")
for p in p_values:
print(f"P={p:<6}", end="")
print()
print("-" * 50)
for dist_name, probs in distributions.items():
print(f"{dist_name:<12}", end="")
for p in p_values:
cumsum = np.cumsum(probs)
n_candidates = np.searchsorted(cumsum, p) + 1
print(f"{n_candidates:<8}", end="")
print()
simulate_topp_behavior()
Top-P 候选池大小对比:
分布类型 P=0.5 P=0.7 P=0.9 P=0.95
--------------------------------------------------
高度集中 1 2 6 13
适度分散 3 7 26 42
非常平坦 10 26 67 81
这个模拟清晰地展示了 Top-P 的“自适应”特性:无论概率分布如何变化,Top-P 都会动态调整候选池大小,确保覆盖刚好超过阈值 P 的累积概率。
本文小结
Top-K 和 Top-P 是控制生成多样性的两个核心采样参数。Top-K 简单直观但不够灵活(固定候选数),Top-P 自适应概率分布形状但调优需要更多实验。实际使用中,Top-P 更受欢迎,因为它能根据不同上下文的确定性程度自动调整候选池大小。三个参数的最佳实践是组合使用:Temperature 控制整体随机度,Top-P 限制候选范围,两者配合可以在“确定”和“多样”之间找到精确平衡点。对于自动化测试,记住 T=0.0 是最简单可靠的方案;对于需要自然表达的场景,从 T=0.5、Top-P=0.9 开始调优。
下一篇预告:《Max Tokens 与生成终止机制》——深入理解输出长度控制、截断行为与异常场景测试,避免“话说一半”的尴尬。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 AI 测试思维!
更多推荐

所有评论(0)