Hello Agents入门
读 Hello Agents 相关核心要点梳理,架构部分
Decode-Only架构
1. Softmax 公式
1.1 公式说明
Softmax 起源于 统计力学 和 概率论,传统的概率分布是由 Softmax 公式 计算得到的:
其中:
x_i:第 i 个元素的原始分数exp(x_i):e 的 x_i 次方Σexp(x_j):所有元素的 exp 值之和
它的作用是能 将任意数值转换为概率分布 且 满足概率的基本要求(≥0,和为1)
比如现在有["GPT", "AI", "小明", "老师"] 经过矩阵运算得出原始分数为 [5.0, 3.0, 1.0, 0.5] 的候选词,其中 5.0 比 3.0 大,说明 "GPT" 比 "AI" 更可能出现,但这些数字不是 概率(概率应该在0-1之间,且和为1)
目标:转换为概率
希望得到如概率分布为 [0.5, 0.3, 0.2, 0] 这种 每个值在 0-1 之间,且所有值相加为 1
1.2 为什么要使用 exp
比如使用最原始的 相加求和再相除 直接归一化行不行
原始分数 = [5.0, 3.0, 1.0, 0.5]
总和 = 5.0 + 3.0 + 1.0 + 0.5 = 9.5
概率 = [5.0/9.5, 3.0/9.5, 1.0/9.5, 0.5/9.5]
= [0.53, 0.32, 0.11, 0.05]这样虽然看起来可以,但有如果概率是负数怎么办?
原始分数 = [2.0, 1.0, -1.0, -2.0]
总和?? = 2.0 + 1.0 + (-1.0) + (-2.0) = 0因此 通过 exp 的特性把负数变正数,exp(x) 的特性:
- exp(正数) = 很大的正数
- exp(0) = 1
- exp(负数) = 很小的正数(但仍然是正数!)
例子:
exp(2.0) = 7.39
exp(1.0) = 2.72
exp(-1.0) = 0.37 # 负数变正数了!
exp(-2.0) = 0.14 # 负数变正数了!此时,所有的值都变成正数了!✅
但此时总和依然 不是 1 ,因此再依据最原始的 相加求和再相除
相加求和 = 7.39 + 2.72 + 0.37 + 0.14 = 10.62
除以总和 概率归一化 = [7.39/10.62, 2.72/10.62, 0.37/10.62, 0.14/10.62]
= [0.7, 0.26, 0.03, 0.01]2. 掩码自注意力 (Masked Self-Attention)
在 Decoder-Only 架构中,这个机制变得至关重要。它的工作原理非常巧妙,其本质如下:
GPT 的掩码本质: 对注意力分数矩阵应用一个布尔掩码矩阵,将需要屏蔽的位置设为 -inf。是 在数值上做加法/替换,然后利用 Softmax 的数学特性 来实现屏蔽。
# 1. 注意力分数矩阵(原始)
attention_scores = [
[2.5, 1.8, 0.3],
[3.2, 2.1, 1.5],
[1.0, 0.8, 2.0]
]
# 2. 掩码矩阵(布尔矩阵)
mask = [
[False, True, True ], # True = 需要屏蔽
[False, False, True ],
[False, False, False]
]
# 3. 应用掩码(矩阵操作)
masked_scores = attention_scores.masked_fill(mask, -inf)
# 结果:True的位置变成-inf,False的位置保持不变用具体例子解释 GPT 掩码机制,说明它如何通过"大负数"实现
2.1 具体步骤演示
假设我们有一个句子:"我 爱 你",计算第2个词"爱"的注意力时:
步骤1:计算注意力分数矩阵
位置: [0:我] [1:爱] [2:你]
[0:我] 2.5 1.8 0.3
[1:爱] `3.2 2.1 1.5` ← 这是"爱"对所有词的注意力分数
[2:你] 1.0 0.8 2.0步骤2:应用掩码(关键步骤)
在预测"爱"时,不能看 "你"(位置2),所以把位置2的分数替换为 "-inf"(或一个很大的负数,如 -1e9):
位置: [0:我] [1:爱] [2:你]
[0:我] 2.5 -inf -inf ← "我"不能看"爱"和"你"
[1:爱] 3.2 2.1 -inf ← "爱"不能看"你"
[2:你] 1.0 0.8 2.0 ← "你"可以看所有(但实际也不能看未来)步骤3:使用 Softmax 公式进行归一化
对 "爱" 这一行 [3.2, 2.1, -inf] 计算:
import math
# exp(3.2) = e^3.2
result1 = math.exp(3.2)
print(result1) # 输出: 24.532530197109352
# exp(2.1) = e^2.1
result2 = math.exp(2.1)
print(result2) # 输出: 8.16616991256765
"""
exp(3.2) = 24.53
exp(2.1) = 8.17
exp(-inf) = 0 ← 关键!e的负无穷次方 = 0
总和 = 24.53 + 8.17 + 0 = 32.70
最终概率:
P(我) = 24.53 / 32.70 = 0.75 (75%)
P(爱) = 8.17 / 32.70 = 0.25 (25%)
P(你) = 0 / 32.70 = 0 (0%) ← 被完全屏蔽了!
"""为什么这样有效?
- 数学特性:
exp(-inf) = 0,所以-inf经过 Softmax 后概率为 0 - 归一化保证: Softmax 保证所有概率和为 1,被屏蔽位置的概率被分配给其他位置
3. 提示工程
3.1 提示工程的本质
提示
提示工程 = 通过设计提示词来引导概率分布
举例:
普通提示,如下提示词会导致 概率分布较均匀,生成结果不确定
prompt1 = "写一个故事" probs1 = softmax(model(prompt1))精心设计的提示,会使 概率分布更集中在相关词汇上
prompt2 = "写一个关于AI的科幻故事,主角是一个机器人" probs2 = softmax(model(prompt2)) # "机器人"、"AI"、"未来"等词概率更高
3.2 采样参数
采样参数的本质就是在此基础上,根据不同策略"重新调整"或"截断"分布,从而改变大模型输出的下一个token
Temperature
温度 是控制模型输出 "随机性" 与 "确定性" 的关键参数, 这个和物理中的温度也很类似:
温度高 → 粒子运动剧烈 → 更随机温度低 → 粒子运动缓慢 → 更有序
其原理是: 引入温度 系数 T > 0 时, 将 Softmax 公式改写为
其中:
z_i:第 i 个原始分数T:温度参数(T > 0)exp(z_i / T):先除以 T,再取指数
它的作用是相当于 "缩放" 原始分数 如:
# 原始分数
z = [5.0, 3.0, 1.0, 0.5]
# 标准 Softmax(T = 1)
exp(5.0 / 1) = exp(5.0) = 148.41
exp(3.0 / 1) = exp(3.0) = 20.09
# 温度 Softmax(T = 2)
exp(5.0 / 2) = exp(2.5) = 12.18 # 缩小了!
exp(3.0 / 2) = exp(1.5) = 4.48 # 缩小了!
# 温度 Softmax(T = 0.5)
exp(5.0 / 0.5) = exp(10.0) = 22026.47 # 变大了!
exp(3.0 / 0.5) = exp(6.0) = 403.43 # 变大了!当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:原始概率候选集:概率最高的 k个词p̂_i:重新归一化后的概率
假设词汇表有 10 个词,并经过 Softmax 后得出了概率分布如下,然后前几个词概率高,最后 1 个词 概率很低,只有 0.005
# 假设词汇表有10个词(简化)
vocab = ["的", "是", "在", "了", "和", "有", "我", "你", "他", "她"]
# 原始概率分布(经过Softmax)
probabilities = {
"的": 0.40,
"是": 0.30,
"在": 0.15,
"了": 0.10,
"和": 0.03,
"有": 0.01,
"我": 0.005,
"你": 0.003,
"他": 0.001,
"她": 0.0005
}
# 验证:所有概率和 = 1.0 ✅如果进行 随机采样,则可能选到概率很低的词,假设选到了概率只有 0.0001 的词,则意味着 → 生成质量差
目标:只从高概率的词中采样
步骤1:按概率从高到低排序
sorted_probs = [ ("的", 0.40), # 第1名 ("是", 0.30), # 第2名 ("在", 0.15), # 第3名 ("了", 0.10), # 第4名 ("和", 0.03), # 第5名 ("有", 0.01), # 第6名 ("我", 0.005), # 第7名 ("你", 0.003), # 第8名 ("他", 0.001), # 第9名 ("她", 0.0005) # 第10名 ]步骤2:选择前 k 个(假设 k=3)
候选集 = [ ("的", 0.40), ("是", 0.30), ("在", 0.15) ] # 候选集的概率和 候选集概率和 = 0.40 + 0.30 + 0.15 = 0.85步骤3:对候选集的概率重新归一化,套公式:
p̂_i = p_i / Σp_j (j ∈ 候选集)p̂_的 = 0.40 / 0.85 = 0.47 p̂_是 = 0.30 / 0.85 = 0.35 p̂_在 = 0.15 / 0.85 = 0.18验证:
0.47 + 0.35 + 0.18 = 1.0✅
此时新的概率分布为(只包含候选集)
new_probs = {
"的": 0.47,
"是": 0.35,
"在": 0.18,
# 其他词的概率 = 0(被排除了)
"了": 0.0,
"和": 0.0,
# ...
}后续只会 根据新的概率分布 从候选集这 3个词 中随机采样
next_token = sample(new_probs)next_token 则可能选到:
- "的"(47%概率)
- "是"(35%概率)
- "在"(18%概率)
Top-p
其原理是: 将所有 token 按概率从高到低排序,从排序后的第一个 token 开始,逐步累加概率,依次加入 集合 S,直到累积和首次达到或超过 阈值 p 时:
当前 集合 S 就是 "核集合", 最后只对 集合 S 中的 token 重新归一化,再进行采样。
当模型预测每个词(token)的概率时,整个词表可能有 几万个候选,Softmax 把这些词转成概率分布。但即便某个字符概率只有 0.01%,如果只用普通采样,它也有机会被选中,导致输出可能突然 "跳车"。
目标:只在 "累积概率重要" 的词里抽样。
提示
Top-p: 能动态适应不同分布的 "长尾" 特性,对概率分布不均匀的极端情况 的适应性更好。
各参数区别
| 方法 | 控制参数 | 含义 | 行为 |
|---|---|---|---|
| 温度 | T | 调整概率形状 (陡 / 平) | 调整 所有词的 概率, 不削减候选,影响全局 |
| Top-k | k | 固定保留 k 个 | 限制 候选词 数量, 不看具体概率大小 |
| Top-p | p | 动态覆盖累计概率 | 自适应分布、数量不定 |
常见组合:
温度采样(T=0.8)+ Top-k(k=50) ,即先平滑分布,再限制候选词
注意
注意:
- 若将 Temperature 设为 0,则 Top-k/p 无意义,因为下一个 Token 除 0 变得无穷大,必是最可能得那个预测的 Token;
- 若将 Top-k 设为 1,则 Temperature 和 Top-p 无意义,因为仅有一个 Token 得到,也必是下一个预测的 Token。
3.3 指令调优
早期的 GPT 模型(如 GPT-3) 主要是 “文本补全” 模型,擅长前文续写,但不一定能很好地理解并执行人类的指令
指令调优 (Instruction Tuning): 是一种微调技术,它使用大量 “指令-回答” 格式的数据对预训练模型进行进一步的训练。调优后,模型能更好地理解并遵循用户的指令。
我们今天日常工作学习中使用的所有模型, 如: ChatGPT, DeepSeek, Qwen 都是其模型家族中经过指令调优过的模型。
对 "文本补全" 模型的提示(你需要用少样本提示 "教会" 模型做什么):
- 这是一段将英文翻译成中文的程序。
# 英文:Hello
# 中文:你好
# 英文:How are you?
# 中文:
对 "指令调优" 模型的提示(你可以直接下达指令):
- 请将下面的英文翻译成中文:
# How are you?尽管模型已经指令调优,但目前的提示词工程依然有如下手段进一步调优来提升回答质量:
- 角色扮演,赋予模型一个特定的角色(更符合特定场景)
- 上下文示例,提供模型少样本提示,给出清晰的输入输出示例等(复杂格式或特定风格的任务)
- 思维链,引导模型 “一步一步地思考”(逻辑推理、计算或多步骤思考)
4. 文本分词
计算机只能使用数字计算,因此处理自然语言文本也要将其文本序列转为数字才能计算。该过程则称为 分词。分词器(Tokenization)的作用就是定义一套规则,将原始文本切分成一个个最小单元,即 词元(Token)。
那么按人类理解的词汇分词(通过空格与标点切分,会导致词表爆炸与未登录词且语义失联)、或切成一个个字符分词(英文一共就26个字母,词表虽小但无独立语义,模型要大量学习其组合,成为有意义的词)都不够科学。因此普遍采用 子词分词。
相关信息
子词分词算法(Subword Tokenization)核心思想:
- 将常见的词(如 "agent")保留为 完整 的 词元
- 同时将不常见的词(如 "Tokenization")拆分成多个有意义的子词片段(如 "Token" 和 "ization")
- 这样既控制了词表的大小,又能让模型通过组合子词来理解和生成新词
4.1 BPE 算法
BPE (Byte-Pair Encoding)字节对编码算法:是一种最主流的子词分词的 贪心算法(GPT系列采用),既用于 训练前,也用于推理时 计算 token 数量 , 可以理解为一个“贪心”的合并过程,通过 逐步合并高频字符对 来构建词表。
其大致步骤如下:
- 初始化:将每个字符作为独立词元初始化词表,如:
"hug" → ['h', 'u', 'g'] - 迭代合并:语料库上,找到出现频率最高的相邻字符对,合并成一个新词元,如果 "u" 和 "g" 经常一起出现,就合并成 "ug",然后加入词表
- 重复:重复合并,直到词表大小达到预设阈值
代码示例:
import re, collections
def 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]] += freq
return pairs
def 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:
break
best = 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>']
--------------------4.2 实际使用示例
在 RAG 中使用 AutoTokenizer 就足够了,它内部已经实现了 BPE 分词,你可以验证 AutoTokenizer 确实在使用 BPE
from transformers import AutoTokenizer
# 这里只加载分词器(还未涉及到 embedding模型和模型权重)
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-base-en-v1.5")
# 查看tokenizer类型
print(type(tokenizer)) # 通常是 PreTrainedTokenizerFast
# 查看实际的token(可以看到BPE的合并结果)
text = "unhug"
tokens = tokenizer.tokenize(text) # 只做分词,不涉及embedding
print(tokens)
### 可能会看到类似 ['un', 'hug'] 或 ['unh', 'ug'] 这样的子词
### 这证明它确实在使用BPE的合并规则当你在 HuggingFace 上发布一个模型时,模型仓库通常包含:
BAAI/bge-base-en-v1.5/
├── config.json # 模型配置
├── pytorch_model.bin # Embedding模型权重
├── tokenizer.json # 分词器文件(BPE规则)
├── tokenizer_config.json
├── vocab.txt # 词表
└── ...所以 AutoTokenizer.from_pretrained("BAAI/bge-base-en-v1.5") 会:
从该模型仓库下载分词器相关文件(
***tokenizer.json***,***vocab.txt***等)此时还不会下载 embedding 模型的权重文件
一个更清晰的示例:
from transformers import AutoTokenizer, AutoModel
# 步骤1:加载分词器(只做分词,不涉及embedding)
tokenizer = AutoTokenizer.from_pretrained("BAAI/bge-base-en-v1.5")
# 下载的文件:tokenizer.json, vocab.txt 等(很小,几MB)
# 步骤2:加载embedding模型(做向量化)
model = AutoModel.from_pretrained("BAAI/bge-base-en-v1.5")
# 下载的文件:pytorch_model.bin(很大,几百MB)
# 使用流程:
text = "Hello world"
# 第一步:分词(只用tokenizer,不涉及model)
tokens = tokenizer(text, return_tensors="pt")
# tokens = {'input_ids': tensor([[101, 7592, 2088, 102]]), ...}
# 第二步:向量化(使用model)
with torch.no_grad():
outputs = model(**tokens)
embeddings = outputs.last_hidden_state
# embeddings = tensor([[[0.1, 0.2, ...], ...]]) # 向量表示为什么 AutoTokenizer、AutoModel 用同一个模型名?
分词不需要 embedding 模型参与,但分词器和模型在训练时是配对的,词表一致, 用同样的 BGE 模型名是因为分词器文件也在同一个仓库里,因此 AutoTokenizer.from_pretrained() 实际上只会下载分词器文件,不下载模型权重,完全可以单独使用分词器,但向量化时必须配套才行(词表一致)。
4.3 分词器的意义
提示
重要:
理解分词算法的细节并非目的,但理其实际影响非常重要,这直接关系到智能体的 性能、成本 和 稳定性
上下文窗口限制:模型的上下文窗口(如 8K, 128K)是以 Token 数量 计算的,而不是字符数或单词数。
- 同样一段话,在不同语言(如中英文)或不同分词器下,Token 数量可能相差巨大
- 精确管理输入长度、避免超出上下文限制是 构建长时记忆智能体的基础
API 成本:大多数模型 API 都是按 Token 数量计费的。了解你的文本会被如何分词,是预估和控制智能体运行成本的关键一步。
模型表现的异常:有时模型的奇怪表现 根源在于分词, 例如:
- 模型可能很擅长计算
2 + 2,但对于2+2(没有空格)就可能出错,因为后者可能被分词器视为一个独立的、不常见的词元RAG层面:分词 影响向量相似度,余弦相似度不同,检索结果也不同
"2 + 2" → 分词 → ['2', '+', '2'] → Embedding → 向量A "2+2" → 分词 → ['2+2'] → Embedding → 向量B
- 同样,一个词因为 首字母大小写不同,也可能被切分成 完全不同的 Token 序列,从而影响模型的理解
LLM生成中:影响的是 token序列本身,不仅仅是相似度
"Hello" → 分词 → [15496] (token ID) "hello" → 分词 → [31373] (完全不同的token ID)模型看到的是不同的 token ID,理解完全不同!
- 在设计提示词和解析模型输出时,考虑到这些 “陷阱” 有助于提升智能体的鲁棒性
解决方案:
文本规范化,在 RAG 检索前规范化
查询扩展, 生成多个查询变体,对每个变体进行检索,合并结果
# 生成多个查询变体 def expand_query(query): variants = [ query, # 原始查询 query.lower(), # 小写 query.replace('+', ' + '), # 添加空格 query.replace(' ', ''), # 移除空格 ] return variants**使用更好的分词器,SentencePiece*(Google 开源) ***之类的更优秀的分词器,通常更稳定