深入解析模型逐词输出的生成机制、幻觉产生的根本原因(可预测的生成过程),以及Mixture of Experts架构的内部原理。
作者:Syed Muhammad Ali Raza
这个系列写到第三篇,我们已经覆盖了注意力机制、训练流程、分词器、训练阶段以及缩放定律。这确实是从原始文本到训练好的模型的完整流程。但我们还没有涉及你真正点击发送提示词的那一刻发生了什么——模型是如何逐字决定实际输出的。我们也没有触及一个诚实且略显尴尬的问题:为什么这些模型有时会自信地编造内容,以及我们还没有看到构建模型本身的另一种真正不同的方式——混合专家(Mixture of Experts),这是目前一些最大型模型背后的架构。本文将一次性覆盖这三个主题,因为一旦你理解了生成机制,幻觉就不再神秘——它开始成为生成机制工作方式的可预测结果。
我在写这篇文章时在自己的机器上运行每个代码示例,并且向你展示真实的终端输出,而不是经过美化的版本,这样你就能看到如果你自己运行会得到什么结果。
想象在一家餐厅点餐,服务员不是问"您想要什么",而是每次都递给你一份排序列表:这是最受欢迎的菜,这是第二受欢迎的,这是第三受欢迎的,每个旁边都标注了百分比,大致说明人们点它的频率。你当然可以每次都选最上面的那个,安全、可预测,但最终每桌都点了完全相同的东西,餐厅变得平淡乏味。或者你可以按照这些百分比加权的骰子来掷——大多数时候选热门菜品,但偶尔也会落到不太常见的选项上,多次造访下来确实更有趣、更丰富,有极小的风险会落到一些更奇怪的东西上。
这确实就是 LLM 生成文本的全部原理。在每个位置上,模型不是输出一个词,而是输出其整个词表上完整的概率分布——每个可能的下一个词元(token)有多大的可能性被选中。然后有一个独立的决策过程,称为采样策略(sampling strategy),来决定实际选择哪个词元。总是选择排名第一的选项是安全、最受欢迎的菜品,每次都如此。
import numpy as np
np.random.seed(7)
# 一个虚构的小词表概率分布,
# 代表真实模型在"the cat sat on the"之后会输出的内容
vocabulary = ["mat", "roof", "moon", "table", "keyboard", "president"]
probabilities = np.array([0.45, 0.20, 0.15, 0.12, 0.06, 0.02])
print("词汇及其概率:")
for word, prob in zip(vocabulary, probabilities):
print(f" {word:12s} {prob:.2f}")
我机器上的输出:
词汇及其概率:
mat 0.45
roof 0.20
moon 0.15
table 0.12
keyboard 0.06
president 0.02
def greedy_decode(vocabulary, probabilities):
best_index = np.argmax(probabilities)
return vocabulary[best_index]
for i in range(5):
print(f"尝试 {i+1}: {greedy_decode(vocabulary, probabilities)}")
尝试 1: mat
尝试 2: mat
尝试 3: mat
尝试 4: mat
尝试 5: mat
每一次都是完全相同的词,因为贪婪解码总是选择概率最高的单一选项,完全没有随机性。对某些任务来说没问题,但对开放式写作来说确实单调乏味。
Temperature 在采样前对概率分布进行重塑,低 temperature 使分布更尖锐,让模型对其首选更加自信,高 temperature 则使分布更平坦,让不太可能的选项真正具有竞争力。
def apply_temperature(probabilities, temperature):
logits = np.log(probabilities)
scaled_logits = logits / temperature
exp_logits = np.exp(scaled_logits)
return exp_logits / np.sum(exp_logits)
for temp in [0.3, 1.0, 1.8]:
adjusted = apply_temperature(probabilities, temp)
print(f"\nTemperature {temp}:")
for word, prob in zip(vocabulary, adjusted):
print(f" {word:12s} {prob:.3f}")
Temperature 0.3:
mat 0.831
roof 0.107
moon 0.043
table 0.017
keyboard 0.002
president 0.000
Temperature 1.0:
mat 0.450
roof 0.200
moon 0.150
table 0.120
keyboard 0.060
president 0.020
Temperature 1.8:
mat 0.311
roof 0.218
moon 0.195
table 0.176
keyboard 0.079
president 0.021
注意低 temperature 下"mat"的概率是 0.831,几乎必定被选中,而高 temperature 下是 0.311,现在与其他几个选项真正具有竞争力了。这正是产品在让你调节"创造力"或"随机性"时暴露的那个旋钮——正是这个 temperature 值在词元实际被采样之前控制着概率分布的尖锐程度和平坦程度。
纯 temperature 采样有一个真正的风险,即使在合理的 temperature 下,也总是有极小概率采样到真正古怪的内容,比如在"the cat sat on the"之后出现"president"。Top-k 和 Top-p 都是一种在采样发生之前就修剪掉低概率尾部的策略。
def top_k_sampling(vocabulary, probabilities, k):
top_k_indices = np.argsort(probabilities)[-k:]
filtered_probs = np.zeros_like(probabilities)
filtered_probs[top_k_indices] = probabilities[top_k_indices]
filtered_probs = filtered_probs / np.sum(filtered_probs)
return filtered_probs
filtered = top_k_sampling(vocabulary, probabilities, k=3)
print("Top-k (k=3) 过滤后的分布:")
for word, prob in zip(vocabulary, filtered):
print(f" {word:12s} {prob:.3f}")
Top-k (k=3) 过滤后的分布:
mat 0.562
roof 0.250
moon 0.187
table 0.000
keyboard 0.000
president 0.000
只有前三名候选者存活下来,其他的一切都得到了零概率,无论随机性如何掷骰子,"president"在这里根本不可能被选中。Top-p,有时称为 nucleus 采样,做类似的事情但更加自适应——不是固定的候选数量,而是不断添加候选直到它们的累计概率超过一个阈值,比如 90%,这在分布平坦时自然保留更多选项,而在某个选项真正占主导时保留更少。
def top_p_sampling(vocabulary, probabilities, p):
sorted_indices = np.argsort(probabilities)[::-1]
sorted_probs = probabilities[sorted_indices]
cumulative = np.cumsum(sorted_probs)
cutoff = np.searchsorted(cumulative, p) + 1
keep_indices = sorted_indices[:cutoff]
filtered_probs = np.zeros_like(probabilities)
filtered_probs[keep_indices] = probabilities[keep_indices]
return filtered_probs / np.sum(filtered_probs)
filtered_p = top_p_sampling(vocabulary, probabilities, p=0.85)
print("Top-p (p=0.85) 过滤后的分布:")
for word, prob in zip(vocabulary, filtered_p):
print(f" {word:12s} {prob:.3f}")
Top-p (p=0.85) 过滤后的分布:
mat 0.529
roof 0.235
moon 0.176
table 0.059
keyboard 0.000
president 0.000
注意这次 top-p 保留了"table",而 k=3 的 top-k 没有,因为它包含候选直到累计概率超过 85%,而不是固定数量,自然地适应了在这个特定位置上分布实际上有多分散或集中。

