AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

采样参数:输出为什么会波动

我的学习 →

正在读取学习记录…

模型生成回答时,会一步步在候选 token 中选择下一项;采样参数影响这一步怎样选。你可以先把 token 当成模型处理的文字片段。比如三个候选的概率是 0.6、0.3、0.1,不同选择规则会改变留下的候选和抽样结果。

这里先用这组三个固定数字手算一次,再运行 Python 模拟。之后回到工单场景:同一句 Prompt 为什么可能产生不同措辞,以及何时应该减少波动。上一篇少量示例改变模型看到的上下文;采样改变给定候选后的选择方式。结果更稳定也不等于事实更正确。

1. 逐个 token 理解生成选择

先把 token 想成模型处理的“小片段”:它可能是一个字、词的一部分,或标点,不一定等于人眼看到的一个词。假设某一步只有“好的”“可以”“抱歉”三个教学候选,概率分别是 0.6、0.3、0.1。选出一个后,模型会结合前文重新计算下一步,不是一次抽完一整篇。下面把候选写成完整词,只为方便手算。

把它想成有10张票的抽签袋:“好的”有6张,“可以”有3张,“抱歉”有1张。温度 T 调整三者的相对票数;top_p 按票数从多到少留下累计达到阈值的一组候选。两步完成后才抽签。先算清这两步,再看种子和平台开关。

候选分数先经温度调整,再经 top-p 截断和抽样,最终结果仍需任务校验
图 1:温度与 top-p 决定怎样选下一个 token;任务校验另行判断生成内容能否使用。

2. 手算一次:温度和 top-p 是连续两道处理

对基准概率 p_i,教学程序使用 p_i^(1/T) 后重新归一化。温度低于 1 时大概率项优势更明显,高于 1 时分布更平;T=1 不变。top_p=0.8 时,按温度调整后的概率从大到小保留最少候选,直到累计概率达到或超过0.8,再对保留项重新归一化。T=1 时仍是原来的0.6、0.3、0.1,前两项累计0.9,因此第三项被截断。0.8控制的是一组选项的累计概率。

示意候选概率条:好的0.6、可以0.3、抱歉0.1;top-p为0.8时保留前两项,累计0.9越过阈值
图2:用 0.6、0.3、0.1 三个固定概率看 top-p。保留越过阈值的候选,累计值会到 0.9。

低温不等于安全。若最高概率答案本来是错的,低温只会更稳定地给出错误。高温也不等于创造力保证;它仅改变候选抽样比例,还可能增加不合契约输出。真实 API 可能有 top_k、惩罚项、长度限制和不同的实现顺序;不要把这里三候选公式外推成所有平台的内部算法。

截断后,概率要重新加到 1

保留前两项后,把 0.6 和 0.3 分别除以总和 0.9,得到约 0.667 和 0.333。这一步叫归一化,即让参与抽样的概率总和回到1。第三项在这次选择中为0;下一步模型重新计算候选,因此仍可能在其他位置生成“抱歉”。

顺序也影响结果:先调温度会改变分布,随后 top-p 留下的集合可能跟着改变。先把这两个中间结果分开看,再看最后的随机计数。

3. Windows 跟做:固定候选的离线实验

将 sampling_lab.py、test_sampling_lab.py 放进同一个新文件夹。在资源管理器打开文件夹,地址栏输入 powershell,确认文件名后运行:

macOS:先在 Python 篇的学习文件夹中打开终端,执行 source .venv/bin/activate。再输入 cd ,把当前例子的文件夹拖入终端并回车,运行下面的命令。

保存为 sampling_lab.py:

Python
"""给三个候选词设置概率,观察温度和top_p怎样改变抽样结果。"""
import random
from collections import Counter

NAMES = ("好的", "可以", "抱歉")
# 与NAMES逐项对应:三个词的初始概率分别为60%、30%、10%。
BASE = (0.6, 0.3, 0.1)

def normalize(values):
    if not values or any(v < 0 for v in values) or sum(values) <= 0:
        raise ValueError("invalid_probabilities")
    total = sum(values)
    return [v / total for v in values]

def temperature(probs, t):
    if not isinstance(t, (int, float)) or t <= 0:
        raise ValueError("temperature_must_be_positive")
    # 先按温度调整每个概率,再缩放到总和为1。
    return normalize([p ** (1.0 / t) for p in normalize(probs)])

