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 测试思维!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