想象一个真正自信的导游,他背下了关于一座城市的海量信息,但偶尔会被问到一座要么根本不存在、要么他根本没有真正学习过的建筑。一个糟糕的导游,那种从不说"我不知道"的导游,会以同样的自信语气为那座建筑编造一个听起来合理的历史,就像他对真正了解的建筑一样,因为他脑子里面的全部职责就是永远有一个答案,而不是标记不确定性。
这确实就是幻觉发生时的真实情况。回想本文第一部分,模型在每个位置上总是在生成概率分布并从中采样,无论它是否在任何真正有意义的意义上"知道"答案。没有一个单独的内置开关会切换到"我实际上不知道这个"模式,生成听起来自信的错误事实的机制和生成听起来自信的正确事实的机制是完全相同的机制,只是根据训练期间出现的模式落在不同的词元上。
def simulate_confident_vs_uncertain(topic_is_known):
if topic_is_known:
# 模型在训练中多次见过这种模式,
# 概率质量急剧集中在正确事实上
probabilities = np.array([0.85, 0.08, 0.04, 0.02, 0.01])
else:
# 模型很少或从未见过这个主题,概率质量
# 分散开来,但采样过程完全相同,仍会
# 自信地产生某个具体答案
probabilities = np.array([0.24, 0.22, 0.20, 0.18, 0.16])
tokens = ["fact_A", "fact_B", "fact_C", "fact_D", "fact_E"]
chosen = np.random.choice(tokens, p=probabilities)
max_confidence = np.max(probabilities)
return chosen, max_confidence
known_choice, known_conf = simulate_confident_vs_uncertain(topic_is_known=True)
unknown_choice, unknown_conf = simulate_confident_vs_uncertain(topic_is_known=False)
print(f"Well known topic: picked '{known_choice}', top probability was {known_conf:.2f}")
print(f"Rarely seen topic: picked '{unknown_choice}', top probability was {unknown_conf:.2f}")
print("\nBoth get sampled and output with the exact same generation mechanism,")
print("there's no separate 'I don't know' pathway triggered automatically")
Well known topic: picked 'fact_A', top probability was 0.85
Rarely seen topic: picked 'fact_C', top probability was 0.24
Both get sampled and output with the exact same generation mechanism,
there's no separate 'I don't know' pathway triggered automatically
注意,即使是对于很少见到的主题,概率分布确实变得平坦了,确实更均匀地分散在各个选项上,但采样过程仍然自信地选出了一个具体答案,并以与常见主题完全相同的流畅、断言式语调输出。这种平坦的分布在数学上确实是模型的不确定性的体现,就隐藏在数字里,但输出的 token 并没有将这种不确定性信息携带到实际文本中——除非它经过专门训练(通常通过上一篇文章中提到的微调和 RLHF 阶段)来识别这种平坦、分散的概率分布,并学会说"我不确定"而不是仍然自信地选择一个答案。
这确实是一个活跃的研究领域,并非已解决的问题,但还是有一些值得了解的发现。研究人员发现,模型通常确实在内部具有某种与不确定性相关的信号,在其内部激活中存在的模式在自信正确的答案和幻觉答案之间存在可测量的差异——即使输出文本本身在两种情况下听起来同样自信。这就是为什么一些更新的技术尝试直接读取这些内部信号,而不是纯粹依赖模型自己生成的文本来自我报告置信度,因为生成的文本是由相同的采样过程产生的,无论模型实际内部多么不确定都一视同仁。这也解释了为什么来自本系列前几篇文章的 RAG 等接地技术能有如此直接的帮助——它们没有修复底层采样机制,但改变了生成时可用的信息,让模型能够依赖检索到的事实,而不是在训练中可能只很少见过的主题上纯粹依赖吸收的模式。

