Hello-agents TASK02 第三章节 大模型基础
大语言模型基础
语言模型与Transformer架构
从 N-gram 到 RNN
语言模型 (Language Model, LM) 是自然语言处理的核心,其根本任务是计算一个词序列(即一个句子)出现的概率。一个好的语言模型能够告诉我们什么样的句子是通顺的、自然的。在多智能体系统中,语言模型是智能体理解人类指令、生成回应的基础。本节将回顾从经典的统计方法到现代深度学习模型的演进历程,为理解后续的 Transformer 架构打下坚实的基础。
统计语言与N-gram的思想
在深度学习兴起之前,统计方法是语言模型的主流。其核心思想是,一个句子出现的概率,等于该句子中每个词出现的条件概率的连乘。对于一个由词 w1,w2,⋯ ,wmw_1,w_2,\cdots,w_mw1,w2,⋯,wm 构成的句子 S,其概率 P(S) 可以表示为:
P(S)=P(w1,w2,…,wm)=P(w1)⋅P(w2∣w1)⋅P(w3∣w1,w2)⋯P(wm∣w1,…,wm−1)P(S)=P(w_1,w_2,…,w_m)=P(w_1)⋅P(w_2∣w_1)⋅P(w_3∣w_1,w_2)⋯P(w_m∣w_1,…,w_{m−1})P(S)=P(w1,w2,…,wm)=P(w1)⋅P(w2∣w1)⋅P(w3∣w1,w2)⋯P(wm∣w1,…,wm−1)
这个公式被称为概率的链式法则。然而,直接计算这个公式几乎是不可能的,因为像 P(wm∣w1,⋯ ,wm−1)P(w_m∣w_1,\cdots,w_{m−1})P(wm∣w1,⋯,wm−1) 这样的条件概率太难从语料库中估计了,词序列 w1,⋯ ,wm−1w_1,\cdots,w_{m−1}w1,⋯,wm−1 可能从未在训练数据中出现过。

