Qwen2.5-7B 真实推理实测:LLM 怎样从 logits 选出下一个 token

3 阅读18分钟

我把一个问题交给 Qwen2.5-7B 基础模型,记下了它在输入末位置给下一个 token 的打分:「温度」15.75、「temperature」15.3125、「实践中」14.375。在完整的 152064 个输出项上做 softmax,T=1 时这三项只有 24.26%、15.67%、6.13%;T=0.5 时「温度」升到 61.79%。按 seed=20261002 实际采样,第一步选中的却是第二名「temperature」;单独执行的贪心分支则选了「温度」。

同一组分数,最后走出了「temperature 用于控制」和「温度参数(temperature」两种开头。这篇就沿着这次真实推理记录下的编号、向量、候选分数和生成轨迹,从 25 个输入 token 一路走到被选中的下一个 token,把 token、embedding、hidden state、LM Head、logits、softmax 和采样放回同一条链路里。

这次输入的问题是:

问题:实践中 LLM 的 temperature 该怎么设置?设置为 0 就能防止幻觉吗?
回答:

我们暂时不评价模型的回答,先观察它怎样开始回答。

实测条件:2026 年 10 月 2 日,Qwen2.5-7B 基础模型,RTX 3090,BF16(bfloat16,16 位浮点格式),PyTorch 2.8.0、Transformers 4.57.1,使用 eager attention;未量化、未套聊天模板、未额外添加特殊 token。模型处于推理模式,权重不更新。下面的编号、向量、分数和生成轨迹来自这次运行;概率表由保存的真实分数计算。

它是一种常见的 decoder-only 自回归 LLM:根据已有内容预测下一 token,把选出的 token 接回已有内容,再预测下一个。「自回归」说的就是这个循环。

1. Tokenizer 把文字变成编号

模型计算时用的不是直接可读的汉字,而是 token ID。Tokenizer(分词/编码器)按它的词表和编码规则,把文字转换为一串整数编号。

Token 是模型处理和生成的单位,可以对应一个词、词片段、标点或字节片段,不能假定「一个汉字就是一个 token」。上面的输入实际得到 25 个 token:

[86119, 5122, 107933, 444, 10994, 43589, 9315, 220, 110801,
 43918, 11319, 43918, 17714, 220, 15, 58230, 109, 26232,
 104431, 100320, 99353, 101037, 94432, 102104, 5122]

挑几个位置看看,位置从 0 开始计数:

输入位置Token ID可读片段
086119问题
3444 L,带前导空格
410994LM
69315 temperature,带前导空格
24,末位置5122:

这里的 LLM 被拆成了两个 token, temperature 则是一个。词表(vocabulary) 保存 token 与 ID 的对应关系;ID 只是编号,9315 并不比 5122「更有意义」。

这次输入里的「 就」还被拆成两个字节片段,对应 ID 58230 和 109。单独解码时可能出现 �,拼起来才能还原完整文字。因此,逐个 token 的显示结果与整个序列的解码结果,需要分开看。

1.1 先分清,25 个输入 token 怎样接出新内容

这 25 个 token 是已经给定的输入上下文,不是回答的长度预算。首次运行时,把这段已知输入一起送入模型,计算各位置的表示;再用末位置的结果,预测并选出第一个新 token。

随后把新 token 接回已有内容,历史长度成为 26。根据这段历史再预测第二个新 token,长度成为 27,继续循环。回答可以超过 25 个 token,直到达到停止条件或可用的长度预算。

先带着这条主线读下去:处理已有输入 → 选出一个新 token → 把它接回历史 → 继续预测。接下来逐步看这些位置怎样参与计算。

2. Embedding 把编号变成向量

编号可以查表,却还不能直接承担模型的连续数值计算。Embedding(嵌入) 为每个 token ID 取出一条训练得到的数字向量。

这次每条向量有 3584 个坐标。下面只展示前四个,四舍五入到六位小数;省略号代表其余坐标,并不表示这是一条四维向量。

9315(带空格的 temperature)
→ [ 0.026245, -0.015442, 0.020874, 0.026245, …]

5122(末位置的冒号)
→ [-0.011597, -0.003036, 0.021240, 0.009399, …]

这些数字是模型学到的表示,我们不需要给每个坐标人为贴上「颜色」「天气」这样的语义标签。本次使用已经训练好的权重;推理过程中查表和计算,不重新学习这些向量。

25 个位置各有一条向量,整体形状就是 [1, 25, 3584]:1 个样本、25 个位置、每个位置 3584 个坐标。

模型还需要位置信息。这次 Qwen 使用 RoPE(旋转位置编码),把位置引入 attention 的匹配过程。

小框:RoPE 怎样让模型感知位置?

Attention 会用 Q(query,查询向量)与 K(key,键向量)计算上下文位置之间的匹配。RoPE 按 token 所在的位置,旋转 Q、K 中成对的坐标。可以把每对坐标想成平面上的一支小箭头:位置编号决定旋转角度,不同坐标对使用不同旋转频率。

这样,Q 与 K 做点积时,匹配分数就能体现相对位置信息,例如两个 token 相隔多少个位置。向量的维数保持不变。在本次 Qwen 中,RoPE 旋转 Q、K,不旋转 V,也不直接给 embedding 加位置向量。RoPE 原论文、Qwen2 的实现

3. Transformer 把上下文融入表示

现在有了每个 token 的初始向量,但我们还需要结合上下文:同一个词放在不同问题里,后面适合接的内容可能完全不同。

这些向量会经过多层 Transformer block(模块)。先认识四种操作的职责:

  • Attention(注意力):从允许访问的上下文位置读取并加权组合信息。因果掩码让当前位置不能读取后面的位置。

  • FFN/MLP(前馈网络):对每个位置的表示进一步做非线性变换。

  • Residual connection(残差连接):把进入子层的表示与子层结果相加,在原有表示上更新信息。

  • Normalization(归一化):按架构规定调整表示的数值尺度;具体形式和位置因模型而异。

3.1 25 个位置一起计算,通过注意力结合上下文

Embedding 为各位置独立查出初始向量。进入 attention 后,各位置会读取可见位置的信息,因此这 25 个 token 会在同一段上下文里发生关联。

本次使用因果注意力,位置从 0 开始编号:

当前位置可以读取的位置
00,自身
60–6,自身及前面的 6 个位置
24,末位置0–24,全部 25 个输入位置

因为输入内容已经给定,同一层可以并行计算这些位置;因果掩码仍限制每个位置可以读取谁。接着,这一层的全部结果送入下一层,按层继续计算。这里的上下文融合包含 attention 的加权汇总,不是把 25 条 embedding 简单相加成一条。

这次模型有 28 层 Transformer。各位置在每层使用该层共享的权重计算,但得到的结果随各自内容与可见上下文变化。输入末位置虽然只是一个冒号,它的最终表示仍能受到前面整个问题影响。Qwen2 的因果注意力与层间计算

这些中间或最终的数字表示叫 hidden state(隐藏状态)。名字里的「隐藏」不代表里面存着一句人类可读的想法,它仍是一组数。常规下一 token 生成主要读取末位置的最终表示,此前位置的信息已经通过网络计算影响了它。

把前面出现的 Q、K 与 **V(value,值向量)**放回 attention:它们都由表示投影得到。Q 与 K 用来计算注意力权重,再按权重组合 V。Attention head 指多组并行的注意力计算,后面还会遇到另一个名字里带 head 的模块。

4. LM Head 把隐藏状态变成候选分数

第一次接触 LM Head,可以先看 4.1;已经了解它的职责,可以直接看 4.2 的实际计算。

4.1 先认识 LM Head,模型的输出打分层

经过 Transformer,我们得到的是隐藏状态,也就是融合了上下文的数字向量。但用户最终看到的是文字,还需要把这条向量与「下一步可以输出哪些 token」接起来。

**LM 是 language model 的缩写;LM Head 是语言模型的输出头。**可以先把它理解为一个输出打分层:接收隐藏状态,为各个候选 token 算出一个分数。

比如,面对当前问题,它会给「温度」「temperature」「实践中」等候选分别打分。这里尚未选定任何一个 token;后面的采样或贪心解码,才会根据这些分数决定这一步输出谁。

对照前面的 embedding,更容易看清两端的职责:

模块接收什么产出什么
Embedding一个 token ID一条输入向量
LM Head一条最终隐藏状态向量各候选编号的分数

这些分数叫 logits,是未归一化的原始分数,还不是概率。「LM Head」与「attention head」也分别位于输出投影和注意力计算中;同样带 head,职责并不相同。

4.2 看真实计算,3584 个坐标怎样变成 152064 个分数

先看输入。实际送入 LM Head 的末位置向量已经过最终归一化,完整长度仍是 3584。用 h 表示这条完整向量,下面只展示它的前四个坐标:

[-1.5546875, -1.6171875, 0.57421875, -1.421875, …]

这条向量属于输入末位置「回答:」的冒号。融合前面的上下文后,它用于预测冒号后面该接哪个 token。

再看权重。这次 Qwen 的 LM Head 是一个无偏置的线性层,使用训练得到的权重矩阵 W。按 PyTorch 的存储方式,它有 152064 行、3584 列:行索引从 0 开始,每行对应一个输出候选编号,列对应隐藏状态的坐标。Qwen2 输出头定义、PyTorch Linear 的权重形状

对候选编号 i,取出 W 的第 i 行,把它与 h 的对应坐标相乘,再把全部乘积相加:

候选 i 的分数
= h[0] × W[i,0] + h[1] × W[i,1] + … + h[3583] × W[i,3583]

这就是这里所说的线性投影,也可以理解为一次点积:**3584 个坐标参与计算,得到这个候选的一个分数。**例如 ID 104273 对应「温度」,它对应的那一行权重与完整 h 做乘加,本次得到的分数为 15.75。

对全部 152064 行执行这套计算,就得到 152064 个分数。实际实现通过矩阵运算一起计算各项;推理过程中,权重 W 固定,随当前上下文变化的是输入 h。

一条隐藏状态 h:3584 个坐标
        ↓ 与 LM Head 的各行权重做乘加
一条 logits 向量:152064 个分数
        ↓ 按分数所在的编号查 tokenizer
可以查看对应的候选 token 片段

本次 tokenizer 的长度为 151665,模型输出维度则为 152064,两项口径不同。这里先完整计算模型的输出,再解释 tokenizer 能映射的编号;下一节的概率计算也按完整模型输出维度进行。

这次末位置分数最高的三项是:

候选 tokenToken ID原始分数
温度10427315.75
temperature,无前导空格3455815.3125
实践中10793314.375

15.75 的含义就是「温度」这个候选的原始分数。Logits 可以为正,也可以为负,不要求加起来等于 1;15.75 不是概率,分数为 0 也不表示「不可能」。下一节才会把整组分数变成概率。

一次前向中,25 个输入位置都经过这个输出头,所以完整输出是 [1, 25, 152064]。这 25 行分别对应各输入位置之后的下一 token 分数;当前要接着整段输入作答,就读取第 24 行,也就是末位置那一行,从中选出一个新 token。一次计算出 25 行分数,并不等于已经生成了 25 个回答 token。

最后核验这条连接:取实际记录的完整 LM Head 输入,再应用同一个输出头,结果与模型返回的末位置 logits 最大绝对差异为 0。前面的四个坐标只是展示片段;核验使用全部 3584 个坐标,不能仅凭那四个数算出 15.75。

到这里,模型已经给候选打完分。接下来,我们再看 temperature、softmax 和采样怎样利用这组分数。

5. Temperature 与 softmax 把分数变成概率

Softmax 把一组分数转换成非负、总和为 1 的概率。正温采样中,temperature(温度参数)先缩放分数,再由 softmax 计算概率。

下文用 T 表示 temperature(温度参数),因此 T=0.5 就是设置 temperature=0.5。表格行中的「温度」「temperature」则是模型可能生成的候选文字。

比较同一组 logits 时,T=1 不做这项缩放,低于 1 会让分布更集中,高于 1 则让它更平。固定刚才的完整 logits,不截断候选,得到:

候选T=0.5T=1T=2
「温度」61.79%24.26%0.89%
「temperature」25.76%15.67%0.72%
「实践中」3.95%6.13%0.45%
其余候选合计,约8.50%53.94%97.94%

每列都在完整的 152064 个输出项上计算。它不是只对前三名归一化,也不是整段答案的正确率。排序保持不变,「温度」的概率优势却明显变化了。

计算口径:表格把保存的同一份 BF16 logits 转为 FP64(双精度浮点数)后计算参考概率,显示两位百分比;模型前向仍是 BF16。原 FP32(单精度)概率在 T=1、T=2 的求和检查中超过了预设的 2×10⁻⁶ 误差阈值。下面的实际采样沿用原 FP32 路径。

现在可以定位 temperature 了:它处理的是 LM Head 已经产生的候选分数。网络内部的 attention 也用 softmax,但它在上下文位置之间分配权重;这里的 softmax 则在输出 token 之间分配概率。

6. 过滤候选,再决定这次选谁

Top_k 保留分数排名靠前的固定数量候选;top_p 按累计概率阈值保留高概率候选集合。过滤之后,还要对保留项重新归一化。

这次 T=1、top_k=2 只留下「温度」和「temperature」,最终概率约为 60.77% 和 39.23%,已经不同于截断前的 24.26% 和 15.67%。T=1、top_p=0.9 则保留 296 个候选编号;它并不是保留 90% 的词表。

Sampling(采样) 按最终概率随机选出一个 token。概率最高的项不必每次都被选中。Seed(随机种子) 用于控制采样的随机数过程,不负责判断候选内容是否有事实依据。

本次实际采样设置为 T=1、top_k=0(关闭)、top_p=1,seed=20261002,使用 FP32 概率在 CPU 上抽取。第一步选中的是第二名「temperature」,ID 34558。

另一条路径是 greedy decoding(贪心解码):直接选择处理后分数最高的项,也叫取 argmax。这次单独执行的贪心分支,第一步选中最高分「温度」,ID 104273。

支持 temperature=0 的服务通常用特殊分支进入贪心;它不执行除以零,也不需要先算 softmax 才能找出最大分数。具体参数和处理顺序要看引擎实现。

7. 追加 token,回到下一轮

第一步选出的 ID 接回原有的 25 个编号,成为下一步的条件。下面把采样分支的实际轨迹与历史长度放在一起看;本次没有使用缓存,每步将完整前缀送入模型:

预测第几个新 token本步前缀长度选出的 token 与 ID累计新增文字
125temperature,34558temperature
226空格,220temperature
327用于,100751temperature 用于
428控制,100359temperature 用于控制

第一步只需已有的 25 个输入;第二步还需要刚生成的「temperature」;第三步又加入刚生成的空格。这些新 token 也会经过 embedding、Transformer 与 LM Head,参与后续预测。生成阶段按先后继续,是因为后一个新 token 的条件包含前一个实际选出的 token。

概念篇-fig-02-逐步生成.png

相同起点的贪心分支则依次选择 [104273, 32665, 9909, 34558],得到:

贪心:温度参数(temperature
采样:temperature 用于控制

只有第一步共享同一组 logits。选出的 token 不同,后续前缀就不同,模型会重新计算相应分数。

Tokenizer.decode 将编号序列还原为文字,它不负责选择编号。流式输出里的一块文字也可能包含多个 token,不能直接当作模型的一次选择。

生成会在遇到适用的 EOS(end-of-sequence,序列结束标记) 或其他停止条件时结束。max_new_tokens 限制新增 token 的数量,模型与服务还会限制可用的上下文长度。本次设置 max_new_tokens=4,两个分支都新增 4 个 token,最终历史共 29 个;它们因这个人为上限停止,没有生成 EOS。上面只是回答的开头,不能据此评出回答质量或幻觉率。

**用 KV cache 时,还要重跑全部历史吗?**首次处理已知输入称为 prefill,接着进入逐 token 生成的 decode 阶段。使用 KV cache 时,保存此前各层 attention 的 key/value,后续通常只把刚生成的一个 token 作为新输入,结合缓存中的历史继续计算。因此送入模型的新编号可以只有一个,逻辑上下文仍包含原输入与已生成内容。Hugging Face 缓存说明

本次正文轨迹未使用缓存。另做的 BF16 缓存对照中,两步选择相同,但 logits 未通过原定数值容差,不能写成缓存前后完全一致。这里的 decode 阶段指继续生成,与 tokenizer.decode 的编号转文字是两个用法。

8. 把概念放回同一条链路

概念篇-fig-01-推理链路.png

这条链路对应的实际形状是:

阶段本次形状或结果
输入编号[1, 25]
Embedding[1, 25, 3584]
28 层 Transformer 与最终归一化之后[1, 25, 3584]
LM Head 输出[1, 25, 152064]
取末位置,进行下一 token 选择152064 个分数 → 1 个 ID

形状相同不代表数字相同:冒号的初始 embedding,与融合上下文后的最终隐藏状态,已经是不同的向量。

除了前面的输出头重算,本次还核对了首层 attention、MLP 的两次残差相加,重算与记录的最大绝对差异均为 0;捕获的首层注意力权重中,未来位置的最大权重为 0。这些检查分别对应具体环节,不能扩展成整个推理系统都已获得一致性保证。

9. 带着这条链路去看 temperature

最值得记住的是三个区别:**隐藏状态是表示,logits 是分数,softmax 后才是概率。**LM Head 连接表示与候选分数,解码过程决定这一步最终选择谁。

temperature 在常见生成接口中调整输出端的分布。降低它,会让本来较高分的候选更容易被选中;候选所表达的内容有没有事实依据,还需要另外判断。

下一篇《temperature 设为 0,就不会产生幻觉了吗?从解码机制到生产选值》会从这里继续:零温保证什么,哪些错误它改变不了,生产系统又该怎样通过评测选择参数。


我是程工造Agent,做 Agent 系统与 RAG 的工程师,记录有数据、有验证过程的工程实践。本文同步发布于知乎和 CSDN,系列文章收录在专栏「Agent 工程手记」,公众号「程工的车间与代码」同步更新。

下一篇继续讨论:temperature 设为 0,为什么仍然可能产生幻觉,以及生产中怎样选值。

本文部分内容包含 AI 辅助创作