hello-agent

第一章 LLM 基础

1.1 语言模型到 Transformer

1.1.1 N-gram 到 RNN

语言模型 (Language Model, LM) 是自然语言处理的核心,其根本任务是计算一个词序列(即一个句子)出现的概率。

(1)统计语言模型与N-gram的思想

在深度学习兴起之前,统计方法是语言模型的主流。其核心思想是,一个句子出现的概率,等于该句子中每个词出现的条件概率的连乘。对于一个由词 w1,w2,⋯ ,wm 构成的句子 S,其概率 P(S) 可以表示为:

image-20260730003235824

这个公式被称为概率的链式法则。然而,直接计算这个公式几乎是不可能的,因为像 P(wm∣w1,⋯ ,wm−1)这样的条件概率太难从语料库中估计了,词序列 w1,⋯ ,wm−1可能从未在训练数据中出现过。

为了解决这个问题,研究者引入了马尔可夫假设 (Markov Assumption) 。其核心思想是:我们不必回溯一个词的全部历史,可以近似地认为,一个词的出现概率只与它前面有限的 n−1个词有关。基于这个假设建立的语言模型,我们称之为 N-gram模型。这里的 “N” 代表我们考虑的上下文窗口大小。

  • Bigram (当 N=2 时) :这是最简单的情况,我们假设一个词的出现只与它前面的一个词有关。
  • Trigram (当 N=3 时) :类似地,我们假设一个词的出现只与它前面的两个词有关

这些概率可以通过在大型语料库中进行最大似然估计(Maximum Likelihood Estimation,MLE) 来计算。

N-gram 模型虽然简单有效,但有两个致命缺陷:

  1. 数据稀疏性 (Sparsity) :如果一个词序列从未在语料库中出现,其概率估计就为 0,这显然是不合理的。虽然可以通过平滑 (Smoothing) 技术缓解,但无法根除。
  2. 泛化能力差:模型无法理解词与词之间的语义相似性。例如datawhale agent learns, datawhale agent works。,即使模型在语料库中见过很多次 agent learns,它也无法将这个知识泛化到语义相似的词上。当我们计算 robot learns 的概率时,如果 robot 这个词从未出现过,或者 robot learns 这个组合从未出现过,模型计算出的概率也会是零。模型无法理解 agent 和 robot 在语义上的相似性。

(2)神经网络语言模型与词嵌入

N-gram 模型的根本缺陷在于它将词视为孤立、离散的符号。研究者们转向了神经网络,并提出了一种思想:用连续的向量来表示词。2003年,Bengio 等人提出的前馈神经网络语言模型 (Feedforward Neural Network Language Model) 是这一领域的里程碑。

其核心思想可以分为两步:

  1. 构建一个语义空间:创建一个高维的连续向量空间,然后将词汇表中的每个词都映射为该空间中的一个点。这个点(即向量)就被称为词嵌入 (Word Embedding) 或词向量。在这个空间里,语义上相近的词,它们对应的向量在空间中的位置也相近。
  2. 学习从上下文到下一个词的映射:利用神经网络的强大拟合能力,来学习一个函数。这个函数的输入是前 n−1个词的词向量,输出是词汇表中每个词在当前上下文后出现的概率分布。

一旦我们将词转换成了向量,我们就可以用数学工具来度量它们之间的关系。最常用的方法是余弦相似度 (Cosine Similarity) ,它通过计算两个向量夹角的余弦值来衡量它们的相似性。通过这种方式,词向量不仅能捕捉到“同义词”这类简单的关系,还能捕捉到更复杂的类比关系。

神经网络语言模型通过词嵌入,成功解决了 N-gram 模型的泛化能力差的问题。然而,它仍然有一个类似 N-gram 的限制:上下文窗口是固定的。它只能考虑固定数量的前文,这为能处理任意长序列的循环神经网络埋下了伏笔。

(3)循环神经网络 (RNN) 与长短时记忆网络 (LSTM)

神经网络语言模型虽然引入了词嵌入解决了泛化问题,但它和 N-gram 模型一样,上下文窗口是固定大小的。为了预测下一个词,它只能看到前 n−1 个词,再早的历史信息就被丢弃了。为了打破固定窗口的限制,循环神经网络 (Recurrent Neural Network, RNN) 应运而生,其核心思想非常直观:为网络增加“记忆”能力.

RNN 的设计引入了一个隐藏状态 (hidden state) 向量,我们可以将其理解为网络的短期记忆。在处理序列的每一步,网络都会读取当前的输入词,并结合它上一刻的记忆(即上一个时间步的隐藏状态),然后生成一个新的记忆(即当前时间步的隐藏状态)传递给下一刻。这个循环往复的过程,使得信息可以在序列中不断向后传递。

image-20260730010951424

标准的 RNN 在实践中存在一个严重的问题:长期依赖问题 (Long-term Dependency Problem) 。在训练过程中,模型需要通过反向传播算法根据输出端的误差来调整网络深处的权重。对于 RNN 而言,序列的长度就是网络的深度。当序列很长时,梯度在从后向前传播的过程中会经过多次连乘,这会导致梯度值快速趋向于零(梯度消失)或变得极大(梯度爆炸)。

梯度消失使得模型无法有效学习到序列早期信息对后期输出的影响,即难以捕捉长距离的依赖关系。

为了解决长期依赖问题,长短时记忆网络 (Long Short-Term Memory, LSTM) 被设计出来。

LSTM 是一种特殊的 RNN,其核心创新在于引入了细胞状态 (Cell State) 和一套精密的门控机制 (Gating Mechanism) 。细胞状态可以看作是一条独立于隐藏状态的信息通路,允许信息在时间步之间更顺畅地传递。门控机制则是由几个小型神经网络构成,它们可以学习如何有选择地让信息通过,从而控制细胞状态中信息的增加与移除。这些门包括:

  • 遗忘门 (Forget Gate):决定从上一时刻的细胞状态中丢弃哪些信息。
  • 输入门 (Input Gate):决定将当前输入中的哪些新信息存入细胞状态。
  • 输出门 (Output Gate):决定根据当前的细胞状态,输出哪些信息到隐藏状态。

1.1.2 Transformer 架构

RNN及LSTM通过引入循环结构来处理序列数据,这在一定程度上解决了捕捉长距离依赖的问题。然而,这种循环的计算方式也带来了新的瓶颈:它必须按顺序处理数据。第 t 个时间步的计算,必须等待第 t−1 个时间步完成后才能开始。这意味着 RNN 无法进行大规模的并行计算,在处理长序列时效率低下,这极大地限制了模型规模和训练速度的提升。

Transformer在2017 年由谷歌团队提出。它完全抛弃了循环结构,转而完全依赖一种名为注意力 (Attention) 的机制来捕捉序列内的依赖关系,从而实现了真正意义上的并行计算。

(1)Encoder-Decoder 整体结构

最初的 Transformer 模型是为端到端任务机器翻译而设计的。在宏观上遵循了一个经典的编码器-解码器 (Encoder-Decoder) 架构。

image-20260730013039882

可以将这个结构理解为一个分工明确的团队:

  1. 编码器 (Encoder) :任务是“理解”输入的整个句子。它会读取所有输入词元,最终为每个词元生成一个富含上下文信息的向量表示。
  2. 解码器 (Decoder) :任务是“生成”目标句子。它会参考自己已经生成的前文,并“咨询”编码器的理解结果,来生成下一个词。

(2)从自注意力到多头注意力

我们阅读句子:“The agent learns because it is intelligent.” 当读到 it 时,为了理解它指代什么,我们会重点关注前面的 agent。自注意力(Self-Attention)就是对这一过程的数学建模:它让每个词从句子中其他相关词那里获取信息,生成一个融合上下文的新表示。

为了完成这个过程,模型会把每个词元的当前向量分别转换成Q、K、V:

  • Q(Query)表示当前词想寻找什么信息。
  • K(Key)表示每个词能够提供什么类型的信息。
  • V(Value)表示每个词实际要传递的内容。

它们的计算方式是:

1
2
3
Q = X × W_Q
K = X × W_K
V = X × W_V
  • X 表示当前自注意力层接收到的所有词元向量。它不是额外生成的固定矩阵,而是由当前输入句子以及前面各层的计算结果决定的。

对于第一层 Transformer,模型首先将句子拆分成词元,并把每个词元转换成对应的ID,再从训练得到的词嵌入矩阵中查出向量。随后加入位置信息,使模型能够区分词元在句子中的先后顺序。最后,将所有词元向量排列起来,组成矩阵 X:

1
句子 → 词元 → 词元ID → 词嵌入 → 加入位置信息 → X

第一层之后,后续层使用的 X 是上一层输出的上下文表示:

1
2
3
第一层的 X = 词嵌入 + 位置信息
第二层的 X = 第一层的输出
第三层的 X = 第二层的输出

因此,X 可以理解为每个词元在进入当前层时“已经掌握的信息”。模型再通过三个训练得到的权重矩阵,从 X 中生成Q、K、V:

1
2
3
Q = X × W_Q
K = X × W_K
V = X × W_V

随着层数增加,X 中包含的上下文信息越来越丰富,所以同一个词在不同句子或不同层中,也可能得到不同的Q、K、V。

  • W_Q、W_K 和 W_V 是模型在训练过程中通过反向传播学习得到的权重矩阵,模型训练完成后通常不再改变。Q、K、V则不是每个词永久固定的向量,而是根据词元当前的表示,在每一层、每个注意力头中重新计算。因此,同一个词出现在不同句子或不同层中,可能得到不同的Q、K、V。

自注意力的计算过程可以分为以下几步:

  • 首先,利用权重矩阵为每个词生成Q、K、V。
  • 然后,用当前词的Q与所有可见词的K做点积,判断它们与当前词的相关程度。
  • 接着,将分数除以 √d_k,避免数值过大,再通过Softmax转换成总和为1的注意力权重。
  • 最后,用这些注意力权重分别乘以各个词的V,并将结果相加,得到当前词融合上下文后的新表示。

整个过程可以简写为:

1
2
attention_weights = softmax((Q × Kᵀ) / √d_k)
output = attention_weights × V

之所以最后用权重乘以V,是因为Q和K只负责判断“应该关注谁”,而V才是每个词真正要传递的信息。可以简单记为:

Q负责提问,K负责匹配,注意力权重决定看多少,V负责提供实际内容。

这个过程可以用一个简洁的公式来概括:

image-20260730013743924

如果只进行一次上述的注意力计算(即单头),模型可能会只学会关注一种类型的关联。比如,在处理 “it” 时,可能只学会了关注主语。

但语言中的关系是复杂的,我们希望模型能同时关注多种关系(如指代关系、时态关系、从属关系等)。多头注意力机制应运而生。它的思想很简单:把一次做完变成分成几组,分开做,再合并。

多头注意力并不是简单地把原始词向量机械切成几份。更准确地说,每个头都有自己的一组权重矩阵,负责将完整的输入 X 投影到一个较低维的表示空间中:

1
2
3
Qᵢ = X × W_Qᵢ
Kᵢ = X × W_Kᵢ
Vᵢ = X × W_Vᵢ

因此,每个头都能读取完整的输入,只是会用不同的方式提取信息。例如,一个头可能更关注指代关系,另一个头可能更关注位置或语法关系。这些分工不是人为指定的,而是模型在训练过程中自动学习形成的,也可能存在部分功能相似的头。

在代码实现中,通常不会为每个头分别执行一次矩阵乘法,而是使用三个大矩阵一次生成完整的Q、K、V,再把结果重新排列成多个头。这在数学上等价于为每个头使用独立的小型权重矩阵,但计算效率更高。

每个头会独立完成一次注意力计算:

1
headᵢ = softmax((Qᵢ × Kᵢᵀ) / √d_head) × Vᵢ

虽然在Softmax之前,完整的 Q × Kᵀ 可以看成各部分点积结果之和,但多头注意力不会先把这些结果相加,而是让每个头分别执行Softmax。这样,每个头都能生成自己独立的注意力权重,并关注不同的词元。如果先相加再统一执行Softmax,这些不同的关注方式就可能被混合或抵消。

最后,将各个头的输出进行拼接:

1
Z = Concat(head₁, head₂, ..., headₕ)

Concat表示将各头的向量首尾连接起来,不是相加或取平均。拼接后,再通过输出权重矩阵 W_O 进行融合:

1
M = Z × W_O

其中,W_O中的 O 表示Output,即输出。它也是训练得到的权重矩阵,主要有两个作用:

  • 学习如何组合不同头提供的信息;
  • 使最终输出恢复为 d_model 维,方便传入后面的网络和下一层Transformer。

W_O不会把所有头简单平均,而是学习哪些信息应该保留、组合或削弱。各头的编号虽然不会继续保留,但它们已经选取的信息会进入最终输出。

M是多头注意力的最终输出,它与当前层输入 X 的形状相同。多头注意力的输出不会直接作为下一层的 X,还要依次经过残差连接、层归一化和前馈神经网络。

前馈神经网络

在每个编码器和解码器层中,多头注意力之后都会连接一个逐位置前馈神经网络(Position-wise Feed-Forward Network,FFN)。

多头注意力负责让不同词元之间交换和聚合信息,而前馈神经网络负责进一步处理每个词元内部的特征。可以简单理解为:

注意力负责“从其他词中收集信息”,前馈网络负责“加工已经收集到的信息”。

“逐位置”表示FFN会分别处理每个词元向量,不会在这一步重新计算词元之间的关系。假设序列中有10个词元,FFN会对10个词元分别执行同样的计算,并且这些位置共享同一组权重。

原始Transformer中的FFN由两个线性变换和一个ReLU激活函数组成:

1
FFN(x) = ReLU(x × W₁ + b₁) × W₂ + b₂

其中:

  • W₁和W₂是训练得到的权重矩阵。
  • b₁和b₂是训练得到的偏置,用于对计算结果进行额外调整。
  • ReLU是激活函数,它会将负数变为0,正数保持不变,从而让网络具有处理复杂非线性关系的能力。

FFN通常会先扩大词元向量的维度,再恢复原来的维度。例如原始Transformer中:

1
2
3
4
输入:512维
经过W₁:扩展为2048维
经过ReLU
经过W₂:恢复为512维

同一层中的所有词元共享同一组 W₁、W₂、b₁、b₂,但不同Transformer层拥有各自的FFN参数。

残差连接与层归一化

在原始Transformer中,多头注意力和FFN之后都会分别进行一次 Add & Norm。

Add表示残差连接,即把子模块的输入直接加到它的输出上。

多头注意力之后:

1
X + M

其中:

  • X是多头注意力原来的输入。
  • M是多头注意力计算得到的输出。

残差连接能够保留原始信息,并为梯度提供更直接的传播路径,从而缓解深层网络训练困难和梯度消失问题。

Norm表示层归一化,也就是LayerNorm。它会分别调整每个词元向量中各个特征的数值范围,使训练过程更加稳定。这里归一化的是一个词元的所有特征维度,而不是把句子中所有词元混在一起归一化。

因此,多头注意力之后的第一次 Add & Norm 可以写成:

1
Y = LayerNorm(X + M)

其中,Y是进入FFN的词元表示。

接下来进行FFN计算:

1
F = FFN(Y)

FFN之后再进行一次残差连接和层归一化:

1
X_next = LayerNorm(Y + F)

其中,X_next就是当前Transformer层的最终输出,也会成为下一层的输入 X。

完整关系为:

1
2
3
4
5
6
7
M = MultiHeadAttention(X)

Y = LayerNorm(X + M)

F = FFN(Y)

X_next = LayerNorm(Y + F)

需要注意,以上是原始Transformer采用的顺序。一些现代模型会先进行LayerNorm,再进入注意力或FFN,但多头注意力、残差连接和FFN的主要作用没有改变。

位置编码

位置编码发生在词元进入第一层Transformer之前,并不是在每个注意力层之后重新添加。

自注意力主要根据词元内容计算它们之间的关系,本身没有直接表示词元先后顺序的结构。如果没有位置信息,模型就很难区分:

1
agent learns

和:

1
learns agent

因此,模型会为每个词元加入一个表示其位置的向量:

1
第一层输入X = 词元嵌入 + 位置编码

其中:

  • 词元嵌入表示“这个词元是什么”。
  • 位置编码表示“这个词元位于什么位置”。

原始Transformer使用固定的正弦和余弦函数生成位置编码:

1
2
PE(pos, 2i)     = sin(pos / 10000^(2i / d_model))
PE(pos, 2i + 1) = cos(pos / 10000^(2i / d_model))

其中:

  • pos表示词元在序列中的位置,例如0、1、2、3。
  • i表示位置向量中的维度编号。
  • d_model表示词元向量的总维度。
  • 2i表示偶数维度,使用正弦函数。
  • 2i + 1表示奇数维度,使用余弦函数。

不同位置会得到不同的位置向量,因此即使两个位置出现相同的词元,它们最终进入Transformer的向量也会不同。

需要注意,“位置编码一定是固定公式生成的”只适用于原始Transformer。后来的模型也可能使用可训练的位置向量,或者使用RoPE等其他位置表示方法。

一个完整的编码器层

第一层编码器开始之前:

1
2
3
4
5
句子
→ 拆分为词元
→ 查找词元嵌入
→ 加入位置编码
→ 得到第一层输入X

进入一个完整的编码器层后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. 根据X生成各头的Q、K、V

2. 每个头独立计算注意力

3. 拼接各头输出,并使用W_O融合
M = MultiHeadAttention(X)

4. 第一次残差连接和层归一化
Y = LayerNorm(X + M)

5. 逐位置前馈网络
F = FFN(Y)

6. 第二次残差连接和层归一化
X_next = LayerNorm(Y + F)

7. 将X_next传给下一层

1.1.3 Decoer-Only 架构

前面一节中的完整的 Transformer 最初采用 Encoder-Decoder 架构,主要面向机器翻译等“输入一个序列,再生成另一个序列”的任务。编码器负责理解输入,解码器则读取编码器的结果并生成输出。

GPT采用了更简洁的 Decoder-Only 架构。它去掉了编码器以及读取编码器输出的交叉注意力,只保留带有因果限制的自注意力和前馈神经网络。它采用一个统一的训练目标:

根据已经出现的词元,预测下一个词元。

这里预测的是词元(Token),不一定是完整的单词,也可能是一个汉字、单词的一部分或标点符号。

Decoder-Only 的基本结构

Decoder-Only 模型首先将输入转换成词元向量并加入位置信息,然后连续经过多层相同结构的 Transformer 层:

1
2
3
4
5
输入文本
→ 拆分成词元
→ 词元嵌入与位置信息
→ 多层Decoder-Only Transformer
→ 输出下一个词元的概率

每一层的主要结构是:

1
2
3
4
5
6
输入X
→ 因果多头自注意力
→ 残差连接与层归一化
→ 前馈神经网络FFN
→ 残差连接与层归一化
→ 下一层输入X_next

其中,因果多头自注意力负责从前文中读取相关信息,FFN负责进一步加工每个词元已经获得的特征。不同模型可能会调整层归一化的位置,但基本数据流保持不变。

与原始Transformer解码器相比,Decoder-Only层中没有读取编码器输出的交叉注意力,因为它根本没有单独的编码器。用户输入、历史对话和模型已经生成的内容会被放在同一段词元序列中,由因果自注意力统一处理。

自回归生成

Decoder-Only采用自回归(Autoregressive)方式生成文本。“自回归”表示模型每次根据已有内容生成一个新词元,再把这个词元加入原序列,继续预测下一个词元。

例如给模型输入:

1
Datawhale Agent is

生成过程可能是:

1
2
3
4
5
6
7
8
Datawhale Agent is
→ 预测 a

Datawhale Agent is a
→ 预测 powerful

Datawhale Agent is a powerful
→ 继续预测下一个词元

模型会不断重复这个过程,直到生成结束标记、满足其他停止条件或达到最大长度。

这种工作方式类似文字接龙:模型每次回顾已经出现的全部内容,并预测接下来最可能出现的词元。

训练时的下一个词元预测

训练时,模型通常会一次读入一整段文本,但训练目标会向后错开一个位置。

假设训练文本为:

1
Datawhale Agent is a powerful tool

模型需要学习:

1
2
3
4
5
6
7
8
9
10
11
看到 Datawhale
→ 预测 Agent

看到 Datawhale Agent
→ 预测 is

看到 Datawhale Agent is
→ 预测 a

看到 Datawhale Agent is a
→ 预测 powerful

虽然这些位置具有前后依赖关系,但训练时可以在一次计算中同时得到所有位置的预测结果。为了防止前面的词元看到后面的正确答案,模型会使用因果掩码。

因果掩码自注意力

因果掩码自注意力也称为掩码自注意力(Masked Self-Attention)。它规定:

第 t 个位置只能关注第1个到第 t 个位置,不能关注第 t+1 个以及更后面的词元。

模型首先正常计算注意力分数:

1
scores = (Q × Kᵀ) / √d_head

然后在Softmax之前加入因果掩码。以4个词元为例,掩码可以表示为:

1
2
3
4
5
6
[
[0, -∞, -∞, -∞],
[0, 0, -∞, -∞],
[0, 0, 0, -∞],
[0, 0, 0, 0]
]

其中:

  • 0表示允许关注这个位置;
  • -∞表示禁止关注这个位置。

掩码会被加到原来的注意力分数上:

1
2
masked_scores = scores + causal_mask
attention_weights = softmax(masked_scores)

由于 -∞ 经过Softmax后对应的权重为0,所以未来词元不会对当前位置产生影响。

例如:

1
2
3
4
第1个词元:只能看自己
第2个词元:可以看第1、2个词元
第3个词元:可以看第1、2、3个词元
第4个词元:可以看第1、2、3、4个词元

在位置 t 得到的向量只包含当前位置及其前文的信息,因此可以用它预测位置 t+1 的词元。