def nucleus(probs, top_p):
    if not isinstance(top_p, (int, float)) or not 0 < top_p <= 1:
        raise ValueError("top_p_out_of_range")
    probs = normalize(probs)
    selected = [0.0] * len(probs)
    mass = 0.0
    # 从概率最大的词开始选,累计概率达到top_p后停止。
    for index in sorted(range(len(probs)), key=lambda i: probs[i], reverse=True):
        selected[index] = probs[index]
        mass += probs[index]
        if mass >= top_p:
            break
    return normalize(selected)

def counts(probs, n=1000, seed=7):
    rng = random.Random(seed)
    return Counter(rng.choices(NAMES, weights=normalize(probs), k=n))

if __name__ == "__main__":
    for t in (0.5, 1.0, 2.0):
        adjusted = temperature(BASE, t)
        print(f"T={t}: {[round(p, 4) for p in adjusted]}")
    filtered = nucleus(BASE, 0.8)
    print(f"top_p=0.8: {[round(p, 4) for p in filtered]}")
    print(f"offline_counts={dict(counts(filtered))}")
完整代码:test_sampling_lab.py
Python
# 核对采样参数的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from sampling_lab import BASE, counts, nucleus, normalize, temperature

class SamplingTests(unittest.TestCase):
    def test_normal_and_temperature(self):
        self.assertAlmostEqual(sum(temperature(BASE, 0.5)), 1)
        self.assertGreater(temperature(BASE, 0.5)[0], BASE[0])
        self.assertLess(temperature(BASE, 2)[0], BASE[0])

    def test_top_p_includes_crossing_candidate(self):
        result = nucleus(BASE, 0.8)
        self.assertAlmostEqual(result[0], 2/3)
        self.assertAlmostEqual(result[1], 1/3)
        self.assertEqual(result[2], 0)
        self.assertEqual(nucleus(BASE, 0.6), [1, 0, 0])

    def test_seed_repeatable(self):
        self.assertEqual(counts(BASE), counts(BASE))
        self.assertEqual(sum(counts(BASE).values()), 1000)

    def test_bad_probabilities(self):
        for values in ([], [0, 0], [0.5, -0.5]):
            with self.subTest(values=values), self.assertRaisesRegex(ValueError, "invalid_probabilities"):
                normalize(values)

    def test_bad_parameters(self):
        for t in (0, -1):
            with self.subTest(t=t), self.assertRaisesRegex(ValueError, "temperature_must_be_positive"):
                temperature(BASE, t)
        for p in (0, 1.2):
            with self.subTest(p=p), self.assertRaisesRegex(ValueError, "top_p_out_of_range"):
                nucleus(BASE, p)

if __name__ == "__main__":
    unittest.main()
Bash
ls
python -B -X utf8 sampling_lab.py
python -B -X utf8 -m unittest -v test_sampling_lab.py

程序先计算新概率,再用固定种子抽样 1000 次。打印的计数只是这份概率夹具的一次伪随机模拟,不是模型测试或性能基准。源码中的 normalize 保证总和为 1;temperature 要求正数;nucleus 先排序、累计、保留越过阈值者,再重归一化。看核心片段:

Python
def temperature(probs, t):
    if t <= 0:
        raise ValueError("temperature_must_be_positive")
    return normalize([p ** (1.0 / t) for p in probs])

主程序底部已经用 for t in (0.5, 1.0, 2.0) 遍历三种温度,无需先改它。先读前三行:约为 [0.7826, 0.1957, 0.0217]、[0.6, 0.3, 0.1]、[0.4727, 0.3343, 0.193]。函数参数 t 就是公式里的温度 T。下一行已用 top_p=0.8,第三项为0。错误输入可直接运行:

Bash
python -c "from sampling_lab import temperature,BASE; print(temperature(BASE,0))"
python -c "from sampling_lab import nucleus,BASE; print(nucleus(BASE,1.2))"

分别应报 temperature_must_be_positive 和 top_p_out_of_range。正常、负概率与边界检查也在测试里;模块未找到时确认两个文件在同一目录。

先写下预测,再运行对照:t=0.5 时 0.6 那一项应占得更大;t=2 时三个候选应更接近;top_p=0.8 时第三项应被排除。若程序输出与预测不同,逐步打印“温度调整后的概率”“截断前累计值”“截断后的重新归一化值”,定位自己错把哪一步当成了最终结果。1000 次抽样的计数会围绕理论分布波动,不应恰好等于 667、333;固定随机种子只方便重放这一次软件实验。

现在只改一个条件:把 top_p 从 0.8 降到 0.5,先猜第二项是否还会被保留。无需修改上方的完整源码,在同一个 PowerShell 窗口直接运行:

Bash
python -c "from sampling_lab import nucleus,BASE; print([round(x,4) for x in nucleus(BASE,0.5)])"
python -c "from sampling_lab import nucleus,BASE; print([round(x,4) for x in nucleus(BASE,0.8)])"

预期依次得到 [1.0, 0.0, 0.0] 和 [0.6667, 0.3333, 0.0]。累计概率到 0.6 时已超过 0.5,因此第一行只保留一项;阈值 0.8 则要加入第二项。若结果不同,检查源码中的 BASE 是否仍为 (0.6, 0.3, 0.1),先核对输入概率和计算步骤,再比较抽样结果。

最后做一个反例:把最高概率的候选文字想象成错误的团队标签。此时低温会增加抽到错误标签的机会。正确做法是回到任务契约和标注数据,看看模型为什么给错答案,再用结构与业务校验阻断错误执行。采样参数调优无法替代这些工作。

要比较的量计算入口接模型后的测量
概率总和normalize 后相加应为1核对接口是否提供概率与其含义
截断集合nucleus 的非零项按接口参数测真实输出变化
固定种子计数counts 默认种子7另测服务端可复现范围
任务质量固定数字未包含工单判据同输入比较业务正确率

对照两种累计阈值

三候选概率 0.6、0.3、0.1 的累计选择:top-p 0.5 只留首项,top-p 0.8 保留前两项再归一化
图 3:先看第一行原始 0.6 / 0.3 / 0.1,再看不同阈值保留哪些项。top_p=0.8 时第二项虽只有 0.3,仍须加入它才能让累计值达到阈值。

图中最下行的 0.667 / 0.333 是把保留下来的 0.6 / 0.3 除以它们的和 0.9,重新变成总和为 1 的分布。你可以拿纸先算一遍,再用刚才的 nucleus(BASE, 0.8) 核对;这比盯着 1000 次随机抽样的计数更容易看清机制。

4. 在 Agent 产品里怎样做参数试验

把任务分成“需要稳定提取字段”和“允许多种措辞起草”两类。前者先明确输出契约、校验和人工回退;后者比较措辞多样性与人工偏好。一轮只改一个采样参数,固定 Prompt、模型和输入,才能解释结果变化。

真实平台的开关、种子与记录字段

贪心选择直接取当前步最大概率项;采样按分布抽取。Hugging Face 的 do_sample 控制是否采样,温度与 top-p 的适用取决于模式;平台对温度0的处理应查具体接口,不能代入这里要求正数的公式。随机种子方便同实现重放伪随机序列,跨模型、硬件或云服务的确定性要另测。

每组记录模型标识与日期、Prompt版本、输入、参数、最大输出长度、响应、解析/业务失败、延迟和费用。重复次数预先确定,保留全部输出。生成配置文档和生成策略给出框架参数定义;这些记录在真实请求时填写。

症状检查证据可能处理
同问不同答模型版本、prompt、采样设置、原始响应固定变量后再复测
JSON 失效原始响应与解析错误类型引入结构化输出,而非只降温
回答稳定但错误人工标注与来源事实修任务定义或检索,不靠温度
输出突然截断结束原因、长度上限核查 token 限制,不误判为采样问题

成本或时延增加可能来自更多候选、更长回复或重试,而不是温度数值本身直接“收费”。供应商参数范围会变化,上线前应核对当前 API 文档。下篇 结构化输出 会说明为什么概率操纵不能替代运行时校验。

自查与继续学习

温度改变候选的相对概率;top-p 按累计概率截取候选。把练习中的 top-p 从 0.8 改成 0.5,保留集合会从两项变为一项。输出更稳定,不意味着事实更准确,也不能替代权限检查。

自查:能手算 0.6/0.3/0.1 在 top_p=0.8 时保留哪些候选;能跑正常、错误与边界测试;能说明本机模拟与真实模型实验的区别;能列出公平对照时要固定的变量。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.1

选择题 · 先判断边界

关于“采样参数与输出波动”,哪种做法更稳妥?

对应知识点:L1-sampling

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 解释失败

写出一个失败输入并说明记录字段。

对应知识点:L1-sampling

练习 3 / 4 · v1.0.0

代码题 · 边界测试

补充正常、边界、失败三组输入和预期结果。

对应知识点:L1-sampling

练习 4 / 4 · v1.0.0

场景题 · 结果未知

证据不足时如何设计状态和人工接管?

对应知识点:L1-sampling

进一步阅读:Hugging Face 生成配置介绍参数定义;生成策略介绍贪心与采样的区别。

图片查看器
100%

文章配图 · 按 Esc 关闭