想象一家大型医院与一位全科医生的区别。一位医生亲自诊治每位患者,必须在每个专科领域都至少有一定能力,这限制了他在任何单一领域的专业知识深度,因为一个人的知识和时间是有限的。而大型医院则有许多专科医生——心脏科医生、皮肤科医生、神经科医生——关键是在前台有一个分诊系统,查看你的症状后将你定向到正确的专科医生,而不是楼里每位医生都来检查每位患者,只有与你的具体案例真正相关的一两位医生会参与诊治。
混合专家(Mixture of Experts,通常缩写为 MoE)正是将这个理念应用到 Transformer 架构中。一个 MoE 模型不再是一个所有 token 都必须经过的庞大前馈网络(还记得注意力机制文章中的前馈层吧),而是拥有许多更小的"专家"前馈网络,以及一个小的路由网络,它查看每个 token 后决定应该由哪一两位专家实际处理它,而不是全部专家都参与。
class SimpleRouter:
def __init__(self, num_experts, d_model):
self.router_weights = np.random.randn(d_model, num_experts) * 0.5
def route(self, token_embedding, top_k=2):
scores = token_embedding @ self.router_weights
probabilities = np.exp(scores) / np.sum(np.exp(scores))
chosen_experts = np.argsort(probabilities)[-top_k:]
return chosen_experts, probabilities[chosen_experts]
d_model = 8
num_experts = 6
router = SimpleRouter(num_experts, d_model)
# 假设这是两个不同 token 的嵌入
token_a = np.random.randn(d_model)
token_b = np.random.randn(d_model)
experts_a, weights_a = router.route(token_a, top_k=2)
experts_b, weights_b = router.route(token_b, top_k=2)
print(f"Token A routed to experts {experts_a} with weights {np.round(weights_a, 3)}")
print(f"Token B routed to experts {experts_b} with weights {np.round(weights_b, 3)}")
Token A routed to experts [4 1] with weights [0.213 0.238]
Token B routed to experts [5 2] with weights [0.198 0.221]
Token A 和 Token B 由于内容不同,被路由到了真正不同的专家。在真实的 MoE 模型中,在训练过程中,不同的专家自然会趋向于专门化,没有人明确编程指定每个专家专注于什么——就像本系列第一篇文章中提到的各个注意力头通过训练而非人工分配来发展出自己的专长一样。
这里有一个真正巧妙的部分,直接连接到扩展定律文章。密集模型中每个 token 都必须经过每个参数,增加参数会成比例地增加运行成本。而 MoE 模型可以拥有大量参数分布在许多专家中,但每个单独的 token 实际上只激活其中很小的一部分,比如 64 个专家中的 2 个。你获得了真正庞大模型的容量优势,更多总参数存储更多模式,而无需为每个 token 支付全部计算成本,因为路由机制意味着大多数参数根本不参与处理任何特定的 token。
def compute_cost_comparison(total_params, active_params_per_token, num_tokens):
dense_cost = total_params * num_tokens
moe_cost = active_params_per_token * num_tokens
print(f"Dense model compute: {dense_cost:,}")
print(f"MoE model compute: {moe_cost:,}")
print(f"MoE uses {(moe_cost/dense_cost)*100:.1f}% of the dense model's compute")
print(f"...while still having access to {total_params:,} total learned parameters")
compute_cost_comparison(total_params=400_000_000_000, active_params_per_token=50_000_000_000, num_tokens=1000)
Dense model compute: 400,000,000,000,000
MoE model compute: 50,000,000,000,000
MoE uses 12.5% of the dense model's compute
...while still having access to 400,000,000,000 total learned parameters
这一个打印数字就完整展示了 MoE 的全部吸引力:每个 token 的计算成本只是一小部分,同时仍能访问量大得多的已学习容量池。这就是为什么当前运行的几个最大、最强大的模型都使用这种架构而不是每个 token 都必须完全通过的单一密集 monolithic 网络。