训练阶段虽然输入了完整文本,但因果掩码阻止模型偷看后文;生成阶段未来词元还没有产生,模型同样只能使用已有内容。这样就保证了训练方式和实际生成方式一致。

从隐藏向量到下一个词元

经过最后一层Transformer后,每个位置都会得到一个包含前文信息的向量。模型使用最后一个位置的向量预测下一个词元。

这个向量会经过输出矩阵,被转换成与词表大小相同的一组分数:

1
2
3
4
5
最后一个位置的向量
→ 输出矩阵
→ 每个候选词元的分数
→ Softmax
→ 每个词元的概率

模型可以选择概率最高的词元,也可以按照概率进行采样。选出的词元会被添加到输入末尾,然后模型继续生成。

因此,Decoder-Only的完整工作流程是:

1
2
3
4
5
6
7
8
已有词元序列
→ 词元嵌入与位置信息
→ 多层因果自注意力和FFN
→ 得到最后位置的上下文向量
→ 计算词表中各词元的概率
→ 选择下一个词元
→ 添加到原序列
→ 重复以上过程

Decoder-Only 的优势

  • 训练目标统一:所有文本都可以转换成下一个词元预测任务,适合使用海量文本进行自监督预训练,不需要人工逐条添加标签。
  • 结构相对简洁:去掉独立编码器和交叉注意力后,模型主要由重复的Decoder-Only层组成,更方便进行规模扩展。
  • 天然适合生成:对话、写作和代码生成都可以表示为“根据已有内容继续生成”。
  • 输入与输出形式统一:问题、指令、历史对话和模型回答都可以放在同一个词元序列中处理。
  • 适合作为智能体的基础模型:模型不仅可以生成自然语言,还可以生成结构化的工具调用或操作指令。不过,完整智能体通常还需要工具、记忆和任务执行系统配合。

Decoder-Only并不是在所有任务上都一定优于其他结构。Encoder-Only模型更适合生成双向上下文表示,Encoder-Decoder模型在某些明确的输入到输出任务中也有优势。Decoder-Only的核心价值在于,它通过统一的下一个词元预测目标,将理解和生成都转化为“根据已有上下文继续文本”这一过程。

总的来说:

Decoder-Only使用因果自注意力读取已有文本,通过连续预测下一个词元完成对话、写作和代码生成等任务。它结构统一、训练目标简单,因此成为当前生成式大语言模型常用的基础架构。

1.2 与 LLM 交互

整个生成流程:模型不是直接输出下一个词元,而是先给词表中的每个候选词元一个分数,然后再根据这些分数选择一个词元。

1
2
3
4
5
6
7
8
9
当前文本
→ 模型计算所有候选词元的分数
→ Temperature调整分数差距
→ Softmax转换为概率
→ Top-k或Top-p筛选候选词元
→ 重新归一化
→ 按概率抽取一个词元
→ 添加到文本末尾
→ 继续预测

1.2.1 提示工程

(1)模型采样参数

  • **Temperature**:温度是控制模型输出 “随机性” 与 “确定性” 的关键参数。其原理是引入温度系数T>0,将 Softmax 改写为image-20260801010622197当T变小时,分布“更加陡峭”,高概率项权重进一步放大,生成更“保守”且重复率更高的文本。当T变大时,分布“更加平坦”,低概率项权重提升,生成更“多样”但可能出现不连贯的内容。
    • 低温度(0 ⩽ Temperature < 0.3)时输出更 “精准、确定”。比如T = 0.1,生成的词表概率:logits = [0.99, 0.002, 0.001]
    • 中温度(0.3 ⩽ Temperature < 0.7):输出 “平衡、自然”。T = 1,logits = [0.844, 0.114, 0.042]
    • 高温度(0.7 ⩽ Temperature < 2):输出 “创新、发散”。T = 2,logits = [0.629, 0.231, 0.140],概率差拉小了
  • **Top-k** :其原理是将所有 token 按概率从高到低排序,取排名前 k 个的 token 组成 “候选集”,随后对筛选出的 k 个 token 的概率进行 “归一化”:image-20260801010646900,比如有8个词和概率生成了,Top-k4只取概率高的前四个词进行归一化。
    • 与温度采样的区别与联系:温度采样通过温度 T 调整所有 token 的概率分布(平滑或陡峭),不改变候选 token 的数量(仍考虑全部 N 个)。Top-k 采样通过 k 值限制候选 token 的数量(只保留前 k 个高概率 token),再从其中采样。当k=1时输出完全确定,退化为 “贪心采样”。
  • **Top-p** :其原理是将所有 token 按概率从高到低排序,从排序后的第一个 token 开始,逐步累加概率,直到累积和首次达到或超过阈值 p,此时累加过程中包含的所有 token 组成 “核集合”,最后对核集合进行归一化。比如,从高到低加起来概率有0.9,就去归一化。
    • 与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。

(2) 零样本、单样本与少样本提示

  • 零样本提示 (Zero-shot Prompting) 这指的是我们不给模型任何示例,直接让它根据指令完成任务。这得益于模型在海量数据上预训练后获得的强大泛化能力。
  • 单样本提示 (One-shot Prompting) 我们给模型提供一个完整的示例,向它展示任务的格式和期望的输出风格。
  • 少样本提示 (Few-shot Prompting) 我们提供多个示例,这能让模型更准确地理解任务的细节、边界和细微差别,从而获得更好的性能。

(3) 指令调优的影响

早期的 GPT 模型(如 GPT-3)主要是“文本补全”模型,它们擅长根据前面的文本续写,但不一定能很好地理解并执行人类的指令。

指令调优 (Instruction Tuning) 是一种微调技术,它使用大量“指令-回答”格式的数据对预训练模型进行进一步的训练。经过指令调优后,模型能更好地理解并遵循用户的指令。

(4)基础提示技巧

  • 角色扮演 (Role-playing) 通过赋予模型一个特定的角色,我们可以引导它的回答风格、语气和知识范围,使其输出更符合特定场景的需求。
  • 上下文示例 (In-context Example) 这与少样本提示的思想一致,通过在提示中提供清晰的输入输出示例,来“教会”模型如何处理我们的请求,尤其是在处理复杂格式或特定风格的任务时非常有效。

(5)思维链

对于需要逻辑推理、计算或多步骤思考的复杂问题,直接让模型给出答案往往容易出错。思维链 (Chain-of-Thought, CoT) 是一种强大的提示技巧,它通过引导模型“一步一步地思考”,提升了模型在复杂任务上的推理能力。

1.2.2 文本分词

在将自然语言文本喂给大语言模型之前,必须先将其转换成模型能够处理的数字格式。这个将文本序列转换为数字序列的过程,就叫做分词 (Tokenization) 。分词器 (Tokenizer) 的作用,就是定义一套规则,将原始文本切分成一个个最小的单元,我们称之为词元 (Token) 。

为了兼顾词表大小和语义表达,现代大语言模型普遍采用子词分词 (Subword Tokenization) 算法。它的核心思想是:将常见的词(如 “agent”)保留为完整的词元,同时将不常见的词(如 “Tokenization”)拆分成多个有意义的子词片段(如 “Token” 和 “ization”)。这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词。

字节对编码 (Byte-Pair Encoding, BPE) 是最主流的子词分词算法之一[6],GPT系列模型就采用了这种算法。其核心思想非常简洁,可以理解为一个“贪心”的合并过程:

  1. 初始化:将词表初始化为所有在语料库中出现过的基本字符。
  2. 迭代合并:在语料库上,统计所有相邻词元对的出现频率,找到频率最高的一对,将它们合并成一个新的词元,并加入词表。
  3. 重复:重复第 2 步,直到词表大小达到预设的阈值。

后续的许多算法都是在BPE的基础上进行优化的。其中,Google 开发的 WordPiece 和 SentencePiece 是影响力最大的两种。

  • WordPiece:Google BERT 模型采用的算法。它与 BPE 非常相似,但合并词元的标准不是“最高频率”,而是“能最大化提升语料库的语言模型概率”。简单来说,它会优先合并那些能让整个语料库的“通顺度”提升最大的词元对。
  • SentencePiece:Google 开源的一款分词工具,Llama 系列模型采用了此算法。它最大的特点是,将空格也视作一个普通字符(通常用下划线 _ 表示)。这使得分词和解码过程完全可逆,且不依赖于特定的语言(例如,它不需要知道中文不使用空格分词)。

1.3 LLM 缩放法则与局限性

1.3.1 缩放法则

缩放法则(Scaling Laws)揭示了模型性能与模型参数量、训练数据量以及计算资源之间存在着可预测的幂律关系。

研究发现,在对数-对数坐标系下,模型的性能(通常用损失 Loss 来衡量)与参数量、数据量和计算量这三个因素都呈现出平滑的幂律关系。简单来说,只要我们持续、按比例地增加这三个要素,模型的性能就会可预测地、平滑地提升,而不会出现明显的瓶颈。这一发现为大模型的设计和训练提供了清晰的指导:在资源允许的范围内,尽可能地扩大模型规模和训练数据量。

该定律指出,在给定的计算预算下,为了达到最优性能,模型参数量和训练数据量之间存在一个最优配比。具体来说,最优的模型应该比之前普遍认为的要小,但需要用多得多的数据进行训练。例如,一个 700 亿参数的 Chinchilla 模型,由于使用了比 GPT-3(1750 亿参数)多 4 倍的数据进行训练,其性能反而超越了后者。

缩放法则最令人惊奇的产物是“能力的涌现”。所谓能力涌现,是指当模型规模达到一定阈值后,会突然展现出在小规模模型中完全不存在或表现不佳的全新能力。例如,链式思考 (Chain-of-Thought) 、指令遵循 (Instruction Following) 、多步推理、代码生成等能力,都是在模型参数量达到数百亿甚至千亿级别后才显著出现的。这种现象表明,大语言模型不仅仅是简单地记忆和复述,它们在学习过程中可能形成了某种更深层次的抽象和推理能力。

1.3.2 模型幻觉

模型幻觉(Hallucination)通常指的是大语言模型生成的内容与客观事实、用户输入或上下文信息相矛盾,或者生成了不存在的事实、实体或事件。幻觉的本质是模型在生成过程中,过度自信地“编造”了信息,而非准确地检索或推理。根据其表现形式,幻觉可以被分为多种类型:

  • 事实性幻觉 (Factual Hallucinations) : 模型生成与现实世界事实不符的信息。
  • 忠实性幻觉 (Faithfulness Hallucinations) : 在文本摘要、翻译等任务中,生成的内容未能忠实地反映源文本的含义。
  • 内在幻觉 (Intrinsic Hallucinations) : 模型生成的内容与输入信息直接矛盾。

幻觉的产生是多方面因素共同作用的结果。

  1. 首先,训练数据中可能包含错误或矛盾的信息。
  2. 其次,模型的自回归生成机制决定了它只是在预测下一个最可能的词元,而没有内置的事实核查模块。
  3. 最后,在面对需要复杂推理的任务时,模型可能会在逻辑链条中出错,从而“编造”出错误的结论。