为了解决这个问题,研究者引入了马尔可夫假设 (Markov Assumption) 。其核心思想是:我们不必回溯一个词的全部历史,可以近似地认为,一个词的出现概率只与它前面有限的 n−1n−1n−1 个词有关,如图3.1所示。基于这个假设建立的语言模型,我们称之为 N-gram模型。这里的 “N” 代表我们考虑的上下文窗口大小。让我们来看几个最常见的例子来理解这个概念:
Bigram (当 N=2 时) :这是最简单的情况,我们假设一个词的出现只与它前面的一个词有关。因此,链式法则中复杂的条件概率 P(wi∣w1,⋯ ,wi−1)P(w_i∣w_1,\cdots,w_{i−1})P(wi∣w1,⋯,wi−1) 就可以被近似为更容易计算的形式:
P(wi∣w1,…,wi−1)≈P(wi∣wi−1)P(w_{i}∣w_{1},…,w_{i−1})≈P(w_{i}∣w_{i−1})P(wi∣w1,…,wi−1)≈P(wi∣wi−1)
Trigram (当 N=3 时) :类似地,我们假设一个词的出现只与它前面的两个词有关:
P(wi∣w1,…,wi−1)≈P(wi∣wi−2,wi−1)P(w_i∣w_1,…,w_{i−1})≈P(w_i∣w_{i−2},w_{i−1})P(wi∣w1,…,wi−1)≈P(wi∣wi−2,wi−1)
这些概率可以通过在大型语料库中进行最大似然估计(Maximum Likelihood Estimation,MLE) 来计算。
这个术语听起来很复杂,但其思想非常直观:最可能出现的,就是我们在数据中看到次数最多的。例如,对于 Bigram 模型,我们想计算在词 wi−1w_{i−1}wi−1 出现后,下一个词是 wiw_iwi 的概率 P(wi∣wi−1)P(w_i∣w_{i−1})P(wi∣wi−1)。根据最大似然估计,这个概率可以通过简单的计数来估算:
P(wi∣wi−1)=Count(wi−1,wi)Count(wi−1)P(w_i∣w_{i−1})=\frac{Count(w_{i−1},w_i)}{Count(w_{i−1})}P(wi∣wi−1)=Count(wi−1)Count(wi−1,wi)
这里的 Count() 函数就代表“计数”:
Count(wi−1,wi)Count(w_{i−1},w_i)Count(wi−1,wi):表示词对 (wi−1,wi)(w_{i−1},w_i)(wi−1,wi) 在语料库中连续出现的总次数。
Count(wi−1)Count(w_{i−1})Count(wi−1):表示单个词 wi−1w_{i−1}wi−1 在语料库中出现的总次数。
公式的含义就是:我们用“词对 Count(wi−1,wi)Count(w_i−1,w_i)Count(wi−1,wi) 出现的次数”除以“词 Count(wi−1)Count(w_{i−1})Count(wi−1) 出现的总次数”,来作为 P(wi∣wi−1)P(w_i∣w_{i−1})P(wi∣wi−1) 的一个近似估计。
为了让这个过程更具体,我们来手动进行一次计算。假设我们拥有一个仅包含以下两句话的迷你语料库:datawhale agent learns, datawhale agent works。
我们的目标是:使用 Bigram (N=2) 模型,估算句子 datawhale agent learns 出现的概率。根据 Bigram 的假设,我们每次会考察连续的两个词(即一个词对)。
第一步: 计算第一个词的概率 P(datawhale)P(datawhale)P(datawhale) 这是 datawhale 出现的次数除以总词数。datawhale 出现了 2 次,总词数是 6。
P(datawhale)=总语料中"datawhale"的数量总语料的词数=26≈0.333P(\text{datawhale}) = \frac{\text{总语料中"datawhale"的数量}}{\text{总语料的词数}} = \frac{2}{6} \approx 0.333P(datawhale)=总语料的词数总语料中"datawhale"的数量=62≈0.333
第二步: 计算条件概率 P(agent∣datawhale)P(agent∣datawhale)P(agent∣datawhale) 这是词对 datawhale agent 出现的次数除以 datawhale 出现的总次数。datawhale agent 出现了 2 次,datawhale 出现了 2 次。
P(agent∣datawhale)=Count(datawhale agent)Count(datawhale)=22=1P(\text{agent}|\text{datawhale}) = \frac{\text{Count}(\text{datawhale agent})}{\text{Count}(\text{datawhale})} = \frac{2}{2} = 1P(agent∣datawhale)=Count(datawhale)Count(datawhale agent)=22=1
第三步: 计算条件概率 P(learns∣agent)P(learns∣agent)P(learns∣agent) 这是词对 agent learns 出现的次数除以 agent 出现的总次数。agent learns 出现了 1 次,agent 出现了 2 次。
P(learns∣agent)=Count(agent learns)Count(agent)=12=0.5P(\text{learns}|\text{agent}) = \frac{\text{Count(agent learns)}}{\text{Count(agent)}} = \frac{1}{2} = 0.5P(learns∣agent)=Count(agent)Count(agent learns)=21=0.5
最后:将概率连乘 所以,整个句子的近似概率为:
P(datawhale agent learns)≈P(datawhale)⋅P(agent∣datawhale)⋅P(learns∣agent)≈0.333⋅1⋅0.5≈0.167P(\text{datawhale agent learns}) \approx P(\text{datawhale}) \cdot P(\text{agent}|\text{datawhale}) \cdot P(\text{learns}|\text{agent}) \approx 0.333 \cdot 1 \cdot 0.5 \approx 0.167P(datawhale agent learns)≈P(datawhale)⋅P(agent∣datawhale)⋅P(learns∣agent)≈0.333⋅1⋅0.5≈0.167
import collections# 示例语料库,与上方案例讲解中的语料库保持一致
corpus = "datawhale agent learns datawhale agent works"
tokens = corpus.split()
total_tokens = len(tokens)# --- 第一步:计算 P(datawhale) ---
count_datawhale = tokens.count('datawhale')
p_datawhale = count_datawhale / total_tokens
print(f"第一步: P(datawhale) = {count_datawhale}/{total_tokens} = {p_datawhale:.3f}")# --- 第二步:计算 P(agent|datawhale) ---
# 先计算 bigrams 用于后续步骤
bigrams = zip(tokens, tokens[1:])
bigram_counts = collections.Counter(bigrams)
count_datawhale_agent = bigram_counts[('datawhale', 'agent')]
# count_datawhale 已在第一步计算
p_agent_given_datawhale = count_datawhale_agent / count_datawhale
print(f"第二步: P(agent|datawhale) = {count_datawhale_agent}/{count_datawhale} = {p_agent_given_datawhale:.3f}")# --- 第三步:计算 P(learns|agent) ---
count_agent_learns = bigram_counts[('agent', 'learns')]
count_agent = tokens.count('agent')
p_learns_given_agent = count_agent_learns / count_agent
print(f"第三步: P(learns|agent) = {count_agent_learns}/{count_agent} = {p_learns_given_agent:.3f}")# --- 最后:将概率连乘 ---
p_sentence = p_datawhale * p_agent_given_datawhale * p_learns_given_agent
print(f"最后: P('datawhale agent learns') ≈ {p_datawhale:.3f} * {p_agent_given_datawhale:.3f} * {p_learns_given_agent:.3f} = {p_sentence:.3f}")>>>
第一步: P(datawhale) = 2/6 = 0.333
第二步: P(agent|datawhale) = 2/2 = 1.000
第三步: P(learns|agent) = 1/2 = 0.500
最后: P('datawhale agent learns') ≈ 0.333 * 1.000 * 0.500 = 0.167
N-gram 模型虽然简单有效,但有两个致命缺陷:
- 数据稀疏性 (Sparsity) :如果一个词序列从未在语料库中出现,其概率估计就为 0,这显然是不合理的。虽然可以通过平滑 (Smoothing) 技术缓解,但无法根除。
- 泛化能力差:模型无法理解词与词之间的语义相似性。例如,即使模型在语料库中见过很多次 agent learns,它也无法将这个知识泛化到语义相似的词上。当我们计算 robot learns 的概率时,如果 robot 这个词从未出现过,或者 robot learns 这个组合从未出现过,模型计算出的概率也会是零。模型无法理解 agent 和 robot 在语义上的相似性。
(2)神经网络语言模型与词嵌入
N-gram 模型的根本缺陷在于它将词视为孤立、离散的符号。为了克服这个问题,研究者们转向了神经网络,并提出了一种思想:用连续的向量来表示词。2003年,Bengio 等人提出的前馈神经网络语言模型 (Feedforward Neural Network Language Model) 是这一领域的里程碑[1]。
其核心思想可以分为两步:
- 构建一个语义空间:创建一个高维的连续向量空间,然后将词汇表中的每个词都映射为该空间中的一个点。这个点(即向量)就被称为词嵌入 (Word Embedding) 或词向量。在这个空间里,语义上相近的词,它们对应的向量在空间中的位置也相近。例如,agent 和 robot 的向量会靠得很近,而 agent 和 apple 的向量会离得很远。
- 学习从上下文到下一个词的映射:利用神经网络的强大拟合能力,来学习一个函数。这个函数的输入是前 n−1n−1n−1 个词的词向量,输出是词汇表中每个词在当前上下文后出现的概率分布。

如图3.2所示,在这个架构中,词嵌入是在模型训练过程中自动学习得到的。模型为了完成“预测下一个词”这个任务,会不断调整每个词的向量位置,最终使这些向量能够蕴含丰富的语义信息。一旦我们将词转换成了向量,我们就可以用数学工具来度量它们之间的关系。最常用的方法是余弦相似度 (Cosine Similarity) ,它通过计算两个向量夹角的余弦值来衡量它们的相似性。

