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

14 KiB
Executable File
Raw Blame History

阶段一 · 小点 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 注意力机制在做什么

一句话版本:模型让每个词去计算"我该重点看句子里的哪些词"。比如读"它跑到河边喝水",模型要知道"它"指的是谁。注意力就是让"它"关联到正确的指代词。

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 自回归生成(流式输出的根本原因)

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 的代码工具。要精准估算用量与成本,必须先过它。
# 估算 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 上下文窗口与两种应对策略

graph LR
    A[输入+输出 总token > 上下文窗口?] -->|是| B{怎么办?}
    B -->|最简单| C[滑动窗口截断: 丢最旧消息,留最近N轮]
    B -->|更聪明| D[上下文压缩: 把久远历史摘要成短文本]
    C --> E[生产常用组合: 近几轮原文 + 更早摘要]
    D --> E
  • 上下文窗口:单次请求"输入 + 输出"最大 token 数(如 128k)。超限必须主动处理,否则请求直接报错。
  • 滑动窗口截断:把最旧消息丢掉、保留最近 N 轮。最简单,但会失去早期记忆。
  • 上下文压缩:让 LLM 把久远的历史总结成一段短摘要,用它替代原文。生产标配是"近几轮原文 + 更早摘要"混合,兼顾记忆与成本。
# 滑动窗口截断: 保留最近 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 上下文压缩示例

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 贪心取最高概率,但不是"绝对唯一",极端情况下仍有平局的取舍。
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 治理手法。