此外,大语言模型还面临着知识时效性不足和训练数据中存在的偏见等挑战。

为了提高大语言模型的可靠性,研究人员和开发者正在积极探索多种检测和缓解幻觉的方法:

  1. 数据层面: 通过高质量数据清洗、引入事实性知识以及强化学习与人类反馈 (RLHF) 等方式,从源头减少幻觉。
  2. 模型层面: 探索新的模型架构,或让模型能够表达其对生成内容的不确定性。
  3. 推理与生成层面:
    1. 检索增强生成 (Retrieval-Augmented Generation, RAG) : 这是目前缓解幻觉的有效方法之一。RAG 系统通过在生成之前从外部知识库(如文档数据库、网页)中检索相关信息,然后将检索到的信息作为上下文,引导模型生成基于事实的回答。
    2. 多步推理与验证: 引导模型进行多步推理,并在每一步进行自我检查或外部验证。
    3. 引入外部工具: 允许模型调用外部工具(如搜索引擎、计算器、代码解释器)来获取实时信息或进行精确计算。

第二章 智能体经典范式构建

2.1 ReAct

2.1.1 ReAct 的工作流程

在ReAct诞生之前,主流的方法可以分为两类:

  1. “纯思考”型,如思维链 (Chain-of-Thought),它能引导模型进行复杂的逻辑推理,但无法与外部世界交互,容易产生事实幻觉;
  2. “纯行动”型,模型直接输出要执行的动作,但缺乏规划和纠错能力。

ReAct的巧妙之处在于,它认识到思考与行动是相辅相成的。思考指导行动,而行动的结果又反过来修正思考。

为此,ReAct范式通过一种特殊的提示工程来引导模型,使其每一步的输出都遵循一个固定的轨迹:

  • Thought (思考): 这是智能体的“内心独白”。它会分析当前情况、分解任务、制定下一步计划,或者反思上一步的结果。
  • Action (行动): 这是智能体决定采取的具体动作,通常是调用一个外部工具,例如 Search['华为最新款手机']。
  • Observation (观察): 这是执行Action后从外部工具返回的结果,例如搜索结果的摘要或API的返回值。

智能体将不断重复这个 Thought -> Action -> Observation 的循环,将新的观察结果追加到历史记录中,形成一个不断增长的上下文,直到它在Thought中认为已经找到了最终答案,然后输出结果。这个过程形成了一个强大的协同效应:推理使得行动更具目的性,而行动则为推理提供了事实依据。

2.1.2 工具的定义与实现

如果说大语言模型是智能体的大脑,那么工具 (Tools) 就是其与外部世界交互的“手和脚”。

实现工具的核心逻辑

  1. 名称 (Name): 一个简洁、唯一的标识符,供智能体在 Action 中调用,例如 Search。
  2. 描述 (Description): 一段清晰的自然语言描述,说明这个工具的用途。这是整个机制中最关键的部分,因为大语言模型会依赖这段描述来判断何时使用哪个工具。
  3. 执行逻辑 (Execution Logic): 真正执行任务的函数或方法。

2.1.3 ReAct智能体的实现

(1)系统提示词设计

提示词是整个 ReAct 机制的基石,它为大语言模型提供了行动的操作指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ReAct 提示词模板
REACT_PROMPT_TEMPLATE = """
请注意,你是一个有能力调用外部工具的智能助手。

可用工具如下:
{tools}

请严格按照以下格式进行回应:

Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
- 当你收集到足够的信息,能够回答用户的最终问题时,你必须在Action:字段后使用 Finish[最终答案] 来输出最终答案。

现在,请开始解决以下问题:
Question: {question}
History: {history}
"""

这个模板定义了智能体与LLM之间交互的规范:

  • 角色定义: “你是一个有能力调用外部工具的智能助手”,设定了LLM的角色。
  • 工具清单 ({tools}): 告知LLM它有哪些可用的“手脚”。
  • 格式规约 (Thought/Action): 这是最重要的部分,它强制LLM的输出具有结构性,使我们能通过代码精确解析其意图。
  • 动态上下文 ({question}/{history}): 将用户的原始问题和不断累积的交互历史注入,让LLM基于完整的上下文进行决策。

(2)核心循环的实现

ReActAgent 的核心是一个循环,它不断地“格式化提示词 -> 调用LLM -> 执行动作 -> 整合结果”,直到任务完成或达到最大步数限制。

(3)输出解析器的实现

LLM 返回的是纯文本,我们需要从中精确地提取出Thought和Action。这是通过几个辅助解析函数完成的,它们通常使用正则表达式来实现。

(4) 工具调用与执行

这是Action的执行中心。它首先检查是否为Finish指令,如果是,则流程结束。否则,它会通过tool_executor获取对应的工具函数并执行,得到observation。

(5)观测结果的整合

最后一步,也是形成闭环的关键,是将Action本身和工具执行后的Observation添加回历史记录中,为下一轮循环提供新的上下文。

2.1.4 ReAct的特点、局限性与调试技巧

(1)ReAct 的主要特点

  1. 高可解释性:ReAct 最大的优点之一就是透明。通过 Thought 链,我们可以清晰地看到智能体每一步的“心路历程”——它为什么会选择这个工具,下一步又打算做什么。这对于理解、信任和调试智能体的行为至关重要。
  2. 动态规划与纠错能力:与一次性生成完整计划的范式不同,ReAct 是“走一步,看一步”。它根据每一步从外部世界获得的 Observation 来动态调整后续的 Thought 和 Action。如果上一步的搜索结果不理想,它可以在下一步中修正搜索词,重新尝试。
  3. 工具协同能力:ReAct 范式天然地将大语言模型的推理能力与外部工具的执行能力结合起来。LLM 负责运筹帷幄(规划和推理),工具负责解决具体问题(搜索、计算),二者协同工作,突破了单一 LLM 在知识时效性、计算准确性等方面的固有局限。

(2)ReAct 的固有局限性

  1. 对LLM自身能力的强依赖:ReAct 流程的成功与否,高度依赖于底层 LLM 的综合能力。如果 LLM 的逻辑推理能力、指令遵循能力或格式化输出能力不足,就很容易在 Thought 环节产生错误的规划,或者在 Action 环节生成不符合格式的指令,导致整个流程中断。
  2. 执行效率问题:由于其循序渐进的特性,完成一个任务通常需要多次调用 LLM。每一次调用都伴随着网络延迟和计算成本。对于需要很多步骤的复杂任务,这种串行的“思考-行动”循环可能会导致较高的总耗时和费用。
  3. 提示词的脆弱性:整个机制的稳定运行建立在一个精心设计的提示词模板之上。模板中的任何微小变动,甚至是用词的差异,都可能影响 LLM 的行为。此外,并非所有模型都能持续稳定地遵循预设的格式,这增加了在实际应用中的不确定性。
  4. 可能陷入局部最优:步进式的决策模式意味着智能体缺乏一个全局的、长远的规划。它可能会因为眼前的 Observation 而选择一个看似正确但长远来看并非最优的路径,甚至在某些情况下陷入“原地打转”的循环中。

(3)调试技巧

  • 检查完整的提示词:在每次调用 LLM 之前,将最终格式化好的、包含所有历史记录的完整提示词打印出来。这是追溯 LLM 决策源头的最直接方式。
  • 分析原始输出:当输出解析失败时(例如,正则表达式没有匹配到 Action),务必将 LLM 返回的原始、未经处理的文本打印出来。这能帮助你判断是 LLM 没有遵循格式,还是你的解析逻辑有误。
  • 验证工具的输入与输出:检查智能体生成的 tool_input 是否是工具函数所期望的格式,同时也要确保工具返回的 observation 格式是智能体可以理解和处理的。
  • 调整提示词中的示例 (Few-shot Prompting):如果模型频繁出错,可以在提示词中加入一两个完整的“Thought-Action-Observation”成功案例,通过示例来引导模型更好地遵循你的指令。
  • 尝试不同的模型或参数:更换一个能力更强的模型,或者调整 temperature 参数(通常设为0以保证输出的确定性),有时能直接解决问题。

2.2 Plan-and-slove

Plan-and-Solve:将任务处理明确地分为两个阶段:先规划 (Plan),后执行 (Solve)。

2.2.1 Plan-and-slove的工作原理

Plan-and-Solve 将整个流程解耦为两个核心阶段:

  1. 规划阶段 (Planning Phase): 首先,智能体会接收用户的完整问题。它的第一个任务不是直接去解决问题或调用工具,而是将问题分解,并制定出一个清晰、分步骤的行动计划。这个计划本身就是一次大语言模型的调用产物。
  2. 执行阶段 (Solving Phase): 在获得完整的计划后,智能体进入执行阶段。它会严格按照计划中的步骤,逐一执行。每一步的执行都可能是一次独立的 LLM 调用,或者是对上一步结果的加工处理,直到计划中的所有步骤都完成,最终得出答案。

这种“先谋后动”的策略,使得智能体在处理需要长远规划的复杂任务时,能够保持更高的目标一致性,避免在中间步骤中迷失方向。

Plan-and-Solve 尤其适用于那些结构性强、可以被清晰分解的复杂任务,例如:

  • 多步数学应用题:需要先列出计算步骤,再逐一求解。
  • 需要整合多个信息源的报告撰写:需要先规划好报告结构(引言、数据来源A、数据来源B、总结),再逐一填充内容。
  • 代码生成任务:需要先构思好函数、类和模块的结构,再逐一实现。

2.2.2 规划阶段

提示词通过以下几点确保了输出的质量和稳定性:

  • 角色设定: “顶级的AI规划专家”,激发模型的专业能力。
  • 任务描述: 清晰地定义了“分解问题”的目标。
  • 格式约束: 强制要求输出为一个 Python 列表格式的字符串,这极大地简化了后续代码的解析工作,使其比解析自然语言更稳定、更可靠。

2.2.3 执行器与状态管理

在规划器 (Planner) 生成了清晰的行动蓝图后,我们就需要一个执行器 (Executor) 来逐一完成计划中的任务。执行器不仅负责调用大语言模型来解决每个子问题,还承担着一个至关重要的角色:状态管理。它必须记录每一步的执行结果,并将其作为上下文提供给后续步骤,确保信息在整个任务链条中顺畅流动。

执行器的提示词与规划器不同。它的目标不是分解问题,而是在已有上下文的基础上,专注解决当前这一个步骤。因此,提示词需要包含以下关键信息:

  • 原始问题: 确保模型始终了解最终目标。
  • 完整计划: 让模型了解当前步骤在整个任务中的位置。
  • 历史步骤与结果: 提供至今为止已经完成的工作,作为当前步骤的直接输入。
  • 当前步骤: 明确指示模型现在需要解决哪一个具体任务。

2.3 Reflection

2.3.1 Reflection 机制的核心思想