这三个主题之间的联系比初看之下更加紧密。生成与幻觉是同一机制——从概率分布中采样——只是从两个不同角度看待:一个是关于控制创造力和多样性,另一个是关于自信输出的文本实际告诉你的诚实限制。而混合专家是扩展定律文章核心发现的结构性答案:更大确实可靠地有帮助,但它找到了一种方法在不让每个 token 都支付全部计算成本的情况下获得巨大规模的好处——将每个 token 路由到仅与其真正相关的特定参数,这类似于本系列第一篇文章中 token 的注意力权重决定哪些其他 token 与它真正相关,只是整条管道的更早一层,决定哪些参数根本有权处理它。
四篇文章完整梳理了整个知识链条:按顺序来看,注意力机制让 token 之间产生关联,训练通过反向传播和梯度下降将随机权重塑造为有用的权重,分词决定了实际被预测的单位,三个训练阶段将原始能力塑造成有帮助的行为,扩展定律解释了为什么更大的模型可靠地更好,而生成则决定了另一端实际输出什么,幻觉是这个生成过程的一个必然结果而非单独的故障,混合专家则展示了一种真正不同的架构方式来更高效地扩展整个系统。这是一个真实的、可行的心智模型,完整地从头构建而非凭信仰接受,描述了这些系统端到端如何运作。
如果你用自己的概率分布运行采样代码,换一些真正不同的主题、不同的置信水平,我想听听你注意到输出如何变化——通常在那一刻,"温度"就不再是设置菜单里一个模糊的滑块,而变成了你能真正想象其发生过程的东西。