这个公式的含义是:
- 如果两个向量方向完全相同,夹角为0°,余弦值为1,表示完全相关。
- 如果两个向量方向正交,夹角为90°,余弦值为0,表示毫无关系。
- 如果两个向量方向完全相反,夹角为180°,余弦值为-1,表示完全负相关。
通过这种方式,词向量不仅能捕捉到“同义词”这类简单的关系,还能捕捉到更复杂的类比关系。
一个著名的例子展示了词向量捕捉到的语义关系: vector(‘King’) - vector(‘Man’) + vector(‘Woman’) 这个向量运算的结果,在向量空间中与 vector(‘Queen’) 的位置惊人地接近。这好比在进行语义的平移:我们从“国王”这个点出发,减去“男性”的向量,再加上“女性”的向量,最终就抵达了“女王”的位置。这证明了词嵌入能够学习到“性别”、“皇室”这类抽象概念
import numpy as np# 假设我们已经学习到了简化的二维词向量
embeddings = {"king": np.array([0.9, 0.8]),"queen": np.array([0.9, 0.2]),"man": np.array([0.7, 0.9]),"woman": np.array([0.7, 0.3])
}def cosine_similarity(vec1, vec2):dot_product = np.dot(vec1, vec2)norm_product = np.linalg.norm(vec1) * np.linalg.norm(vec2)return dot_product / norm_product# king - man + woman
result_vec = embeddings["king"] - embeddings["man"] + embeddings["woman"]# 计算结果向量与 "queen" 的相似度
sim = cosine_similarity(result_vec, embeddings["queen"])print(f"king - man + woman 的结果向量: {result_vec}")
print(f"该结果与 'queen' 的相似度: {sim:.4f}")>>>
king - man + woman 的结果向量: [0.9 0.2]
该结果与 'queen' 的相似度: 1.0000
神经网络语言模型通过词嵌入,成功解决了 N-gram 模型的泛化能力差的问题。然而,它仍然有一个类似 N-gram 的限制:**上下文窗口是固定的。**它只能考虑固定数量的前文,这为能处理任意长序列的循环神经网络埋下了伏笔。
循环神经网络 (RNN) 与长短时记忆网络 (LSTM)
前一节的神经网络语言模型虽然引入了词嵌入解决了泛化问题,但它和 N-gram 模型一样,上下文窗口是固定大小的。为了预测下一个词,它只能看到前 n−1 个词,再早的历史信息就被丢弃了。这显然不符合我们人类理解语言的方式。为了打破固定窗口的限制,循环神经网络 (Recurrent Neural Network, RNN) 应运而生,其核心思想非常直观:为网络增加==“记忆”==能力[2]。
如图3.3所示,RNN 的设计引入了一个隐藏状态 (hidden state) 向量,我们可以将其理解为网络的短期记忆。在处理序列的每一步,网络都会读取当前的输入词,并结合它上一刻的记忆(即上一个时间步的隐藏状态),然后生成一个新的记忆(即当前时间步的隐藏状态)传递给下一刻。这个循环往复的过程,使得信息可以在序列中不断向后传递。

然而,标准的 RNN 在实践中存在一个严重的问题:长期依赖问题 (Long-term Dependency Problem) 。在训练过程中,模型需要通过反向传播算法根据输出端的误差来调整网络深处的权重。对于 RNN 而言,序列的长度就是网络的深度。当序列很长时,梯度在从后向前传播的过程中会经过多次连乘,这会导致梯度值快速趋向于零(梯度消失)或变得极大(梯度爆炸)。梯度消失使得模型无法有效学习到序列早期信息对后期输出的影响,即难以捕捉长距离的依赖关系。
为了解决长期依赖问题,长短时记忆网络 (Long Short-Term Memory, LSTM) 被设计出来[3]。LSTM 是一种特殊的 RNN,其核心创新在于引入了**【细胞状态】** (Cell State) 和一套精密的**【门控机制】** (Gating Mechanism) 。细胞状态可以看作是一条独立于隐藏状态的信息通路,允许信息在时间步之间更顺畅地传递。门控机制则是由几个小型神经网络构成,它们可以学习如何有选择地让信息通过,从而控制细胞状态中信息的增加与移除。这些门包括:
- 遗忘门 (Forget Gate):决定从上一时刻的细胞状态中丢弃哪些信息。
- 输入门 (Input Gate):决定将当前输入中的哪些新信息存入细胞状态。
- 输出门 (Output Gate):决定根据当前的细胞状态,输出哪些信息到隐藏状态
Transformer 架构解析
在上一节中,我们看到RNN及LSTM通过引入循环结构来处理序列数据,这在一定程度上解决了捕捉长距离依赖的问题。然而,这种循环的计算方式也带来了新的瓶颈:它必须按顺序处理数据。——第 t 个时间步的计算,必须等待第 t−1 个时间步完成后才能开始。
这意味着 RNN 无法进行大规模的【并行计算】,在处理长序列时效率低下,这极大地限制了模型规模和训练速度的提升。Transformer在2017 年由谷歌团队提出[4]。它完全抛弃了循环结构,转而完全依赖一种名为注意力 (Attention) 的机制来捕捉序列内的依赖关系,【从而实现了真正意义上的并行计算】。
Encoder-Decoder 整体结构
最初的 Transformer 模型是为【端到端任务机器翻译】而设计的。如图3.4所示,它在宏观上遵循了一个经典的编码器-解码器 (Encoder-Decoder) 架构。

我们可以将这个结构理解为一个分工明确的团队:
编码器 (Encoder) :任务是“理解”输入的整个句子。它会读取所有输入词元(这个概念会在3.2.2节介绍),最终为每个词元生成一个富含上下文信息的向量表示。
解码器 (Decoder) :任务是“生成”目标句子。它会参考自己已经生成的前文,并“咨询”编码器的理解结果,来生成下一个词
为了真正理解 Transformer 的工作原理,最好的方法莫过于亲手实现它。在本节中,我们将采用一种“自顶向下”的方法:首先,我们搭建出 Transformer 完整的代码框架,定义好所有需要的类和方法。然后,我们将像完成拼图一样,逐一实现这些类的具体功能。
从自注意力到多头注意力
现在,我们来填充骨架中最关键的模块,注意力机制。
想象一下我们阅读这个句子:“The agent learns because it is intelligent.”。当我们读到加粗的 “it” 时,为了理解它的指代,我们的大脑会不自觉地将更多的注意力放在前面的 “agent” 这个词上。
自注意力 (Self-Attention) 机制就是对这种现象的数学建模。它允许模型在处理序列中的每一个词时,都能兼顾句子中的所有其他词,并为这些词分配不同的“注意力权重”。权重越高的词,代表其与当前词的关联性越强,其信息也应该在当前词的表示中占据更大的比重。
为了实现上述过程,自注意力机制为每个输入的词元向量引入了三个可学习的角色:
- 查询 (Query, Q):代表当前词元,它正在主动地“查询”其他词元以获取信息。
- 键 (Key, K):代表句子中可被查询的词元“标签”或“索引”。
- 值 (Value, V):代表词元本身所携带的“内容”或“信息”。
这三个向量都是由原始的词嵌入向量乘以三个不同的、可学习的权重矩阵 (WQ,WK,WV)(W^Q,W^K,W^V)(WQ,WK,WV)得到的。整个计算过程可以分为以下几步,我们可以把它想象成一次高效的开卷考试:
- 准备**“考题”和“资料”**:对于句子中的每个词,都通过权重矩阵生成其Q,K,VQ,K,VQ,K,V向量。
- 计算相关性得分:要计算词AAA的新表示,就用词AAA的QQQ向量,去和句子中所有词(包括AAA自己)的KKK向量进行点积运算。这个得分反映了其他词对于理解词AAA的重要性。
- 稳定化与归一化:将得到的所有分数除以一个缩放因子dk\sqrt{d_{k}}dk(dkd_{k}dk是KKK向量的维度),以防止梯度过小,然后用Softmax函数将分数转换成总和为1的权重,也就是归一化的过程。
- 加权求和:将上一步得到的权重分别乘以每个词对应的VVV向量,然后将所有结果相加。最终得到的向量,就是词AAA融合了全局上下文信息后的新表示。
这个过程可以用一个简洁的公式来概括:

如果只进行一次上述的注意力计算(即单头),模型可能会只学会关注一种类型的关联。比如,在处理 “it” 时,可能只学会了关注主语。但语言中的关系是复杂的,我们希望模型能同时关注多种关系(如指代关系、时态关系、从属关系等)。
多头注意力机制应运而生。它的思想很简单:把一次做完变成分成几组,分开做,再合并。
它将原始的 Q, K, V 向量在维度上切分成 h 份(h 就是“头”数),每一份都独立地进行一次单头注意力的计算。这就好比让 h 个不同的“专家”从不同的角度去审视句子,每个专家都能捕捉到一种不同的特征关系。最后,将这 h 个专家的“意见”(即输出向量)拼接起来,再通过一个线性变换进行整合,就得到了最终的输出。

如图3.5所示,这种设计让模型能够共同关注来自不同位置、不同表示子空间的信息,极大地增强了模型的表达能力。以下是多头注意力的简单实现可供参考。
class MultiHeadAttention(nn.Module):"""多头注意力机制模块"""def __init__(self, d_model, num_heads):super(MultiHeadAttention, self).__init__()assert d_model % num_heads == 0, "d_model 必须能被 num_heads 整除"self.d_model = d_modelself.num_heads = num_headsself.d_k = d_model // num_heads# 定义 Q, K, V 和输出的线性变换层self.W_q = nn.Linear(d_model, d_model)self.W_k = nn.Linear(d_model, d_model)self.W_v = nn.Linear(d_model, d_model)self.W_o = nn.Linear(d_model, d_model)def scaled_dot_product_attention(self, Q, K, V, mask=None):# 1. 计算注意力得分 (QK^T)attn_scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(self.d_k)# 2. 应用掩码 (如果提供)if mask is not None:# 将掩码中为 0 的位置设置为一个非常小的负数,这样 softmax 后会接近 0attn_scores = attn_scores.masked_fill(mask == 0, -1e9)# 3. 计算注意力权重 (Softmax)attn_probs = torch.softmax(attn_scores, dim=-1)# 4. 加权求和 (权重 * V)output = torch.matmul(attn_probs, V)return outputdef split_heads(self, x):# 将输入 x 的形状从 (batch_size, seq_length, d_model)# 变换为 (batch_size, num_heads, seq_length, d_k)batch_size, seq_length, d_model = x.size()return x.view(batch_size, seq_length, self.num_heads, self.d_k).transpose(1, 2)def combine_heads(self, x):# 将输入 x 的形状从 (batch_size, num_heads, seq_length, d_k)# 变回 (batch_size, seq_length, d_model)batch_size, num_heads, seq_length, d_k = x.size()return x.transpose(1, 2).contiguous().view(batch_size, seq_length, self.d_model)def forward(self, Q, K, V, mask=None):# 1. 对 Q, K, V 进行线性变换Q = self.split_heads(self.W_q(Q))K = self.split_heads(self.W_k(K))V = self.split_heads(self.W_v(V))# 2. 计算缩放点积注意力attn_output = self.scaled_dot_product_attention(Q, K, V, mask)# 3. 合并多头输出并进行最终的线性变换output = self.W_o(self.combine_heads(attn_output))return output
前馈神经网络
在每个 Encoder 和 Decoder 层中,多头注意力子层之后都跟着一个逐位置前馈网络**(Position-wise Feed-Forward Network, FFN)** 。如果说注意力层的作用是从整个序列中“动态地聚合”相关信息,那么前馈网络的作用从这些聚合后的信息中提取更高阶的特征。
这个名字的关键在于“逐位置”。它意味着这个前馈网络会独立地作用于序列中的每一个词元向量。换句话说,对于一个长度为 seq_len 的序列,这个 FFN 实际上会被调用 seq_len 次,每次处理一个词元。重要的是,所有位置共享的是同一组网络权重。这种设计既保持了对每个位置进行独立加工的能力,又大大减少了模型的参数量。这个网络的结构非常简单,由两个线性变换和一个 ReLU 激活函数组成:

其中,xxx是注意力子层的输出。W1,b1,W2,b2W_1,b_1,W_2,b_2W1,b1,W2,b2是可学习的参数。通常,第一个线性层的输出维度 d_ff 会远大于输入的维度 d_model(例如 d_ff = 4 * d_model),经过 ReLU 激活后再通过第二个线性层映射回 d_model 维度。这种【“先扩大再缩小”的模式,被认为有助于模型学习更丰富的特征表示】。
在我们的 PyTorch 骨架中,我们可以用以下代码来实现这个模块:
class PositionWiseFeedForward(nn.Module):"""位置前馈网络模块"""def __init__(self, d_model, d_ff, dropout=0.1):super(PositionWiseFeedForward, self).__init__()self.linear1 = nn.Linear(d_model, d_ff)self.dropout = nn.Dropout(dropout)self.linear2 = nn.Linear(d_ff, d_model)self.relu = nn.ReLU()def forward(self, x):# x 形状: (batch_size, seq_len, d_model)x = self.linear1(x)x = self.relu(x)x = self.dropout(x)x = self.linear2(x)# 最终输出形状: (batch_size, seq_len, d_model)return x
残差连接与层归一化
在 Transformer 的每个编码器和解码器层中,所有子模块(如多头注意力和前馈网络)都被一个 Add & Norm 操作包裹。这个组合是为了保证 Transformer 能够稳定训练。
这个操作由两个部分组成:
- 残差连接 (Add):该操作将子模块的输入 x 直接加到该子模块的输出 Sublayer(x) 上。这一结构解决了深度神经网络中的梯度消失 (Vanishing Gradients) 问题。在反向传播时,梯度可以绕过子模块直接向前传播,从而保证了即使网络层数很深,模型也能得到有效的训练。其公式可以表示为:Output=x+Sublayer(x)\text{Output} = x + \text{Sublayer}(x)Output=x+Sublayer(x)。
- 层归一化 (Norm):该操作对单个样本的所有特征进行归一化,使其均值为0,方差为1。这解决了模型训练过程中的内部协变量偏移 (Internal Covariate Shift) 问题,使每一层的输入分布保持稳定,从而加速模型收敛并提高训练的稳定性。
位置编码
我们已经了解,Transformer 的核心是自注意力机制,它通过计算序列中任意两个词元之间的关系来捕捉依赖。然而,这种计算方式有一个固有的问题:它本身不包含任何关于词元顺序或位置的信息。对于自注意力来说,“agent learns” 和 “learns agent” 这两个序列是完全等价的,因为它只关心词元之间的关系,而忽略了它们的排列。为了解决这个问题,Transformer 引入了位置编码 (Positional Encoding) 。
位置编码的核心思想是,为输入序列中的每一个词元嵌入向量,都额外加上一个能代表其绝对位置和相对位置信息的“位置向量”。这个位置向量不是通过学习得到的,而是通过一个固定的数学公式直接计算得出。这样一来,即使两个词元(例如,两个都叫 agent 的词元)自身的嵌入是相同的,但由于它们在句子中的位置不同,它们最终输入到 Transformer 模型中的向量就会因为加上了不同的位置编码而变得独一无二。原论文中提出的位置编码使用正弦和余弦函数来生成,其公式如下:

其中:
- pos是词元在序列中的位置(例如,000,111,222,…)
- i 是位置向量中的维度索引(从 0到dmodel/20到 d_{model}/20到dmodel/2)
现在,我们来实现PositionalEncoding模块,并完成我们 Transformer 骨架代码的最后一部分。
class PositionalEncoding(nn.Module):"""为输入序列的词嵌入向量添加位置编码。"""def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):super().__init__()self.dropout = nn.Dropout(p=dropout)# 创建一个足够长的位置编码矩阵position = torch.arange(max_len).unsqueeze(1)div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))# pe (positional encoding) 的大小为 (max_len, d_model)pe = torch.zeros(max_len, d_model)# 偶数维度使用 sin, 奇数维度使用 cospe[:, 0::2] = torch.sin(position * div_term)pe[:, 1::2] = torch.cos(position * div_term)# 将 pe 注册为 buffer,这样它就不会被视为模型参数,但会随模型移动(例如 to(device))self.register_buffer('pe', pe.unsqueeze(0))def forward(self, x: torch.Tensor) -> torch.Tensor:# x.size(1) 是当前输入的序列长度# 将位置编码加到输入向量上x = x + self.pe[:, :x.size(1)]return self.dropout(x)
Decoder-only架构
前面一节中,我们动手构建了一个完整的Transformer 模型,它能在很多端到端的场景表现出色。但是当任务转换为构建一个与人对话、创作、作为智能体大脑的通用模型时,或许我们并不需要那么复杂的结构。
Transformer的设计哲学是“先理解,再生成”。编码器负责深入理解输入的整个句子,形成一个包含全局信息的上下文记忆,然后解码器基于这份记忆来生成翻译。但 OpenAI 在开发 GPT (Generative Pre-trained Transformer) 时,提出了一个更简单的思想[5]:语言的核心任务,不就是预测下一个最有可能出现的词吗?
无论是回答问题、写故事还是生成代码,本质上都是在一个已有的文本序列后面,一个词一个词地添加最合理的内容。基于这个思想,GPT 做了一个大胆的简化:它完全抛弃了编码器,只保留了解码器部分。 这就是 Decoder-Only 架构的由来。
Decoder-Only 架构的工作模式被称为自回归 (Autoregressive) 。这个听起来很专业的术语,其实描述了一个非常简单的过程:
- 给模型一个起始文本(例如 “Datawhale Agent is”)。
- 模型预测出下一个最有可能的词(例如 “a”)。
- 模型将自己刚刚生成的词 “a” 添加到输入文本的末尾,形成新的输入(“Datawhale Agent is a”)。
- 模型基于这个新输入,再次预测下一个词(例如 “powerful”)。
- 不断重复这个过程,直到生成完整的句子或达到停止条件。
模型就像一个在玩“文字接龙”的游戏,它不断地“回顾”自己已经写下的内容,然后思考下一个字该写什么。
你可能会问,解码器是如何保证在预测第 t 个词时,不去“偷看”第 t+1 个词的答案呢?
答案就是掩码自注意力 (Masked Self-Attention) 。在 Decoder-Only 架构中,这个机制变得至关重要。它的工作原理非常巧妙:
在自注意力机制计算出注意力分数矩阵(即每个词对其他所有词的关注度得分)之后,但在进行 Softmax 归一化之前,模型会应用一个“掩码”。这个掩码会将所有位于当前位置之后(即目前尚未观测到)的词元对应的分数,替换为一个非常大的负数。当这个带有负无穷分数的矩阵经过 Softmax 函数时,这些位置的概率就会变为 0。这样一来,模型在计算任何一个位置的输出时,都从数学上被阻止了去关注它后面的信息。这种机制保证了模型在预测下一个词时,能且仅能依赖它已经见过的、位于当前位置之前的所有信息,从而确保了预测的公平性和逻辑的连贯性。
Decoder-Only 架构的优势
这种看似简单的架构,却带来了巨大的成功,其优势在于:
训练目标统一:模型的唯一任务就是“预测下一个词”,这个简单的目标非常适合在海量的无标注文本数据上进行预训练。
结构简单,易于扩展:更少的组件意味着更容易进行规模化扩展。今天的 GPT-4、Llama 等拥有数千亿甚至万亿参数的巨型模型,都是基于这种简洁的架构。
天然适合生成任务:其自回归的工作模式与所有生成式任务(对话、写作、代码生成等)完美契合,这也是它能成为构建通用智能体基础的核心原因。
总而言之,从 Transformer 的解码器演变而来的 Decoder-Only 架构,通过“预测下一个词”这一简单的范式,开启了我们今天所处的大语言模型时代
与大语言模型交互
(1)模型采样参数
在使用大模型时,你会经常看到类似Temperature这类的可配置参数,其本质是通过调整模型【对 “概率分布” 的采样策略】,让输出匹配具体场景需求,配置合适的参数可以提升Agent在特定场景的性能。
传统的概率分布使由 Softmax 公式计算得到的:pi=ezi∑j=1kezjp_i = \frac{e^{z_i}}{\sum_{j=1}^k e^{z_j}}pi=∑j=1kezjezi,采样参数的本质就是在此基础上,根据不同策略“重新调整”或“截断”分布,从而改变大模型输出的下一个token。
Temperature:温度是控制模型输出 “随机性” 与 “确定性” 的关键参数。其原理是引入温度系数T>0T\gt0T>0,将 Softmax 改写为pi(T)=ezi/T∑j=1kezj/Tp_i^{(T)} = \frac{e^{z_i / T}}{\sum_{j=1}^k e^{z_j / T}}pi(T)=∑j=1kezj/Tezi/T。
当T变小时,分布“更加陡峭”,高概率项权重进一步放大,生成更“保守”且重复率更高的文本。当T变大时,分布“更加平坦”,低概率项权重提升,生成更“多样”但可能出现不连贯的内容。
- 低温度(0 < Temperature < 0.3) 时输出更"精准、确定”。适用场景: 事实性任务:如问答、数据计算、代码生成;严谨性场景:法律条文解读、技术文档撰写、学术概念解释等场景。
- 中温度(0.3 < Temperature < 0.7):输出“平衡、自然”。适用场景: 日常对话:如客服交互、聊天机器人;常规创作:如邮件撰写、产品文案、简单故事创作。
- 高温度(0.7 < Temperature < 2):输出“创新、发散"。适用场景: 创意性任务:如诗歌创作、科幻故事构思、广告 slogan brainstorm、艺术灵感启发; 发散性思考。
Top-k :其原理是将所有 token 按概率从高到低排序,取排名前 k 个的 token 组成 “候选集”,随后对筛选出的 k 个 token 的概率进行 “归一化”: p^i=pi∑j∈候选集pj\hat{p}i = \frac{p_i}{\sum{j \in \text{候选集}} p_j}p^i=∑j∈候选集pjpi
- 与温度采样的区别与联系:温度采样通过温度 T 调整所有 token 的概率分布(平滑或陡峭),不改变候选 token 的数量(仍考虑全部 N 个)。
- Top-k 采样通过 k 值限制候选 token 的数量(只保留前 k 个高概率 token),再从其中采样。当k=1时输出完全确定,退化为 “贪心采样”。
Top-p :其原理是将所有 token 按概率从高到低排序,从排序后的第一个 token 开始,逐步累加概率,直到累积和首次达到或超过阈值 p:∑i∈Sp(i)≥p\sum_{i \in S} p_{(i)} \geq p∑i∈Sp(i)≥p 此时累加过程中包含的所有 token 组成 “核集合”,最后对核集合进行归一化。
- 与Top-k的区别与联系:相对于固定截断大小的 Top-k,Top-p 能动态适应不同分布的“长尾”特性,对概率分布不均匀的极端情况的适应性更好。
在文本生成中,当同时设置 Top-p、Top-k 和温度系数时,这些参数会按照分层过滤的方式协同工作,其优先级顺序为:温度调整→Top-k→Top-p。温度调整整体分布的陡峭程度,Top-k 会先保留概率最高的 k 个候选,然后 Top-p 会从 Top-k 的结果中选取累积概率≥p 的最小集合作为最终的候选集。不过,通常 Top-k 和 Top-p 二选一即可,若同时设置,实际候选集为两者的交集。 需要注意的是,如果将温度设置为 0,则 Top-k 和 Top-p 将变得无关紧要,因为最有可能的 Token 将成为下一个预测的 Token;如果将 Top-k 设置为 1,温度和 Top-p 也将变得无关紧要,因为只有一个 Token 通过 Top-k 标准,它将是下一个预测的 Token。
文本分词
在将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。
将文本序列转换为数字序列的过程,就叫做分词 (Tokenization) 。分词器 (Tokenizer) 的作用,就是定义一套规则,将原始文本切分成一个个最小的单元,我们称之为词元 (Token) 。
为何需要分词
早期的自然语言处理任务可能会采用简单的分词策略:
- 按词分词 (Word-based) :直接用空格或标点符号将句子切分成单词。这种方法很直观,但也面临挑战:
- 词表爆炸与未登录词:一个语言的词汇量是巨大的,如果每个词都作为一个独立的词元,词表会变得难以管理。更糟糕的是,模型将无法处理任何未在词表中出现过的词(例如 “DatawhaleAgent”),这种现象我们称为“未登录词” (Out-Of-Vocabulary, OOV)。
- 语义关联的缺失:模型难以捕捉词形相近的词之间的语义关系。例如,“look”、“looks” 和 “looking” 会被视为三个完全不同的词元,尽管它们有共同的核心含义。同样,训练数据中的低频词由于出现次数少,其语义也难以被模型充分学习。
- 按字符分词 (Character-based) :将文本切分成单个字符。这种方法词表很小(例如英文字母、数字和标点),不存在 OOV 问题。但它的缺点是,单个字符大多不具备独立的语义,模型需要花费更多的精力去学习如何将字符组合成有意义的词,导致学习效率低下。
为了兼顾词表大小和语义表达,现代大语言模型普遍采用子词分词 (Subword Tokenization) 算法。它的核心思想是:将【常见的词】(如 “agent”)保留为【完整的词元】,同时将不常见的词(如 “Tokenization”)拆分成多个有意义的子词片段(如 “Token” 和 “ization”)。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。
字节对编码算法解析
字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一[6],GPT系列模型就采用了这种算法。其核心思想非常简洁,可以理解为一个“贪心”的合并过程:
- 初始化:将词表初始化为所有在语料库中出现过的基本字符。
- 迭代合并:在语料库上,统计所有相邻词元对的出现频率,找到频率最高的一对,将它们合并成一个新的词元,并加入词表。
- 重复:重复第 2 步,直到词表大小达到预设的阈值。
案例演示: 假设我们的迷你语料库是 {“hug”: 1, “pug”: 1, “pun”: 1, “bun”: 1},并且我们想构建一个大小为 10 的词表。BPE 的训练过程可以用下表3.1来表示:

训练结束后,词表大小达到 10,我们就得到了新的分词规则。现在,对于一个未见过的词 “bug”,分词器会先查找 “bug” 是否在词表中,发现不在;然后查找 “bu”,发现不在;最后查找 “b” 和 “ug”,发现都在,于是将其切分为 [‘b’, ‘ug’]。
下面我们用一段简单的 Python 代码来模拟上述过程:
import re, collectionsdef get_stats(vocab):"""统计词元对频率"""pairs = collections.defaultdict(int)for word, freq in vocab.items():symbols = word.split()for i in range(len(symbols)-1):pairs[symbols[i],symbols[i+1]] += freqreturn pairsdef merge_vocab(pair, v_in):"""合并词元对"""v_out = {}bigram = re.escape(' '.join(pair))p = re.compile(r'(?<!\S)' + bigram + r'(?!\S)')for word in v_in:w_out = p.sub(''.join(pair), word)v_out[w_out] = v_in[word]return v_out# 准备语料库,每个词末尾加上</w>表示结束,并切分好字符
vocab = {'h u g </w>': 1, 'p u g </w>': 1, 'p u n </w>': 1, 'b u n </w>': 1}
num_merges = 4 # 设置合并次数for i in range(num_merges):pairs = get_stats(vocab)if not pairs:breakbest = max(pairs, key=pairs.get)vocab = merge_vocab(best, vocab)print(f"第{i+1}次合并: {best} -> {''.join(best)}")print(f"新词表(部分): {list(vocab.keys())}")print("-" * 20)>>>
第1次合并: ('u', 'g') -> ug
新词表(部分): ['h ug </w>', 'p ug </w>', 'p u n </w>', 'b u n </w>']
--------------------
第2次合并: ('ug', '</w>') -> ug</w>
新词表(部分): ['h ug</w>', 'p ug</w>', 'p u n </w>', 'b u n </w>']
--------------------
第3次合并: ('u', 'n') -> un
新词表(部分): ['h ug</w>', 'p ug</w>', 'p un </w>', 'b un </w>']
--------------------
第4次合并: ('un', '</w>') -> un</w>
新词表(部分): ['h ug</w>', 'p ug</w>', 'p un</w>', 'b un</w>']
--------------------
分词器对开发者的意义
理解分词算法的细节并非目的,但作为智能体的开发者,理解分词器的实际影响是重要,这直接关系到智能体的性能、成本和稳定性:
上下文窗口限制:模型的上下文窗口(如 8K, 128K)是以 Token 数量计算的,而不是字符数或单词数。【同样一段话,在不同语言(如中英文)或不同分词器下,Token 数量可能相差巨大】。精确管理输入长度、避免超出上下文限制是构建长时记忆智能体的基础。
API 成本:大多数模型 API 都是按 Token 数量计费的。了解你的文本会被如何分词,是预估和控制智能体运行成本的关键一步。
模型表现的异常:有时模型的奇怪表现根源在于分词。例如,模型可能很擅长计算 2 + 2,但对于 2+2(没有空格)就可能出错,因为后者可能被分词器视为一个独立的、不常见的词元。同样,一个词因为首字母大小写不同,也可能被切分成完全不同的 Token 序列,从而影响模型的理解。在设计提示词和解析模型输出时,考虑到这些“陷阱”有助于提升智能体的鲁棒性。
模型选型的关键考量
性能与能力:这是最核心的考量。不同的模型擅长的任务不同,有的长于逻辑推理和代码生成,有的则在创意写作或多语言翻译上更胜一筹。您可以参考一些公开的基准测试排行榜(如 LMSys Chatbot Arena Leaderboard)来评估模型的综合能力。
成本:对于闭源模型,成本主要体现在 API 调用费用,通常按 Token 数量计费。对于开源模型,成本则体现在本地部署所需的硬件(GPU、内存)和运维上。需要根据应用的预期使用量和预算做出选择。
速度(延迟):对于需要实时交互的智能体(如客服、游戏 NPC),模型的响应速度至关重要。一些轻量级或经过优化的模型(如 GPT-3.5 Turbo, Claude 3.5 Sonnet)在延迟上表现更优。
上下文窗口:模型能一次性处理的 Token 数量上限。对于需要理解长文档、分析代码库或维持长期对话记忆的智能体,选择一个拥有较大上下文窗口(如 128K Token 或更高)的模型是必要的。
部署方式:使用 API 的方式最简单便捷,但数据需要发送给第三方,且受限于服务商的条款。本地部署则能确保数据隐私和最高程度的自主可控,但对技术和硬件要求更高。
生态与工具链:一个模型的流行程度也决定了其周边生态的成熟度。主流模型通常拥有更丰富的社区支持、教程、预训练模型、微调工具和兼容的开发框架(如 LangChain, LlamaIndex, Hugging Face Transformers),这能极大地加速开发进程,降低开发难度。选择一个拥有活跃社区和完善工具链的模型,可以在遇到问题时更容易找到解决方案和资源。
可微调性与定制化:对于需要处理特定领域数据或执行特定任务的智能体,模型的微调能力至关重要。一些模型提供了便捷的微调接口和工具,允许开发者使用自己的数据集对模型进行定制化训练,从而显著提升模型在特定场景下的性能和准确性。开源模型在这方面通常提供更大的灵活性。
安全性与伦理:随着大语言模型的广泛应用,其潜在的安全风险和伦理问题也日益凸显。选择模型时,需要考虑其在偏见、毒性、幻觉等方面的表现,以及服务商或开源社区在模型安全和负责任AI方面的投入。对于面向公众或涉及敏感信息的应用,模型的安全性和伦理合规性是不可忽视的考量。
Scaling Laws
缩放法则(Scaling Laws)是近年来大语言模型领域最重要的发现之一。它揭示了模型性能与模型参数量、训练数据量以及计算资源之间存在着可预测的幂律关系。这一发现为大语言模型的持续发展提供了理论指导,阐明了增加资源投入能够系统性提升模型性能的底层逻辑。
研究发现,在对数-对数坐标系下,模型的性能(通常用损失 Loss 来衡量)与参数量、数据量和计算量这三个因素都呈现出平滑的幂律关系[9]。简单来说,只要我们持续、按比例地增加这三个要素,模型的性能就会可预测地、平滑地提升,而不会出现明显的瓶颈。这一发现为大模型的设计和训练提供了清晰的指导:在资源允许的范围内,尽可能地扩大模型规模和训练数据量。
早期的研究更侧重于增加模型参数量,但 DeepMind 在 2022 年提出的“Chinchilla 定律”对此进行了重要修正[10]。该定律指出,在给定的计算预算下,为了达到最优性能,模型参数量和训练数据量之间存在一个最优配比。具体来说,最优的模型应该比之前普遍认为的要小,但需要用多得多的数据进行训练。例如,一个 700 亿参数的 Chinchilla 模型,由于使用了比 GPT-3(1750 亿参数)多 4 倍的数据进行训练,其性能反而超越了后者。这一发现纠正了“越大越好”的片面认知,强调了数据效率的重要性,并指导了后续许多高效大模型(如 Llama 系列)的设计。
缩放法则最令人惊奇的产物是“能力的涌现”。所谓能力涌现,是指当模型规模达到一定阈值后,会突然展现出在小规模模型中完全不存在或表现不佳的全新能力。例如,链式思考 (Chain-of-Thought) 、指令遵循 (Instruction Following) 、多步推理、代码生成等能力,都是在模型参数量达到数百亿甚至千亿级别后才显著出现的。这种现象表明,大语言模型不仅仅是简单地记忆和复述,它们在学习过程中可能形成了某种更深层次的抽象和推理能力。对于智能体开发者而言,能力的涌现意味着选择一个足够大规模的模型,是实现复杂自主决策和规划能力的前提。
模型幻觉
模型幻觉(Hallucination)通常指的是大语言模型生成的内容与客观事实、用户输入或上下文信息相矛盾,或者生成了不存在的事实、实体或事件。幻觉的本质是模型在生成过程中,过度自信地“编造”了信息,而非准确地检索或推理。根据其表现形式,幻觉可以被分为多种类型[11],例如:
- 事实性幻觉 (Factual Hallucinations) : 模型生成与现实世界事实不符的信息。
- 忠实性幻觉 (Faithfulness Hallucinations) : 在文本摘要、翻译等任务中,生成的内容未能忠实地反映源文本的含义。
- 内在幻觉 (Intrinsic Hallucinations) : 模型生成的内容与输入信息直接矛盾
幻觉的产生是多方面因素共同作用的结果。首先,训练数据中可能包含错误或矛盾的信息。其次,模型的自回归生成机制决定了它只是在预测下一个最可能的词元,而【没有内置的事实核查模块】。最后,在面对需要复杂推理的任务时,模型可能会在逻辑链条中出错,从而“编造”出错误的结论。例如:一个旅游规划 Agent,可能会为你推荐一个现实中不存在的景点,或者预订一个航班号错误的机票。
此外,大语言模型还面临着知识时效性不足和训练数据中存在的偏见等挑战。大语言模型的能力来源于其训练数据。这意味着模型所掌握的知识是其训练数据收集时的最新材料。对于在此日期之后发生的事件、新出现的概念或最新的事实,模型将无法感知或正确回答。与此同时训练数据往往包含了人类社会的各种偏见和刻板印象。当模型在这些数据上学习时,它不可避免地会吸收并反映出这些偏见[12]。
为了提高大语言模型的可靠性,研究人员和开发者正在积极探索多种检测和缓解幻觉的方法:
-
数据层面: 通过高质量数据清洗、引入事实性知识以及强化学习与人类反馈 (RLHF) 等方式[13],从源头减少幻觉。
-
模型层面: 探索新的模型架构,或让模型能够表达其对生成内容的不确定性。
-
推理与生成层面:
- 检索增强生成 (Retrieval-Augmented Generation, RAG) [14]: 这是目前缓解幻觉的有效方法之一。RAG 系统通过在生成之前从外部知识库(如文档数据库、网页)中检索相关信息,然后将检索到的信息作为上下文,引导模型生成基于事实的回答。
- 多步推理与验证: 引导模型进行多步推理,并在每一步进行自我检查或外部验证。
- 引入外部工具: 允许模型调用外部工具(如搜索引擎、计算器、代码解释器)来获取实时信息或进行精确计算。
-
N-gram模型的核心假设是马尔可夫假设。请解释这个假设的含义,以及N-gram模型存在哪些根本性局限?
马尔可夫假设:第 n 个词的出现仅依赖于前 n-1 个词(与更早的词无关)。
N-gram 根本局限:
- 稀疏性:高阶 N-gram 在真实语料中极少出现,导致大量概率为0
- 无法捕捉长距离依赖(如主语在100词前)
- 词汇表爆炸:子词无法泛化(OOV问题)
- 参数效率极低:参数量随 N 指数增长
RNN/LSTM 和 Transformer 如何克服?各自优势
| 模型 | 克服方式 | 优势 |
|---|---|---|
| RNN/LSTM | 用隐藏状态压缩整个历史,理论上可捕捉任意长依赖 | 可变长输入、参数共享 |
| Transformer | 自注意力直接计算任意两位置依赖,摆脱递归 | 并行训练、长距离依赖建模强、训练速度快 |
Transformer架构[4]是现代大语言模型的基础。其中:
提示:可以结合本章3.1.2节的代码实现来辅助理解
自注意力机制(Self-Attention)的核心思想是什么?
思想:让序列中每个位置都能“看到”所有其他位置,通过查询-键匹配动态决定关注哪些信息。解决了 RNN 信息瓶颈和长距离衰减问题。
为什么Transformer能够并行处理序列,而RNN必须串行处理?位置编码(Positional Encoding)在其中起什么作用?
RNN 必须串行:ht依赖ht−1h_t 依赖 h_{t-1}ht依赖ht−1,无法并行
Transformer 并行:自注意力层所有位置同时计算,无递归依赖
位置编码作用:
- 纯注意力机制是排列不变的(permutation invariant)
- 加入 Positional Encoding 注入顺序信息(正弦/学习型)
- 使模型知道“agent 在第2位,works 在第3位”
Decoder-Only架构与完整的Encoder-Decoder架构有什么区别?为什么现在主流的大语言模型都采用Decoder-Only架构?
| 架构 | 特点 | 代表模型 |
|---|---|---|
| Encoder-Decoder | 双向编码 + 自回归解码 | T5, BART |
| Decoder-Only | 仅自回归,统一预训练-微调 | GPT, Llama, Qwen, Grok |
主流采用 Decoder-Only 原因:
- 更适合自回归生成任务(对话、写作、代码)
- 预训练目标统一(next-token prediction)
- 训练数据利用率更高(无需平行语料)
- 推理速度更快(缓存 KV)
为什么不用字符/单词?BPE 解决了什么?
| 单元 | 问题 |
|---|---|
| 字符 | 序列过长(>4096易超上下文)、拼写错误难恢复 |
| 单词 | OOV 问题、词汇表过大(>10万)、稀有词/新词无法处理 |
BPE 解决的问题:
- 合并频繁字节对 → 动态构建子词词汇表
- 平衡词汇量与序列长度(典型 32k~128k)
- 解决 OOV(所有词都能拆成子词)
- 多语言鲁棒(字节级)