核心工作流程可以概括为一个简洁的三步循环:执行 -> 反思 -> 优化。

  1. 执行 (Execution):首先,智能体使用我们熟悉的方法(如 ReAct 或 Plan-and-Solve)尝试完成任务,生成一个初步的解决方案或行动轨迹。这可以看作是“初稿”。
  2. 反思 (Reflection):接着,智能体进入反思阶段。它会调用一个独立的、或者带有特殊提示词的大语言模型实例,来扮演一个“评审员”的角色。这个“评审员”会审视第一步生成的“初稿”,并从多个维度进行评估,例如:
    • 事实性错误:是否存在与常识或已知事实相悖的内容?
    • 逻辑漏洞:推理过程是否存在不连贯或矛盾之处?
    • 效率问题:是否有更直接、更简洁的路径来完成任务?
    • 遗漏信息:是否忽略了问题的某些关键约束或方面? 根据评估,它会生成一段结构化的反馈 (Feedback),指出具体的问题所在和改进建议。
  3. 优化 (Refinement):最后,智能体将“初稿”和“反馈”作为新的上下文,再次调用大语言模型,要求它根据反馈内容对初稿进行修正,生成一个更完善的“修订稿”。

Reflection 的价值在于:

  • 它为智能体提供了一个内部纠错回路,使其不再完全依赖于外部工具的反馈(ReAct 的 Observation),从而能够修正更高层次的逻辑和策略错误。
  • 它将一次性的任务执行,转变为一个持续优化的过程,显著提升了复杂任务的最终成功率和答案质量。
  • 它为智能体构建了一个临时的“短期记忆”。整个“执行-反思-优化”的轨迹形成了一个宝贵的经验记录,智能体不仅知道最终答案,还记得自己是如何从有缺陷的初稿迭代到最终版本的。更进一步,这个记忆系统还可以是多模态的,允许智能体反思和修正文本以外的输出(如代码、图像等),为构建更强大的多模态智能体奠定了基础。

第三章 框架开发

3.1 框架简介

3.1.1 用智能体框架的理由

  1. 提升代码复用与开发效率:这是最直接的价值。一个好的框架会提供一个通用的 Agent 基类或执行器,它封装了智能体运行的核心循环(Agent Loop)。无论是 ReAct 还是 Plan-and-Solve,都可以基于框架提供的标准组件快速搭建,从而避免重复劳动。
  2. 实现核心组件的解耦与可扩展性:一个健壮的智能体系统应该由多个松散耦合的模块组成。框架的设计会强制我们分离不同的关注点:
    • 模型层 (Model Layer):负责与大语言模型交互,可以轻松替换不同的模型(OpenAI, Anthropic, 本地模型)。
    • 工具层 (Tool Layer):提供标准化的工具定义、注册和执行接口,添加新工具不会影响其他代码。
    • 记忆层 (Memory Layer):处理短期和长期记忆,可以根据需求切换不同的记忆策略(如滑动窗口、摘要记忆)。 这种模块化的设计使得整个系统极具可扩展性,更换或升级任何一个组件都变得简单。
  3. 标准化复杂的状态管理:我们在 ReflectionAgent 中实现的 Memory 类只是一个简单的开始。在真实的、长时运行的智能体应用中,状态管理是一个巨大的挑战,它需要处理上下文窗口限制、历史信息持久化、多轮对话状态跟踪等问题。一个框架可以提供一套强大而通用的状态管理机制,开发者无需每次都重新处理这些复杂问题。
  4. 简化可观测性与调试过程:当智能体的行为变得复杂时,理解其决策过程变得至关重要。一个精心设计的框架可以内置强大的可观测性能力。例如,通过引入事件回调机制(Callbacks),我们可以在智能体生命周期的关键节点(如 on_llm_start, on_tool_end, on_agent_finish)自动触发日志记录或数据上报,从而轻松地追踪和调试智能体的完整运行轨迹。这远比在代码中手动添加 print 语句要高效和系统化。

3.1.2 主流框架选型与对比

LangChain 和 LlamaIndex 定义了第一代通用 LLM 应用框架的范式,新一代的框架则更加专注于解决特定领域的深层挑战,尤其是多智能体协作 (Multi-Agent Collaboration) 和 复杂工作流控制 (Complex Workflow Control)。

img

  • AutoGen:核心思想是通过对话实现协作。它将多智能体系统抽象为一个由多个“可对话”智能体组成的群聊。开发者可以定义不同角色(如 Coder, ProductManager, Tester),并设定它们之间的交互规则(例如,Coder 写完代码后由 Tester 自动接管)。任务的解决过程,就是这些智能体在群聊中通过自动化消息传递,不断对话、协作、迭代直至最终目标达成的过程。
  • AgentScope:一个专为多智能体应用设计的、功能全面的开发平台。它的核心特点是易用性和工程化。它提供了一套非常友好的编程接口,让开发者可以轻松定义智能体、构建通信网络,并管理整个应用的生命周期。其内置的消息传递机制和对分布式部署的支持,使其非常适合构建和运维复杂、大规模的多智能体系统。
  • CAMEL:提供了一种新颖的、名为角色扮演 (Role-Playing) 的协作方法。其核心理念是,我们只需要为两个智能体(例如,AI研究员 和 Python程序员)设定好各自的角色和共同的任务目标,它们就能在“初始提示 (Inception Prompting)”的引导下,自主地进行多轮对话,相互启发、相互配合,共同完成任务。它极大地降低了设计多智能体对话流程的复杂度。
  • LangGraph:作为 LangChain 生态的扩展,LangGraph 另辟蹊径,将智能体的执行流程建模为图 (Graph)。在传统的链式结构中,信息只能单向流动。而 LangGraph 将每一步操作(如调用LLM、执行工具)定义为图中的一个节点 (Node),并用边 (Edge) 来定义节点之间的跳转逻辑。这种设计天然支持循环 (Cycles),使得实现如 Reflection 这样的迭代、修正、自我反思的复杂工作流变得异常简单和直观。

3.2 AutoGen

将复杂的任务解决流程,映射为不同角色的智能体之间的一系列自动化对话。

img

3.2.1 AutoGen核心机制

(1)框架结构

清晰的分层和异步优先的设计理念:

  • 分层设计:框架被拆分为两个核心模块:
    • autogen-core:作为框架的底层基础,封装了与语言模型交互、消息传递等核心功能。它的存在保证了框架的稳定性和未来扩展性。
    • autogen-agentchat:构建于 core 之上,提供了用于开发对话式智能体应用的高级接口,简化了多智能体应用的开发流程。 这种分层策略使得各组件职责明确,降低了系统的耦合度。
  • 异步优先: 新架构全面转向异步编程 (async/await)。在多智能体协作场景中,网络请求(如调用 LLM API)是主要耗时操作。异步模式允许系统在等待一个智能体响应时处理其他任务,从而避免了线程阻塞,显著提升了并发处理能力和系统资源的利用效率。

(2)核心智能体组件

  • AssistantAgent (助理智能体): 这是任务的主要解决者,其核心是封装了一个大型语言模型(LLM)。它的职责是根据对话历史生成富有逻辑和知识的回复,例如提出计划、撰写文章或编写代码。通过不同的系统消息(System Message),我们可以为其赋予不同的“专家”角色。
  • UserProxyAgent (用户代理智能体): 这是 AutoGen 中功能独特的组件。它扮演着双重角色:既是人类用户的“代言人”,负责发起任务和传达意图;又是一个可靠的“执行器”,可以配置为执行代码或调用工具,并将结果反馈给其他智能体。这种设计清晰地区分了“思考”(由 AssistantAgent 完成)与“行动”。

(3)从 GroupChatManager 到 Team

当任务需要多个智能体协作时,就需要一个机制来协调对话流程。在早期版本中,GroupChatManager 承担了这一职责。而在新架构中,引入了更灵活的 Team 或群聊概念,例如 RoundRobinGroupChat。

  • 轮询群聊 (RoundRobinGroupChat): 这是一种明确的、顺序化的对话协调机制。它会让参与的智能体按照预定义的顺序依次发言。这种模式非常适用于流程固定的任务,例如一个典型的软件开发流程:产品经理先提出需求,然后工程师编写代码,最后由代码审查员进行检查。
  • 工作流:
    1. 首先,创建一个 RoundRobinGroupChat 实例,并将所有参与协作的智能体(如产品经理、工程师等)加入其中。
    2. 当一个任务开始时,群聊会按照预设的顺序,依次激活相应的智能体。
    3. 被选中的智能体根据当前的对话上下文进行响应。
    4. 群聊将新的回复加入对话历史,并激活下一个智能体。
    5. 这个过程会持续进行,直到达到最大对话轮次或满足预设的终止条件。

3.2.2 场景举例-软件开发团队

我们将构建一个模拟的软件开发团队,该团队由多个具有不同专业技能的智能体组成,它们将协作完成一个真实的软件开发任务。

(1)业务目标

我们的目标是开发一个功能明确的 Web 应用:实时显示比特币当前价格。这个任务虽小,却完整地覆盖了软件开发的典型环节:从需求分析、技术选型、编码实现到代码审查和最终测试。这使其成为检验 AutoGen 自动化协作流程的理想场景。

(2)智能体团队角色

为了模拟真实的软件开发流程,我们设计了四个职责分明的智能体角色:

  • ProductManager (产品经理): 负责将用户的模糊需求转化为清晰、可执行的开发计划。
  • Engineer (工程师): 依据开发计划,负责编写具体的应用程序代码。
  • CodeReviewer (代码审查员): 负责审查工程师提交的代码,确保其质量、可读性和健壮性。
  • UserProxy (用户代理): 代表最终用户,发起初始任务,并负责执行和验证最终交付的代码。

(3)定义团队协作流程

在本案例中,软件开发的流程是相对固定的(需求->编码->审查->测试),因此 RoundRobinGroupChat (轮询群聊) 是理想的选择。我们按照业务逻辑顺序,将四个智能体加入到参与者列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import TextMentionTermination

# 定义团队聊天和协作规则
team_chat = RoundRobinGroupChat(
participants=[
product_manager,
engineer,
code_reviewer,
user_proxy
],
termination_condition=TextMentionTermination("TERMINATE"),
max_turns=20,
)
  • 参与者顺序: participants 列表的顺序决定了智能体发言的先后次序。
  • 终止条件: termination_condition 是控制协作流程何时结束的关键。这里我们设定,当任何消息中包含关键词 “TERMINATE” 时,对话便结束。在我们的设计中,这个指令由 UserProxy 在完成最终测试后发出。
  • 最大轮次: max_turns 是一个安全阀,用于防止对话陷入无限循环,避免不必要的资源消耗。

3.2.3 优势与局限性

(1)优势

  • 如案例所示,我们无需为智能体团队设计复杂的状态机或控制流逻辑,而是将一个完整的软件开发流程,自然地映射为产品经理、工程师和审查员之间的对话。这种方式更贴近人类团队的协作模式,显著降低了为复杂任务建模的门槛。开发者可以将更多精力聚焦于定义“谁(角色)”以及“做什么(职责)”,而非“如何做(流程控制)”。
  • 框架允许通过系统消息(System Message)为每个智能体赋予高度专业化的角色。在案例中,ProductManager 专注于需求,而 CodeReviewer 则专注于质量。一个精心设计的智能体可以在不同项目中被复用,易于维护和扩展。
  • 对于流程化任务,RoundRobinGroupChat 这样机制提供了清晰、可预测的协作流程。同时,UserProxyAgent 的设计为“人类在环”(Human-in-the-loop)提供了天然的接口。它既可以作为任务的发起者,也可以是流程的监督者和最终的验收者。这种设计确保了自动化系统始终处于人类的监督之下。

