Files
python-agent/docs/01-阶段一-前置核心基础/05-大模型基础原理.md
T
2026-09-22 15:42:25 +08:00

202 lines
14 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段一 · 小点 5:大模型基础原理
> 所属:阶段一 大模型基础与 Prompt 工程(Agent 核心根基)
> 定位:做 Agent 不要求你手推反向传播,但你必须理解"模型为什么会这么表现"——为什么流式输出是一块块的、为什么上下文会超限、为什么调参会影响答案。理解了这几条,你写的 Agent 才不是瞎碰。
## 精简大纲
1. Transformer 核心思想:注意力机制通俗理解、自回归生成
2. Token 计算方式、上下文窗口、截断 / 压缩策略
3. 采样参数:temperature / top-p / top-k / max_tokens 与生产调优
## 学习内容详情
> **新一代模型现状(2025-2026)**:闭源侧 GPT-5 家族(2025 年 8 月发布:GPT-5 / GPT-5 mini / GPT-5 nano;API 定价输入 $1.25/百万 token、输出 $10/百万 token,mini 为 $0.25/$2)、Anthropic Claude 4 / 4.5 系列;开源侧 DeepSeek-V3/R1、Qwen3 等。⚠️ 模型迭代很快(截至 2026 已有 Claude 4.5、DeepSeek-V3.1、Qwen3-Max 等更新版本),具体型号与价格以官方最新为准;但本节学的是**原理层不变量**(上下文窗口 / 采样参数 / token 计费),它稳定不变,学习重点不受影响。
> ⏳ **短期可不深究**:具体哪个模型叫什么名、怎么定价、看哪些型号的细节第一遍不必背——模型迭代快、随时会过时,需要时查官方最新文档即可,别把时间花在记型号/价格上。
### 1. Transformer 与生成原理
#### 1.1 注意力机制在做什么
一句话版本:模型让每个词去计算"我该重点看句子里的哪些词"。比如读"它跑到河边喝水",模型要知道"它"指的是谁。注意力就是让"它"关联到正确的指代词。
```mermaid
graph LR
A[每个token] --> B[计算与其他token的关联权重]
B --> C[重点加权关注相关词]
C --> D[携带上下文语义 继续预测]
```
- 这解释了模型**理解上下文**的能力来源。应用层不用深挖数学,但要清楚它带来的约束(上下文长度受限、对长文本处理有压力、指令遵循有波动)。
> 🔬 **深度理解 · 注意力机制(Attention)**:
> 1. **本质**:让每个 token 为"预测自己"而给句子里其他 token 分配"关注权重",从而把**远距离相关**的信息拉近——解决"距离越远、语义关联越弱"的天然缺陷。
> 2. **机制**:每个 token 被编码成 Query(我要找什么)/ Key(我是谁)/ Value(我携带什么信息);它拿自己的 Query 去和所有 token 的 Key 做点积打分,经 softmax 归一化成权重,再按权重加权求和所有 Value,得到融合上下文的表示;多头注意力=同时学多套这种"关注关系"。
> 3. **为什么重要**:它是 Transformer 能整句并行处理(而非像 RNN 逐个串行)的关键,也是理解指代、推理逻辑的能力来源;注意力开销随文本长度平方式增长,这正是上下文窗口受限的深层原因。
> 4. **易错点**:注意力是"加权平均",本身不含绝对位置(靠额外位置编码注入);应用层不必推数学,但要理解它带来的约束——长文本开销大、窗口有限,所以才有截断/压缩策略。
#### 1.2 自回归生成(流式输出的根本原因)
```mermaid
sequenceDiagram
participant M as 模型
participant T as Token流
M->>T: 根据已生成内容 预测下一个token A
M->>T: 根据【A】再预测下一个token B
M->>T: 根据【A,B】再预测下一个token C
loop 持续到结束/达到max_tokens
M->>T: 一个接一个地生成本token
end
```
- 模型**一次只预测一个 token**,拿"已生成内容"当条件去猜下一个。这就是为什么流式输出能一块块推给前端——它本来就是一个字一个字蹦出来的。
> 🔬 **深度理解 · 自回归生成**:
> 1. **本质**:把"生成任意长文本"降维成"反复执行同一个条件概率预测"——`P(下一个token | 已生成的tokens + 输入上下文)`,一次只决定一个 token。
> 2. **机制**:每个新 token 会拼回输入做"下一轮条件",所以它是严格从左到右、逐 token 推进的自回归结构;这正是流式输出"天生一字一字蹦"的根本原因(不是网络特意分片,而是生成就是逐 token 的)。
> 3. **为什么重要**:理解这一点就能解释三个现象——为什么流式能低延迟输出(首 token 到得快)、为什么输出够长时会撞 `max_tokens` 截断、为什么截断会得到"残句"。Agent 里组织流式/控制输出长度都源于此。
> 4. **易错点**:模型输出长度受 `max_tokens`(输出侧上限)约束,而上下文窗口是"输入+输出"总上限;"复读/提前断"常是 max_tokens 过小或触发保护所致,不是模型"坏"了。
> **应用层启示**:既然是一次吐一个,那"输出质量"和"max_tokens 给多少"就直接挂钩。输出太长被截断,答案可能残缺;太短提前断,则属于复读机触发保护。
### 2. Token 与上下文窗口
#### 2.1 Token 是什么
- **Token**:模型理解文本的最小单位。英文 1 词约 1.3 个 token;中文 1 字约 1~2 token;标点、空格也算。
- **分词器**(如 OpenAI 的 tiktoken):把文本切成 token 的代码工具。要精准估算用量与成本,必须先过它。
```python
# 估算 token 成本 (以 OpenAI tiktoken 为例)
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
text = "你好,我想开发一个 AI Agent。"
tokens = enc.encode(text)
print("token 列表:", tokens)
print("token 数量:", len(tokens)) # 一句话中文可能十几甚至几十个token
```
> ⚠️ 不能按"字数"估算,中文尤其要命。上线前用分词器实测,比你拍脑袋准得多。
> ⏳ **短期可不深究**:分词器内部的具体编码算法(BPE 合并规则、词表是怎么构建的)属于底层实现细节,第一遍会用官方 `tiktoken`/`tokenizers` 做估算即可,不必背规则,用到再深入。
#### 2.2 上下文窗口与两种应对策略
```mermaid
graph LR
A[输入+输出 总token > 上下文窗口?] -->|是| B{怎么办?}
B -->|最简单| C[滑动窗口截断: 丢最旧消息,留最近N轮]
B -->|更聪明| D[上下文压缩: 把久远历史摘要成短文本]
C --> E[生产常用组合: 近几轮原文 + 更早摘要]
D --> E
```
- **上下文窗口**:单次请求"输入 + 输出"最大 token 数(如 128k)。超限必须主动处理,否则请求直接报错。
- **滑动窗口截断**:把最旧消息丢掉、保留最近 N 轮。最简单,但会失去早期记忆。
- **上下文压缩**:让 LLM 把久远的历史总结成一段短摘要,用它替代原文。**生产标配是"近几轮原文 + 更早摘要"混合**,兼顾记忆与成本。
```python
# 滑动窗口截断: 保留最近 N 条消息
def trim_history(history: list[dict], keep: int = 10) -> list[dict]:
"""超限时丢最旧的, 保留最近 keep 条(不含 system 系统消息)。"""
system_msgs = [m for m in history if m["role"] == "system"] # 系统消息要保留
rest = [m for m in history if m["role"] != "system"]
return system_msgs + rest[-keep:] # 系统 + 最近 keep 条
history = [
{"role": "system", "content": "你是助手"},
{"role": "user", "content": "第1问"}, {"role": "assistant", "content": "第1答"},
{"role": "user", "content": "第2问"}, {"role": "assistant", "content": "第2答"},
{"role": "user", "content": "第3问"}, {"role": "assistant", "content": "第3答"},
]
print(trim_history(history, keep=2))
# 保留 system + 最近2条(第3轮), 前两轮被丢弃
```
#### 2.3 上下文压缩示例
```python
def compress_old(history: list[dict], llm, age: int = 6):
"""
把超过 age 条的早期历史让 LLM 压成一段摘要, 再拼接最新几轮原文。
这样既保留早期记忆, 又不撑爆上下文。
"""
if len(history) <= age:
return history # 不够长, 不压缩
old = history[:-age] # 早期这段
recent = history[-age:] # 最近这段原文
summary = llm(f"请把这段对话压缩成三句话摘要:{old}")
return [{"role": "system", "content": f"早期对话摘要:{summary}"}] + recent
```
### 3. 采样参数(生产经验值)
| 参数 | 用途 | 经验值 |
|-|-|-|
| temperature | 随机度(越高越"发散") | 工具调用/结构化 0~0.3;创意文案 0.7+ |
| top-p | 累计概率前 p 的候选词采样 | 与 temperature 二选一调优,别同时乱叠 |
| top-k | 只在前 k 名里采样 | 一般不叠加调整 |
| max_tokens | 限制输出长度 | 成本闸门 + 防模型复读 |
> **关键原则**:结构化输出(抽 JSON / 工具调用)要把 temperature 调低(0~0.3),保证稳定可复现;创意类内容才调高。别真以为 temperature=1 就"锦上添花"。
> 🔬 **深度理解 · 采样参数(temperature / top-p / top-k)**:
> 1. **本质**:语言模型输出的其实是一个"每个候选 token 的概率分布";采样参数决定"从这份分布里怎么挑下一个 token",是控制"确定性 vs 随机性"的旋钮。
> 2. **机制**:temperature 通过重缩放 `softmax(logits / temperature)` 实现——越低分布越尖(高概率项更突出→更确定),越高越平(低概率项出货机会↑→更发散);top-p 取累计概率刚过 p 的最小候选集合再在其中采;top-k 只在前 k 名之间采。
> 3. **为什么重要**:工具调用/JSON 结构化必须高确定性(temperature≈0),否则抽出的动作或参数飘忽导致程序解析崩溃;创意文案则需要温度放宽以增加多样性。合理调参就是在那条"稳定可复现 ↔ 发散有创意"的轴上取平衡。
> 4. **易错点**:temperature 与 top-p 都在影响随机性,同时乱叠会双重缩放难以预期,通常二选一调优;temperature=0 近似 greedy 贪心取最高概率,但不是"绝对唯一",极端情况下仍有平局的取舍。
```python
def build_llm_kwargs(task: str) -> dict:
"""按任务返回合理的采样参数。"""
if task == "structured": # 工具调用 / JSON 输出
return {"temperature": 0.1, "max_tokens": 1024}
if task == "coding": # 代码生成
return {"temperature": 0.2, "max_tokens": 2048}
return {"temperature": 0.8, "max_tokens": 512} # 创意文案
```
## 本节自检
- [ ] 能估算一段中文文本的 token 量,并知道上下文超限的三种处理策略
- [ ] 能针对不同任务(工具调用 vs 创意)设置合理采样参数
- [ ] 能说出流式输出为什么"天生是一个 token 一个 token 蹦出来的"
## 本节配套思考题(快速入门的检验)
1. 为什么工具调用场景要把 temperature 调到接近 0?如果调到 1 会发生什么?
2. 中文 100 字的对话,用 tiktoken 估算它大概多少 token?能不能直接用"字数"当 token 数?
3. "滑动窗口截断"和"上下文压缩"各自的代价是什么?什么时候该用哪种?
## 本节常见面试题(深度解析)
> 针对本节核心知识的面试高频点,配合"本质+机制"式理解,能让你答得既有深度又有广度。
### 面试题 1:为什么模型是"一次只预测一个 token"的?它对流式输出和 max_tokens 有什么影响?
- **面试官想考察**:是否理解自回归生成的本质,以及由此推导出流式、截断等现象,而非背结论。
- **专业作答(含深度)**:
1. **自回归本质**:生成文本被降维成反复执行条件概率 `P(下一个token | 已生成内容+输入)`,一次决定一个 token,新 token 再拼回输入继续。
2. **流式的根本原因**:正因为逐 token 生成,服务端才能"生成一块回传一块",实现首字低延迟、前端打字机效果。
3. **对 max_tokens 的影响**:输出是逐 token 累加的,输出侧有长度上限;超上限会被截断成"残句",提前断常是保护/上限过小所致。
4. **工程启示**:Agent 组织流式输出、控制响应长度都要顺着"逐 token"这个事实来设。
- **加分亮点 / 深度追问**:可提 KV Cache(已算过的上下文缓存以加速后续 token)、以及"输出 token 也占上下文窗口配额"这个常见误解。
### 面试题 2:temperature 和 top-p 都是控制随机性的,它们有何区别?为什么工具调用要把 temperature 调低?
- **面试官想考察**:能否说清两者的作用层级与适用场景,理解"确定性高"对结构化输出的价值。
- **专业作答(含深度)**:
1. **temperature 的机制**:通过重缩放 logits 改变概率分布的"尖锐度"——低则更确定、高则更发散,属"全局重塑分布"。
2. **top-p 的机制**:截取累计概率到 p 的最小候选集再采样,属"裁掉尾部低概率项",属于局部的候选集裁剪。
3. **区别**:temperature 改变相对概率大小,top-p 决定"从哪些候选里挑";强行双占用会导致难以预期的叠加,一般二选一。
4. **工具调用要低温**:工具名/参数必须稳,若温度高导致动作或参数飘忽,下游程序解析会崩;JSON 结构化同理。创意文案才需放宽温度增加多样性。
- **加分亮点 / 深度追问**:可提 top-k 与两者的关系(同时卡 top-k 更激进)、固定输出场景的服务端默认参数(如 temperature 默认值因模型而异),以及 temperature=0 与 greedy 的细微区别。
### 面试题 3:上下文窗口有限,你的 Agent 会如何应对超长对话?截断和压缩各自的取舍是什么?
- **面试官想考察**:是否理解"窗口是硬约束",能对两种主流策略做成本权衡。
- **专业作答(含深度)**:
1. **问题本质**:单次请求"输入+输出"总 token 不能超窗口,超限要么报错要么失真。
2. **滑动窗口截断**:丢最旧消息留最近 N 轮。实现简单、省成本,但**丢失早期记忆**,上下文越长失真越明显。
3. **上下文压缩**:把久远历史让 LLM 压成短摘要,较完整保留语义,"更聪明"却**每次压缩有延迟与成本**,且压缩本身也有精度损失。
4. **生产标配**:近几轮原文 + 更早摘要的混合策略,兼顾记忆完整度与成本/延迟;对超长且高度相关的早期上下文(如代码现场)可能仍需摘要或外部记忆。
- **加分亮点 / 深度追问**:可提上下文之外的外挂记忆(向量检索/摘要存档)作为"无限上下文"的折中,以及"分段/分页"、"只回传工具结果摘要而非原文"等 token 治理手法。