(2)局限性

  • 虽然 RoundRobinGroupChat 提供了顺序化的流程,但基于 LLM 的对话本质上具有不确定性。智能体可能会产生偏离预期的回复,导致对话走向意外的分支,甚至陷入循环。
  • 当智能体团队的工作结果未达预期时,调试过程可能非常棘手。与传统程序不同,我们得到的不是清晰的错误堆栈,而是一长串的对话历史。这被称为“对话式调试”的难题。

3.3 AgentScope

3.3.1 AgentScope核心设计

与 AutoGen 相比,AgentScope 的核心差异在于其消息驱动的架构设计和工业级的工程实践。与许多框架采用的继承式设计不同,AgentScope 选择了组合式架构和消息驱动模式。这种设计不仅增强了系统的模块化程度,也为其出色的并发性能和分布式能力奠定了基础。

(1)分层架构体系

img

最底层是基础组件层 (Foundational Components),它为整个框架提供了核心的构建块。Message 组件定义了统一的消息格式,支持从简单的文本交互到复杂的多模态内容;Memory 组件提供了短期和长期记忆管理;Model API 层抽象了对不同大语言模型的调用;而 Tool 组件则封装了智能体与外部世界交互的能力。

在基础组件之上,智能体基础设施层 (Agent-level Infrastructure) 提供了更高级的抽象。这一层不仅包含了各种预构建的智能体(如浏览器使用智能体、深度研究智能体),还实现了经典的 ReAct 范式,支持智能体钩子、并行工具调用、状态管理等高级特性。特别值得注意的是,这一层原生支持异步执行与实时控制,这是 AgentScope 相比其他框架的一个重要优势。

多智能体协作层 (Multi-Agent Cooperation) 是 AgentScope 的核心创新所在。MsgHub 作为消息中心,负责智能体间的消息路由和状态管理;而 Pipeline 系统则提供了灵活的工作流编排能力,支持顺序、并发等多种执行模式。这种设计使得开发者可以轻松构建复杂的多智能体协作场景。

(2)消息驱动

AgentScope 的核心创新在于其消息驱动架构。在这个架构中,所有的智能体交互都被抽象为消息的发送和接收,而不是传统的函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
from agentscope.message import Msg

# 消息的标准结构
message = Msg(
name="Alice", # 发送者名称
content="Hello, Bob!", # 消息内容
role="user", # 角色类型
metadata={ # 元数据信息
"timestamp": "2024-01-15T10:30:00Z",
"message_type": "text",
"priority": "normal"
}
)

将消息作为交互的基础单元,带来了几个关键优势:

  • 异步解耦: 消息的发送方和接收方在时间上解耦,无需相互等待,天然支持高并发场景。
  • 位置透明: 智能体无需关心另一个智能体是在本地进程还是在远程服务器上,消息系统会自动处理路由。
  • 可观测性: 每一条消息都可以被记录、追踪和分析,极大地简化了复杂系统的调试与监控。
  • 可靠性: 消息可以被持久化存储和重试,即使系统出现故障,也能保证交互的最终一致性,提升了系统的容错能力。

(3)智能体生命周期管理

在 AgentScope 中,每个智能体都有明确的生命周期(初始化、运行、暂停、销毁等),并基于一个统一的基类 AgentBase 来实现。开发者通常只需要关注其核心的 reply 方法。

这种设计模式分离了智能体的内部逻辑与外部通信,开发者只需在 reply 方法中定义智能体“思考和回应”的方式即可。

(4)消息传递机制

AgentScope 内置了一个消息中心 (MsgHub),它是整个消息驱动架构的中枢。MsgHub 不仅负责消息的路由和分发,还集成了持久化和分布式通信等高级功能,它有以下这些特点。

  • 灵活的消息路由: 支持点对点、广播、组播等多种通信模式,可以构建灵活复杂的交互网络。
  • 消息持久化: 能够将所有消息自动保存到数据库(如 SQLite, MongoDB),确保了长期运行任务的状态可以被恢复。
  • 原生分布式支持: 这是 AgentScope 的标志性特性。智能体可以被部署在不同的进程或服务器上,MsgHub 会通过 RPC(远程过程调用)自动处理跨节点的通信,对开发者完全透明。

3.3.2 场景举例-三国杀游戏

它演示了如何在一个需要实时协作、角色扮演和策略博弈的场景中,充分发挥消息驱动架构的威力。与传统狼人杀不同,我们的”三国狼人杀”将刘备、关羽、诸葛亮等经典角色引入游戏,每个智能体不仅要完成狼人杀的基本任务(如狼人击杀、预言家查验、村民推理),还要体现出对应三国人物的性格特点和行为模式。这种设计让我们能够观察到 AgentScope 在处理多层次角色建模方面的表现。

(1)架构设计与核心组件

将游戏逻辑划分为三个独立的层次,每个层次都映射了 AgentScope 的一个或多个核心组件:

  • 游戏控制层 (Game Control Layer):由一个 ThreeKingdomsWerewolfGame 类作为游戏的主控制器,负责维护全局状态(如玩家存活列表、当前游戏阶段)、推进游戏流程(调用夜晚阶段、白天阶段)以及裁定胜负。
  • 智能体交互层 (Agent Interaction Layer):完全由 MsgHub 驱动。所有智能体间的通信,无论是狼人间的秘密协商,还是白天的公开辩论,都通过消息中心进行路由和分发。
  • 角色建模层 (Role Modeling Layer):每个玩家都是一个基于 DialogAgent 的实例。我们通过精心设计的系统提示词,为每个智能体注入了“游戏角色”和“三国人格”的双重身份。

(2)消息驱动的游戏流程

本案例最核心的设计是以消息驱动代替状态机来管理游戏流程。在传统实现中,游戏阶段的转换通常由一个中心化的状态机(State Machine)控制。而在 AgentScope 的范式下,游戏流程被自然地建模为一系列定义好的消息交互模式。

例如,狼人阶段的实现,并非一个简单的函数调用,而是通过 MsgHub 动态创建一个临时的、仅包含狼人玩家的私密通信频道。

(3)用结构化输出约束游戏规则

狼人杀游戏的一个关键挑战是如何确保智能体的行为符合游戏规则。AgentScope 的结构化输出机制为这个问题提供了解决方案。

(4)角色建模的双重挑战

在这个案例中,最有趣的技术挑战是如何让智能体同时扮演好两个层面的角色:游戏功能角色(狼人、预言家等)和文化人格角色(刘备、曹操等)。

(5)并发处理与容错机制

AgentScope 的异步架构在这个多智能体游戏中发挥了重要作用。游戏中经常出现需要同时收集多个智能体决策的场景,比如投票阶段

1
2
3
4
5
6
7
# 并行收集所有玩家的投票决策
vote_msgs = await fanout_pipeline(
self.alive_players,
await self.moderator.announce("请投票选择要淘汰的玩家"),
structured_model=get_vote_model_cn(self.alive_players),
enable_gather=False,
)

fanout_pipeline 允许我们并行地向所有智能体发送相同的消息,并异步收集它们的响应。这不仅提高了游戏的执行效率,更重要的是模拟了真实狼人杀游戏中”同时投票”的场景。、

3.3.3 优势与局限性

该框架以其消息驱动的架构为核心,将复杂的游戏流程优雅地映射为一系列并发、异步的消息传递事件,从而避免了传统状态机的僵硬与复杂。结合其强大的结构化输出能力,我们将游戏规则直接转化为代码层面的约束,极大地提升了系统的稳定性和可预测性。这种设计范式不仅在性能上展现了其原生并发的优势,更在容错处理上保证了即使单个智能体出现异常,整体流程也能稳健运行。

然而,AgentScope 的工程化优势也带来了一定的复杂性成本。其消息驱动架构虽然强大,但对开发者的技术要求较高,需要理解异步编程、分布式通信等概念。对于简单的多智能体对话场景,这种架构可能显得过于复杂,存在”过度工程化”的风险。此外,作为相对较新的框架,其生态系统和社区资源还有待进一步完善。

3.4 CAMEL

CAMEL最初的核心目标是探索如何在最少的人类干预下,让两个智能体通过“角色扮演”自主协作解决复杂任务。

3.4.1 CAMEL的自主协作

CAMEL 实现自主协作的基石是两大核心概念:角色扮演 (Role-Playing) 和 引导性提示 (Inception Prompting)。

(1)角色扮演

在 CAMEL 最初的设计中,一个任务通常由两个智能体协作完成。这两个智能体被赋予了互补的、明确定义的“角色”。一个扮演“AI 用户” (AI User),负责提出需求、下达指令和构思任务步骤;另一个则扮演“AI 助理” (AI Assistant),负责根据指令执行具体操作和提供解决方案。

(2)引导性提示

仅仅设定角色还不够,如何确保两个 AI 在没有人类持续监督的情况下,能始终“待在自己的角色里”,并且高效地朝着共同目标前进呢?这就是 CAMEL 最核心的技术,引导性提示发挥作用的地方。“引导性提示”是在对话开始前,分别注入给两个智能体的一段精心设计的、结构化的初始指令(System Prompt)。这段指令就像是为智能体植入的“行动纲领”,它通常包含以下几个关键部分:

  • 明确自身角色:例如,“你是一位资深的股票交易员…”
  • 告知协作者角色:例如,“你正在与一位优秀的 Python 程序员合作…”
  • 定义共同目标:例如,“你们的共同目标是开发一个股票交易策略分析工具。”
  • 设定行为约束和沟通协议:这是最关键的一环。例如,指令会要求 AI 用户“一次只提出一个清晰、具体的步骤”,并要求 AI 助理“在完成上一步之前不要追问更多细节”,同时规定双方需在回复的末尾使用特定标志(如 <SOLUTION>)来标识任务的完成。

3.4.2 场景举例-AI科普电子书

这个案例体现了 CAMEL 的核心优势,让两个智能体在各自专业领域发挥所长,协作完成单个智能体难以胜任的复杂创作任务。

(1)任务设定

场景设定:创作一本面向普通读者的拖延症心理学科普电子书,要求既有科学严谨性,又具备良好的可读性。

智能体角色:

  • 心理学家(Psychologist):具备深厚的心理学理论基础,熟悉认知行为科学、神经科学等相关领域,能够提供专业的学术见解和实证研究支持
  • 作家(Writer):拥有优秀的写作技巧和叙述能力,善于将复杂的学术概念转化为生动易懂的文字,注重读者体验和内容的可读性

(2)定义协作任务

首先,我们需要明确两位 AI 专家的共同目标。我们通过一个内容详实的字符串 task_prompt 来定义这个任务。

task_prompt 是整个协作的“任务说明书”。它不仅是我们要完成的目标,也将在幕后被 CAMEL 用来生成“引导性提示”,确保两位智能体的对话始终围绕这个核心目标展开。

(3)初始化角色扮演“社会”

接下来,我们创建 RolePlaying 会话实例。这是 CAMEL 的核心操作,它根据我们提供的角色和任务,快速构建一个双智能体协作“社会”。

1
2
3
4
5
6
7
8
9
10
11
12
# 初始化角色扮演会话
# AI 作家作为 "user",负责提出写作结构和要求
# AI 心理学家作为 "assistant",负责提供专业知识和内容
role_play_session = RolePlaying(
assistant_role_name="心理学家",
user_role_name="作家",
task_prompt=task_prompt,
model=model,
with_task_specify=False, # 在本例中,我们直接使用给定的task_prompt
)

print(Fore.CYAN + f"具体任务描述:\n{role_play_session.task_prompt}\n")

RolePlaying 是 CAMEL 提供的高级 API,它封装了复杂的提示工程。我们只需传入两个角色的名称和任务即可。在 CAMEL 的设计中,user 角色是对话的“推动者”和“需求方”,而 assistant 角色是“执行者”和“方案提供方”。

3.4.3 优势与局限性

(1)优势

CAMEL 最大的优势在于其”轻架构、重提示”的设计哲学。相比 AutoGen 的复杂对话管理和 AgentScope 的分布式架构,CAMEL 通过精心设计的初始提示就能实现高质量的智能体协作。这种自然涌现的协作行为,往往比硬编码的工作流更加灵活和高效。

值得注意的是,CAMEL 框架正在经历快速的发展和演进。从其 GitHub 仓库 可以看到,CAMEL 已经远不止是一个简单的双智能体协作框架,目前已经具备:

  • 多模态能力:支持文本、图像、音频等多种模态的智能体协作
  • 工具集成:内置了丰富的工具库,包括搜索、计算、代码执行等
  • 模型适配:支持 OpenAI、Anthropic、Google、开源模型等多种 LLM 后端
  • 生态联动:与 LangChain、CrewAI、AutoGen 等主流框架实现了互操作性

(2)主要局限性

  1. 对提示工程的高度依赖

CAMEL 的成功很大程度上取决于初始提示的质量。这带来了几个挑战:

  • 提示设计门槛:需要深入理解目标领域和 LLM 的行为特性
  • 调试复杂性:当协作效果不佳时,很难定位是角色定义、任务描述还是交互规则的问题
  • 一致性挑战:不同的 LLM 对相同提示的理解可能存在差异
  1. 协作规模的限制

虽然 CAMEL 在双智能体协作上表现出色,但在处理大规模多智能体场景时面临挑战:

  • 对话管理:缺乏像 AutoGen 那样的复杂对话路由机制
  • 状态同步:没有 AgentScope 那样的分布式状态管理能力
  • 冲突解决:当多个智能体意见分歧时,缺乏有效的仲裁机制
  1. 任务适用性的边界

CAMEL 特别适合需要深度协作和创造性思维的任务,但在某些场景下可能不是最优选择:

  • 严格流程控制:对于需要精确步骤控制的任务,LangGraph 的图结构更合适
  • 大规模并发:AgentScope 的消息驱动架构在高并发场景下更有优势
  • 复杂决策树:AutoGen 的群聊模式在多方决策场景下更加灵活

3.5 LangGraph

3.5.1 LangGraph的结构

与前面介绍的基于“对话”的框架(如 AutoGen 和 CAMEL)不同,LangGraph 将智能体的执行流程建模为一种状态机(State Machine),并将其表示为有向图(Directed Graph)。在这种范式中,图的节点(Nodes)代表一个具体的计算步骤(如调用 LLM、执行工具),而边(Edges)则定义了从一个节点到另一个节点的跳转逻辑。这种设计的革命性之处在于它天然支持循环,使得构建能够进行迭代、反思和自我修正的复杂智能体工作流变得前所未有的直观和简单。

三个基本构成要素

  1. 全局状态(State)。整个图的执行过程都围绕一个共享的状态对象进行。

    1
    2
    3
    4
    5
    6
    7
    8
    from typing import TypedDict, List

    # 定义全局状态的数据结构
    class AgentState(TypedDict):
    messages: List[str] # 对话历史
    current_task: str # 当前任务
    final_answer: str # 最终答案
    # ... 任何其他需要追踪的状态
  2. 节点(Nodes)。每个节点都是一个接收当前状态作为输入、并返回一个更新后的状态作为输出的 Python 函数。节点是执行具体工作的单元。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 定义一个“规划者”节点函数
    def planner_node(state: AgentState) -> AgentState:
    """根据当前任务制定计划,并更新状态。"""
    current_task = state["current_task"]
    # ... 调用LLM生成计划 ...
    plan = f"为任务 '{current_task}' 生成的计划..."

    # 将新消息追加到状态中
    state["messages"].append(plan)
    return state

    # 定义一个“执行者”节点函数
    def executor_node(state: AgentState) -> AgentState:
    """执行最新计划,并更新状态。"""
    latest_plan = state["messages"][-1]
    # ... 执行计划并获得结果 ...
    result = f"执行计划 '{latest_plan}' 的结果..."

    state["messages"].append(result)
    return state
  3. 边(Edges)。边负责连接节点,定义工作流的方向。最简单的边是常规边,它指定了一个节点的输出总是流向另一个固定的节点。而 LangGraph 最强大的功能在于条件边(Conditional Edges)。它通过一个函数来判断当前的状态,然后动态地决定下一步应该跳转到哪个节点。这正是实现循环和复杂逻辑分支的关键。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def should_continue(state: AgentState) -> str:
    """条件函数:根据状态决定下一步路由。"""
    # 假设如果消息少于3条,则需要继续规划
    if len(state["messages"]) < 3:
    # 返回的字符串需要与添加条件边时定义的键匹配
    return "continue_to_planner"
    else:
    state["final_answer"] = state["messages"][-1]
    return "end_workflow"

3.5.2 场景举例-三步问答助手

构建一个简化的问答对话助手,它会遵循一个清晰、固定的三步流程来回答用户的问题:

  1. 理解 (Understand):首先,分析用户的查询意图。
  2. 搜索 (Search):然后,模拟搜索与意图相关的信息。
  3. 回答 (Answer):最后,基于意图和搜索到的信息,生成最终答案。

将代码分解为四个核心步骤:定义状态、创建节点、构建图、以及运行应用。

(1)定义全局状态

这是一个共享的数据结构,它在图的每个节点之间传递,作为工作流的持久化上下文。 每个节点都可以读取该结构中的数据,并对其进行更新。

(2)定义工作流节点

创建构成工作流的各个节点。在 LangGraph 中,每个节点都是一个执行具体任务的 Python 函数。这些函数接收当前的状态对象作为输入,并返回一个包含更新后字段的字典。包括:

  1. 理解与查询节点:是工作流的第一步,此节点的职责是理解用户意图,并为其生成一个最优化的搜索查询。
  2. 搜索节点:负责执行智能体的“工具使用”能力,它将调用 Tavily API 进行真实的互联网搜索,并具备基础的错误处理功能。
  3. 回答节点:根据上一步的搜索是否成功,来选择不同的回答策略,具备了一定的弹性。

(3)构建图

将所有节点连接起来。

3.5.3 优势与局限性

(1)优势

  • 如我们的智能搜索助手案例所示,LangGraph 将一个完整的实时问答流程,显式地定义为一个由状态、节点和边构成的“流程图”。这种设计的最大优势是高度的可控性与可预测性。开发者可以精确地规划智能体的每一步行为,这对于构建需要高可靠性和可审计性的生产级应用至关重要。其最强大的特性在于对循环(Cycles)的原生支持。通过条件边,我们可以轻松构建“反思-修正”循环,例如在我们的案例中,如果搜索失败,可以设计一个回退到备用方案的路径。这是构建能够自我优化和具备容错能力的智能体的关键。
  • 此外,由于每个节点都是一个独立的 Python 函数,这带来了高度的模块化。同时,在流程中插入一个等待人类审核的节点也变得非常直接,为实现可靠的“人机协作”(Human-in-the-loop)提供了坚实的基础。

(2)局限性

  • 与基于对话的框架相比,LangGraph 需要开发者编写更多的前期代码(Boilerplate)。定义状态、节点、边等一系列操作,使得对于简单任务而言,开发过程显得更为繁琐。开发者需要更多地思考“如何控制流程(how)”,而不仅仅是“做什么(what)”。由于工作流是预先定义的,LangGraph 的行为虽然可控,但也缺少了对话式智能体那种动态的、“涌现”式的交互。它的强项在于执行一个确定的、可靠的流程,而非模拟开放式的、不可预测的社会性协作。
  • 调试过程同样存在挑战。虽然流程比对话历史更清晰,但问题可能出在多个环节:某个节点内部的逻辑错误、在节点间传递的状态数据发生异变,或是边跳转的条件判断失误。这要求开发者对整个图的运行机制有全局性的理解。

第四章 记忆与检索

4.1 认知科学到智能体记忆

4.1.1 记忆与RAG的必要性

(1)局限一:无状态导致的对话遗忘

  1. 上下文丢失:在长对话中,早期的重要信息可能会因为上下文窗口限制而丢失
  2. 个性化缺失:Agent无法记住用户的偏好、习惯或特定需求
  3. 学习能力受限:无法从过往的成功或失败经验中学习改进
  4. 一致性问题:在多轮对话中可能出现前后矛盾的回答

(2)局限二:模型内置知识的局限性

  1. 知识时效性:大模型的训练数据有时间截止点,无法获取最新信息
  2. 专业领域知识:通用模型在特定领域的深度知识可能不足
  3. 事实准确性:通过检索验证,减少模型的幻觉问题
  4. 可解释性:提供信息来源,增强回答的可信度

4.1.2 记忆与RAG架构设计

在实现上,我们将记忆和RAG设计为两个独立的工具:

  • memory_tool负责存储和维护对话过程中的交互信息;
  • rag_tool则负责从用户提供的知识库中检索相关信息作为上下文,并可将重要的检索结果自动存储到记忆系统中。
HelloAgents记忆与RAG系统架构图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HelloAgents记忆系统
├── 基础设施层 (Infrastructure Layer)
│ ├── MemoryManager - 记忆管理器(统一调度和协调)
│ ├── MemoryItem - 记忆数据结构(标准化记忆项)
│ ├── MemoryConfig - 配置管理(系统参数设置)
│ └── BaseMemory - 记忆基类(通用接口定义)
├── 记忆类型层 (Memory Types Layer)
│ ├── WorkingMemory - 工作记忆(临时信息,TTL管理)
│ ├── EpisodicMemory - 情景记忆(具体事件,时间序列)
│ ├── SemanticMemory - 语义记忆(抽象知识,图谱关系)
│ └── PerceptualMemory - 感知记忆(多模态数据)
├── 存储后端层 (Storage Backend Layer)
│ ├── QdrantVectorStore - 向量存储(高性能语义检索)
│ ├── Neo4jGraphStore - 图存储(知识图谱管理)
│ └── SQLiteDocumentStore - 文档存储(结构化持久化)
└── 嵌入服务层 (Embedding Service Layer)
├── DashScopeEmbedding - 通义千问嵌入(云端API)
├── LocalTransformerEmbedding - 本地嵌入(离线部署)
└── TFIDFEmbedding - TFIDF嵌入(轻量级兜底)
1
2
3
4
5
6
7
8
9
10
11
12
13
HelloAgents RAG系统
├── 文档处理层 (Document Processing Layer)
│ ├── DocumentProcessor - 文档处理器(多格式解析)
│ ├── Document - 文档对象(元数据管理)
│ └── Pipeline - RAG管道(端到端处理)
├── 嵌入表示层 (Embedding Layer)
│ └── 统一嵌入接口 - 复用记忆系统的嵌入服务
├── 向量存储层 (Vector Storage Layer)
│ └── QdrantVectorStore - 向量数据库(命名空间隔离)
└── 智能问答层 (Intelligent Q&A Layer)
├── 多策略检索 - 向量检索 + MQE + HyDE
├── 上下文构建 - 智能片段合并与截断
└── LLM增强生成 - 基于上下文的准确问答

4.2 记忆系统

4.2.1 记忆系统工作流程

hello-agents的工作流程如下:

记忆系统工作流程

4.2.2 四种记忆类型

记忆系统由四种不同类型的记忆模块构成:

  • 工作记忆 (Working Memory),它扮演着智能体“短期记忆”的角色,主要用于存储当前对话的上下文信息。为确保高速访问和响应,其容量被有意限制(例如,默认50条),并且生命周期与单个会话绑定,会话结束后便会自动清理。
    • 采用了纯内存存储方案,配合TTL(Time To Live)机制进行自动清理。这种设计的优势在于访问速度极快,但也意味着工作记忆的内容在系统重启后会丢失。
    • 工作记忆的检索采用了混合检索策略,首先尝试使用TF-IDF向量化进行语义检索,如果失败则回退到关键词匹配。这种设计确保了在各种环境下都能提供可靠的检索服务。评分算法结合了语义相似度、时间衰减和重要性权重,最终得分公式为:(相似度 × 时间衰减) × (0.8 + 重要性 × 0.4)。
  • 情景记忆 (Episodic Memory),它负责长期存储具体的交互事件和智能体的学习经历。与工作记忆不同,情景记忆包含了丰富的上下文信息,并支持按时间序列或主题进行回顾式检索,是智能体“复盘”和学习过往经验的基础。
    • 采用了SQLite+Qdrant的混合存储方案,SQLite负责结构化数据的存储和复杂查询,Qdrant负责高效的向量检索。
    • 情景记忆的检索实现展现了复杂的多因素评分机制。它不仅考虑了语义相似度,还加入了时间近因性的考量,最终通过重要性权重进行调节。评分公式为:(向量相似度 × 0.8 + 时间近因性 × 0.2) × (0.8 + 重要性 × 0.4),确保检索结果既语义相关又时间相关。
  • 语义记忆 (Semantic Memory),它存储的是更为抽象的知识、概念和规则。例如,通过对话了解到的用户偏好、需要长期遵守的指令或领域知识点,都适合存放在这里。这部分记忆具有高度的持久性和重要性,是智能体形成“知识体系”和进行关联推理的核心。
    • 记忆系统中最复杂的部分,它负责存储抽象的概念、规则和知识。语义记忆的设计重点在于知识的结构化表示和智能推理能力。语义记忆采用了Neo4j图数据库和Qdrant向量数据库的混合架构,这种设计让系统既能进行快速的语义检索,又能利用知识图谱进行复杂的关系推理。
    • 语义记忆的评分公式为:(向量相似度 × 0.7 + 图相似度 × 0.3) × (0.8 + 重要性 × 0.4)。这种设计的核心思想是:
      • 向量检索权重(0.7):语义相似度是主要因素,确保检索结果与查询语义相关
      • 图检索权重(0.3):关系推理作为补充,发现概念间的隐含关联
      • 重要性权重范围[0.8, 1.2]:避免重要性过度影响相似度排序,保持检索的准确性
  • 感知记忆 (Perceptual Memory)。该模块专门处理图像、音频等多模态信息,并支持跨模态检索。其生命周期会根据信息的重要性和可用存储空间进行动态管理。
    • 感知记忆支持文本、图像、音频等多种模态的数据存储和检索。它采用了模态分离的存储策略,为不同模态的数据创建独立的向量集合,这种设计避免了维度不匹配的问题。
    • 感知记忆的评分公式为:(向量相似度 × 0.8 + 时间近因性 × 0.2) × (0.8 + 重要性 × 0.4)。感知记忆的评分机制还支持跨模态检索,通过统一的向量空间实现文本、图像、音频等不同模态数据的语义对齐。当进行跨模态检索时,系统会自动调整评分权重,确保检索结果的多样性和准确性。

4.3 RAG系统

4.3.1 RAG相关知识

检索增强生成(Retrieval-Augmented Generation,RAG)是一种结合了信息检索和文本生成的技术。它的核心思想是:在生成回答之前,先从外部知识库中检索相关信息,然后将检索到的信息作为上下文提供给大语言模型,从而生成更准确、更可靠的回答。

检索是指从知识库中查询相关内容;增强是将检索结果融入提示词,辅助模型生成;生成则输出兼具准确性与透明度的答案。

基本工作流程

  1. 在数据准备阶段,系统通过数据提取、文本分割和向量化,将外部知识构建成一个可检索的数据库。
  2. 随后在应用阶段,系统会响应用户的提问,从数据库中检索相关信息,将其注入Prompt,并最终驱动大语言模型生成答案。

发展历程

  1. 第一阶段:朴素RAG(Naive RAG, 2020-2021)。这是RAG技术的萌芽阶段,其流程直接而简单,通常被称为“检索-读取”(Retrieve-Read)模式。
    • 检索方式:主要依赖传统的关键词匹配算法,如TF-IDF或BM25。这些方法计算词频和文档频率来评估相关性,对字面匹配效果好,但难以理解语义上的相似性。
    • 生成模式:将检索到的文档内容不加处理地直接拼接到提示词的上下文中,然后送给生成模型。
  2. 第二阶段:高级RAG(Advanced RAG, 2022-2023)。随着向量数据库和文本嵌入技术的成熟,RAG进入了快速发展阶段。研究者和开发者们在“检索”和“生成”的各个环节引入了大量优化技术。
    • 检索方式:转向基于稠密嵌入(Dense Embedding)的语义检索。通过将文本转换为高维向量,模型能够理解和匹配语义上的相似性,而不仅仅是关键词。
    • 生成模式:引入了很多优化技术,例如查询重写,文档分块,重排序等。
  3. 第三阶段:模块化RAG(Modular RAG, 2023-至今)。在高级RAG的基础上,现代RAG系统进一步向着模块化、自动化和智能化的方向发展。系统的各个部分被设计成可插拔、可组合的独立模块,以适应更多样化和复杂的应用场景。
    • 检索方式:如混合检索,多查询扩展,假设性文档嵌入等。
    • 生成模式:思维链推理,自我反思与修正等。
RAG系统核心原理

4.3.2 RAG架构设计

因为Memory_tool是系统性的实现,而RAG在我们的设计中被定义为一种工具,可以梳理为一条pipeline。我们的RAG系统的核心架构可以概括为”五层七步”的设计模式:

1
2
3
4
5
6
7
8
9
用户层:RAGTool统一接口
↓
应用层:智能问答、搜索、管理
↓
处理层:文档解析、分块、向量化
↓
存储层:向量数据库、文档存储
↓
基础层:嵌入模型、LLM、数据库

全流程

1
任意格式文档 → MarkItDown转换 → Markdown文本 → 智能分块 → 向量化 → 存储检索

(1)多模态文档载入

RAG系统的核心优势之一是其强大的多模态文档处理能力。系统使用MarkItDown作为统一的文档转换引擎,支持几乎所有常见的文档格式。无论输入是PDF、Word、Excel、图片还是音频,最终都会转换为标准的Markdown格式,然后进入统一的分块、向量化和存储流程。

(2)智能分块策略

经过MarkItDown转换后,所有文档都统一为标准的Markdown格式。这为后续的智能分块提供了结构化的基础。HelloAgents实现了专门针对Markdown格式的智能分块策略,充分利用Markdown的结构化特性进行精确分割。

Markdown结构感知的分块流程:

1
2
3
4
标准Markdown文本 → 标题层次解析 → 段落语义分割 → Token计算分块 → 重叠策略优化 → 向量化准备
↓ ↓ ↓ ↓ ↓ ↓
统一格式 #/##/### 语义边界 大小控制 信息连续性 嵌入向量
结构清晰 层次识别 完整性保证 检索优化 上下文保持 相似度匹配

(3)统一嵌入与向量存储

嵌入模型是RAG系统的核心,它负责将文本转换为高维向量,使得计算机能够理解和比较文本的语义相似性。RAG系统的检索能力很大程度上取决于嵌入模型的质量和向量存储的效率。

4.3.3 高级检索策略

RAG系统的检索能力是其核心竞争力。在实际应用中,用户的查询表述与文档中的实际内容可能存在用词差异,导致相关文档无法被检索到。为了解决这个问题,HelloAgents实现了三种互补的高级检索策略:多查询扩展(MQE)、假设文档嵌入(HyDE)和统一的扩展检索框架。

(1)多查询扩展(MQE)

多查询扩展(Multi-Query Expansion)是一种通过生成语义等价的多样化查询来提高检索召回率的技术。这种方法的核心洞察是:同一个问题可以有多种不同的表述方式,而不同的表述可能匹配到不同的相关文档。例如,”如何学习Python”可以扩展为”Python入门教程”、”Python学习方法”、”Python编程指南”等多个查询。通过并行执行这些扩展查询并合并结果,系统能够覆盖更广泛的相关文档,避免因用词差异而遗漏重要信息。

(2)假设文档嵌入(HyDE)

假设文档嵌入(Hypothetical Document Embeddings,HyDE)是一种创新的检索技术,它的核心思想是“用答案找答案“。传统的检索方法是用问题去匹配文档,但问题和答案在语义空间中的分布往往存在差异——问题通常是疑问句,而文档内容是陈述句。HyDE通过让LLM先生成一个假设性的答案段落,然后用这个答案段落去检索真实文档,从而缩小了查询和文档之间的语义鸿沟。

这种方法的优势在于,假设答案与真实答案在语义空间中更加接近,因此能够更准确地匹配到相关文档。

(3)扩展检索框架

将MQE和HyDE两种策略整合到统一的扩展检索框架中。系统通过enable_mqe和enable_hyde参数让用户可以根据具体场景选择启用哪些策略:对于需要高召回率的场景可以同时启用两种策略,对于性能敏感的场景可以只使用基础检索。

评论加载中...