从原理到工程・从训练到落地|零基础 14 章中文大模型工程师完整系统教材
从原理到工程 · 从训练到落地
大模型工程师
从零基础到训练、对齐与应用落地
开源仓库地址:https://github.com/wei1305/llm-engineer-from-zero
在线网页阅读(支持目录跳转、公式渲染):大模型工程师:从零到实践 - 大模型工程师:从零到实践
有用请点 Star 收藏,作者会持续更新案例、补充面试题与实验代码!
面向零基础读者的系统教材
涵盖 Python、Transformer、RAG、Agent、训练、对齐、多模态、分布式与推理优化
十四章 · 四份附录 · 二十九幅结构图 · 十九份 Python 手写代码
2026 完整修订版
目录
第一章 Python、数学与神经网络:建立可计算的直觉... 17
第二章 Transformer 与 MoE:从矩阵计算到稀疏专家... 28
2.2 Self-Attention:每一步都写出形状... 29
2.3 Encoder、Decoder 与 Cross-Attention.. 31
第三章 Llama 与 DeepSeek:现代解码器的关键改造... 39
3.3 MHA、MQA、GQA 与 KV Cache. 42
3.7 DeepSeek-V3:MoE、MLA、MTP 与系统协同... 45
3.8 DeepSeek-R1:推理能力来自后训练路线... 46
第四章 部署、压测与应用开发:把模型变成可靠服务... 50
4.7 Function Calling:模型提议,程序执行... 56
第七章 Agent:让模型在受控边界内观察、决策与行动... 80
8.4 Scaling Laws:用小实验规划大训练... 94
第九章 微调:从数据设计到 LoRA/QLoRA 工程... 102
第十章 偏好对齐:Reward Model、PPO、DPO 与 GRPO... 112
第十一章 多模态大模型:视觉编码、跨模态对齐与训练... 121
11.1 ViT:把图像变成 token 序列... 122
11.3 BLIP/BLIP-2 与 Q-Former. 124
11.6 微调 Qwen-VL 类模型的通用步骤... 126
第十二章 分布式训练:并行策略、通信与故障恢复... 131
12.7 Accelerate 与 DeepSpeed.. 137
第十三章 推理优化:量化、蒸馏、KV Cache 与高效服务... 141
13.7 KV Cache 与 PagedAttention.. 146
A.3 安装 PyTorch 前先核对兼容关系... 163
提示:在 Word 中选择“更新目录”可刷新页码。
前言:把大模型学成一套可验证的工程能力
大模型领域的术语很多,更新也很快。对零基础读者而言,真正困难的往往不是某一个公式,而是不知道这些公式、代码和工程组件之间是什么关系:Python 为什么要学到类和生成器,矩阵乘法为什么会出现在注意力里,模型已经预训练好了为什么还要 RAG,LoRA 节省的究竟是哪部分显存,Agent 为什么不能简单理解成“让模型自己调用工具”。
本书围绕一个统一问题展开:给定数据、算力和业务目标,怎样构建一个可训练、可评估、可部署、可解释其边界的大模型系统? 十四章不是十四份彼此独立的知识清单,而是一条逐层展开的工程链路。

图 0-1 全书知识地图:底层能力决定上层系统能否被正确理解、调试与评估。
第一章先建立 Python、张量、梯度和训练循环的直觉;第二、三章进入 Transformer、MoE、Llama 与 DeepSeek 的模型内部;第四至第七章讨论怎样把模型变成服务、RAG 和 Agent;第八至第十三章回到模型生产链,讲预训练、微调、偏好对齐、多模态、分布式训练与推理优化;第十四章把这些知识整理成简历证据、项目表达、手写代码和系统设计能力。
这本书适合谁
如果你只会一点 Python,甚至只运行过几个 Notebook,也可以从第一章开始。零基础不等于不需要基础,而是本书不会假设你已经理解张量形状、反向传播、注意力 mask 或 GPU 显存。每次引入新概念时,我们都会先说明它解决什么问题,再写输入输出、最小例子、常见失败和验证方式。
以下几类读者可以采用不同路线:
- 完全初学者:按章节顺序阅读。每章至少完成一个最小代码任务,不要跳过第一章和第二章。
- Python 开发者:第一章重点看张量、自动微分和训练闭环,之后进入部署、RAG 与 Agent,再回头补训练章节。
- 传统算法或深度学习学习者:可快速复习第一章,但应完成注意力、RoPE、LoRA 和 DPO 的手写代码。
- 准备求职的读者:先读第十四章了解能力证据,再按薄弱项回到对应章节;不要把面试题答案与真实项目经验混为一谈。
无论哪条路线,都建议保留一个实验仓库。真正学会的标志不是“看懂了”,而是能在没有正文提示时重新实现,能解释中间形状,能构造失败输入,也能说清实现没有覆盖什么。
第一次学习前的环境准备
本书的基础示例以 Python 3.10 及以上版本为目标。第一至第三章的大部分原理代码在 CPU 上即可运行;真实模型推理、微调和分布式训练通常需要 Linux、NVIDIA GPU、匹配的驱动/CUDA/PyTorch,以及足够的显存。没有 GPU 不妨碍理解核心原理,先用小张量和微型模型验证计算,再选择云端或实验室环境运行大模型。
建议每个项目使用独立虚拟环境。下面展示的是通用流程,不要把版本号永久照抄;安装 GPU 版 PyTorch 时应以官方安装页面给出的兼容命令为准。
BASH
python --version
python -m venv .venv
# Windows PowerShell
.venv\Scripts\Activate.ps1
# Linux / macOS
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install numpy pytest
安装后先做健康检查,而不是直接下载几十 GB 权重:
PYTHON
import platform
import numpy as np
print("Python:", platform.python_version())
print("NumPy:", np.__version__)
try:
import torch
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
if torch.cuda.is_available():
print("GPU:", torch.cuda.get_device_name(0))
except ImportError:
print("PyTorch 尚未安装;第一章前半部分仍可使用 NumPy 学习。")
如果使用 Windows 学习训练与部署,常见选择是 WSL2 或远程 Linux 主机。路径、文件权限、换行符和多进程启动方式与原生 Windows 不同,遇到问题时应先确认命令究竟在哪个环境执行。涉及 CUDA 的错误要同时记录 GPU 型号、驱动、CUDA runtime 和 PyTorch build;只说“CUDA 版本是 12”通常不够定位问题。
本书的代码分成三层
- 原理层:只依赖 Python、NumPy 或 PyTorch,手写反向传播、注意力、RoPE、LoRA、DPO、量化等核心过程。原理代码刻意保持小,方便手算和测试。
- 框架层:使用 Transformers、PEFT、TRL、FastAPI、LlamaIndex、vLLM、Accelerate、DeepSpeed 等工具完成规范实现。框架会变化,理解接口与数据流比记住一条命令更重要。
- 工程层:讨论服务指标、数据版本、评估集、权限、灰度发布、可观测性、成本与故障恢复。几十行 Demo 不是生产系统,本书会明确两者之间还缺什么。
每次运行代码前,先回答四个问题:输入张量或对象是什么?输出是什么?哪部分状态会改变?用什么证据判断更好?如果答不出来,先不要急着调参。
怎样阅读公式和张量形状
公式不是装饰。阅读公式的顺序是:先确定变量含义和形状,再确认求和或归一化发生在哪个轴,最后把公式翻译成程序。以注意力为例:
\operatorname{Attention}(Q,K,V)=\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}+M\right)V
假设 Q 为 (B,H,Tq,Dh)、K 为 (B,H,Tk,Dh),那么 Q @ K.transpose(-2,-1) 为 (B,H,Tq,Tk)。Softmax 应沿最后一个 Tk 轴归一化,表示每个查询位置对所有键位置的权重。再乘 (B,H,Tk,Dh) 的 V,输出回到 (B,H,Tq,Dh)。mask M 必须能广播到分数矩阵;因果 mask 与 padding mask 解决的是两种不同问题。
遇到求和符号时,写出被消掉的轴;遇到期望时,问随机变量和采样分布是什么;遇到梯度时,问它对哪个标量求导。手算 2×2 或 2×3 的小例子,比一次性读十页推导更能建立直觉。
怎样阅读和调试代码
不要从安装完整训练框架开始。推荐遵循“最小输入—中间量—断言—扩规模”的顺序:
- 给函数构造可以手算的输入,先写出预期形状和数值范围。
- 打印或返回关键中间量,例如 mask、注意力权重、label、梯度和检索候选。
- 用断言固定不变量,例如概率和为 1、未来位置权重为 0、padding 标签为 -100、引用 id 必须存在。
- 再增加 batch、长度、混合精度和并发;每次只改变一个主要变量。
- 失败时保存最小样例,修复后加入回归测试。
模型与数据文件也可能带来安全风险。不要执行来源不明的自定义代码或反序列化不可信 checkpoint;不要把 API Key 写进 Notebook、截图或 Git 仓库。公开仓库提交前应检查历史记录,因为删除当前文件并不会自动删除旧提交里的密钥。
实验记录模板
建议为每次实验保留以下内容:
- 任务与假设:想验证什么,为什么预期如此。
- 数据:来源、许可、清洗、切分、版本和样本数量。
- 环境:代码提交、依赖、硬件、随机种子和启动命令。
- 配置:模型、优化器、学习率、批量、精度、序列长度和解码参数。
- 结果:训练曲线、离线指标、延迟、成本和失败样例。
- 结论:哪些证据支持或否定假设,尚有哪些替代解释,下一步只改什么。
一个简单的实验目录可以这样组织:
TEXT
experiment-001/
README.md # 问题、运行方法、结论与限制
config.yaml # 可机器读取的配置
environment.txt # 依赖和硬件
data_manifest.json # 数据版本与哈希
logs/ # 原始日志
results/ # 指标、曲线、失败样例
tests/ # 回归测试
这种记录习惯会贯穿全书。它也是面试中区分“做过项目”和“看过项目”的关键:真正做过的人能解释取舍、指标定义、失败、恢复条件和结果边界。
关于论文、开源教材与视频
正文中的架构和算法结论优先核对论文原文与官方文档。GitHub 开源教材用于发现讲解顺序、练习题和可运行代码入口,本书会给出链接,但不会复制其受版权保护的正文。建议搭配阅读 Happy-LLM、LLMs-from-scratch、LLM-Universe、All-in-RAG、Hello-Agents、LLaMA-Factory 等公开项目,并核对各自许可证。
视频只作为辅助材料。观看时暂停并手写中间结果,不要把“播放完”当作掌握。原始文档中的私有课程、付费社群和不可公开访问的视频链接不会出现在本书;推荐视频优先使用可公开访问的 B 站入口。
安全、合规与知识边界
大模型可能生成错误内容;RAG 可能检索到过期或无权限证据;Agent 会放大工具权限带来的风险;训练数据还涉及隐私、版权和偏见。医疗、法律、金融等高风险场景必须有领域专家、数据治理、权限控制、人工复核和明确责任边界。本书示例用于学习,不构成专业结论,也不建议直接接入真实敏感数据。
当资料没有公开、实验无法复现或证据互相冲突时,科学的写法是明确“不知道”“尚未验证”或“根据现有证据推测”,而不是用流畅语言填补空白。大模型工程真正稀缺的能力,不是记住所有名词,而是把复杂问题拆成可验证的假设,用数据和代码说明取舍,并在证据不足时守住边界。
第一章 Python、数学与神经网络:建立可计算的直觉
本章的目标不是“快速看完 Python”,而是建立后续所有章节都会用到的三种能力:用数组表达数据,用函数表达模型,用梯度表达学习。学完后,你应当能读懂一段 PyTorch 训练代码,能手写一个小型神经网络,并能定位形状、数值和数据三类常见错误。

图 1-1 训练不是一次函数调用,而是数据、预测、损失、梯度和参数更新组成的闭环。
1.1 Python:先掌握会反复出现的部分
先建立直觉。 Python 在本书中不是考语法,而是用来表达数据怎样流动。初学者先把变量、容器、函数、类和异常看成组织状态与行为的工具。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 用 list、dict 与 dataclass 表示一条样本。
- 用函数把读取、变换、评估拆开。
- 用类型标注和断言固定输入输出契约。
最小例子。 把一条问答样本写成包含 prompt、answer、metadata 的字典,再写函数只返回模型需要的字段;打印每个字段的类型与长度。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 能运行不等于可维护。最常见的问题是可变默认参数、维度含义藏在变量名里、异常被无条件捕获,以及循环中反复做昂贵初始化。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:实现 batch_iter(samples, batch_size),覆盖空列表、最后一个不足批次和非法 batch_size 三种边界。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
大模型代码里最常见的 Python 能力包括容器、函数、类、迭代器、上下文管理器和类型标注。列表适合有序样本,字典适合配置与结构化记录,生成器适合不一次性加载全部数据。下面的批处理生成器不会复制完整数据集:
PYTHON
from collections.abc import Iterator, Sequence
def batches(data: Sequence, batch_size: int) -> Iterator[Sequence]:
if batch_size <= 0:
raise ValueError("batch_size 必须大于 0")
for start in range(0, len(data), batch_size):
yield data[start:start + batch_size]
for batch in batches(list(range(10)), 4):
print(batch)
yield 让函数返回一个迭代器。真实训练中的 DataLoader 还会处理并行读取、随机打乱、采样和拼接,但“按需产生批次”的思想相同。调试 Python 时,不要只打印最终结果;优先打印类型、长度、形状和少量样本。
面向对象在模型代码中的主要价值是组织状态。torch.nn.Module 把参数、子模块和前向计算组合在同一对象中。函数式编程则适合无状态的数据变换、损失计算和指标计算。两者并不冲突:模型用类封装,纯计算尽量写成小函数,便于测试。
1.2 数学基础:向量、矩阵、概率和导数

图 01-2 先标注批次、特征和隐藏轴,再进行矩阵乘法与反向传播。从问题出发。 线性代数负责描述批量变换,概率负责描述不确定性,微积分负责说明参数怎样改变损失。三者在神经网络里不是三门分开的课,而是一条计算链。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 先写出每个张量的形状与轴含义。
- 再判断运算是逐元素、点积还是矩阵乘。
- 最后沿计算图用链式法则追踪梯度。
用小数据走一遍。 若 X 为 (B,D),W 为 (D,H),则 XW 为 (B,H)。其中每个输出元素是 D 个乘积的求和;反向传播时 dW=X^T dY。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 只背公式而不写形状会掩盖大多数错误;把概率当作频数、把梯度当作参数更新量、忽略广播轴也会造成似是而非的理解。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:手算一个 2×3 矩阵与 3×2 矩阵的乘积,再用 NumPy 验证;对其中一个元素做有限差分梯度检查。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
一个词向量可写成 $x\in\mathbb{R}^d$,一批长度为 $T$ 的序列可写成 $X\in\mathbb{R}^{B\times T\times d}$。这里 $B$ 是批量大小,$T$ 是序列长度,$d$ 是隐藏维度。后续阅读任何模型,先写出每个张量的形状,通常比背模块名称更有效。
线性层为
Y=XW+b,
若 $X$ 的末维是 $d_{in}$,$W\in\mathbb{R}^{d_{in}\times d_{out}}$,则输出末维是 $d_{out}$。偏置 $b$ 会按广播规则加到前面的所有位置。广播方便,但也可能掩盖错误;(B,T,1) 与 (B,T) 相加会得到意外形状,调试时应显式检查。
概率分布满足非负且总和为 1。语言模型把词表上的 logits $z$ 通过 Softmax 转为概率:
p_i=\frac{e^{z_i}}{\sum_j e^{z_j}}.
实际实现先减去最大值,避免指数溢出。分类常用交叉熵 $-\log p_y$;自回归语言模型则对每个位置的下一个 token 计算交叉熵,再对有效位置求平均。困惑度 $\mathrm{PPL}=e^{\text{loss}}$ 便于解释,但跨 tokenizer 的数值不可直接比较。
导数描述输出对输入的局部敏感度。复合函数通过链式法则传播梯度:若 $L=f(g(x))$,则
\frac{\partial L}{\partial x}=\frac{\partial L}{\partial f}\frac{\partial f}{\partial g}\frac{\partial g}{\partial x}.
反向传播不是另一套学习算法,而是高效应用链式法则。计算图记录前向运算和依赖关系,反向阶段从标量损失出发,把梯度沿图反向累积到参数。
1.3 神经网络的基本组件
先看它解决什么。 神经网络可以理解为可微函数的层叠:线性层改变坐标,非线性层提供表达能力,损失函数把预测质量压缩成一个可优化标量。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 前向计算保存中间量。
- 损失函数比较预测与目标。
- 反向传播计算每个参数的偏导。
- 优化器依据梯度更新参数。
一个可以手算的例子。 二分类网络把 x 送入 Linear-ReLU-Linear,输出一个 logit;BCEWithLogitsLoss 同时完成数值稳定的 Sigmoid 与交叉熵。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 激活函数并非越多越好;最后一层是否加 Softmax 取决于损失函数是否已包含它。训练和评估模式还会影响 Dropout 与 BatchNorm。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:画出两层网络的计算图,标注每个节点的形状,并说明哪个节点必须是标量才能直接调用 backward。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
多层感知机可写成
h=\phi(XW_1+b_1),\qquad \hat{y}=hW_2+b_2.
激活函数让网络具有非线性。ReLU 简单高效,但负半轴梯度为零;GELU 平滑地按输入大小“门控”,Transformer 中很常见;SwiGLU 将两条线性投影相乘,后续 Llama 章节会展开。初始化要让各层信号不过度放大或衰减,线性层常结合 Xavier 或 Kaiming 方法。
优化器根据梯度更新参数。最基础的随机梯度下降为 $\theta\leftarrow\theta-\eta\nabla_\theta L$。AdamW 维护一阶、二阶矩估计,并把权重衰减与梯度更新解耦。学习率过大可能发散,过小则学习缓慢;梯度裁剪只能限制异常大梯度,不能修复错误数据或错误损失。
训练集用于更新参数,验证集用于选超参数和早停,测试集只用于最终报告。若同一用户、同一文档或高度相似样本跨集合出现,就会发生数据泄漏,指标会虚高。
1.4 NumPy 手写两层网络
抓住这一节的主线。 手写 NumPy 网络的目的不是替代框架,而是亲眼看到缓存、中间梯度和更新规则。只要能解释每一行为什么存在,之后使用自动微分才不会变成黑箱。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 固定随机种子并初始化小权重。
- 完成前向与稳定损失。
- 按链式法则反传到每个参数。
- 更新后重新计算损失。
先做最小实验。 对一个权重 w,用 (L(w+ε)-L(w-ε))/(2ε) 近似真实梯度;它应与解析梯度接近,但 ε 过大有截断误差、过小有浮点误差。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 常见错误包括忘记除以 batch 大小、Sigmoid 溢出、原地修改缓存、矩阵转置方向写反以及梯度检查时仍在更新参数。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:给现有两层网络加入 ReLU,并对 W1、b1、W2、b2 各抽一个元素做中心差分检查。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
下面用 NumPy 完成二分类网络的前向与反向传播。代码刻意展开中间量,便于检查每一步。
PYTHON
import numpy as np
rng = np.random.default_rng(7)
X = rng.normal(size=(128, 2))
y = ((X[:, 0] * X[:, 1]) > 0).astype(np.float32)[:, None]
W1 = rng.normal(scale=0.2, size=(2, 16))
b1 = np.zeros((1, 16))
W2 = rng.normal(scale=0.2, size=(16, 1))
b2 = np.zeros((1, 1))
def sigmoid(x):
x = np.clip(x, -30, 30)
return 1.0 / (1.0 + np.exp(-x))
lr = 0.1
for step in range(1000):
z1 = X @ W1 + b1
h = np.tanh(z1)
logits = h @ W2 + b2
p = sigmoid(logits)
eps = 1e-7
loss = -(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps)).mean()
# 对 sigmoid + 二元交叉熵,dL/dlogits 可化简为 (p-y)/N
dlogits = (p - y) / len(X)
dW2 = h.T @ dlogits
db2 = dlogits.sum(axis=0, keepdims=True)
dh = dlogits @ W2.T
dz1 = dh * (1 - h ** 2)
dW1 = X.T @ dz1
db1 = dz1.sum(axis=0, keepdims=True)
W1 -= lr * dW1
b1 -= lr * db1
W2 -= lr * dW2
b2 -= lr * db2
if step % 200 == 0:
print(step, float(loss))
代码解读:h.T @ dlogits 把 128 个样本对第二层权重的贡献求和;sum(..., keepdims=True) 保持偏置的二维形状;1-h**2 是 tanh 的导数。若损失不降,先检查数据是否可学,再做数值梯度检查:对单个参数加减很小的 $\epsilon$,比较有限差分与解析梯度。
1.5 用 PyTorch 重写训练闭环
先把概念落到可观察对象上。 PyTorch 把手写缓存换成动态计算图,把参数集合交给 Module 管理,但训练闭环仍是同一件事:取批次、前向、计算损失、清梯度、反传、更新。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- Dataset 定义单个样本。
- DataLoader 负责批处理与打乱。
- Module 定义可学习计算。
- optimizer.step 执行更新。
把它缩小到能逐项检查。 调用 loss.backward 后查看 model.linear.weight.grad;再比较 zero_grad 前后的梯度,理解 PyTorch 默认累积梯度的语义。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 遗漏 model.train/model.eval、把张量错误搬到不同设备、在评估时保留计算图、混用 float32 与 float64,都会引入隐蔽问题。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:在训练循环中记录 train loss、validation loss 和梯度范数;故意把学习率放大 100 倍,观察并解释曲线。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
PYTHON
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(7)
X = torch.randn(1024, 2)
y = ((X[:, 0] * X[:, 1]) > 0).long()
loader = DataLoader(TensorDataset(X, y), batch_size=64, shuffle=True)
model = nn.Sequential(
nn.Linear(2, 32),
nn.GELU(),
nn.Linear(32, 2),
)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-3, weight_decay=1e-2)
criterion = nn.CrossEntropyLoss()
for epoch in range(20):
model.train()
total_loss = 0.0
for xb, yb in loader:
optimizer.zero_grad(set_to_none=True)
logits = model(xb)
loss = criterion(logits, yb)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
total_loss += loss.item() * len(xb)
print(f"epoch={epoch:02d} loss={total_loss / len(X):.4f}")
关键顺序是:清梯度、前向、算损失、反向、更新。PyTorch 默认把多次反向的梯度累加到 .grad,因此忘记清梯度会改变优化过程。model.train() 与 model.eval() 会切换 Dropout、BatchNorm 等模块行为;推理时还应使用 torch.inference_mode() 关闭梯度记录。
1.6 环境、模型仓库与可复现性
先建立直觉。 可复现不是一句 random seed,而是能够说明数据、代码、依赖、硬件和随机状态。模型仓库中的权重还伴随配置、tokenizer、许可证与自定义代码风险。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 锁定 Python 与关键库版本。
- 记录数据哈希和切分清单。
- 保存配置、种子和代码提交。
- 核验模型卡与许可证。
最小例子。 同一模型若 tokenizer 版本不同,特殊 token id 可能改变;即使权重相同,输入序列与输出也可能不同。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 盲目开启 trust_remote_code、只保存 model.bin、不记录 CUDA/驱动版本,以及用测试集调参,都会破坏复现或安全边界。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:写一个环境报告脚本,输出 Python、PyTorch、CUDA、GPU、随机种子和 git commit,并保存为 JSON。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
建议为项目创建独立虚拟环境,锁定依赖,并记录 GPU 驱动、CUDA 与 PyTorch 版本。Hugging Face Hub 和 ModelScope 都可托管模型与数据集;下载模型前要检查许可证、模型卡、所需显存和自定义代码。不要对来源不明的仓库轻易启用 trust_remote_code=True。
随机种子只能改善复现,不保证所有 GPU 算子逐比特一致。完整复现还需要固定数据顺序、预处理、代码提交、依赖版本、混合精度和分布式配置。
1.7 常见故障与练习
从问题出发。 排错要从最便宜、最确定的证据开始:语法与导入、形状与 dtype、数值范围、梯度、数据,再到性能。不要一出现问题就换模型或调参。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 构造能复现问题的最小输入。
- 在边界打印形状与统计量。
- 加断言把错误定位到最早位置。
- 修复后补回归测试。
用小数据走一遍。 损失变成 NaN 时先检查输入是否有限、logits 范围、学习率和梯度范数,而不是立即换优化器。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 只看最后一行异常、一次修改多个变量、用 try/except 吞掉堆栈、没有保存失败样本,会让同一问题反复出现。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:为本章代码设计五个自动测试:形状、有限值、梯度、损失下降和固定种子复现。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
- 损失为 nan:检查学习率、除零、对数输入、混合精度溢出和异常数据。
- 训练准确率高、验证准确率低:检查过拟合、分布漂移和数据泄漏。
- GPU 利用率低:检查数据加载、批量过小、频繁 CPU/GPU 拷贝和同步打印。
- 显存不断增长:检查是否保存了带计算图的张量,应改用 tensor.detach() 或 loss.item()。
练习:为 NumPy 网络实现数值梯度检查;为 PyTorch 训练加入验证集、准确率、早停和模型保存;故意交换标签或放大学习率,记录曲线并解释失败原因。
1.8 延伸资源
先看它解决什么。 延伸材料应围绕当前缺口使用:数学直觉不清就做小矩阵实验,PyTorch 不熟就逐行运行官方教程,注意力不懂再进入下一章,而不是同时收藏几十门课。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 带着一个明确问题阅读。
- 复现最小代码并改变一个变量。
- 用自己的话记录结论。
- 把结论写成可失败的测试。
一个可以手算的例子。 阅读自动微分教程时,不只复制代码;把中间张量改为 requires_grad=False,预测结果,再运行验证。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 把观看时长当学习进度、依赖过期安装命令、跳过练习和不记录失败,都会形成虚假的熟悉感。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:建立学习日志,记录问题、假设、实验、证据和结论;下一章继续沿用同一模板。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/01_numpy_mlp.py`:NumPy 两层网络。
- `examples/01_gradient_check.py`:有限差分梯度检查。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 实现 batch_iter(samples, batch_size),覆盖空列表、最后一个不足批次和非法 batch_size 三种边界。
- 手算一个 2×3 矩阵与 3×2 矩阵的乘积,再用 NumPy 验证;对其中一个元素做有限差分梯度检查。
- 画出两层网络的计算图,标注每个节点的形状,并说明哪个节点必须是标量才能直接调用 backward。
- 给现有两层网络加入 ReLU,并对 W1、b1、W2、b2 各抽一个元素做中心差分检查。
- 在训练循环中记录 train loss、validation loss 和梯度范数;故意把学习率放大 100 倍,观察并解释曲线。
- 写一个环境报告脚本,输出 Python、PyTorch、CUDA、GPU、随机种子和 git commit,并保存为 JSON。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“Python 在本书中不是考语法,而是用来表达数据怎样流动”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“线性代数负责描述批量变换,概率负责描述不确定性,微积分负责说明参数怎样改变损失”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“神经网络可以理解为可微函数的层叠:线性层改变坐标,非线性层提供表达能力,损失函数把预测质量压缩成一个可优化标量”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“手写 NumPy 网络的目的不是替代框架,而是亲眼看到缓存、中间梯度和更新规则”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“PyTorch 把手写缓存换成动态计算图,把参数集合交给 Module 管理,但训练闭环仍是同一件事:取批次、前向、计算损失、清梯度、反传、更新”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“可复现不是一句 random seed,而是能够说明数据、代码、依赖、硬件和随机状态”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第二章 Transformer 与 MoE:从矩阵计算到稀疏专家
Transformer 的核心并不神秘:每个位置先从其他位置汇总信息,再独立通过前馈网络加工;残差连接和归一化保证深层网络可训练。难点在于掩码、形状、数值稳定性和训练/推理差异。本章从一次注意力计算开始,逐步组装完整结构,再解释 MoE 如何用稀疏激活扩大参数容量。

图 2-1 Dense Transformer 每个 token 经过同一 FFN;MoE 用路由器为 token 选择少数专家。
2.1 Token、嵌入与位置
先建立直觉。 语言模型不能直接处理字符串,必须先把文本切成 token id,再通过嵌入表把离散 id 映射为连续向量。位置表示则回答相同 token 出现在不同位置时怎样区分。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 规范化与分词得到 token。
- 查词表得到整数 id。
- Embedding 查表得到 d_model 维向量。
- 叠加或注入位置信息。
最小例子。 词表大小 V=10,000、维度 D=512 的嵌入层本质是一个 V×D 参数矩阵;一批 B×T 的 id 会得到 B×T×D。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 token 不等于汉字或单词;不同 tokenizer 不能随意共用权重。padding、BOS、EOS 的 id 和 mask 也必须与训练配置一致。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:用任意公开 tokenizer 编码中英文、代码和数字,比较 token 数并解释差异。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
模型不直接读取汉字或单词,而是读取 tokenizer 产生的 token id。BPE、WordPiece、Unigram 等算法都试图在词表规模、序列长度和未登录词之间折中。tokenizer 是模型的一部分:更换 tokenizer 会改变 id 语义,不能只替换词表文件。
设词表大小为 $V$、隐藏维度为 $d$,嵌入矩阵 $E\in\mathbb{R}^{V\times d}$。查表把 (B,T) 的 token id 变为 (B,T,d)。注意力本身对输入顺序置换等变,因此还需注入位置信息。原始 Transformer 使用正弦位置编码,现代解码器常用 RoPE;第二者将在第三章推导。
2.2 Self-Attention:每一步都写出形状

图 02-2 注意力实现的第一道正确性门槛,是每次 reshape、transpose 和矩阵乘都能写出形状。从问题出发。 Self-Attention 让每个位置根据内容选择其他位置的信息。Q 表示当前要找什么,K 表示每个位置可被怎样匹配,V 表示真正被汇总的内容。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- X 线性投影为 Q/K/V。
- 计算 QK^T 并除以 sqrt(dk)。
- 叠加因果或 padding mask。
- Softmax 后乘 V。
用小数据走一遍。 B=2、T=4、D=8、H=2 时,每头维度为 4;Q/K/V 形状均为 (2,2,4,4),注意力矩阵为 (2,2,4,4)。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 Softmax 轴必须是 key 轴;mask 的布尔含义与填充值要核对。把缩放因子写成 sqrt(d_model) 或在半精度中使用过小负数都可能出错。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:在手写注意力中返回权重,验证每行和为 1,并检查未来位置权重为 0。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
输入 $X\in\mathbb{R}^{B\times T\times d}$ 经过三组线性投影:
$$Q=XW_Q,\qquad K=XW_K,\qquad V=XW_V.$$
对单头注意力,$QK^\top$ 得到 (B,T,T) 分数矩阵。第 $i$ 行表示第 $i$ 个查询对所有键的相似度。除以 $\sqrt{d_k}$ 是为了避免维度增大时点积方差过大,导致 Softmax 过饱和。掩码 $M$ 把不可见位置加上极小值;解码器的因果掩码保证位置 $i$ 只能看到不晚于自己的 token。
多头注意力把隐藏维分为 $H$ 个头,每个头维度 $d_h=d/H$。头并非简单重复:不同投影可学习不同关系。各头输出拼接后再经过 $W_O$ 混合。
PYTHON
import math
import torch
from torch import nn
class MultiHeadSelfAttention(nn.Module):
def __init__(self, d_model: int, n_heads: int, causal: bool = True):
super().__init__()
if d_model % n_heads != 0:
raise ValueError("d_model 必须能被 n_heads 整除")
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.causal = causal
self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
self.out = nn.Linear(d_model, d_model, bias=False)
def forward(self, x, padding_mask=None):
b, t, d = x.shape
qkv = self.qkv(x).view(b, t, 3, self.n_heads, self.head_dim)
q, k, v = qkv.unbind(dim=2)
q, k, v = [z.transpose(1, 2) for z in (q, k, v)] # (B,H,T,Dh)
scores = q @ k.transpose(-2, -1) / math.sqrt(self.head_dim)
if self.causal:
mask = torch.triu(
torch.ones(t, t, dtype=torch.bool, device=x.device), diagonal=1
)
scores = scores.masked_fill(mask, torch.finfo(scores.dtype).min)
if padding_mask is not None:
# padding_mask: (B,T),True 表示有效 token
scores = scores.masked_fill(~padding_mask[:, None, None, :],
torch.finfo(scores.dtype).min)
attn = torch.softmax(scores.float(), dim=-1).to(x.dtype)
context = attn @ v
context = context.transpose(1, 2).contiguous().view(b, t, d)
return self.out(context), attn
代码解读:view 之前要确认内存布局,转置后合并维度使用 contiguous() 更稳妥;Softmax 临时升到 FP32 可减小低精度下的数值风险;padding mask 作用在键的位置,因果 mask 作用在时间关系。若某一行全部被屏蔽,Softmax 可能产生 nan,数据拼接和 mask 逻辑必须避免这种情况。
2.3 Encoder、Decoder 与 Cross-Attention
先看它解决什么。 Encoder 让所有位置双向交互,Decoder 的自注意力必须因果遮蔽;Cross-Attention 则让 Decoder 的查询读取 Encoder 产生的键和值。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- Encoder 形成上下文化记忆。
- Decoder 读取已生成前缀。
- Cross-Attention 对齐目标与源。
- 输出层预测下一个 token。
一个可以手算的例子。 翻译中,生成目标词时 Q 来自目标端当前状态,K/V 来自整句源语言表示,因此可以动态关注源句不同位置。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 BERT 是 Encoder-only,GPT 是 Decoder-only,T5 是 Encoder-Decoder;三者差异不能简化为是否含注意力。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:画出三种架构的数据流,并为分类、翻译、开放式生成各选择一种架构说明理由。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
Encoder 使用双向 Self-Attention,每个位置可读取整个输入,适合分类、抽取和表征学习。Decoder 使用因果 Self-Attention,自回归预测下一个 token,GPT、Llama、Qwen 等生成模型采用这种结构。Encoder-Decoder 模型先编码输入,再由 Decoder 通过 Cross-Attention 读取编码结果,适合翻译、摘要和条件生成。
Cross-Attention 中,$Q$ 来自 Decoder 当前状态,$K,V$ 来自 Encoder 输出。它回答的是“生成当前 token 时,输入中的哪些位置最相关”。T5 是典型 Encoder-Decoder;BERT 是 Encoder-only;GPT 系列是 Decoder-only。三者的差异不仅是层数排列,还包括预训练目标与注意力可见范围。
2.4 FFN、残差和归一化
抓住这一节的主线。 注意力负责 token 之间通信,FFN 负责每个 token 内部的非线性变换;残差通路保留信息与梯度,归一化控制数值尺度。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 子层计算变换 F(x)。
- Dropout 或门控调节输出。
- 与残差 x 相加。
- 按 Pre-Norm 或 Post-Norm 放置归一化。
先做最小实验。 FFN 通常先把 D 扩张到约数倍隐藏维,再压回 D。它对每个位置使用同一组权重,因此不混合序列位置。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 把 FFN 当卷积、把 LayerNorm 放在 batch 维、忘记残差两端形状必须一致,都会破坏结构。Pre-Norm 稳定性也不代表任何模型都可随意改。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:记录一个 Transformer Block 经过 Attention、残差、FFN 后的均值、标准差与梯度范数。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
注意力在 token 之间混合信息,FFN 在每个 token 内独立变换通道:
$$\operatorname{FFN}(x)=W_2\,\phi(W_1x+b_1)+b_2.$$
中间维度通常大于隐藏维度。原始 Transformer 使用 ReLU,BERT 常用 GELU,Llama 使用 SwiGLU。残差连接写作 $x+F(x)$,让网络能学习对恒等映射的修正,并给梯度提供短路径。
Post-Norm 先做子层再归一化,Pre-Norm 先归一化再做子层。深层语言模型常采用 Pre-Norm,因为训练通常更稳定:
PYTHON
class TransformerBlock(nn.Module):
def __init__(self, d_model=256, n_heads=8, mlp_ratio=4):
super().__init__()
self.norm1 = nn.LayerNorm(d_model)
self.attn = MultiHeadSelfAttention(d_model, n_heads, causal=True)
self.norm2 = nn.LayerNorm(d_model)
self.ffn = nn.Sequential(
nn.Linear(d_model, mlp_ratio * d_model),
nn.GELU(),
nn.Linear(mlp_ratio * d_model, d_model),
)
def forward(self, x, padding_mask=None):
a, _ = self.attn(self.norm1(x), padding_mask)
x = x + a
x = x + self.ffn(self.norm2(x))
return x
LayerNorm 对每个 token 的隐藏维做标准化;RMSNorm 只按均方根缩放,不减均值。归一化中的 eps 太小会在低精度下不稳定,过大又改变尺度。
2.5 从输入到下一个 token
先把概念落到可观察对象上。 自回归模型训练时一次并行预测所有位置的下一个 token,推理时却必须逐 token 生成。这解释了训练吞吐高而生成延迟明显的根本差异。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 右移标签形成输入与目标。
- 用因果 mask 并行计算 logits。
- 交叉熵只统计有效标签。
- 推理时按解码策略循环。
把它缩小到能逐项检查。 序列 [BOS,我,爱,猫,EOS] 的输入可为前四个 token,标签为后四个;每个位置只学习预测紧邻的下一个 token。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 训练标签未右移、padding 未设为 ignore_index、推理时忘记 EOS、把 temperature 用在训练损失上,都是常见混淆。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:构造长度不同的两条序列,完成 padding、attention mask 和 label mask,并逐位置核对。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
解码语言模型的训练输入通常是 token 序列 $[x_0,x_1,\ldots,x_{T-1}]$,目标是右移后的 $[x_1,x_2,\ldots,x_T]$。模型一次并行计算所有位置的 logits,因果掩码防止偷看未来。推理时没有真实的下一个 token,只能生成一个、追加到序列、再生成下一个,因此解码天然串行。KV Cache 通过复用历史键值减少重复计算,第十三章会详细说明。
采样策略决定如何从概率分布选 token。贪心每次取最大概率,稳定但容易僵化;temperature 调整分布锐度;top-k 只保留概率最高的 $k$ 个;top-p 保留累计概率达到阈值的最小集合。Beam Search 更适合有明确序列得分的任务,不一定适合开放对话。
2.6 MoE:容量扩大不等于计算同比扩大
先建立直觉。 MoE 用路由器让每个 token 只经过少数专家,从而扩大总参数容量而控制单 token 计算量。它省的是激活的专家计算,不会自动消除通信、存储和负载不均。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 路由器产生专家分数。
- 选 Top-k 专家并归一化权重。
- 按专家重排 token。
- 专家计算后按权重合并。
最小例子。 8 个专家、Top-2 路由时,一个 token 只计算两个 FFN;但所有专家权重仍要分布在设备上,且热门专家可能溢出容量。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 总参数量不能直接当激活参数量;只写 Top-k 而不处理容量、辅助损失、token 丢弃和专家并行,不能构成可训练 MoE。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:模拟 100 个 token 的路由计数,计算每个专家负载和变异系数,再尝试加入均衡惩罚。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
Mixture of Experts 通常把 Dense FFN 替换为多个专家 FFN。路由器为每个 token 计算专家分数,选择 Top-K 专家:
$$g(x)=\operatorname{softmax}(W_rx),\qquady=\sum_{e\in\operatorname{TopK}(g)}\tilde{g}_e(x)E_e(x).$$
总参数量可以很大,但每个 token 只激活少量专家,因此激活参数量和计算量受控。代价是路由、All-to-All 通信、负载不均和专家容量管理。
PYTHON
class ToyMoE(nn.Module):
def __init__(self, d_model=128, hidden=256, n_experts=4, top_k=2):
super().__init__()
self.top_k = top_k
self.router = nn.Linear(d_model, n_experts, bias=False)
self.experts = nn.ModuleList([
nn.Sequential(nn.Linear(d_model, hidden), nn.GELU(),
nn.Linear(hidden, d_model))
for _ in range(n_experts)
])
def forward(self, x):
shape = x.shape
flat = x.reshape(-1, shape[-1])
probs = torch.softmax(self.router(flat), dim=-1)
weights, indices = probs.topk(self.top_k, dim=-1)
weights = weights / weights.sum(dim=-1, keepdim=True)
out = torch.zeros_like(flat)
# 教学写法:逐专家分派;生产实现会使用分组、融合内核和跨卡通信
for expert_id, expert in enumerate(self.experts):
token_pos, slot = torch.where(indices == expert_id)
if token_pos.numel() == 0:
continue
expert_out = expert(flat[token_pos])
out[token_pos] += weights[token_pos, slot, None] * expert_out
return out.view(shape), probs
仅有 Top-K 还不够。若路由器把多数 token 送给少数专家,热门专家会溢出,冷门专家学不到东西。常见做法包括辅助负载均衡损失、容量因子、路由噪声和无辅助损失的动态偏置策略。负载均衡不是越均匀越好;专家适度分工是 MoE 的价值,目标是避免塌缩和硬件闲置。
2.7 复杂度与常见误区
从问题出发。 复杂度分析必须说明变量和瓶颈。标准注意力的分数矩阵随 T² 增长,但短序列时 FFN、投影、kernel 启动和内存访问可能占主导。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 分别估算参数量、FLOPs 与激活。
- 区分训练和推理。
- 区分 prefill 与 decode。
- 用 profiler 验证理论判断。
用小数据走一遍。 Attention 分数约需 O(T²D),FFN 约需 O(TD·Dff)。T 增大时前者更快增长,但真实速度还受实现与硬件影响。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把大 O 当实际耗时、忽略 batch/head/dtype、声称 FlashAttention 改变数学结果,或用参数量推断显存,都是错误。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:选择三组 T 和 D,计算注意力分数矩阵元素数与 FFN 乘加量,画出交叉趋势。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
标准注意力的分数矩阵大小为 $T\times T$,长序列下时间和显存压力显著;FFN 的计算量则随 $T$ 线性增长但常占大量 FLOPs。FlashAttention 改变内存访问方式,不改变精确注意力的数学结果;线性注意力或稀疏注意力则会改变计算结构。
- “注意力权重就是解释”:权重可提供线索,但不是充分的因果解释。
- “多头越多越好”:固定隐藏维时,头数增加会减小单头维度,存在表达与效率折中。
- “MoE 的总参数都参与一次推理”:应区分总参数和每 token 激活参数。
- “Decoder 训练也必须逐 token”:训练可并行计算所有位置,推理生成才逐 token。
2.8 练习与资料
先看它解决什么。 练习应能证明你会推导、实现和验证,而不是只会复述。一个合格的注意力练习至少包含形状断言、mask 测试、数值稳定性和与框架结果对齐。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 先手算极小输入。
- 再写无多头版本。
- 扩展为多头与 mask。
- 与 PyTorch 官方实现比较。
一个可以手算的例子。 固定同一组投影权重,将自写模块与 scaled_dot_product_attention 输出比较,最大绝对误差应在 dtype 合理范围内。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只看 loss 是否下降不能证明注意力实现正确;错误 mask 有时仍能在小数据上拟合。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:完成一个测试文件,至少覆盖因果遮蔽、padding、不同 batch、不同头数和半精度有限值。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
练习:用小矩阵手算一次 $QK^\top$、缩放、掩码、Softmax 和加权求和;为注意力代码加入 Dropout 与 KV Cache;统计 ToyMoE 的专家负载并设计一个辅助损失。
- Attention Is All You Need
- PyTorch `scaled_dot_product_attention`
- Switch Transformers:稀疏专家模型
- B 站:6 分钟理解词嵌入与注意力
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/02_attention.py`:带 mask 的多头注意力。
- `examples/02_tokenizer_and_mask.py`:token、padding、因果 mask 与标签右移。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 用任意公开 tokenizer 编码中英文、代码和数字,比较 token 数并解释差异。
- 在手写注意力中返回权重,验证每行和为 1,并检查未来位置权重为 0。
- 画出三种架构的数据流,并为分类、翻译、开放式生成各选择一种架构说明理由。
- 记录一个 Transformer Block 经过 Attention、残差、FFN 后的均值、标准差与梯度范数。
- 构造长度不同的两条序列,完成 padding、attention mask 和 label mask,并逐位置核对。
- 模拟 100 个 token 的路由计数,计算每个专家负载和变异系数,再尝试加入均衡惩罚。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“语言模型不能直接处理字符串,必须先把文本切成 token id,再通过嵌入表把离散 id 映射为连续向量”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Self-Attention 让每个位置根据内容选择其他位置的信息”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Encoder 让所有位置双向交互,Decoder 的自注意力必须因果遮蔽”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“注意力负责 token 之间通信,FFN 负责每个 token 内部的非线性变换”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“自回归模型训练时一次并行预测所有位置的下一个 token,推理时却必须逐 token 生成”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“MoE 用路由器让每个 token 只经过少数专家,从而扩大总参数容量而控制单 token 计算量”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第三章 Llama 与 DeepSeek:现代解码器的关键改造
Llama、Qwen 和 DeepSeek 并没有抛弃 Transformer,而是在归一化、位置编码、注意力、FFN、数据与训练工程上持续改进。本章先拆解一个 Llama 风格的 Decoder Block,再解释 DeepSeek-V3/R1 中与工程最相关的 MoE、MLA、MTP 和强化学习路线。阅读模型报告时,要区分“架构创新”“训练策略”“数据策略”和“系统优化”,不要把所有提升都归因于某一个模块。

图 3-1 典型 Llama Block:Pre-RMSNorm、RoPE 注意力、残差和 SwiGLU FFN。
3.1 RMSNorm:只控制尺度
先建立直觉。 RMSNorm 只用均方根控制向量尺度,不减去均值。它保留方向信息、计算更简单,现代解码器常把它放在注意力和 FFN 之前。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 沿最后一维计算平方均值。
- 加 epsilon 后开方。
- 输入除以均方根。
- 乘可学习缩放参数。
最小例子。 向量整体乘 10 后,RMSNorm 输出尺度近似不变;但所有元素同时加常数时,它与 LayerNorm 的响应不同。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 epsilon 放在根号内外会改变数值;归一化维度必须是隐藏维。RMSNorm 不是没有参数,也不等同 L2 normalize。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:实现 RMSNorm,与 torch RMSNorm 或参考实现比较前向和梯度。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
LayerNorm 会减均值再除标准差;RMSNorm 只根据均方根缩放:
\operatorname{RMSNorm}(x)=\gamma\odot\frac{x}{\sqrt{\frac{1}{d}\sum_i x_i^2+\epsilon}}.
PYTHON
import torch
from torch import nn
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x):
dtype = x.dtype
x32 = x.float()
scale = torch.rsqrt(x32.pow(2).mean(dim=-1, keepdim=True) + self.eps)
return (x32 * scale).to(dtype) * self.weight
升到 FP32 计算均方根是常见的稳定性处理。RMSNorm 不是“没有参数”,仍有可学习缩放 weight;也不是 BatchNorm,它不依赖批次统计。
3.2 RoPE:让相对位置信息进入点积
从问题出发。 RoPE 把偶数维与奇数维配成二维平面,并按位置旋转。旋转后的 Q/K 点积依赖位置角度差,从而让相对位置信息进入注意力。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 为每对维度生成频率。
- 位置乘频率得到角度。
- 分别旋转 Q 与 K。
- 在点积中形成相对相位。
用小数据走一遍。 二维向量在位置 m、n 分别旋转 mθ、nθ,二者点积等价于只旋转 (m-n)θ 后的点积。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 RoPE 通常作用于 Q/K 而非 V;维度配对、position_ids、缓存偏移和长上下文缩放必须一致。只改 max_position_embeddings 不会自然获得外推能力。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:对同一内容向量放在不同位置,验证点积随相对距离而不是绝对起点变化。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Rotary Position Embedding 把隐藏维两两配对,并按位置旋转。对二维向量 $(x_{2i},x_{2i+1})$,位置 $m$ 的旋转为
\begin{bmatrix}x'_{2i}\\x'_{2i+1}\end{bmatrix} = \begin{bmatrix}\cos(m\theta_i)&-\sin(m\theta_i)\\ \sin(m\theta_i)&\cos(m\theta_i) \end{bmatrix} \begin{bmatrix}x_{2i}\\x_{2i+1}\end{bmatrix}.
同样旋转 $Q$ 和 $K$ 后,二者点积自然包含相对位置差。RoPE 不直接作用于 $V$。下面是便于理解的实数实现:
PYTHON
def rotate_half(x):
x1, x2 = x[..., 0::2], x[..., 1::2]
return torch.stack((-x2, x1), dim=-1).flatten(-2)
def rope(q, k, positions, base=10000.0):
# q/k: (B,H,T,D),D 必须为偶数;positions: (T,)
d = q.size(-1)
inv_freq = base ** (-torch.arange(0, d, 2, device=q.device).float() / d)
angles = torch.outer(positions.float(), inv_freq)
cos = torch.repeat_interleave(angles.cos(), 2, dim=-1)[None, None]
sin = torch.repeat_interleave(angles.sin(), 2, dim=-1)[None, None]
return q * cos + rotate_half(q) * sin, k * cos + rotate_half(k) * sin
上下文扩展不是简单把 max_position_embeddings 改大。位置外推可能降低高频分量的有效性,实际方案会采用频率缩放、分段策略或继续训练,并用长文本检索、困惑度和下游任务验证。
3.3 MHA、MQA、GQA 与 KV Cache

图 03-2 位置旋转、KV 头共享和历史缓存共同决定现代解码器的生成路径。先看它解决什么。 MHA 为每个查询头保留独立 K/V 头;MQA 让所有查询头共享一组 K/V;GQA 介于两者之间。减少 KV 头能显著降低生成阶段缓存与带宽。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 按层保存历史 K/V。
- 新 token 只计算自身 Q/K/V。
- 将 KV 头扩展匹配查询头。
- 与历史缓存做注意力。
一个可以手算的例子。 层数 L、batch B、长度 T、KV 头 Hkv、头维 Dh、字节数 s 时,K+V 缓存约为 2LBTHkvDh·s。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 缓存减少不等于计算完全按同倍数减少;repeat_kv 应是视图式扩展或 kernel 支持,物理复制会浪费内存。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为一个假想 32 层模型分别计算 MHA 与 GQA 在 8k 上下文的 KV Cache。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
标准 MHA 中每个查询头都有自己的 K/V 头。Multi-Query Attention 让所有查询头共享一组 K/V;Grouped-Query Attention 把查询头分组,每组共享 K/V,在质量与 KV Cache 大小之间折中。
若层数为 $L$,批量为 $B$,序列长度为 $T$,K/V 头数为 $H_{kv}$,头维为 $d_h$,元素字节数为 $s$,KV Cache 粗略占用为
$$2LBTH_{kv}d_hs.$$
前面的 2 对应 K 和 V。GQA 减少 $H_{kv}$,因此长上下文和高并发下收益明显。缓存只避免重复计算历史 K/V,并不能让新 token 之间并行产生。
PYTHON
def repeat_kv(x, n_rep: int):
# x: (B, H_kv, T, D) -> (B, H_q, T, D)
if n_rep == 1:
return x
b, h, t, d = x.shape
return x[:, :, None].expand(b, h, n_rep, t, d).reshape(b, h * n_rep, t, d)
教学实现可物理重复 K/V,生产内核通常通过广播或专用 kernel 避免额外拷贝。
3.4 SwiGLU:门控前馈网络
抓住这一节的主线。 SwiGLU 用一条分支产生候选值,另一条分支经 SiLU 产生门,再逐元素相乘。门控让网络按输入选择信息通过程度。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- x 分别投影为 gate 与 value。
- gate 经过 SiLU。
- 两支逐元素相乘。
- down projection 回到隐藏维。
先做最小实验。 若 gate 的某维为大负数,SiLU 后接近零,该维 value 即使很大也被抑制。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 SwiGLU 通常需要三组矩阵;把 SiLU 放错分支、忘记逐元素乘或沿错误维度切分,会得到另一种网络。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:实现 SwiGLU,并与普通 GELU FFN 在参数量近似相等时比较输出形状和参数数。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
SwiGLU 常写作
$$\operatorname{SwiGLU}(x)=W_2\left(\operatorname{SiLU}(W_gx)\odot(W_ux)\right).$$
PYTHON
class SwiGLU(nn.Module):
def __init__(self, dim: int, hidden_dim: int):
super().__init__()
self.gate = nn.Linear(dim, hidden_dim, bias=False)
self.up = nn.Linear(dim, hidden_dim, bias=False)
self.down = nn.Linear(hidden_dim, dim, bias=False)
def forward(self, x):
return self.down(torch.nn.functional.silu(self.gate(x)) * self.up(x))
两条投影分别产生门和值,再逐元素相乘。由于有三组矩阵,比较 FFN 参数量时不能只看中间维度;许多配置会选择约为传统 $4d$ 的某个折算维度并对齐硬件倍数。
3.5 组装一个 Llama 风格 Block
先把概念落到可观察对象上。 组装 Block 时最重要的是接口:输入、mask、position_ids、past_key_values 和返回缓存必须一致。组件单独正确,组合时仍可能因形状和残差次序出错。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- Pre-Norm 后进入注意力。
- 注意力输出与残差相加。
- 再次归一化进入 SwiGLU。
- FFN 输出与第二条残差相加。
把它缩小到能逐项检查。 输入 (B,T,D) 经 GQA 后仍必须是 (B,T,D),否则无法做残差。缓存只含 K/V,不应把 Q 跨步保存。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 用 Identity 冒充注意力只能测试外壳,不能证明模型正确;还要验证因果性、缓存增量结果与全序列结果一致。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:用长度 4 的序列比较一次性前向与逐 token 带缓存前向的最后一步 logits。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
PYTHON
class LlamaStyleBlock(nn.Module):
def __init__(self, dim, hidden_dim, attention: nn.Module):
super().__init__()
self.attn_norm = RMSNorm(dim)
self.ffn_norm = RMSNorm(dim)
# attention 由外部传入,便于分别测试 MHA、GQA 或带 KV Cache 的实现
self.attn = attention
self.ffn = SwiGLU(dim, hidden_dim)
def forward(self, x):
x = x + self.attn(self.attn_norm(x))
x = x + self.ffn(self.ffn_norm(x))
return x
这段代码只负责组织 Block,注意力由外部注入。这样可以分别测试 MHA、GQA 或带 KV Cache 的实现,也避免把过度简化的占位模块伪装成完整 Llama。配套示例 examples/03_llama_components.py 给出可执行的 RMSNorm、RoPE、GQA 形状测试和 SwiGLU。
3.6 Llama 3 应该怎样读
先建立直觉。 读模型技术报告要分清架构、数据、训练配方、评估和安全五层证据。公开参数并不等于公开训练数据或完整生产系统。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 先列出模型族与上下文配置。
- 再定位 tokenizer 和架构改动。
- 核对预训练与后训练数据说明。
- 比较同设置评估。
最小例子。 看到某个 benchmark 分数时,同时记录提示模板、shot 数、解码设置、评测脚本版本和是否使用工具。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把模型卡营销语当因果结论、跨版本混比分数、忽略许可证与安全限制,会造成错误选型。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为 Llama 3 报告做一页证据表:已公开事实、合理推断、未公开信息。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
Llama 3 技术报告同时讨论模型架构、数据、训练基础设施、后训练、安全和多模态实验。其最大公开模型是 405B 的 Dense Transformer,支持长上下文,并发布预训练与后训练版本。阅读时建议按以下问题做笔记:
- tokenizer 与词表怎样影响多语言和代码效率?
- 上下文长度如何训练和评估,而不是只修改配置?
- 预训练数据经历了哪些过滤、去重和质量控制?
- 后训练如何组合 SFT、偏好优化和安全数据?
- 训练稳定性、并行策略和故障恢复如何保证长周期运行?
参数规模只是结果的一部分。一个模型是否适合你的任务,还取决于许可证、语言覆盖、上下文、推理成本、工具调用能力和安全边界。
3.7 DeepSeek-V3:MoE、MLA、MTP 与系统协同
从问题出发。 DeepSeek-V3 的关键不只是 MoE,而是模型结构、训练稳定性、通信与推理成本的协同。MLA 压缩需要缓存的表示,MTP 在训练中增加未来 token 预测信号。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 区分总参数与激活参数。
- 理解专家路由与共享专家。
- 理解 MLA 的低秩潜变量。
- 把系统优化与算法指标分开。
用小数据走一遍。 若 KV 不直接逐头缓存,而是缓存可恢复 K/V 的低维潜变量,内存占用可下降,但解码 kernel 必须配合重构。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 不要把 MLA 说成普通 GQA,也不要把 MTP 等同推理时一次永久输出多个 token;训练目标和服务策略是不同层次。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:根据技术报告画出一层的数据流,标注哪些张量跨 token 缓存、哪些只在当前步使用。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
DeepSeek-V3 报告给出的模型总参数为 671B,每个 token 激活约 37B 参数。其 DeepSeekMoE 通过更细粒度专家与共享专家增强专业化;路由需要兼顾专家分工和设备负载。
Multi-head Latent Attention(MLA)的核心目标之一是压缩推理时需要缓存的 K/V 表示。它先把隐藏状态投影到低维潜在表示,再恢复注意力所需分量,从而减少 KV Cache。理解 MLA 时要区分“训练时表达”“推理时可吸收的矩阵变换”和“实际缓存内容”,不能把它简单说成普通低秩压缩。
Multi-Token Prediction(MTP)在训练时增加对多个未来 token 的预测目标,为模型提供更密集的学习信号;推理系统还可利用相关预测进行投机式加速,但训练目标与部署收益不是自动等价的。DualPipe 则是面向大规模训练的流水线并行设计,重点在计算与通信重叠,属于系统层创新。
3.8 DeepSeek-R1:推理能力来自后训练路线
先看它解决什么。 DeepSeek-R1 说明推理行为可通过可验证奖励、强化学习与后续数据整理增强。推理能力不是简单由长答案产生,关键是奖励、探索、数据与基础模型能力。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 准备具有可判定结果的任务。
- 采样多条推理轨迹。
- 依据规则或模型计算奖励。
- 更新策略并做蒸馏/对齐。
一个可以手算的例子。 数学题可用最终答案验证器提供相对客观奖励,但证明质量、格式和安全仍需额外检查。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 把所有后训练都称为 GRPO、把思维长度当正确性、公开展示敏感内部推理或忽略 reward hacking,都会带来问题。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:设计一个字符串运算任务的奖励函数,分别列出正确性、格式、长度和作弊路径。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
DeepSeek-R1 与 R1-Zero 基于 DeepSeek-V3-Base。R1-Zero 展示了在没有先做 SFT 的条件下,通过大规模强化学习激励自验证、反思和长推理轨迹的可能,但出现重复、可读性差和语言混杂等问题。R1 加入冷启动数据,并组合多个 SFT/RL 阶段以改善可用性与偏好对齐。其蒸馏模型把大模型产生的推理数据迁移到 Qwen/Llama 系列的较小 Dense 模型。
因此要避免两个误解:GRPO 不是“DPO 的泛化形式”,它是面向组内相对奖励的策略优化方法;R1 的推理表现也不能归结为单一损失,数据、可验证奖励、采样、训练稳定性与基础模型能力共同作用。
3.9 练习与资料
抓住这一节的主线。 练习现代解码器要强调等价性测试:组件实现正确、缓存路径与全序列路径一致、混合精度有限、模型配置能完整序列化。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 先测单组件。
- 再测 Block。
- 再测多层与缓存。
- 最后测生成停止条件。
先做最小实验。 对同一随机种子,full forward 第 t 位 logits 应与增量缓存第 t 步 logits 接近;不接近时优先检查 position_ids 与 mask。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只验证形状不验证数值、只跑 CPU 不测半精度、忽略 batch 中不同长度,都会漏掉真实错误。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:建立 Llama 组件测试矩阵,并把失败样例保存成最小回归用例。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
练习:验证 RoPE 旋转前后向量范数不变;比较 MHA 与 GQA 的 KV Cache 理论大小;实现带缓存的单层 GQA,并验证逐 token 结果与整段前向在数值误差内一致。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/03_llama_components.py`:RMSNorm、RoPE、SwiGLU。
- `examples/03_rope_gqa_cache.py`:RoPE、GQA 与 KV Cache。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 实现 RMSNorm,与 torch RMSNorm 或参考实现比较前向和梯度。
- 对同一内容向量放在不同位置,验证点积随相对距离而不是绝对起点变化。
- 为一个假想 32 层模型分别计算 MHA 与 GQA 在 8k 上下文的 KV Cache。
- 实现 SwiGLU,并与普通 GELU FFN 在参数量近似相等时比较输出形状和参数数。
- 用长度 4 的序列比较一次性前向与逐 token 带缓存前向的最后一步 logits。
- 为 Llama 3 报告做一页证据表:已公开事实、合理推断、未公开信息。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“RMSNorm 只用均方根控制向量尺度,不减去均值”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“RoPE 把偶数维与奇数维配成二维平面,并按位置旋转”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“MHA 为每个查询头保留独立 K/V 头”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“SwiGLU 用一条分支产生候选值,另一条分支经 SiLU 产生门,再逐元素相乘”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“组装 Block 时最重要的是接口:输入、mask、position_ids、past_key_values 和返回缓存必须一致”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“读模型技术报告要分清架构、数据、训练配方、评估和安全五层证据”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第四章 部署、压测与应用开发:把模型变成可靠服务
模型“能生成”与服务“可上线”之间隔着一整套工程系统:模型加载、批处理、流式传输、并发控制、超时、指标、鉴权、配额、结构化输出和工具权限。本章以 FastAPI 与 vLLM 为例说明通用方法,模型可替换为任何提供兼容接口的本地或云端实现。

图 4-1 一次请求经过网关、应用编排和推理引擎;监控与安全横跨所有层。
4.1 先建立性能指标语言
先建立直觉。 服务指标必须从用户体验和系统资源同时定义。吞吐高并不保证单请求快,平均延迟也会掩盖排队造成的长尾。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- TTFT 衡量首次反馈。
- TPOT 衡量持续生成节奏。
- P50/P95/P99 描述分布。
- tokens/s 与并发描述容量。
最小例子。 两个系统平均都为 2 秒,但一个 P99 为 3 秒、另一个 P99 为 20 秒,生产体验和容量风险完全不同。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 混用端到端延迟与纯模型延迟、忽略输入输出长度、用单并发结果推算高并发容量,都会误导选型。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为聊天、批量摘要、代码补全分别写 SLO,并说明为何指标权重不同。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
吞吐量与延迟不能混为一谈。常见指标包括:
- QPS/TPS:每秒完成请求数、每秒生成 token 数。
- TTFT:从请求到首 token 的时间,主要受排队和 prefill 影响。
- TPOT:首 token 后每个输出 token 的平均时间,主要反映 decode。
- P50/P95/P99:延迟分位数;平均值会掩盖长尾。
- 并发数:同时在途请求,而不是一秒请求总数。
- 有效吞吐:在错误率、超时率和质量约束下的吞吐。
压测必须说明输入长度、输出长度、并发模型、采样参数、预热、硬件和模型精度。只报告“每秒多少请求”几乎没有可比性。
4.2 FastAPI:异步不等于计算更快
从问题出发。 FastAPI 的 async 适合等待网络、磁盘和队列,不会让同步 GPU kernel 神奇变快。应用层应把请求校验、调度和推理解耦。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- Pydantic 校验输入。
- 异步接收与取消。
- 通过队列提交推理。
- 流式返回并记录指标。
用小数据走一遍。 请求等待模型服务器响应时可释放事件循环;若在 async 路由里执行长时间纯 Python 计算,整个 worker 仍会阻塞。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 全局可变状态无锁、请求断开后不释放资源、超时只包在客户端、每次请求加载模型,都是常见服务事故来源。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现一个带请求 id、超时、并发信号量和取消处理的模拟生成接口。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
async 适合等待网络、数据库等 I/O;GPU 推理仍由推理引擎执行。若在事件循环里直接做长时间 CPU 计算,会阻塞所有请求。下面用异步生成器模拟流式 token:
PYTHON
import asyncio
from collections.abc import AsyncIterator
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
app = FastAPI()
gate = asyncio.Semaphore(16)
class ChatRequest(BaseModel):
prompt: str = Field(min_length=1, max_length=8000)
max_tokens: int = Field(default=256, ge=1, le=2048)
async def fake_generate(req: ChatRequest) -> AsyncIterator[str]:
for token in ["这是", "一个", "流式", "示例"]:
await asyncio.sleep(0.05)
yield f"data: {token}\n\n"
@app.post("/chat/stream")
async def stream_chat(req: ChatRequest):
try:
await asyncio.wait_for(gate.acquire(), timeout=2.0)
except TimeoutError as exc:
raise HTTPException(503, "服务繁忙,请稍后重试") from exc
async def guarded():
try:
async for chunk in fake_generate(req):
yield chunk
finally:
gate.release()
return StreamingResponse(guarded(), media_type="text/event-stream")
代码解读:Pydantic 在入口处验证长度;信号量限制应用层在途请求,避免无限排队;finally 保证客户端断开时释放名额。生产系统还要处理取消传播、上游超时、SSE 心跳、错误帧和请求追踪。
4.3 vLLM 与连续批处理
先看它解决什么。 vLLM 的核心价值来自调度与 KV Cache 管理:把不同到达时间、不同长度的请求动态组成批次,并用分页思想减少缓存碎片。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 请求进入等待队列。
- 调度器分配 token 预算。
- prefill/decode 共享执行批次。
- PagedAttention 管理缓存块。
一个可以手算的例子。 静态批处理必须等一批请求结束;连续批处理可在旧请求完成后立即插入新请求,提高 GPU 利用率。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 吞吐提升依赖长度分布与并发;max_model_len、显存利用率和并行配置不合理仍会 OOM 或增加排队。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:构造短输入长输出与长输入短输出两类负载,分别压测并解释 TTFT/TPOT 差异。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
传统静态批处理要等整批请求都结束,长短请求相互拖累。连续批处理在每个调度步动态加入新请求、移除已完成请求,提高 GPU 利用率。PagedAttention 把 KV Cache 划分为可管理的块,减少连续大内存分配和碎片。它们改变的是服务调度与内存管理,不改变语言模型的语义目标。
典型启动方式会暴露 OpenAI 兼容接口;具体参数随版本变化,应以 vLLM 官方文档 为准。配置时重点理解:张量并行度、最大上下文、GPU 内存利用率、最大并发序列、量化格式和模型是否支持自定义代码。不要盲目把并发上限调大;KV Cache、排队时间和 OOM 会共同限制系统。
4.4 同步、异步和流式调用

图 04-2 请求从校验、排队到流式返回,每一步都需要超时、取消和审计。抓住这一节的主线。 同步、异步和流式是调用语义,不是模型能力。同步简单,异步适合并发等待,流式改善感知延迟但增加状态、取消和错误处理复杂度。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 定义请求生命周期。
- 明确重试是否幂等。
- 为流式片段设计事件格式。
- 在结束事件中给出用量与状态。
先做最小实验。 SSE 可发送 token、tool_call、error、done 等事件;客户端必须能处理连接中断和半条 JSON。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 把网络重试直接复制到有副作用的工具调用、流式途中改变响应 schema、没有心跳和断开检测,都会造成重复操作或资源泄漏。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:写一个流式客户端状态机,覆盖正常结束、用户取消、超时和服务端错误。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
同步调用简单,适合离线脚本;异步调用适合高并发 I/O 编排;流式调用改善感知延迟,但不会减少总计算量。客户端应设置连接、读取和总超时,并对幂等请求做有限次数、带抖动的指数退避。非幂等工具调用不能无脑重试,否则可能重复扣款或重复写数据。
4.5 提示词工程与上下文工程
先把概念落到可观察对象上。 提示词工程组织指令,上下文工程决定模型实际看见哪些规则、历史、证据和工具。可靠系统不依赖一句‘请严格遵守’,而依赖清晰优先级与外部验证。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 分离系统规则和用户数据。
- 只放与当前任务相关证据。
- 明确输出契约与失败方式。
- 用评估集比较版本。
把它缩小到能逐项检查。 摘要任务可规定受众、长度、必须保留的数字和未知时的处理;但引用是否真实仍要由程序检查证据映射。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 把不可信网页拼进系统指令、上下文无限累积、没有版本号、用少数顺手样例判断提示效果,都会放大风险。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:为同一客服任务写三个提示版本,用十条固定样例比较格式正确率和事实错误率。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
一个可维护的提示通常分为:角色与目标、输入数据、约束、输出格式、示例和失败策略。系统提示不是安全边界,客户端传来的文本、网页和检索文档都属于不可信数据。
不要要求模型展示私密思维过程。需要可审计性时,让模型输出简短依据、引用或可验证步骤。Self-Consistency 是对多个独立候选进行聚合,成本随采样次数增长;它适合存在可比较答案的任务,不是所有生成任务的默认配置。
上下文窗口有限,应优先保留指令、当前任务、关键事实和最近对话。历史消息可以摘要,但摘要本身也可能丢信息;重要状态应放入结构化存储,而不是完全依赖聊天记录。
4.6 结构化输出:验证比提示更重要
先建立直觉。 结构化输出的目标是把自然语言结果变成可验证数据。Schema 负责约束类型和必填字段,业务验证还要检查取值范围、跨字段关系和外部事实。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 定义最小 JSON Schema。
- 让模型按 schema 生成。
- 解析并进行二次业务校验。
- 失败时有限重试或降级。
最小例子。 日期字段通过字符串格式校验仍可能是不存在的日期;订单金额非负也不代表币种和税额关系正确。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 仅在提示中贴 JSON 示例、用正则解析任意 JSON、无限自动重试、把校验错误原样暴露给用户,都是脆弱设计。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为旅行计划定义 schema,加入日期顺序、预算总和和城市白名单验证。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
仅在提示中写“输出 JSON”不能保证合法 JSON,更不能保证字段语义。应使用 JSON Schema/Pydantic 描述结构,并在应用侧验证:
PYTHON
from pydantic import BaseModel, Field, ValidationError
class TravelPlan(BaseModel):
city: str
days: int = Field(ge=1, le=30)
highlights: list[str] = Field(min_length=1, max_length=10)
def parse_model_json(text: str) -> TravelPlan:
try:
return TravelPlan.model_validate_json(text)
except ValidationError as exc:
# 记录原始输出和 schema 版本;可触发一次受控修复,而不是无限重试
raise ValueError(f"模型输出不符合结构: {exc}") from exc
支持 Structured Outputs 的 API 可以把 Schema 约束纳入解码,但业务规则仍需应用验证。Schema 要版本化;字段描述应明确单位、枚举和是否允许空值。
4.7 Function Calling:模型提议,程序执行
从问题出发。 Function Calling 中模型只提出工具名与参数,程序才拥有执行权。安全边界必须放在执行器:白名单、身份、作用域、确认、审计和幂等。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 把工具契约提供给模型。
- 解析并验证参数。
- 执行器检查权限与风险。
- 把结果作为新观察返回。
用小数据走一遍。 模型提出 send_email(to,body) 不等于邮件已发送;程序应在真正发送前检查收件人、敏感信息和是否需要用户确认。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 允许模型拼接任意命令、把隐藏凭据放进工具结果、把读取与写入使用同一权限、错误后盲目重试,都会造成严重风险。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现只读天气工具和有副作用的日历工具,比较两者确认与重试策略。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
工具调用的正确边界是:模型根据工具描述生成“调用意图 + 参数”,程序验证参数、检查权限、执行工具,再把结果返回模型。模型不能直接获得数据库管理员权限。
PYTHON
TOOLS = {
"weather": {
"description": "查询指定城市的公开天气,不处理历史私人位置",
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
}
}
def execute_tool(name: str, arguments: dict, user_scope: set[str]):
if name not in user_scope:
raise PermissionError("当前用户无权调用该工具")
if name == "weather":
city = arguments["city"].strip()
if not city or len(city) > 80:
raise ValueError("非法城市参数")
return {"city": city, "temperature_c": 26}
raise KeyError(name)
工具描述要说明用途、边界和副作用。写操作默认需要幂等键或人工确认;工具结果要限制长度并转义不可信内容;审计日志记录调用者、工具、参数摘要、结果和耗时。
4.8 压测方法与容量规划
先看它解决什么。 压测不是把并发数字调大,而是复现真实长度、到达率和解码参数,并在稳定区间测量资源、排队和错误。容量规划还要留故障与流量突增余量。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 构造代表性请求分布。
- 预热模型与 kernel。
- 逐级提升到达率。
- 记录延迟分位、吞吐、GPU 与失败。
一个可以手算的例子。 固定 100 个请求并发与按泊松到达的 100 QPS 不是同一种压力;前者更像瞬时洪峰,后者反映持续负载。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 没有预热、客户端先成为瓶颈、只报最好一次、忽略返回 token 数和错误请求,都会产生漂亮但无效的数字。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:写压测计划,明确样本来源、运行时长、并发模型、成功标准和停止条件。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
压测分三层:单请求基线确认模型可用;逐级增加并发找到吞吐拐点;长稳测试观察泄漏、碎片和长尾。输入长度与输出长度应来自真实分布,并单独测短问长答、长文短答等场景。
容量不是“峰值 QPS ÷ 单卡 QPS”这么简单。还要给发布、故障和突发流量留余量;按租户限流;把 prefill 密集型与 decode 密集型流量分别观察;对超长输入提前拒绝或转异步任务。
4.9 上线检查清单
抓住这一节的主线。 上线清单把隐含假设变成可验证条件,覆盖模型、数据、接口、性能、安全、监控、回滚与责任人。它应随事故和架构变化持续更新。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 离线评估达标。
- 容量与降级演练。
- 权限和隐私审查。
- 监控告警与回滚验证。
先做最小实验。 模型版本回滚不仅换权重,还要确认 tokenizer、提示模板、工具 schema、索引版本和缓存兼容。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只检查服务能启动、没有灰度样本、告警无负责人、回滚脚本从未执行,都会在事故中暴露。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为本章服务建立发布门禁表,每项写证据、负责人、截止时间和回滚动作。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
- 模型与 tokenizer 版本固定,许可证可用于目标场景。
- 输入大小、输出大小、并发、超时和预算都有上限。
- 记录 TTFT、TPOT、tokens/s、队列、GPU、错误率与质量抽检。
- 敏感数据最小化,日志脱敏,租户隔离。
- 工具调用有白名单、参数验证、权限和审计。
- 有离线评估集、灰度策略、回滚版本和故障降级。
延伸阅读:FastAPI 异步说明、FastAPI 流式响应、Qwen 官方快速开始。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/04_async_service.py`:流式 FastAPI 服务骨架。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 为聊天、批量摘要、代码补全分别写 SLO,并说明为何指标权重不同。
- 实现一个带请求 id、超时、并发信号量和取消处理的模拟生成接口。
- 构造短输入长输出与长输入短输出两类负载,分别压测并解释 TTFT/TPOT 差异。
- 写一个流式客户端状态机,覆盖正常结束、用户取消、超时和服务端错误。
- 为同一客服任务写三个提示版本,用十条固定样例比较格式正确率和事实错误率。
- 为旅行计划定义 schema,加入日期顺序、预算总和和城市白名单验证。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“服务指标必须从用户体验和系统资源同时定义”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“FastAPI 的 async 适合等待网络、磁盘和队列,不会让同步 GPU kernel 神奇变快”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“vLLM 的核心价值来自调度与 KV Cache 管理:把不同到达时间、不同长度的请求动态组成批次,并用分页思想减少缓存碎片”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“同步、异步和流式是调用语义,不是模型能力”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“提示词工程组织指令,上下文工程决定模型实际看见哪些规则、历史、证据和工具”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“结构化输出的目标是把自然语言结果变成可验证数据”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第五章 RAG 基础:让生成建立在可追溯证据上
检索增强生成(RAG)把参数化语言模型与外部知识库组合起来。它适合知识频繁变化、需要引用来源或包含私有领域文档的任务。RAG 不能保证绝对真实:检索可能漏掉证据,文档可能过期,模型也可能无视证据。一个合格系统必须同时评估检索与生成。

图 5-1 离线阶段解析、切块、向量化和建索引;在线阶段查询、检索、组装上下文并生成。
5.1 为什么不把所有知识都微调进去
先建立直觉。 微调更擅长改变行为和风格,RAG 更擅长提供可更新、可引用的外部事实。两者可以组合,但不能用微调代替权限、版本和证据管理。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 判断知识是否频繁变化。
- 判断是否要求引用。
- 判断行为是否需要稳定塑形。
- 评估延迟、成本与维护。
最小例子。 企业制度每月更新且回答必须给出处,优先 RAG;固定输出格式和专业语气可用 SFT;两者一起使用时仍以检索证据为事实来源。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把训练数据当数据库、认为 RAG 能保证答案正确、把整篇文档直接塞入上下文,都会造成错误。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:列出三个业务场景,分别选择 RAG、微调或组合方案,并给出证据。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
微调擅长改变行为、风格和任务模式,但不适合频繁更新事实,也很难给出精确来源。RAG 可以更新索引而不重训模型,并把证据随请求传入。两者并非互斥:可用 SFT 改善指令遵循,用 RAG 提供动态知识。
原始 RAG 论文把参数记忆与非参数记忆结合。工程系统通常不是端到端训练的单一模型,而是文档管道、检索器、重排器、提示模板、生成模型和评估系统的组合。
5.2 文档解析与切块

图 05-2 RAG 的质量上限常在解析和切块阶段就已经决定。从问题出发。 解析决定系统实际上看到了什么,切块决定检索的最小证据单位。页面、标题、表格、列表和来源信息应尽量保留,而不是只提取一串纯文本。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 识别格式与版面。
- 清洗页眉页脚和乱码。
- 按语义结构切块。
- 附加来源、页码和层级元数据。
用小数据走一遍。 一个表格若按行打散而丢失列名,检索到‘30%’也无法知道它对应哪个指标;应把表头与行内容共同编码。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 固定字符切割截断句子、overlap 过大制造重复、扫描 PDF 不做 OCR 质量检查、元数据不随块保存,都会降低召回与引用。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:对同一篇含标题和表格的文档实现三种切块,人工比较十个问题的可检索证据。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
文档进入索引前需要保留结构:标题层级、页码、段落、表格、图片说明和来源。只抽取纯文本会丢失表头、跨页关系和引用位置。每个 chunk 至少应带 doc_id、版本、标题路径、页码、时间、权限和内容哈希。
固定字符切块简单但可能截断语义;递归切块按段落、句子逐级回退;语义切块根据相邻句向量变化分段;命题切块把复合句拆成原子事实。chunk 越小越容易精确命中,却可能缺背景;越大上下文完整,却降低检索分辨率并占用窗口。
重叠能缓解边界截断,但会增加索引和重复召回。合理做法是先按文档结构切,再用小规模标注查询搜索 chunk 大小与重叠,而不是照抄固定参数。
PYTHON
def recursive_chunks(text: str, max_chars=500, overlap=80):
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current = [], ""
for paragraph in paragraphs:
candidate = f"{current}\n\n{paragraph}".strip()
if len(candidate) <= max_chars:
current = candidate
continue
if current:
chunks.append(current)
# 超长段落退回滑动窗口;生产版本应优先按句子或 token 切分
if len(paragraph) > max_chars:
step = max_chars - overlap
chunks.extend(paragraph[i:i + max_chars]
for i in range(0, len(paragraph), step))
current = ""
else:
current = paragraph
if current:
chunks.append(current)
return chunks
5.3 Embedding 与相似度
先看它解决什么。 Embedding 把文本映射到向量空间,相似度只表示模型学习到的语义接近,不等于事实蕴含或答案正确。向量是否归一化决定点积与余弦的关系。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 用同一模型编码查询和文档。
- 按模型要求做归一化。
- 建立近似最近邻索引。
- 返回分数与元数据。
一个可以手算的例子。 归一化后点积等于余弦相似度;未归一化时向量范数会影响排序。不同模型的向量维度和空间不能直接混用。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只因维度更大就认为模型更好、跨语言场景未评测、把相似度阈值从一个语料直接搬到另一个语料,都会失败。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:构造同义、相关但不回答、完全无关三类文本,观察相似度分布并选择初始阈值。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
Embedding 把文本映射为向量。语义检索通常用余弦相似度或内积;若向量已归一化,两者排序等价。查询和文档可能需要不同前缀或不同编码接口,应遵循模型卡。
选择 Embedding 时考虑语言、领域、最大长度、维度、速度、许可和查询/文档训练方式。MTEB 等榜单提供参考,但你的真实查询集更重要。维度更大不必然更好,还会增加存储和搜索成本。
PYTHON
import numpy as np
def cosine_topk(query_vec, doc_matrix, k=5):
q = query_vec / (np.linalg.norm(query_vec) + 1e-12)
docs = doc_matrix / (np.linalg.norm(doc_matrix, axis=1, keepdims=True) + 1e-12)
scores = docs @ q
k = min(k, len(scores))
ids = np.argpartition(-scores, k - 1)[:k]
ids = ids[np.argsort(-scores[ids])]
return [(int(i), float(scores[i])) for i in ids]
这段代码是精确搜索,适合小数据和教学。百万级向量常用 FAISS、Milvus 等近似最近邻索引。近似索引用少量召回损失换速度与内存,需调节 HNSW、IVF 等索引参数并测 Recall@K。
5.4 从检索结果到上下文
抓住这一节的主线。 检索结果要经过排序、去重、裁剪和格式化后才能成为上下文。上下文的目标是让模型找到证据边界,而不是把 token 窗口塞满。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 过滤权限与版本。
- 去除近重复块。
- 按相关性与多样性排序。
- 加来源标签后控制预算。
先做最小实验。 同一段在五个版本中重复出现时,应优先保留当前有效版本,否则模型可能引用已废止政策。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 把检索分数当可信度、丢失来源 id、上下文顺序随机、截断时切掉标题或单位,都会损害回答。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:写 build_context 函数,按 token 预算选择块,并保证每块含唯一证据编号。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
召回的 chunk 不能直接无脑拼接。先按权限过滤,再去重、限制同一文档占比、按重排分数选择,并保留稳定引用编号。提示模板应告诉模型只根据证据回答,证据不足时明确拒答,同时要求引用对应编号。
PYTHON
def build_context(hits, max_chars=5000):
blocks, used = [], 0
for idx, hit in enumerate(hits, 1):
block = (f"[证据{idx}] 来源={hit['title']} 页={hit.get('page', '?')}\n"
f"{hit['text'].strip()}")
if used + len(block) > max_chars:
break
blocks.append(block)
used += len(block)
return "\n\n".join(blocks)
引用必须能回到原文位置。模型生成的 [证据3] 还要验证是否真的存在、是否支撑对应陈述。引用格式正确不等于内容忠实。
5.5 LlamaIndex 等框架的角色
先把概念落到可观察对象上。 框架负责连接加载器、切块器、索引、Retriever 和生成器,但不会替你定义正确的数据边界、评估集和权限模型。先手写最小链路,再使用框架更容易定位问题。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 确认组件接口与数据结构。
- 显式记录每步输入输出。
- 替换单个组件做对照。
- 锁定版本并保留原始样本。
把它缩小到能逐项检查。 当最终回答错误时,若能独立调用 Retriever 查看 top-k,就能区分是召回失败还是生成器忽略证据。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 把框架默认参数当最佳实践、升级后不跑回归、深层 callback 隐藏异常和成本,都会使系统难以解释。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:用同一评估集比较手写检索器与框架检索器,记录结果差异而非只比较代码行数。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
LlamaIndex 把加载器、节点、索引、Retriever、QueryEngine 与 Response Synthesizer 等组件标准化,适合快速组装。学习时要能画出真实数据流,不要把 query_engine.query() 当作魔法。遇到质量问题,应能分别替换解析器、切块器、Embedding、向量库、重排器和生成器。
框架升级较快,示例 API 可能变化。先掌握组件契约:输入是什么、输出是什么、元数据是否保留、是否异步、是否可观测,再看当前官方文档。
5.6 一个从零 RAG 的最小接口
先建立直觉。 最小 RAG 接口应明确查询、检索结果、上下文和回答四个对象,保留证据链。即使不接真实模型,也能用确定性函数测试检索与拼接。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- search 返回结构化 Chunk。
- build_context 生成编号证据。
- generate 只接收问题与证据。
- response 同时返回答案和引用。
最小例子。 Chunk 至少包含 id、source、page、text、score;Response 至少包含 answer、citations、trace_id,便于评估和排错。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 函数直接返回拼接字符串、索引与生成共用全局状态、没有空召回处理、引用由模型自由编造,都会让接口失真。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:扩展最小接口,加入 no_answer 状态、检索耗时和引用存在性验证。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
PYTHON
from dataclasses import dataclass
@dataclass
class Chunk:
text: str
source: str
page: int | None
vector: np.ndarray
class TinyRetriever:
def __init__(self, chunks: list[Chunk]):
self.chunks = chunks
self.matrix = np.stack([c.vector for c in chunks])
def search(self, query_vector: np.ndarray, k=5):
return [(self.chunks[i], score)
for i, score in cosine_topk(query_vector, self.matrix, k)]
这个类没有负责 Embedding、持久化或权限,恰好说明模块边界。生产系统中查询向量要记录模型版本;文档更新要能增量重建;删除文档必须同步删除索引与缓存。
5.7 评估检索,而不是只看最终回答
从问题出发。 最终回答正确可能掩盖检索失败,因为模型可能凭参数记忆猜对。必须单独标注相关文档并计算 Recall@K、MRR、nDCG 等检索指标。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 定义查询与相关块集合。
- 冻结索引和评估版本。
- 计算排名指标。
- 按问题类型分析失败。
用小数据走一遍。 若正确块在第 8 名,Recall@10 为 1 但 Recall@5 为 0;MRR 还能反映首次命中的位置。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 只有一个参考块、相关性标注含糊、用生成答案反推相关块、调参后仍在同一测试集反复选择,都会污染指标。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:手工标注至少 30 个查询,计算 BM25 和向量检索的 Recall@5 与 MRR。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
先构建小而可信的评估集:查询、相关文档/片段、参考答案和不可回答标记。检索指标包括 Recall@K、Precision@K、MRR、nDCG;生成指标包括答案正确性、证据忠实度、引用准确率、拒答质量。还要记录延迟、成本和失败类型。
Recall@K 回答“相关证据是否出现在前 K 个结果中”;MRR 重视第一个相关结果的位置。没有标注相关文档时,可先人工标注几十到几百条高价值查询。用另一个大模型做评委可以扩展规模,但要用人工样本校准偏差。
5.8 常见失败
先看它解决什么。 RAG 失败应沿数据摄取、索引、查询、召回、重排、上下文和生成逐层定位。每层都要能输出可检查证据。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 确认目标文档确实入库。
- 确认切块含完整答案。
- 确认查询表达与过滤。
- 确认模型引用与回答一致。
一个可以手算的例子。 答案缺失可能不是 embedding 不好,而是 PDF 表格解析丢列、权限过滤过严或当前版本被错误标记过期。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 一遇到 bad case 就更换大模型、只调 top-k、没有保存 trace、修复后不建回归集,会导致问题迁移而非解决。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为三个失败样例写故障树,每个假设设计一个最便宜的验证实验。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 查询与文档措辞差异大:尝试查询改写、多查询或混合检索。
- 召回内容正确但答案错误:检查上下文排序、冲突证据、提示和生成模型。
- 表格问答失败:保留表头与行列结构,必要时走结构化查询。
- 新旧制度冲突:用生效时间、版本和权威级别过滤。
- 权限泄漏:权限过滤必须在检索或数据层完成,不能只在提示中声明。
练习:为同一批文档比较 200/500/1000 字符切块的 Recall@5;加入页码和标题路径;设计五条“知识库没有答案”的查询并检查拒答。
延伸阅读:RAG 原始论文、Milvus 快速开始、LlamaIndex 评估文档。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/05_rag_minimal.py`:最小可追溯 RAG。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 列出三个业务场景,分别选择 RAG、微调或组合方案,并给出证据。
- 对同一篇含标题和表格的文档实现三种切块,人工比较十个问题的可检索证据。
- 构造同义、相关但不回答、完全无关三类文本,观察相似度分布并选择初始阈值。
- 写 build_context 函数,按 token 预算选择块,并保证每块含唯一证据编号。
- 用同一评估集比较手写检索器与框架检索器,记录结果差异而非只比较代码行数。
- 扩展最小接口,加入 no_answer 状态、检索耗时和引用存在性验证。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“微调更擅长改变行为和风格,RAG 更擅长提供可更新、可引用的外部事实”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“解析决定系统实际上看到了什么,切块决定检索的最小证据单位”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Embedding 把文本映射到向量空间,相似度只表示模型学习到的语义接近,不等于事实蕴含或答案正确”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“检索结果要经过排序、去重、裁剪和格式化后才能成为上下文”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“框架负责连接加载器、切块器、索引、Retriever 和生成器,但不会替你定义正确的数据边界、评估集和权限模型”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“最小 RAG 接口应明确查询、检索结果、上下文和回答四个对象,保留证据链”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第六章 工业级 RAG:优化、评估与可运营系统
朴素 RAG 解决“能不能检索”,工业 RAG 解决“在真实数据、真实权限和真实流量下,能否稳定给出可追溯答案”。本章把优化分为查询、索引、召回、重排、上下文、生成和运营七层。优化必须由错误分析驱动,不要一次叠加十种技巧后只看主观示例。

图 6-1 离线评估找到失败类型,线上反馈产生新样本,数据与组件按版本迭代。
6.1 查询变换
先建立直觉。 用户查询常缺少上下文、包含代词或一次询问多个目标。查询变换要提高可检索性,同时保留原意并避免引入用户没有说过的事实。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 结合对话消解指代。
- 生成独立检索查询。
- 必要时拆为子问题。
- 保存原查询用于最终回答。
最小例子。 ‘它支持退款吗’需要从对话确定‘它’指哪个产品;若不确定,应先澄清而不是擅自选实体。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 改写模型加入答案、把所有查询扩展成冗长段落、多个子查询结果不去重,会降低精度与成本。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为十个多轮问题人工写标准独立查询,评估自动改写的实体保持率。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
用户查询常缺省上下文,例如“它什么时候生效”。首先要结合对话把问题改写为独立查询,但不能把模型猜测当事实。可让模型输出 standalone_query 和 assumptions,对关键假设要求用户确认。
Multi-query 生成多个检索视角,适合术语不一致;HyDE 先生成假设文档再嵌入,可能改善语义对齐,也可能把错误假设带入检索;查询分解把复杂问题拆成多个子问题,再合并证据。是否启用应看评估集,不是“越高级越好”。
6.2 混合检索与融合
从问题出发。 稀疏检索擅长关键词和编号,稠密检索擅长语义改写。混合检索用融合而非简单拼分数,因为两类分数尺度通常不可比。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 分别运行 BM25 与 Dense。
- 保留各自排名。
- 用 RRF 或学习融合。
- 再做去重和权限过滤。
用小数据走一遍。 RRF 按 1/(k+rank) 累积分数,不要求原始分数同尺度;同一文档在两路都靠前会获得更高融合排名。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 直接相加余弦与 BM25 分数、先截断过小 top-k、融合后丢失来源路由,会让结果不稳定。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 RRF,用包含产品型号与同义问法的评估集比较三种检索。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Dense 检索擅长语义相似,BM25 擅长专有名词、编号和精确词。混合检索分别召回后可用 Reciprocal Rank Fusion:
\operatorname{RRF}(d)=\sum_r\frac{1}{k+\operatorname{rank}_r(d)}.
PYTHON
from collections import defaultdict
def reciprocal_rank_fusion(rank_lists, k=60):
scores = defaultdict(float)
for ranked_ids in rank_lists:
for rank, doc_id in enumerate(ranked_ids, start=1):
scores[doc_id] += 1.0 / (k + rank)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
RRF 不依赖不同检索器分数的量纲,容易作为强基线。若业务有时间、地区、产品线和权限条件,应先做元数据过滤或按业务规则融合。
6.3 重排与上下文压缩
先看它解决什么。 重排器在候选集上做更昂贵的查询—文档联合判断,上下文压缩则只保留支持回答的句段。二者目标不同:前者改顺序,后者省窗口。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 宽召回获得候选。
- Cross-Encoder 逐对评分。
- 按预算选择文档。
- 抽取关键句并保留来源映射。
一个可以手算的例子。 召回 50 个块、重排取 8 个,再压缩为 2,000 token;任何抽取句都必须能回指原块与页码。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 在召回不足时指望重排创造正确文档、压缩器改写事实、批处理不当造成巨大延迟,都是常见问题。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:分别测召回 top-k、重排 top-n 与压缩长度对 Recall、引用准确率和 P95 的影响。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
双塔 Embedding 预先编码文档,召回快;Cross-Encoder 把 query 与候选文本共同编码,交互充分但更慢。因此常先召回几十到几百条,再重排到少量证据。重排模型要匹配语言和领域,并测批处理延迟。
上下文压缩不只是摘要。可从 chunk 中抽取与查询相关句子、合并相邻片段、保留标题链和表头。摘要会引入生成误差,高风险场景应保留原文引用并允许回看。
“Lost in the Middle” 提醒我们:长上下文不保证每段都被同等利用。把最强证据放在模型更容易关注的位置,并减少相互矛盾与重复片段,往往比无限扩充上下文有效。
6.4 自适应 RAG 与反馈回路
抓住这一节的主线。 自适应 RAG 先判断问题是否需要检索、需要哪类数据源和检索深度,再依据结果质量决定补检索或拒答。反馈必须可审计,不能直接让线上点击无限改变索引。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 查询分类与路由。
- 质量门控检查证据。
- 必要时改写重试。
- 记录反馈进入离线评审。
先做最小实验。 闲聊可不检索,订单状态走结构化 API,政策问题走文档索引;没有足够证据时返回限制说明。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 路由标签没有评估、失败无限循环、把用户点赞直接当事实正确、不同租户反馈混合,都会带来风险。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:设计路由混淆矩阵,并为每个路由规定最大步数与降级答案。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
不是每个问题都需要检索。系统可先分类:闲聊直接回答;知识问题检索;实时问题走搜索/API;结构化数据走 SQL;高风险问题转人工。分类错误会导致系统性失败,所以要保留兜底路径。
反馈回路可基于低置信度、引用缺失、用户点踩和人工纠错触发再次检索。不要让模型无限自我反思;设置最大轮数、预算和停止条件。反馈数据进入训练或评估前要去重、脱敏和人工抽检,避免把恶意输入写回知识库。
6.5 多模态、表格与图 RAG
先把概念落到可观察对象上。 多模态与表格 RAG 的难点是保持结构关系;图 RAG 则显式表示实体与关系。它们不是默认更好,只有普通文本检索无法表达问题结构时才值得引入。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 保留页面布局与坐标。
- 为图像生成可追溯描述或向量。
- 为表格保存表头关系。
- 图谱实体链接并记录证据。
把它缩小到能逐项检查。 询问‘2024 年华东区哪个季度增长最高’需要保留年份、区域、季度和指标列,单行文本块可能丢失这些关系。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 OCR 文本与图像重复入库、实体消歧错误、图谱边无来源、把图 RAG 当万能推理器,都会降低可信度。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:选择一张表和一页图文 PDF,设计可回答五个问题的结构化表示。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
扫描 PDF 需要 OCR 与版面分析;图片应生成可检索描述并保留原图坐标;表格应保留表头、单位和合并单元格关系。多模态 Embedding 可以统一召回,但最终证据展示仍要回到原始页面。
Graph RAG 适合实体关系、多跳问题和全局主题总结。它需要实体消歧、关系抽取、社区或路径检索,构建和更新成本更高。若问题主要是单文档事实查找,普通 RAG 往往更简单可靠。
6.6 评估体系:从数据集到故障归因

图 06-2 把检索和回答拆开评估,才能识别参数记忆猜对与证据被忽略。先建立直觉。 工业评估从真实流量抽样,建立可版本化数据集,并把错误归因到组件。检索、回答、忠实度、引用、延迟、成本与安全必须分别测量。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 定义任务与失败分类。
- 分层抽样并双人标注。
- 建立基线和置信区间。
- 发布前后持续回归。
最小例子。 总体准确率不变时,政策题可能提升而表格题下降;分桶结果能揭示回归。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 只用 LLM judge、没有人工校准、测试集泄漏到提示、指标定义随版本变化,会让结果不可比较。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:写一份 100 题评估方案,规定抽样比例、标注规范、冲突仲裁和报告模板。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
评估集应覆盖头部问题、长尾问题、不可回答、冲突文档、过期文档、权限边界、表格、长文和对抗输入。每条样本至少标注:期望证据、参考结论、可接受变体、风险级别。
将端到端错误拆为:解析错误、切块错误、召回漏失、排序错误、上下文组装错误、生成不忠实、引用错误和业务规则错误。只有先归因,才能知道该换 Embedding、改 chunk、加重排还是改提示。
线上监控除延迟和错误率,还要看空召回率、证据覆盖、引用点击、拒答率、每查询 token、缓存命中和不同租户的质量差异。用户满意度受界面和期望影响,不能替代客观正确性。
6.7 可运营架构
从问题出发。 可运营 RAG 需要数据面与服务面分离:摄取流水线负责版本、权限和索引发布,在线服务负责查询、检索、生成与观测。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 原始文档不可变存档。
- 增量解析与索引构建。
- 影子验证后原子切换。
- 在线 trace 关联每个版本。
用小数据走一遍。 删除一份文档时,不只从向量库删块,还要更新缓存、倒排索引、图谱和审计记录,并证明用户无法再检索。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 原地更新唯一索引、权限只在生成前检查、没有文档 lineage、缓存键不含租户与版本,都会造成泄漏或不一致。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:画出双索引蓝绿发布架构,并写回滚、删除和权限变更流程。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
把文档接入与在线问答解耦。离线服务负责解析、去重、切块、Embedding、索引和版本发布;在线服务负责路由、检索、重排、生成和引用;评估服务复放固定数据集;观测平台关联请求 trace 与组件版本。
索引发布应原子切换并可回滚。文档删除要传播到向量、关键词、缓存和备份策略。多租户系统应把权限字段写入索引并在查询层强制过滤,不能生成后再删敏感句子。
6.8 成本与延迟优化
先看它解决什么。 延迟优化先看时间分解:查询改写、两路召回、重排、生成各占多少。成本优化要看 token、模型调用、索引与存储,而不是只换便宜模型。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 埋点得到阶段耗时。
- 并行独立检索。
- 批处理重排与 embedding。
- 缓存稳定且无权限风险的结果。
一个可以手算的例子。 若 70% 时间花在生成,继续优化向量索引收益有限;若 TTFT 被串行改写和重排占满,应先并行或路由。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 缓存包含用户敏感结果、为了省 token 截断关键证据、只看均值不看长尾、压测数据过短,都会制造假优化。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为一次请求画甘特图,提出三项优化并预估对 P50/P95、成本和质量的影响。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
常用手段包括:缓存查询 Embedding 与稳定答案、缩小重排候选、并行关键词/向量检索、批量 Embedding、对长文异步建索引、对低风险查询使用小模型。缓存键必须包含知识库版本、权限范围和提示版本,否则会返回过期或越权答案。
性能优化要保持质量门槛。例如把 top_k 从 20 降到 5 可降低重排成本,却可能伤害长尾 Recall。每个优化都应在同一评估集上同时报告质量、P95 和成本。
6.9 项目表达:不要只写“搭建 RAG”
抓住这一节的主线。 项目表达要让读者或面试官能复现你的判断:问题、约束、基线、个人动作、实验、指标、失败和遗留问题缺一不可。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 说明为什么需要 RAG。
- 给出数据规模与评估定义。
- 比较至少一个基线。
- 明确自己的决策和边界。
先做最小实验。 ‘Recall@5 从 0.69 到 0.84’还需说明查询数、相关性定义、置信区间、索引版本和硬件。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只写使用了某框架、数字无出处、把团队成果全算个人、没有 bad case,会让项目不可信。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:把自己项目写成 200 字摘要,再列出可能被追问的十个证据问题。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
一条可信的项目经历应包含业务约束、数据规模、本人负责、技术取舍、评估方法和量化结果。例如:
为内部制度问答构建可追溯 RAG;负责 PDF 版面解析、标题感知切块和混合检索。基于 420 条人工标注查询将 Recall@5 从 0.71 提升至 0.86,引用准确率从 78% 提升至 91%;通过批量重排与缓存把 P95 从 3.2 秒降至 1.9 秒。所有数字来自固定版本评估与压测环境。
不能验证的百分比不要写。面试官会追问数据怎么标、基线是什么、是否显著、线上是否一致、你做了哪部分。
6.10 实战路线
先把概念落到可观察对象上。 实战应从小而真实的数据集开始,先建立可解释基线,再逐项增加复杂度。每次只改变一个主要变量并保留回归结果。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 建立 50—200 条标注查询。
- BM25 与 Dense 做基线。
- 加入混合和重排。
- 上线监控并维护错误簿。
把它缩小到能逐项检查。 先确保目标证据可被 BM25 找到,再比较 embedding;否则切块或解析问题会被误判成模型问题。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 一开始就上图谱、多 Agent 和复杂框架,评估集却只有几个演示问题,会让系统无法收敛。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:完成一个两阶段里程碑:第一阶段可追溯基线,第二阶段针对最大错误类型优化并写实验报告。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
选择一组公开报告或产品手册,建立 100 条查询评估集。先做 BM25 与 Dense 两个基线,再加入 RRF、重排和查询改写,每次只改一个因素。输出四张表:检索指标、答案指标、延迟成本、错误类型分布。最后实现文档版本、权限过滤、引用跳转与一键回滚。
延伸资料:RAG Survey、Milvus 索引说明、Datawhale Agent/RAG 面试问题整理。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/06_hybrid_rrf.py`:混合检索 RRF 融合。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 为十个多轮问题人工写标准独立查询,评估自动改写的实体保持率。
- 实现 RRF,用包含产品型号与同义问法的评估集比较三种检索。
- 分别测召回 top-k、重排 top-n 与压缩长度对 Recall、引用准确率和 P95 的影响。
- 设计路由混淆矩阵,并为每个路由规定最大步数与降级答案。
- 选择一张表和一页图文 PDF,设计可回答五个问题的结构化表示。
- 写一份 100 题评估方案,规定抽样比例、标注规范、冲突仲裁和报告模板。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“用户查询常缺少上下文、包含代词或一次询问多个目标”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“稀疏检索擅长关键词和编号,稠密检索擅长语义改写”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“重排器在候选集上做更昂贵的查询—文档联合判断,上下文压缩则只保留支持回答的句段”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“自适应 RAG 先判断问题是否需要检索、需要哪类数据源和检索深度,再依据结果质量决定补检索或拒答”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“多模态与表格 RAG 的难点是保持结构关系”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“工业评估从真实流量抽样,建立可版本化数据集,并把错误归因到组件”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第七章 Agent:让模型在受控边界内观察、决策与行动
Agent 不是“加一个提示让模型自己想”。它是一个运行时系统:模型接收目标和当前状态,选择工具或给出答案,程序执行动作并返回观察,循环直到完成、失败或需要人类决策。真正困难的是工具契约、状态管理、停止条件、权限、安全和评估。

图 7-1 模型负责提出下一步,运行时负责执行、校验、记录与终止。
7.1 Workflow 与 Agent
先建立直觉。 Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步。任务路径稳定、风险高时优先 Workflow;开放探索且可验证时才需要更多自主性。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 判断步骤是否可预先枚举。
- 判断工具结果是否可验证。
- 评估失败成本。
- 设置最大自主范围。
最小例子。 发票审批流程适合确定性工作流;跨来源研究问题可能需要动态决定下一次搜索。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把一次模型调用称为 Agent、把所有流程都交给模型、没有人工接管点,会同时增加成本和风险。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
若步骤固定、规则明确,优先使用工作流:分类后走不同分支、并行调用多个模型、由评审器打分等。Agent 适合路径难以预先写死、需要基于中间结果动态决策的任务。自治程度越高,成本和风险越高。
ReAct 可概括为“推理—行动—观察”的交替。工程实现不应依赖模型输出冗长私密思维;只需结构化地返回动作、参数和简短理由,详细 trace 由运行时记录。
7.2 一个最小工具循环
从问题出发。 最小 Agent 循环只有观察、决策、行动和停止。状态必须由程序保存,模型输出只是候选决策;每一步都要有预算与 trace。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 把目标和当前状态交给模型。
- 解析 action 或 final。
- 验证并执行工具。
- 把结构化观察写回状态。
用小数据走一遍。 搜索工具返回 title、url、snippet 与错误码;模型看到的是受控结果,不应直接获得浏览器或系统任意权限。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 工具异常后无限循环、观察文本没有长度限制、停止完全靠模型自觉、状态原地混乱修改,都会失控。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 max_steps=5 的工具循环,用假工具测试成功、未知工具、超时和达到步数上限。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
PYTHON
from dataclasses import dataclass
from typing import Any, Callable
@dataclass
class Tool:
name: str
description: str
fn: Callable[..., Any]
class AgentRuntime:
def __init__(self, model, tools: list[Tool], max_steps=8):
self.model = model
self.tools = {t.name: t for t in tools}
self.max_steps = max_steps
def run(self, goal: str):
state = {"goal": goal, "observations": []}
for step in range(self.max_steps):
decision = self.model.decide(state, list(self.tools.values()))
if decision["type"] == "final":
return {"status": "completed", "answer": decision["answer"],
"steps": step + 1}
if decision["type"] != "tool":
return {"status": "failed", "reason": "非法动作"}
name = decision["name"]
if name not in self.tools:
state["observations"].append({"error": f"未知工具 {name}"})
continue
try:
result = self.tools[name].fn(**decision["arguments"])
state["observations"].append({"tool": name, "result": result})
except Exception as exc:
state["observations"].append({"tool": name, "error": str(exc)})
return {"status": "stopped", "reason": "达到最大步数"}
代码解读:运行时而非模型控制最大步数;未知工具和异常被转成观察;每次工具调用都应在 fn 内再做参数、权限与副作用检查。真实系统还要限制总 token、金额、网络域名和墙钟时间。
7.3 写好工具契约

图 07-2 模型负责提议,Schema、权限、确认和审计共同决定动作能否执行。先看它解决什么。 工具契约既是模型说明书,也是执行器的安全边界。名称、用途、参数、必填项、枚举、返回值、错误和副作用必须明确。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 用 JSON Schema 定义参数。
- 让执行器做二次校验。
- 区分读操作和写操作。
- 错误返回稳定机器码。
一个可以手算的例子。 get_weather 的 city 应是字符串,但还要限制长度和允许字符;delete_file 则必须限制根目录并要求确认。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 描述含糊、参数可接受任意 SQL/命令、异常直接返回密钥或堆栈、工具名相似,会诱发错误调用。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为三个工具写契约并设计十个非法参数测试。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
工具名要稳定,描述要说明“何时用、何时不用”,参数 Schema 要少而明确。一个“万能搜索”工具往往不如拆成内部知识库、公开网页、数据库查询等有清楚边界的工具。返回值使用结构化字段,避免把整页 HTML 塞回上下文。
写操作应分类:只读可自动执行;可逆写操作可要求一次确认;不可逆或高影响操作必须明确目标并人工批准。凭据由运行时保管,绝不放进提示词。工具输出属于不可信输入,要防提示注入。
7.4 规划、反思与停止
抓住这一节的主线。 规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索。三者都应由外部预算和验证器约束。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 生成带依赖的任务列表。
- 完成一步后更新状态。
- 检查目标是否已被证据满足。
- 触发成功、失败或预算停止。
先做最小实验。 研究报告可要求每个结论至少一条来源;若缺来源则继续检索,但达到最大查询数后必须明确不确定。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 反思只让同一模型重复说一遍、计划一次生成后从不更新、没有硬停止和成本上限,会产生冗长轨迹。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
复杂目标可先产生短计划,但计划不是承诺;观察改变时要更新。任务可分解为有依赖关系的子目标,用状态机或 DAG 比一段自由文本更可控。
Reflection 适合在失败后总结“证据缺什么、下一步验证什么”,不适合无限自我批评。停止条件包括:目标已满足、关键证据不足、需要用户偏好、工具连续失败、预算耗尽、风险升级。可靠 Agent 知道什么时候停。
7.5 记忆与上下文工程
先把概念落到可观察对象上。 记忆是可持久化信息,上下文是本轮实际提供给模型的信息。长期记忆必须经过写入筛选、来源记录、检索和遗忘,而不是保存全部对话。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 短期状态保存当前任务。
- 长期记忆只写稳定有用信息。
- 按当前问题检索。
- 对过期与敏感信息删除。
把它缩小到能逐项检查。 用户明确偏好素食可存为带来源和时间的偏好;一次临时说‘今天想吃辣’不应自动成为永久身份属性。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 把模型猜测写成用户事实、跨用户混用记忆、没有删除入口、把检索结果当系统指令,都会伤害隐私与正确性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
短期记忆是当前任务状态和最近观察;长期记忆应是经过筛选的结构化事实或可检索记录。不要把所有对话永久保存并全部塞回模型。建议把记忆分为:用户明确偏好、任务事实、可复用程序经验和原始审计日志。
写入长期记忆前检查来源、置信度、时效、隐私和删除机制。摘要记忆会漂移,重要事实应链接到原始证据。RAG 可做长期检索,但“相似”不等于“仍然正确”。
7.6 Deep Research 的工程结构
先建立直觉。 Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作。难点在证据质量,而不是生成篇幅。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 把开放问题拆成可检索子问。
- 记录每个来源与摘录。
- 识别重复和冲突。
- 按结论—证据映射生成报告。
最小例子。 统计数据应优先原始发布机构,新闻可用于发现线索但不替代官方数据;不同时间口径必须在报告中说明。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 只搜索一次、引用搜索摘要而未打开原文、生成不存在的 URL、把发布时间当事件时间,都会造成虚假研究。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
研究型 Agent 常执行:澄清问题、制定检索子题、搜索多源资料、读取原文、提取证据、交叉验证、生成带引用报告。关键不是搜索次数,而是证据质量与覆盖。
可把每条证据存成 {claim, source_url, excerpt, date, confidence},生成结论时只引用可回溯来源。对时间敏感主题检查发布日期与事件发生日期;对技术主题优先论文和官方文档;对冲突来源明确说明差异。
7.7 MCP:标准化模型与外部能力的连接
从问题出发。 MCP 规定 client、host、server 之间怎样发现并调用工具、资源和提示,使能力连接标准化。它解决互操作,不自动解决权限、可信度和业务安全。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- Host 管理会话与用户界面。
- Client 与单个 Server 连接。
- Server 暴露工具或资源。
- 传输层交换协议消息。
用小数据走一遍。 文件系统 MCP server 即使遵循协议,也必须限制允许目录;标准化的调用格式不意味着可访问整块磁盘。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把 MCP 当 Agent 框架、让同一 server 同时拥有过宽读写权限、忽略 server 返回内容不可信,都会扩大攻击面。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:画出 Host—Client—Server 架构,并为一个只读知识库 server 写最小权限表。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Model Context Protocol 使用 client-host-server 架构,让服务器暴露工具、资源和提示等能力。MCP 解决“怎样描述和连接能力”,不自动解决权限与可信问题。安装任何 MCP server 前,应审查来源、它能访问的数据、可执行动作、凭据范围和日志策略。
工具调用与 MCP 的关系可以理解为:模型仍生成调用意图,MCP 提供标准化发现与传输,宿主负责授权、用户交互和安全边界。不要把“协议标准化”误解为“所有工具天然安全”。
7.8 为什么 Agent 会失败
先看它解决什么。 Agent 失败通常来自目标不清、工具契约错误、状态丢失、上下文污染、权限过宽、重试不当和缺少评估。模型只是系统中的一个组件。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 保存完整 trace。
- 找到首次偏离目标的位置。
- 把失败归因到具体组件。
- 加入最小修复和回归。
一个可以手算的例子。 重复调用同一工具可能是观察字段不清,也可能是停止条件未表达;仅更换更大模型无法证明根因。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只看最终答案、事后人工补轨迹、失败样本不版本化、把所有问题归因于 hallucination,都会阻碍改进。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为十条失败轨迹标注第一错误步骤和责任组件,统计最大失败类别。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 工具描述含糊,模型选错工具或填错参数。
- 上下文堆积导致关键约束被淹没。
- 计划依赖错误事实,后续步骤不断放大偏差。
- 缺少幂等、重试和状态恢复,工具部分成功后重复执行。
- 没有明确完成标准,循环不停或过早结束。
- 评估只看最终文本,不看实际副作用和轨迹。
改进顺序通常是:缩小任务范围,简化工具,增强 Schema 与验证,加入可观测性,最后才考虑更复杂框架或多 Agent。
7.9 多 Agent 不是默认答案
抓住这一节的主线。 多 Agent 适合可独立并行、角色边界清晰、结果可合并验证的任务。否则通信、冲突和重复工作可能大于收益。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 按可交付物拆分角色。
- 定义共享状态与消息契约。
- 限制每个角色工具权限。
- 由确定性规则或审阅者合并。
先做最小实验。 三个 Agent 分别检索不同数据源可以并行;让三个 Agent 都自由规划同一任务通常只产生重复。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 用角色提示代替权限隔离、共享无限对话、没有冲突解决和总预算,都会使系统更脆弱。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:估算一个多 Agent 方案的额外调用数、最长依赖链和失败传播路径,再与单 Agent 比较。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
多个 Agent 可按研究、执行、评审等角色分工,但会增加通信、冲突、重复工作和故障定位难度。只有当子任务能清楚分割、并行收益明显、输出契约可验证时才值得使用。多数业务先用单 Agent + 明确工具 + 工作流路由更可靠。
7.10 评估与安全
先把概念落到可观察对象上。 Agent 评估要覆盖任务完成、步骤效率、工具正确性、恢复能力、成本、延迟和安全。最终答案正确也不能掩盖越权或不可重复的过程。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 建立带初始状态的任务集。
- 记录期望工具与允许动作。
- 模拟超时和错误。
- 审计副作用与权限。
把它缩小到能逐项检查。 预订测试中可使用沙箱日历,检查是否选对时间、是否重复创建、是否在写入前获得确认。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只用自然语言 judge、测试工具永不失败、没有 adversarial prompt、线上真实写操作直接评测,都会高估可靠性。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计 20 个 Agent 测试,至少含注入、工具超时、空结果、重复调用和权限拒绝。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
Agent 评估要同时看任务成功率、步骤数、工具错误、成本、延迟、恢复能力和安全违规。测试集应包含工具超时、空结果、恶意网页、权限不足、重复请求、歧义目标与需要人工确认的场景。
防提示注入采用分层策略:不可信内容与系统指令隔离;工具最小权限;危险动作确认;限制网络与文件范围;输出验证;异常检测;完整审计。没有单一提示能彻底解决注入。
练习:实现天气与计算器两个只读工具;加入每工具超时、总预算和 trace;构造含“忽略之前指令”的恶意工具结果,验证运行时不会扩大权限。
延伸资料:Anthropic《Building Effective Agents》、MCP 架构、Agent 评估实践。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/07_safe_agent_loop.py`:安全边界明确的工具循环。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 把一个业务过程画成 Workflow 版和 Agent 版,比较可测试性与失败面。
- 实现 max_steps=5 的工具循环,用假工具测试成功、未知工具、超时和达到步数上限。
- 为三个工具写契约并设计十个非法参数测试。
- 为旅行规划定义成功条件、三个失败条件和 token/工具/时间预算。
- 设计 memory record schema,包含主体、内容、来源、置信、时间、权限和过期策略。
- 完成一个五来源研究报告,为每条事实保存 claim、source、quote span 和访问日期。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“Workflow 由程序预先规定路径,Agent 让模型在运行时选择下一步”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“最小 Agent 循环只有观察、决策、行动和停止”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“工具契约既是模型说明书,也是执行器的安全边界”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“规划把目标拆成可执行子任务,反思检查证据缺口,停止条件防止无穷探索”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“记忆是可持久化信息,上下文是本轮实际提供给模型的信息”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Deep Research 是受控的信息获取流水线:拆解问题、多轮搜索、页面阅读、证据去重、矛盾处理和带引用写作”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第八章 预训练:数据、目标、规模与训练稳定性
预训练让模型从大规模语料中学习语言统计、知识与通用表示。它不是把网页“存进参数”,而是在下一个 token 等目标上优化概率分布。完整预训练工程包括数据治理、tokenizer、模型设计、规模实验、分布式训练、监控、评估和发布。

图 8-1 预训练是可版本化的数据—模型—评估闭环,数据治理贯穿全程。
8.1 训练目标与架构
先建立直觉。 预训练把大量 token 变成自监督样本。Decoder-only 用前缀预测下一个 token,Encoder-only 常用掩码恢复,Encoder-Decoder 可用去噪重建;目标决定可用信息与适合任务。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 文本经过 tokenizer。
- 构造输入与标签。
- 模型输出每位置 logits。
- 只在有效标签上计算损失。
最小例子。 长度 T 的自回归序列可产生 T-1 个预测目标,训练时并行计算,推理时只能按前缀逐步生成。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把训练并行误解为推理并行、标签未右移、EOS 与 padding 混淆、把 perplexity 跨 tokenizer 直接比较,都会误导。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:对同一短句分别写出 causal LM、masked LM 与 seq2seq 去噪的输入和标签。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
Decoder-only 模型最常用因果语言建模:
\mathcal{L}=-\sum_{t=1}^{T}\log p_\theta(x_t\mid x_{<t}).
Encoder-only 模型可用掩码语言建模,适合表征与理解;Encoder-Decoder 可用 span corruption,把被遮蔽片段作为生成目标。目标决定可见信息与下游归纳偏置,不能只比较模型参数量。
训练时采用 teacher forcing:所有位置的真实前缀同时输入,在因果掩码下并行预测。标签通常是输入右移一位;padding 或跨样本拼接位置要设为忽略值,避免计算无效损失。
8.2 数据获取与合规

图 08-2 来源登记、去重和安全过滤都应保留可审计记录。从问题出发。 数据质量、合法性和可追踪性是预训练的第一层模型能力。采集只是开始,还要做格式识别、语言过滤、去重、质量评分、隐私与版权处理。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 建立来源与许可清单。
- 抽取正文并过滤模板噪声。
- 文档级与段落级去重。
- PII/恶意内容处理并分层抽样。
用小数据走一遍。 同一新闻被数千站点转载会放大记忆和数据泄漏风险;MinHash 可近似发现相似文档,但阈值需要通过人工样本校准。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 只按关键词过滤、训练集与评测集去重不足、删除数据后索引仍保留、来源权重无记录,都会造成不可解释风险。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:为 1 万篇文档设计数据卡,记录来源、许可、语言、质量、去重簇和过滤原因。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
数据首先要有可用权利和清楚来源。建立数据卡记录来源、许可、语言、时间、清洗规则、PII 处理、已知偏差与删除流程。公开可抓取不等于可随意训练或再分发。
清洗通常包括编码修复、语言识别、文档质量过滤、样板/广告去除、PII 与恶意内容处理。去重分精确去重与近似去重;训练/评估集之间还要做污染检测。过度过滤会损失方言、少数语言和领域多样性,质量规则应抽样审计。
混合数据时,不能只按原始体量采样,否则高资源语种和重复网页会主导训练。可按领域、语言与质量分层设权重,并跟踪每类 token 的实际占比。
8.3 Tokenizer 训练
先看它解决什么。 Tokenizer 决定模型看到的基本符号、序列长度和多语言效率。BPE、Unigram 等算法从字符或字节出发学习可复用子词,不是寻找唯一正确的词语边界。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 规范化与预分词。
- 统计候选片段。
- 迭代合并或概率选择。
- 加入特殊 token 并验证可逆性。
一个可以手算的例子。 代码缩进、中文、生僻字符和 emoji 都应能无损编码;byte fallback 可避免未知字符,但可能增加 token 数。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 在全量测试集上训练词表、特殊 token id 与模型配置不一致、只看词表大小不看压缩率,会产生隐患。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:训练两个不同词表大小的 tokenizer,比较中英代码语料的字符/token 比和未知字符处理。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
Tokenizer 词表过小会产生长序列,过大增加嵌入和输出层参数并稀释低频 token。中文、代码、数学和多语言对分词有不同需求。评估指标包括平均每字符 token 数、不同语种压缩率、特殊符号和空白保真、未知/字节回退行为。
特殊 token 的 id、对话模板和 BOS/EOS 规则必须固定并写入模型配置。预训练完成后随意更换 tokenizer 相当于改变输入语义。
8.4 Scaling Laws:用小实验规划大训练
抓住这一节的主线。 Scaling Law 用小规模实验估计模型、数据和计算之间的趋势,帮助在固定预算下选择参数量与 token 数。它是经验规律,不是对所有架构和数据永恒不变的定律。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 选择多个模型和数据规模。
- 保持训练配方可比。
- 拟合 loss 与 compute 曲线。
- 用外推结果规划并留安全余量。
先做最小实验。 只训练更大的模型但 token 不足可能处于欠训练状态;更多高质量 token 有时比继续增加参数更划算。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 用单个实验点拟合、跨 tokenizer 比 perplexity、把训练 FLOPs 当总成本、忽略数据质量变化,都会破坏结论。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:用三个小模型和三种 token 预算记录验证损失,画 compute—loss 图并说明不确定性。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
Scaling Law 描述损失与模型规模、数据量和计算量之间的经验幂律关系。Chinchilla 工作指出,在给定计算预算下,模型参数和训练 token 应协调扩展;它不是“永远固定 20 token/参数”的硬定律,具体最优点受数据质量、架构、优化器和目标影响。
实践中先训练多组小模型,保持数据和训练设置可比,拟合损失曲线,再估算目标规模。外推存在误差,应留预算和容错。只扩大参数而数据不足会欠训练;只增加数据而模型太小也会进入收益递减。
8.5 一个微型自回归模型
先把概念落到可观察对象上。 微型自回归模型把完整链路缩小到单机可观察尺度:embedding、多个 Decoder Block、最终归一化和词表投影。它用于验证实现,不代表真实大模型训练配方。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- token/position 进入隐藏状态。
- 逐层注意力与 FFN。
- 输出投影到词表。
- 交叉熵训练并采样生成。
把它缩小到能逐项检查。 权重绑定可让输入 embedding 与输出 projection 共用参数;是否使用必须与架构配置和维度一致。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 位置长度超界、因果 mask 错误、初始化过大、训练语料太小却期望知识能力,都会导致误判。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:让模型在一小段文本上过拟合,确认 loss 接近零并能复述,再换验证文本观察泛化差距。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
PYTHON
import torch
from torch import nn
class TinyLM(nn.Module):
def __init__(self, vocab_size=256, dim=128, layers=4, heads=4, max_len=512):
super().__init__()
self.token_emb = nn.Embedding(vocab_size, dim)
self.pos_emb = nn.Embedding(max_len, dim) # 教学版用绝对位置
layer = nn.TransformerEncoderLayer(
d_model=dim, nhead=heads, dim_feedforward=4 * dim,
batch_first=True, norm_first=True, activation="gelu"
)
self.blocks = nn.TransformerEncoder(layer, num_layers=layers)
self.norm = nn.LayerNorm(dim)
self.lm_head = nn.Linear(dim, vocab_size, bias=False)
self.lm_head.weight = self.token_emb.weight
def forward(self, tokens):
b, t = tokens.shape
pos = torch.arange(t, device=tokens.device)
x = self.token_emb(tokens) + self.pos_emb(pos)[None]
causal = torch.triu(torch.ones(t, t, device=tokens.device, dtype=torch.bool), 1)
x = self.blocks(x, mask=causal)
return self.lm_head(self.norm(x))
def next_token_loss(model, batch):
logits = model(batch[:, :-1])
labels = batch[:, 1:]
return nn.functional.cross_entropy(logits.reshape(-1, logits.size(-1)),
labels.reshape(-1))
这里借用 TransformerEncoder 但传入因果 mask,使其表现为因果堆叠;命名容易误导,因此生产实现应使用明确的 Decoder Block。权重绑定让输入嵌入与输出投影共享参数。reshape 前要确认 logits 与 labels 的位置严格对齐。
8.6 数据打包与有效 token
先建立直觉。 打包的目标是减少 padding,让每个设备处理尽可能多的有效 token。样本边界、EOS、document mask 与 position_ids 必须与训练目标一致。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 按长度分桶或流式拼接。
- 插入 EOS 保留边界。
- 构造 attention 与 label mask。
- 统计有效 token 比例。
最小例子。 两个短文档拼成一条序列时,若允许跨文档注意力,模型可能把无关结尾当作下一文档上下文;是否隔离要显式选择。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 把 padding 计入 loss、最后残片大量丢弃、packing 与缓存模板不一致、只报告 batch size 不报告 token 数,都会影响训练。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:实现 fixed-length packer,验证每个原文 token 恰好出现一次且标签边界正确。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
把短文逐条 padding 会浪费计算,可将多个文档 token 拼接再切固定长度。文档边界是否允许相互注意取决于训练设计;至少要插入 EOS,必要时使用 block-diagonal mask。记录 tokens_per_second 应只统计非 padding、真正参与损失的 token。
数据加载器要能断点续训:保存数据 shard、样本游标、随机数状态和混合权重。若恢复后数据顺序改变,训练虽能继续,却不再严格复现。
8.7 优化器与学习率
从问题出发。 AdamW、学习率预热和衰减共同控制更新尺度。预热降低训练初期不稳定,权重衰减与 Adam 的自适应矩估计解耦。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 计算梯度并做全局裁剪。
- 优化器更新一阶/二阶矩。
- 按 schedule 得到当前学习率。
- 记录参数与更新范数。
用小数据走一遍。 相同学习率在不同有效 batch、参数化和精度下含义不同;可观察 update/weight ratio 判断更新是否过激。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把 weight decay 施加到所有 bias/norm、梯度累积时错误缩放 loss、恢复 checkpoint 后学习率重置,都会影响收敛。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:比较 constant、linear warmup+cosine 两种曲线,并解释训练早期 loss 的差异。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
常见配置为 AdamW、warmup 后余弦衰减、梯度裁剪和 BF16。Warmup 在训练初期逐渐提高学习率,减少随机初始化下的剧烈更新。全局批量大小 = 每卡微批量 × 梯度累积 × 数据并行卡数。
大批量可提高吞吐,但可能需要调整学习率并影响泛化。梯度累积只减少单步显存,不减少总计算。BF16 相比 FP16 有更大指数范围,通常更稳定,但硬件支持和部分算子仍需检查。
8.8 监控与异常处理
先看它解决什么。 长训练必须把异常当预期事件处理。监控不仅有 loss,还要有学习率、梯度范数、吞吐、有效 token、显存、数据延迟和硬件错误。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 每步记录核心标量。
- 定期保存可恢复 checkpoint。
- 异常时保留最近数据批次。
- 恢复后验证状态连续。
一个可以手算的例子。 loss spike 若只出现在某个数据 shard,可能是异常长序列或坏文本;若所有 rank 同时出现则可能是学习率或数值问题。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只保存权重不保存优化器和 RNG、告警阈值固定不分阶段、遇 NaN 自动跳过且不留样本,会掩盖根因。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为训练循环加入非有限值检查和故障包,保存 step、样本 id、配置与张量统计。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
至少监控训练/验证 loss、学习率、梯度范数、参数范数、吞吐、MFU、数据等待、显存、通信和异常样本。loss 突增可能来自坏数据、数值溢出、学习率、恢复错误或硬件故障,不能只回滚而不定位。
Checkpoint 要包含模型、优化器、调度器、梯度缩放器、随机状态和数据位置。周期性做恢复演练,确认文件存在不等于可以恢复。大集群还需处理节点抢占、慢卡、网络错误和检查点写入瓶颈。
8.9 评估与涌现
抓住这一节的主线。 验证 loss 衡量平均预测难度,不能完整代表知识、推理、安全和生成质量。涌现现象还会受到指标阈值、提示和评测规模影响。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 使用无泄漏验证语料。
- 定期跑稳定 benchmark。
- 检查生成与记忆样例。
- 按能力、安全和语言分桶。
先做最小实验。 选择题准确率从随机附近跨过阈值可能看似突然,但底层连续概率已有平滑变化。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 评测集进入训练、只报最好 checkpoint、用不同 prompt 比模型、把 benchmark 提升直接等同真实业务提升,都会夸大结果。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:建立预训练评估表,分别包含 loss、常识、阅读、代码、记忆和安全,并写局限。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
验证困惑度反映预测分布,但不等同于指令遵循、事实性或推理。建立多层评估:语言建模损失、基础任务、领域任务、生成质量、安全和污染检查。所谓“涌现”有时受指标阈值与尺度影响,应报告连续曲线和不确定性,避免把单个跳变过度神秘化。
8.10 从 Nano 模型走向真实预训练
先把概念落到可观察对象上。 从 Nano 模型到真实预训练,变化的不只是规模,还包括分布式数据、容错、精度、通信、评估、治理与成本。应通过阶段门逐步放大,而不是一次跳到昂贵训练。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 单批次过拟合验证实现。
- 单机小语料验证稳定性。
- 多卡短跑验证通信与恢复。
- 小比例数据验证完整管线。
把它缩小到能逐项检查。 先用 0.1% 数据跑完加载—训练—保存—恢复—评估,能提前发现比模型结构更常见的工程错误。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 未经缩放实验直接投入全部算力、没有停止标准、训练中频繁更改数据配方、评估代码不版本化,都会浪费预算。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:为一次真实预训练写四级放大计划,每级规定通过指标和最大预算。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
微型项目应完成 tokenizer、数据 shard、模型、训练、验证、生成、checkpoint 和实验记录。目标不是复现大模型能力,而是验证全链路。扩到多卡前先确保单卡能过拟合一个小批次;若连小批次都学不会,增加 GPU 只会更快地产生错误。
练习:训练字符级 TinyLM 并观察上下文长度变化;实现断点续训并比较恢复前后下一个 batch;做三组模型/数据规模实验,绘制验证 loss 与计算量的关系。
延伸阅读:Training Compute-Optimal Large Language Models、Llama 3 技术报告、DeepSeek-V3 报告。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/08_tiny_lm.py`:可训练微型 Decoder-only LM。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 对同一短句分别写出 causal LM、masked LM 与 seq2seq 去噪的输入和标签。
- 为 1 万篇文档设计数据卡,记录来源、许可、语言、质量、去重簇和过滤原因。
- 训练两个不同词表大小的 tokenizer,比较中英代码语料的字符/token 比和未知字符处理。
- 用三个小模型和三种 token 预算记录验证损失,画 compute—loss 图并说明不确定性。
- 让模型在一小段文本上过拟合,确认 loss 接近零并能复述,再换验证文本观察泛化差距。
- 实现 fixed-length packer,验证每个原文 token 恰好出现一次且标签边界正确。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“预训练把大量 token 变成自监督样本”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“数据质量、合法性和可追踪性是预训练的第一层模型能力”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Tokenizer 决定模型看到的基本符号、序列长度和多语言效率”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Scaling Law 用小规模实验估计模型、数据和计算之间的趋势,帮助在固定预算下选择参数量与 token 数”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“微型自回归模型把完整链路缩小到单机可观察尺度:embedding、多个 Decoder Block、最终归一化和词表投影”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“打包的目标是减少 padding,让每个设备处理尽可能多的有效 token”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第九章 微调:从数据设计到 LoRA/QLoRA 工程
微调的核心问题不是“选哪个框架”,而是“希望模型的什么行为发生变化”。知识更新优先考虑 RAG;输出风格、任务格式、工具选择和领域表达可考虑 SFT;偏好与安全边界进入第十章。先定义评估,再决定是否微调。

图 9-1 冻结原权重 $W$,只训练低秩增量 $BA$;部署时可保留适配器或合并权重。
9.1 全量微调与参数高效微调
先建立直觉。 全量微调更新全部参数,表达能力强但显存与遗忘风险高;参数高效微调冻结大部分权重,只训练适配参数,更适合受限资源和多任务版本管理。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 明确想改变知识还是行为。
- 估算可训练参数与优化器状态。
- 选择目标模块。
- 与基础模型做同条件比较。
最小例子。 LoRA 把更新写成 BA,rank r 远小于输入输出维;基础 W 不变,训练和保存的参数明显减少。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 参数少不代表激活显存小、所有任务都适合相同 rank、忘记保存 tokenizer/chat template,都会影响结果。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:计算一个 4096×4096 线性层全量参数与 rank=16 LoRA 参数之比。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
全量微调更新所有参数,容量最大,但需要保存梯度、优化器状态和主权重,显存与存储成本高,也更容易破坏原能力。PEFT 只训练少量新增参数。
LoRA 假设任务适配所需的权重变化可近似为低秩矩阵:
W'=W+\Delta W,\qquad \Delta W=\frac{\alpha}{r}BA,
其中 $A\in\mathbb{R}^{r\times d_{in}}$、$B\in\mathbb{R}^{d_{out}\times r}$。通常初始化使初始 $\Delta W=0$,不改变基础模型输出。
PYTHON
import math
import torch
from torch import nn
class LoRALinear(nn.Module):
def __init__(self, base: nn.Linear, rank=8, alpha=16, dropout=0.0):
super().__init__()
self.base = base
for p in self.base.parameters():
p.requires_grad = False
self.rank = rank
self.scale = alpha / rank
self.dropout = nn.Dropout(dropout)
self.A = nn.Parameter(torch.empty(rank, base.in_features))
self.B = nn.Parameter(torch.zeros(base.out_features, rank))
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
def forward(self, x):
delta = (self.dropout(x) @ self.A.T) @ self.B.T
return self.base(x) + self.scale * delta
代码解读:B 零初始化让增量初始为零;rank 控制容量与参数量;alpha 控制缩放。目标模块不能机械照抄,注意力的 q/k/v/o 投影与 FFN 投影对任务影响不同,应结合模型结构和消融实验选择。
Adapter 在层间插入瓶颈模块;IA3 学习通道缩放;Prefix/Prompt/P-Tuning 在隐藏空间或嵌入层学习软提示。不同方法的参数少不等于训练显存一定最低,激活、序列长度和基础模型精度仍占主要部分。
9.2 QLoRA
从问题出发。 QLoRA 将冻结的基础权重量化存储,在其上训练较高精度 LoRA 参数。4-bit 权重节省存储,但计算通常会反量化到合适精度,并不等于原生 4-bit 训练。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 加载量化基础权重。
- 准备 k-bit 训练与 norm。
- 注入 LoRA。
- 用高精度优化器状态更新适配器。
用小数据走一遍。 NF4 针对近似正态分布权重设计量化格点;double quantization 进一步压缩量化尺度。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把量化 dtype、计算 dtype 和参数 dtype 混为一谈、量化后全量更新、CPU offload 带宽不足,都会导致错误或变慢。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:记录 QLoRA 运行时各类参数 dtype、可训练参数数和峰值显存。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
QLoRA 将冻结的基础模型以 4-bit 形式存储和计算,同时训练 LoRA 参数。常见 NF4 面向近似正态分布权重,双重量化进一步压缩量化常数。训练仍会在较高计算精度中进行部分运算,不能把“4-bit 加载”理解为全流程所有张量都是 4-bit。
量化会引入误差,显存收益也依赖 kernel、优化器、序列长度和 checkpointing。开始前用实际 batch 做显存剖析,不要只按参数数 × 0.5 字节估算。
9.3 指令数据格式

图 09-2 用户和系统 token 可作为上下文输入,但通常不作为要预测的训练标签。先看它解决什么。 指令数据不仅是三段文本,还包含角色、轮次、工具消息、系统规则与损失掩码。模板决定模型实际学习哪些 token。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 统一消息 schema。
- 用模型官方 chat template 编码。
- 只对目标 assistant 区域计算 loss。
- 检查截断后轮次完整性。
一个可以手算的例子。 多轮数据中用户 token 通常设为 -100,不参与交叉熵;若模板把 EOS 放错,模型可能学不会正常停止。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 手写模板与推理模板不一致、重复 BOS、所有消息都算 loss、长样本截断只剩答案尾部,都会造成训练异常。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:打印一条样本的 token、role 与 label 三列,人工核对每个特殊 token。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
一条样本通常含 system、user、assistant 多轮消息。必须使用目标模型的 chat template,并只对期望模型学习的 token 计算损失。若把用户问题也作为标签,模型可能学习复述输入。
PYTHON
sample = {
"messages": [
{"role": "system", "content": "你是设备维修助手,只根据手册回答。"},
{"role": "user", "content": "E17 报警是什么意思?"},
{"role": "assistant", "content": "E17 表示进水超时。请先检查进水阀和水压;断电后再操作。"},
],
"source": "manual_v3_p42",
"quality": "human_verified",
}
高质量数据应覆盖正常、边界、拒答、纠错和多轮场景;答案要事实正确、格式一致、难度分层。自动生成数据可扩规模,但需要去重、规则验证和人工抽检。划分训练/验证集时按文档、用户或任务模板分组,避免近重复泄漏。
9.4 使用 PEFT/TRL 的标准流程
抓住这一节的主线。 PEFT/TRL 等框架减少样板代码,但数据、模板、目标模块、精度、保存与评估仍需显式确认。标准流程先做几十步可解释 smoke test,再扩大训练。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 加载模型与 tokenizer。
- 格式化数据并检查标签。
- 配置 LoRA/Trainer。
- 训练、保存、合并并独立加载验证。
先做最小实验。 训练后只保存 adapter 时,部署必须同时指定正确基础模型版本;合并权重则要记录合并 dtype 与校验输出。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 直接复制旧版本参数、target_modules 漏掉实际层名、保存目录覆盖基础模型、训练完成未从磁盘重载测试,都会埋雷。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:写一个训练前检查脚本,拒绝无有效标签、模板不匹配和无可训练参数的配置。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
框架接口会变化,稳定流程是:加载 tokenizer 与模型;应用正确模板;构造只监督回答的标签;配置 LoRA;训练与保存 adapter;离线评估;必要时合并并验证。
PYTHON
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir="outputs/sft_adapter",
learning_rate=2e-4,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
num_train_epochs=2,
logging_steps=10,
save_strategy="steps",
save_steps=200,
bf16=True,
)
# model 与 train_dataset 省略:必须先确认 chat template 和标签掩码
trainer = SFTTrainer(model=model, train_dataset=train_dataset,
args=args, peft_config=peft_config)
trainer.train()
trainer.save_model()
学习率示例不是通用最优值。先做短跑检查 loss、梯度和生成样例,再扩完整训练。保存 adapter 后记录基础模型精确版本;没有基础模型就无法正确复原。
9.5 超参数与显存
先把概念落到可观察对象上。 显存由权重、梯度、优化器状态、激活、临时 buffer 与碎片共同构成。LoRA 主要减少前三项中的可训练部分,长序列仍可能让激活成为主因。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 先测单样本峰值。
- 调 batch 与梯度累积。
- 选择序列长度和 packing。
- 必要时梯度检查点与量化。
把它缩小到能逐项检查。 micro batch×累积步数×数据并行度决定有效 batch,但 token 长度分布还会改变每步实际 token。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只看平均长度、用累积步数解决单样本 OOM、盲目开 gradient checkpointing、学习率不随有效 batch 重新验证,都会失败。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:建立显存预算表并通过 profiler 校准误差,找出最大占用项。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
关键参数包括学习率、rank、alpha、目标模块、dropout、有效 batch、序列长度、epoch、warmup 和梯度裁剪。rank 越大容量越高但更易过拟合;数据少时多轮 epoch 可能造成灾难性遗忘和输出模式僵化。
显存主要由权重、梯度、优化器状态和激活组成。LoRA 大幅减少可训练参数相关内存,但激活仍随 batch × 序列长度 × 层数增长。梯度 checkpointing 用额外计算换激活显存;packing 提高有效 token 比例,但要正确处理边界。
9.6 评估:与基础模型做成对比较
先建立直觉。 微调评估要回答‘在哪些目标上变好、在哪些基础能力上变坏’。必须与未微调模型使用同一模板、解码和评估集成对比较。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 目标任务离线指标。
- 通用能力回归。
- 安全与拒答测试。
- 人工盲评和失败分类。
最小例子。 格式正确率提高但事实正确率下降,说明模型更会服从形式却不一定更可靠;两个指标必须同时报告。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 只展示精选样例、基础模型提示不同、评测数据与训练重复、没有置信区间,会产生虚假收益。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为 50 条目标样例和 30 条回归样例做 A/B 盲评,记录胜/平/负与原因。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
建立固定 prompt 集,比较基础模型与微调模型:任务正确率、格式遵循、事实性、拒答、安全、原通用能力和延迟。只看训练 loss 无法判断是否学会目标行为。
对生成任务可用规则、单元测试、人工盲评和模型评委组合。报告置信区间与失败样例。检查数据记忆:把训练答案中的独特字符串放入测试会产生虚假提升。
9.7 常见失败
从问题出发。 微调失败应从数据重复、模板、标签、截断、学习率、训练轮数、解码和部署版本逐层排查。复读和风格坍缩往往首先是数据与过拟合问题。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 抽样解码训练前数据。
- 统计有效标签与重复率。
- 观察 train/val 差距。
- 比较不同 checkpoint 生成。
用小数据走一遍。 train loss 持续下降而 validation loss 上升,同时输出复现训练短语,说明继续训练可能加剧过拟合。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 一出现复读就只调 repetition_penalty、没有验证 EOS、训练与推理模板不同、adapter 加载两次,都会误诊。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:建立微调故障表,为 NaN、OOM、无学习、复读和遗忘各写三个证据检查。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
- 输出重复:数据模板重复、epoch 过多、解码参数或 EOS 处理错误。
- 微调后变笨:学习率过高、数据窄、标签掩码错误、灾难性遗忘。
- 训练 loss 降但格式不对:chat template 或推理模板不一致。
- 合并后结果变化:dtype、缩放、目标模块或 tokenizer 版本不一致。
- 法律/医疗答案“更专业”却不可靠:语言风格提升不代表事实正确,必须领域评估与人工复核。
9.8 一份可信的微调工程手册
先看它解决什么。 可信工程手册应让另一个人能从原始数据复现模型:数据卡、配置、环境、日志、checkpoint、评估和模型卡必须互相指向。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 数据与代码版本化。
- 实验命名包含关键变量。
- 每次训练自动生成报告。
- 发布前独立重载与安全检查。
一个可以手算的例子。 模型卡写清基础模型、adapter、许可、目标任务、已知限制和不适用场景,不能只写一个下载链接。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 配置只存在命令历史、日志无样本版本、失败实验被删除、指标无评测脚本,会让结果不可审计。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为一次 LoRA 实验生成最小可复现包,并让全新环境按 README 重跑评估。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
项目应记录数据 Schema、过滤与抽检、模板、基础模型、量化配置、LoRA 参数、训练曲线、评估集、对比结果、失败样例、模型卡和回滚方式。不要把框架命令当作项目亮点;真正的亮点是数据与评估设计、资源约束下的取舍以及可复现结果。
练习:把一个线性层替换为 LoRALinear,验证初始输出一致;比较 rank 4/16/64 的参数量与验证指标;构造含拒答样本的数据集并检查微调前后越界回答率。
延伸阅读:LoRA 论文、Hugging Face PEFT 文档、TRL 与 PEFT 集成、B 站:LoRA 原理与实战。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/09_lora.py`:手写 LoRA Linear。
- `examples/09_sft_label_mask.py`:多轮 SFT 标签掩码。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 计算一个 4096×4096 线性层全量参数与 rank=16 LoRA 参数之比。
- 记录 QLoRA 运行时各类参数 dtype、可训练参数数和峰值显存。
- 打印一条样本的 token、role 与 label 三列,人工核对每个特殊 token。
- 写一个训练前检查脚本,拒绝无有效标签、模板不匹配和无可训练参数的配置。
- 建立显存预算表并通过 profiler 校准误差,找出最大占用项。
- 为 50 条目标样例和 30 条回归样例做 A/B 盲评,记录胜/平/负与原因。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“全量微调更新全部参数,表达能力强但显存与遗忘风险高”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“QLoRA 将冻结的基础权重量化存储,在其上训练较高精度 LoRA 参数”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“指令数据不仅是三段文本,还包含角色、轮次、工具消息、系统规则与损失掩码”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“PEFT/TRL 等框架减少样板代码,但数据、模板、目标模块、精度、保存与评估仍需显式确认”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“显存由权重、梯度、优化器状态、激活、临时 buffer 与碎片共同构成”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“微调评估要回答‘在哪些目标上变好、在哪些基础能力上变坏’”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第十章 偏好对齐:Reward Model、PPO、DPO 与 GRPO
预训练学习“什么文本可能出现”,SFT 学习“怎样按示范回答”,偏好对齐进一步学习“多个可行回答中,哪一个更符合目标”。目标可能包括有用、诚实、安全、风格或可验证正确性。偏好数据只代表特定标注规则与人群,不等于抽象的“人类价值”。

图 10-1 PPO 使用显式奖励与在线策略更新;DPO 直接学习成对偏好;GRPO 对同一问题的一组采样计算相对优势。
10.1 RLHF 基本流程
先建立直觉。 RLHF 典型链路包括 SFT 策略、偏好数据、奖励模型和受约束策略优化。每一步都可能引入偏差,因此对齐不是把一个分数最大化,而是管理多目标取舍。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- SFT 建立可用初始策略。
- 收集同 prompt 回答偏好。
- 训练或定义奖励。
- 在 KL 等约束下优化并评估。
最小例子。 标注者更喜欢礼貌长回答,奖励模型可能把长度当捷径;需要平衡数据和反事实测试识别。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 偏好等同事实正确、奖励分数跨版本可比、只看平均 reward、不检查能力回归,都会造成 reward hacking。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:为客服回答写偏好标注指南,区分正确、帮助性、语气、安全和引用。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
经典 RLHF 可分为:SFT 初始策略;对同一 prompt 采样多个回答并收集偏好;训练 Reward Model;用 PPO 等算法优化策略,同时用 KL 约束避免偏离参考策略过远。InstructGPT 工作系统展示了这条路线。
奖励模型常用 Bradley-Terry 偏好概率:
$$P(y_w\succ y_l\mid x)=\sigma(r_\phi(x,y_w)-r_\phi(x,y_l)).$$
对应损失是负对数似然。若标注者偏好长答案,奖励模型也可能学会长度捷径;需要平衡数据、检查分组偏差和对抗样例。
10.2 PPO:受约束地更新策略
从问题出发。 PPO 在当前策略采样轨迹,估计优势,再用裁剪目标限制一次更新不要偏离太远。价值模型帮助降低回报估计方差,KL 约束限制相对参考策略漂移。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- rollout 采样回答。
- 奖励与价值估计优势。
- 多轮小批量更新策略/价值。
- 监控 KL、clip fraction 与 reward。
用小数据走一遍。 概率比 r=πnew/πold 超出 [1-ε,1+ε] 时,裁剪目标阻止单个样本推动过大更新。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 旧策略 logprob 与新 tokenizer 不一致、padding token 计入奖励、价值损失失控、只追 reward 不看 KL,都会导致不稳。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:用四个样本手算 ratio、clipped ratio 和 surrogate objective。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
PPO 使用旧策略采样,根据奖励与价值估计计算优势,再优化裁剪目标:
L^{clip}=\mathbb{E}\left[\min(r_tA_t,\operatorname{clip}(r_t,1-\epsilon,1+\epsilon)A_t)\right],
其中 $r_t=\pi_\theta(a_t|s_t)/\pi_{old}(a_t|s_t)$。裁剪限制单次更新幅度;完整训练还含价值损失、熵、KL 惩罚等。语言模型中“状态”是前缀,“动作”是 token。
PPO 工程复杂:要同时管理策略、参考模型、奖励模型、价值模型和 rollout;奖励尺度、KL、生成长度、优势估计都影响稳定性。Reward hacking 表示策略找到高奖励但不符合真实目标的捷径,因此必须人工抽检与独立评估。
10.3 DPO:从偏好对直接优化策略
先看它解决什么。 DPO 直接从 chosen/rejected 对学习相对偏好:策略相对参考模型提高 chosen 的对数概率差,同时降低 rejected。它省去显式奖励模型和在线 rollout,但仍依赖偏好数据质量。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 同一 prompt 构造偏好对。
- 计算策略与参考 logprob。
- 形成相对 log-ratio 差。
- 用 logistic loss 优化。
一个可以手算的例子。 若 chosen 和 rejected 只差一个事实,DPO 会学习该差异;若还同时差长度与语气,模型可能学习混杂特征。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 prompt 边界不同、参考模型与初始化不匹配、chosen/rejected 长度偏差严重、把序列总 logprob 未做一致处理,都会影响结果。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:实现最小 DPO loss,对调 chosen/rejected 后验证梯度方向改变。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
DPO 利用 KL 正则化奖励优化的闭式关系,把奖励差写成策略与参考策略的对数概率差。常见损失为
-\log\sigma\left(\beta[(\log\pi_\theta(y_w|x)-\log\pi_{ref}(y_w|x)) -(\log\pi_\theta(y_l|x)-\log\pi_{ref}(y_l|x))]\right).
PYTHON
import torch
import torch.nn.functional as F
def dpo_loss(policy_chosen, policy_rejected,
ref_chosen, ref_rejected, beta=0.1):
"""输入均为每条完整回答的 token log-prob 之和,形状 (B,)。"""
policy_margin = policy_chosen - policy_rejected
ref_margin = ref_chosen - ref_rejected
logits = beta * (policy_margin - ref_margin)
losses = -F.logsigmoid(logits)
chosen_reward = beta * (policy_chosen - ref_chosen).detach()
rejected_reward = beta * (policy_rejected - ref_rejected).detach()
return losses.mean(), (chosen_reward - rejected_reward).mean()
代码解读:必须对回答 token 求和并正确屏蔽 prompt/padding;policy 与 reference 使用同一 tokenizer 和模板;第二个返回值是隐式奖励 margin 的监控量,不是独立真实奖励。长度差可能影响序列 log-prob,应在数据和评估中控制。
DPO 省去显式奖励模型和在线 RL rollout,流程更简单,但依赖离线偏好数据覆盖。它不会自动纠正数据之外的新策略分布,也可能过拟合偏好噪声。
10.4 GRPO:组内相对优势
抓住这一节的主线。 GRPO 对同一 prompt 采样一组回答,用组内奖励均值与标准差形成相对优势,从而不依赖独立价值模型。它适合有可验证奖励的多样采样任务。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 每个 prompt 采样 G 条回答。
- 计算每条奖励。
- 组内标准化得到优势。
- 结合概率比与 KL 更新。
先做最小实验。 若一组奖励完全相同,标准差接近零,归一化优势缺少有效信号;实现需加 epsilon 并监控奖励多样性。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 组大小太小、奖励几乎常数、同质采样、规则验证器可被格式投机,都会削弱训练。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:模拟两组奖励,手算相对优势并讨论离群高奖励的影响。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
GRPO 为同一 prompt 采样一组回答,计算每个回答的奖励,再用组内均值和标准差标准化,形成相对优势:
$$A_i=\frac{r_i-\operatorname{mean}(r)}{\operatorname{std}(r)+\epsilon}.$$
随后以类似策略比率裁剪和 KL 正则的目标更新策略。它可以避免单独训练与策略同规模的价值模型,特别适合数学、代码等有可验证奖励的推理任务。
PYTHON
def group_relative_advantage(rewards: torch.Tensor, eps=1e-6):
# rewards: (batch_of_prompts, samples_per_prompt)
mean = rewards.mean(dim=1, keepdim=True)
std = rewards.std(dim=1, keepdim=True, unbiased=False)
return (rewards - mean) / (std + eps)
若一组回答奖励完全相同,优势接近零,无法提供有效学习信号。组大小、采样多样性和奖励判定决定信号质量。GRPO 与 DPO 的数据和目标不同,不能简单称为“DPO 的泛化形式”。
10.5 奖励设计
先把概念落到可观察对象上。 奖励函数把产品目标转成训练信号,必须防止模型利用未预期漏洞。可验证正确性、格式、效率和安全往往需要多项奖励与硬约束组合。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 列出真正目标与代理指标。
- 设计正常和对抗样例。
- 检查可被投机的捷径。
- 对各奖励尺度校准并消融。
把它缩小到能逐项检查。 代码任务只按单元测试通过奖励可能鼓励硬编码;增加隐藏测试、静态检查和资源限制可降低投机。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 复杂加权分数无解释、规则奖励泄漏答案、用同一 judge 训练和评测、惩罚项尺度压倒主目标,都会扭曲行为。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:为数学解题设计奖励卡,列出目标、实现、攻击方式和监控指标。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
可验证任务可以使用单元测试、数学答案、格式 Schema、编译结果等规则奖励。开放问答常需奖励模型或模型评委,但评委也会偏向特定风格并可能被答案欺骗。组合奖励时要防止某一项尺度压倒其他项。
过程奖励评价中间步骤,结果奖励只看最终结果。过程奖励能提供密集信号,却需要可靠步骤标注;错误的过程监督可能限制模型发现新策略。任何奖励都要配独立 holdout 和人工审计。
10.6 数据与训练管线
先建立直觉。 偏好数据需要 prompt 分布、多样回答、清晰标注标准与一致性检查。在线算法还要管理 rollout 版本、奖励版本和策略版本的对应关系。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 分层采样真实任务。
- 生成多样候选而非模板变体。
- 双人标注与仲裁。
- 保存所有模型与配置版本。
最小例子。 若所有 rejected 都明显更短,模型可能只学长度;可通过长度匹配或反向样本减少混杂。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 同一回答出现在训练和评测、标注者看到模型名、低一致性样本直接混入、数据无许可证,都会降低可信度。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:对 100 对偏好数据统计长度差、主题分布、标注一致率和重复率。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
偏好数据格式包含 prompt、chosen、rejected 和元数据。确保差异来自质量而非模板、长度或敏感词等捷径。训练/测试按 prompt 语义去重;标注指南明确正确性、有用性、安全、引用等优先级;对分歧样本保留多标注者信息。
PPO/GRPO 需要高吞吐 rollout,通常把生成引擎与训练进程协调;策略版本、采样参数和奖励版本必须记录。离线 DPO 更容易复现,但同样要固定参考模型。
10.7 评估对齐而不损伤能力
从问题出发。 对齐评估必须同时测目标行为、基础能力、过度拒答、欺骗与分布外稳健性。一个更安全的模型若无害问题大量拒答,也不是成功。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 帮助性与正确性。
- 危险请求与越狱。
- 无害边界请求的过拒。
- 通用能力和格式回归。
用小数据走一遍。 安全拒答测试要包含真正危险、明显无害和语义相近的边界样本,才能区分拒答能力与关键词触发。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 只用单一 judge、不给 judge 参考证据、对齐集与评测集重叠、只报胜率不报失败类型,都会遗漏损伤。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:建立对齐评估矩阵,至少包含正确拒绝、错误拒绝、错误服从和正常帮助四格。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
同时评估偏好胜率、任务正确率、安全、拒答、校准、格式、长度和原有通用能力。只优化单一胜率可能得到冗长讨好回答;安全训练过强可能过度拒答。红队测试应包含提示注入、角色扮演、编码变体和工具副作用。
在线 A/B 需要用户保护、停止阈值和日志审计。高风险领域不能用点击率替代事实正确性。
10.8 选择方法

图 10-2 先识别离线偏好、在线采样和奖励形态,再比较 DPO、PPO 与 GRPO。先看它解决什么。 方法选择取决于数据、在线采样能力、奖励可验证性、稳定性与预算。DPO 适合高质量离线偏好,PPO/GRPO 适合需要探索或可验证奖励的任务。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 先用 SFT 建立基线。
- 判断是否有偏好对或标量奖励。
- 评估 rollout 和训练复杂度。
- 设置能力与安全门禁。
一个可以手算的例子。 只有几千对人工偏好且算力有限时先试 DPO;数学验证器可靠且希望探索新策略时可评估 GRPO。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 追逐算法名称、没有可靠评估就上 RL、把不同实现超参直接照搬,都会浪费资源。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为三个场景写决策表,说明为什么不选择另外两种方法。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 有高质量成对偏好、资源有限:先做 DPO 基线。
- 有可靠奖励、需要在线探索和精细 KL 控制:考虑 PPO。
- 同题可采多答案且奖励可验证、希望省去价值模型:考虑 GRPO。
- 目标主要是格式和任务示范:可能 SFT 已足够。
练习:手算一对 chosen/rejected 的 DPO loss;构造组奖励验证 GRPO 优势和为零;设计一个会被长度欺骗的奖励,并提出诊断方法。
延伸阅读:InstructGPT、DPO、DeepSeek-R1、TRL 文档。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/10_dpo.py`:DPO 损失与组内优势。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 为客服回答写偏好标注指南,区分正确、帮助性、语气、安全和引用。
- 用四个样本手算 ratio、clipped ratio 和 surrogate objective。
- 实现最小 DPO loss,对调 chosen/rejected 后验证梯度方向改变。
- 模拟两组奖励,手算相对优势并讨论离群高奖励的影响。
- 为数学解题设计奖励卡,列出目标、实现、攻击方式和监控指标。
- 对 100 对偏好数据统计长度差、主题分布、标注一致率和重复率。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“RLHF 典型链路包括 SFT 策略、偏好数据、奖励模型和受约束策略优化”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“PPO 在当前策略采样轨迹,估计优势,再用裁剪目标限制一次更新不要偏离太远”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“DPO 直接从 chosen/rejected 对学习相对偏好:策略相对参考模型提高 chosen 的对数概率差,同时降低 rejected”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“GRPO 对同一 prompt 采样一组回答,用组内奖励均值与标准差形成相对优势,从而不依赖独立价值模型”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“奖励函数把产品目标转成训练信号,必须防止模型利用未预期漏洞”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“偏好数据需要 prompt 分布、多样回答、清晰标注标准与一致性检查”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第十一章 多模态大模型:视觉编码、跨模态对齐与训练
多模态模型要解决三个问题:不同模态如何表示,如何对齐到可交互空间,如何让语言模型利用视觉信息生成答案。图片不是“另一种文本”;分辨率、空间位置、OCR、颜色和对象关系都会影响视觉 token。

图 11-1 视觉编码器产生视觉特征,连接模块将其映射到语言模型可使用的表示。
11.1 ViT:把图像变成 token 序列

图 11-2 图像尺寸和 patch 大小共同决定视觉序列长度与计算成本。先建立直觉。 ViT 把图像切成固定大小 patch,将每块展平并线性投影为视觉 token,再像文本 token 一样使用 Transformer。分辨率和 patch 大小共同决定序列长度。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 图像规范化与缩放。
- patchify 得到 N 个小块。
- 线性投影并加位置。
- Transformer 编码视觉关系。
最小例子。 224×224 图像、patch=16 时有 14×14=196 个 patch;分辨率翻倍会让 patch 数约变为四倍。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 高分辨率只改输入尺寸却不处理位置编码、不同宽高强行拉伸、通道顺序和归一化不匹配,都会损伤效果。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:用 reshape/permute 手写 patchify,再重组图像验证无信息丢失。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
给定 $H\times W\times C$ 图像,以 $P\times P$ 切块,patch 数为 $N=HW/P^2$。每个 patch 展平后线性投影到隐藏维,加位置编码,再送入 Transformer Encoder。Patch 越小,空间细节越多,但视觉 token 数和注意力成本快速增加。
PYTHON
import torch
from torch import nn
class PatchEmbedding(nn.Module):
def __init__(self, image_size=224, patch_size=16, in_ch=3, dim=768):
super().__init__()
if image_size % patch_size:
raise ValueError("image_size 必须能被 patch_size 整除")
self.proj = nn.Conv2d(in_ch, dim, kernel_size=patch_size,
stride=patch_size)
self.n_patches = (image_size // patch_size) ** 2
def forward(self, images):
x = self.proj(images) # (B,D,H/P,W/P)
return x.flatten(2).transpose(1, 2) # (B,N,D)
卷积核大小和步长都等于 patch 大小,等价于对每个不重叠 patch 做共享线性投影。真实模型还会处理任意分辨率、动态切块或多尺度特征。
11.2 CLIP:图文对比学习
从问题出发。 CLIP 用图像编码器和文本编码器把配对样本映射到同一空间,通过批内对比学习拉近匹配对、推远不匹配对。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 分别编码并归一化。
- 计算批内相似度矩阵。
- 图到文与文到图双向交叉熵。
- 学习温度调节分布。
用小数据走一遍。 batch 为 N 时得到 N×N logits,对角线是正样本,其他位置作为批内负样本。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 批中重复语义被当假负样本、只训练单向损失、忘记归一化、文本模板差异,都会影响检索。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:实现 CLIP loss,验证交换图文顺序后双向损失含义。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
CLIP 分别编码一批图片与文本,把向量归一化,计算批内相似度矩阵。正确图文对位于对角线,损失同时做 image-to-text 与 text-to-image 分类。
PYTHON
import torch.nn.functional as F
def clip_loss(image_features, text_features, logit_scale):
image = F.normalize(image_features, dim=-1)
text = F.normalize(text_features, dim=-1)
logits = logit_scale.exp() * image @ text.T
labels = torch.arange(len(image), device=image.device)
return (F.cross_entropy(logits, labels) +
F.cross_entropy(logits.T, labels)) / 2
批内其他样本充当负例,因此 batch 组成很重要;重复/近义 caption 会产生“假负例”。CLIP 学到共享表征,适合检索和零样本分类,但本身不是能长篇回答的生成式视觉语言模型。
11.3 BLIP/BLIP-2 与 Q-Former
先看它解决什么。 BLIP 统一理解与生成目标,BLIP-2 用 Q-Former 在冻结视觉编码器与冻结语言模型之间提取少量查询表示,降低端到端训练成本。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 视觉编码器产生图像特征。
- 可学习 query 通过 Q-Former 读取。
- 投影到语言模型输入空间。
- 用图文目标训练连接。
一个可以手算的例子。 Q-Former 的 query 数固定,可把大量视觉 patch 压缩成较少表示,但压缩也可能丢失细粒度信息。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 把 Q-Former 说成简单线性层、冻结模块仍处于错误训练模式、视觉与语言 tokenizer 混淆,都会导致实现偏差。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:画出 BLIP-2 三个模块的梯度流,标注冻结与训练参数。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
BLIP 同时面向理解与生成,并通过 captioner/filter 改善噪声图文数据。BLIP-2 冻结已有视觉编码器和大语言模型,用轻量 Q-Former 跨越模态差距。可学习 query token 从视觉特征抽取与语言相关的信息,再连接到 LLM。
冻结大模块降低训练成本,但连接模块必须适配两端的表示与尺度。理解 Q-Former 的关键是“少量查询向量主动读取视觉特征”,而不是把所有视觉 patch 原样塞入语言模型。
11.4 LLaVA 两阶段训练
抓住这一节的主线。 LLaVA 典型路线先训练视觉—语言投影层,再用图文指令数据联合微调,让语言模型学会在对话中使用视觉 token。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 预训练投影对齐特征。
- 构造多轮图文指令。
- 按阶段解冻部分模块。
- 用多能力评测和人工检查。
先做最小实验。 图像占位 token 在模板中的位置必须与视觉特征插入位置一致;标签通常只覆盖 assistant 文本。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 指令数据答案由模型生成却不清洗、图像与文本错配、阶段二学习率过大破坏视觉特征,都会造成幻觉。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为一条图文多轮样本画出最终 token 序列和 label mask。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
经典 LLaVA 结构使用 CLIP ViT 作为视觉编码器,通过投影层连接语言模型。第一阶段主要训练投影层,使图像特征与语言嵌入对齐;第二阶段用视觉指令数据做指令微调,使模型学会问答和对话。
两阶段的目的不同:对齐阶段解决“视觉特征如何进入语言空间”,指令阶段解决“如何按任务使用视觉信息”。若只做第二阶段且初始化不当,训练更难稳定;若只做对齐,模型未必会遵循复杂视觉指令。
现代 VLM 可能使用任意分辨率、视觉 token 压缩、窗口注意力、原生多模态预训练和视频/文档增强,不能把所有模型都等同于 LLaVA。
11.5 数据构建
先把概念落到可观察对象上。 多模态数据至少包含媒体、消息、任务类型、来源与许可。还要考虑 OCR、版面、语言、重复、隐私和可能造成偏见的分布。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 验证文件可读与配对。
- 按内容与感知哈希去重。
- 清洗 OCR/标注。
- 按任务和难度分层切分。
把它缩小到能逐项检查。 同一图片配多个近似问题若跨训练/测试切分,会形成视觉泄漏;应按图像簇而非单条样本切分。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只检查文本重复、EXIF 泄漏隐私、低质量自动 caption 当真值、版权不明图片入库,都会带来风险。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:设计多模态数据卡,含图像来源、许可、分辨率、OCR 质量、语言和任务标签。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
多模态样本至少包含媒体、消息、任务类型和来源。图片与文本必须真正对应;OCR 文档要保留阅读顺序、页面和区域;VQA 要防止仅凭问题语言猜答案。敏感图像、人物隐私、版权和未成年人数据需要专门治理。
下游数据可覆盖描述、OCR、图表、定位、比较、多图和拒答。随机切分可能让同一视频帧或同一文档页面跨集合,导致泄漏,应按原始媒体分组。
11.6 微调 Qwen-VL 类模型的通用步骤
先建立直觉。 微调视觉语言模型要以官方 processor 和 chat template 为准,先确认模型怎样插入视觉 token,再决定冻结、LoRA 目标和像素预算。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- processor 同时处理媒体与文本。
- 检查输入 id、pixel values 与 grid。
- 只在目标文本上算 loss。
- 按视觉长度分桶并控制 OOM。
最小例子。 一张高分辨率图可能产生远多于普通图的视觉 token,batch 按样本数相同也会有巨大显存差异。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 仅给语言层加 LoRA 却期望视觉对齐改变、视觉占位符重复、训练推理预处理不同、padding mask 错误,都会失败。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:对三个分辨率样本打印视觉 token 数、总长度和峰值显存,制定 batch 规则。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
- 按官方 processor 和 chat template 编码图像与消息。
- 决定冻结视觉编码器、连接层和 LLM 的哪些部分。
- 对 assistant token 计算损失,屏蔽系统、用户和视觉占位 token。
- 控制像素/视觉 token 上限,按长度分桶,防止极端样本 OOM。
- 使用 LoRA 时明确目标模块是否覆盖视觉、投影或语言部分。
- 分任务评估,不只看综合平均分。
多模态显存除了模型参数,还受像素分辨率和视觉 token 影响。同一 batch 中一张超大图可能显著抬高峰值。先记录每样本视觉 token,再决定动态 batch。
11.7 评估
从问题出发。 多模态评估不能只用一个综合分数。要分别测感知、OCR、图表、空间、知识、推理、grounding、拒答和幻觉,并核对评测协议。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 为每类能力准备独立样本。
- 统一提示和解码。
- 自动指标与人工审阅结合。
- 记录图像许可和错误类型。
用小数据走一遍。 VQA exact match 对同义表达敏感,OCR 字符错误率不反映回答解释;不同任务需要不同指标。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 测试图像训练泄漏、只看英文、裁剪导致答案丢失、用无视觉 baseline 仍能答对而未识别,都会高估视觉能力。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:加入遮蔽图像或打乱图文配对的对照,判断模型是否真正使用视觉。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
自动指标包括分类准确率、VQA exact match、OCR 字符错误率、caption CIDEr 等,但开放回答还需事实性和人评。特别测试:不存在对象、遮挡、小字、旋转、颜色、空间关系、多图指代和诱导问题。
视觉幻觉指模型描述图中不存在内容。缓解手段包括更好的视觉对齐数据、区域/grounding 监督、拒答样本和基于检测/OCR 的外部验证,但无法彻底消除。
11.8 常见错误
先看它解决什么。 多模态常见错误来自三种错位:媒体与文本错位、视觉 token 与位置错位、训练与推理模板错位。定位时先检查输入而不是先调优化器。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 可视化实际送入的图像。
- 解码文本 token 与占位符。
- 核对标签 mask。
- 比较单样本前向与生成。
一个可以手算的例子。 如果图像预处理后上下颠倒或颜色通道错误,模型再大也无法稳定回答;把 tensor 还原成图像是最便宜的检查。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只检查 loss、在数据加载器中静默跳过坏图、把所有错误归因于‘多模态幻觉’,都会延迟定位。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:建立五类故障注入:错图、空图、过大图、占位符缺失和 OCR 噪声,并记录系统行为。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
- 图像归一化或色彩通道不匹配预训练配置。
- 视觉占位 token 数与视觉特征数不一致。
- 标签 mask 错误,模型被训练预测用户问题或图像占位。
- 只用语言可猜的数据,模型表面指标高却忽略图像。
- 训练图像过于同质,真实照片/文档分布下失效。
练习:实现 PatchEmbedding 并验证 token 数;用随机特征计算 CLIP loss,观察正确配对相似度提高时损失变化;构造一个无需看图即可回答的 VQA 数据集,说明其评估缺陷。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/11_vit_patchify.py`:ViT patchify 与逆变换。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 用 reshape/permute 手写 patchify,再重组图像验证无信息丢失。
- 实现 CLIP loss,验证交换图文顺序后双向损失含义。
- 画出 BLIP-2 三个模块的梯度流,标注冻结与训练参数。
- 为一条图文多轮样本画出最终 token 序列和 label mask。
- 设计多模态数据卡,含图像来源、许可、分辨率、OCR 质量、语言和任务标签。
- 对三个分辨率样本打印视觉 token 数、总长度和峰值显存,制定 batch 规则。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“ViT 把图像切成固定大小 patch,将每块展平并线性投影为视觉 token,再像文本 token 一样使用 Transformer”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“CLIP 用图像编码器和文本编码器把配对样本映射到同一空间,通过批内对比学习拉近匹配对、推远不匹配对”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“BLIP 统一理解与生成目标,BLIP-2 用 Q-Former 在冻结视觉编码器与冻结语言模型之间提取少量查询表示,降低端到端训练成本”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“LLaVA 典型路线先训练视觉—语言投影层,再用图文指令数据联合微调,让语言模型学会在对话中使用视觉 token”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“多模态数据至少包含媒体、消息、任务类型、来源与许可”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“微调视觉语言模型要以官方 processor 和 chat template 为准,先确认模型怎样插入视觉 token,再决定冻结、LoRA 目标和像素预算”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第十二章 分布式训练:并行策略、通信与故障恢复
分布式训练的本质是把参数、数据、激活、梯度和优化器状态分配到多个设备,并让通信与计算协调。目标不是“用更多卡”,而是在内存上可容纳、数值上等价、吞吐上有效、故障后可恢复。

图 12-1 数据并行复制模型;张量/流水线/专家并行切分模型计算;ZeRO/FSDP 切分训练状态。
12.1 DP 与 DDP
先建立直觉。 DataParallel 在单进程聚合,DDP 为每个进程维护模型副本并用 All-Reduce 同步梯度。DDP 通常性能和隔离更好,但每张卡仍保存完整权重与优化器。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 每个 rank 初始化进程组。
- DistributedSampler 切分数据。
- 前向反向触发梯度 bucket 通信。
- 所有 rank 一致更新参数。
最小例子。 4 卡 DDP 若每卡 micro batch=8、累积 2 步,有效 batch 通常为 64,但最后不齐批次和 sampler 设置会影响样本数。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 每个 rank 读取相同样本、只在 rank0 backward、随机种子完全相同导致增强重复、保存冲突,都会出错。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:运行两卡最小 DDP,打印每个 rank 样本 id 并验证无重复覆盖。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
数据并行让每张卡持有模型副本,处理不同微批次,反向后同步梯度。单进程 DataParallel 有主卡瓶颈,实践优先一进程一卡的 DistributedDataParallel(DDP)。DDP 通过 All-Reduce 聚合梯度,所有 rank 以相同梯度和优化器状态更新,因此参数保持一致。
PYTHON
# torchrun --standalone --nproc_per_node=4 train_ddp.py
import os
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler, TensorDataset
def main():
dist.init_process_group("nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
x = torch.randn(4096, 32)
y = torch.randn(4096, 1)
dataset = TensorDataset(x, y)
sampler = DistributedSampler(dataset, shuffle=True)
loader = DataLoader(dataset, batch_size=64, sampler=sampler, pin_memory=True)
model = DDP(torch.nn.Linear(32, 1).cuda(local_rank), device_ids=[local_rank])
opt = torch.optim.AdamW(model.parameters(), lr=1e-3)
for epoch in range(3):
sampler.set_epoch(epoch)
for xb, yb in loader:
xb, yb = xb.cuda(local_rank, non_blocking=True), yb.cuda(local_rank, non_blocking=True)
opt.zero_grad(set_to_none=True)
loss = torch.nn.functional.mse_loss(model(xb), yb)
loss.backward()
opt.step()
if dist.get_rank() == 0:
torch.save(model.module.state_dict(), "model.pt")
dist.destroy_process_group()
if __name__ == "__main__":
main()
DistributedSampler 防止所有 rank 读取相同样本;每轮 set_epoch 改变一致的 shuffle;只有 rank 0 写普通 checkpoint。真实代码还需全局指标 All-Reduce、异常协调和恢复逻辑。
12.2 ZeRO 与 FSDP
从问题出发。 ZeRO/FSDP 将优化器状态、梯度乃至参数分片到各 rank,降低单卡冗余。节省显存的代价是更多集合通信、参数聚合和更复杂 checkpoint。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- Stage1 分优化器状态。
- Stage2 再分梯度。
- Stage3/FSDP 再分参数。
- 计算前按需聚合、计算后释放或重分片。
用小数据走一遍。 参数分片并不意味着前向永远只看本地参数;计算某层前往往要 All-Gather 得到完整层。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 把 ZeRO stage 当单纯开关、wrap 粒度太细、CPU offload 受 PCIe 限制、保存 full state 时 OOM,都会影响训练。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:为一个模型估算 DDP 与 ZeRO-1/2/3 的理论状态显存,并说明遗漏项。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Adam 混合精度训练中,参数、梯度和优化器状态占大量显存。ZeRO 分阶段切分:Stage 1 切优化器状态;Stage 2 再切梯度;Stage 3 再切参数,并在前向/反向需要时 All-Gather。FSDP 同样围绕参数分片、按需聚合和梯度 Reduce-Scatter。
Stage 越高显存越省,但通信和实现复杂度增加。Offload 把状态移到 CPU/NVMe,可进一步省显存,却可能受 PCIe、CPU 和磁盘瓶颈限制。
JSON
{
"bf16": {"enabled": true},
"gradient_accumulation_steps": 8,
"train_micro_batch_size_per_gpu": 1,
"zero_optimization": {
"stage": 3,
"overlap_comm": true,
"contiguous_gradients": true,
"reduce_bucket_size": 50000000,
"stage3_prefetch_bucket_size": 50000000
}
}
配置中的 batch 三元关系必须一致。桶大小影响通信聚合与峰值内存,不应盲目复制;用 profiler 测量。
12.3 模型并行

图 12-2 数据、张量、层和专家是四种不同切分维度,通信模式也随之不同。先看它解决什么。 Tensor Parallel 切单层矩阵,Pipeline Parallel 切层,Sequence/Context Parallel 切序列,Expert Parallel 切专家。选择取决于模型哪一维无法放入设备和通信拓扑。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 识别最大单层与总状态。
- 选择切分轴。
- 安排集合通信与流水线。
- 再叠加数据并行形成多维网格。
一个可以手算的例子。 列并行线性层把输出列分到多卡,后续操作若需要完整输出就 All-Gather;行并行常需要 Reduce-Scatter 或 All-Reduce。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只乘并行度不考虑整除、跨节点放高频通信、流水线 microbatch 太少导致 bubble,都会降低效率。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为 8 卡单机和 2×8 卡集群分别设计并行网格并解释通信。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
张量并行(TP)把单个矩阵乘按列或行切到多卡,需要在层内频繁集合通信;适合高速互连。流水线并行(PP)把层分阶段,微批次流水执行;会有 pipeline bubble,需调度与微批数量平衡。序列并行(SP)在序列维切分部分计算,长上下文下有价值。专家并行(EP)把 MoE 专家分布到设备,token 通过 All-to-All 路由。
混合并行通常组合 DP×TP×PP×EP。映射要尊重硬件拓扑:把通信最频繁的 TP 放在节点内高速连接,把数据并行扩到节点间。并行维度乘积应等于总设备数。
12.4 集合通信
抓住这一节的主线。 集合通信是并行算法的语言:All-Reduce 汇总并复制结果,Reduce-Scatter 汇总后分片,All-Gather 收集分片,All-to-All 重新分发不同数据。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 明确每个 rank 初始张量。
- 写出通信后每个 rank 所有内容。
- 计算传输量与次数。
- 匹配 DDP、FSDP、TP 或 EP。
先做最小实验。 DDP 梯度 bucket 常用 All-Reduce;FSDP 梯度可用 Reduce-Scatter;MoE token 路由常用 All-to-All。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 把带宽和延迟混为一谈、通信与计算无法重叠、张量大小不均、某 rank 先退出造成死锁,都会损害稳定性。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:用四个 rank 的小向量手工演示四种集合通信结果。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
常见原语:All-Reduce 汇总并复制结果;Reduce-Scatter 汇总后分片;All-Gather 收集所有分片;All-to-All 交换不同目标的数据。通信时间近似由延迟和带宽共同决定,小消息受延迟支配,大消息受带宽支配。
DDP 用梯度 bucket 尝试让反向计算与 All-Reduce 重叠。若存在未使用参数、控制流不一致或不同 rank 执行不同步,可能死锁。所有 rank 必须以一致顺序进入集合通信。
12.5 混合精度
先把概念落到可观察对象上。 FP16 范围小,常需 loss scaling;BF16 指数范围接近 FP32,通常更稳但尾数精度低。混合精度把敏感运算保留更高精度,并使用适配 kernel。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 选择 autocast dtype。
- loss scaling 后反传。
- 更新前反缩放与裁剪。
- 检查非有限梯度并调整 scale。
把它缩小到能逐项检查。 Softmax、归一化统计和优化器状态常需更高精度;具体由框架与 kernel 实现决定。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 仅把模型 .half、裁剪缩放前梯度、硬件不支持 BF16、不同 rank 出现 Inf 却继续同步,都会导致错误。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:比较 FP32、FP16+scaler、BF16 的损失曲线、显存和吞吐。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
FP16 指数范围小,通常需 loss scaling;BF16 指数范围接近 FP32,训练更稳但尾数精度较低。部分归一化、Softmax、损失和统计仍常在 FP32 计算。混合精度是否更快取决于硬件与 kernel。
梯度累积时,若用 DDP,可在非最后一个微步使用 no_sync() 避免每次同步。梯度裁剪应在反缩放之后、优化器更新之前执行。
12.6 Checkpoint 与断点续训
先建立直觉。 真正可续训的 checkpoint 包含模型、优化器、scheduler、scaler、随机状态、数据位置和并行拓扑信息。分片 checkpoint 还要能适配 world size 或有转换流程。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 在一致 step 建立保存屏障。
- 写临时目录并校验完整。
- 原子发布成功标记。
- 恢复后做短程连续性验证。
最小例子。 只恢复权重会丢失 Adam 动量与学习率进度,loss 可能突变;数据迭代位置丢失则重复或跳过样本。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 所有 rank 写同一文件、成功标记先于分片完成、恢复后重置 seed、从不演练损坏分片,都会让备份失效。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:训练 20 步在第 10 步保存,比较不中断与恢复路径第 11—20 步的指标。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
分片训练的 checkpoint 可能包含每 rank 的分片、元数据和优化器状态。恢复时世界大小变化并非所有格式都支持。保存内容包括模型、优化器、调度器、随机数、数据位置、全局步和 scaler。
采用临时目录写完后原子发布,保留完成标记和校验和;定期在独立作业中恢复并跑若干步。只测试“能加载模型权重”不足以证明能继续训练。
12.7 Accelerate 与 DeepSpeed
从问题出发。 Accelerate 提供较薄的设备与分布式抽象,DeepSpeed 提供 ZeRO、offload 与训练引擎。工具减少配置工作,但不能替代对 batch、状态分片和通信的理解。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 先在单卡验证数据和 loss。
- 用配置文件声明精度与并行。
- 检查启动后的实际 world size。
- 保存并恢复完整状态。
用小数据走一遍。 同一 YAML 在库版本变化后默认行为可能改变,运行日志应打印最终解析配置,而不只保存输入文件。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 同时让多个框架接管梯度累积、配置键拼错被忽略、只在 rank0 初始化不一致对象,都会产生隐蔽 bug。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:用 Accelerate 将单卡脚本改为多卡,并逐项说明代码变化与未变化部分。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
Accelerate 统一设备放置、混合精度和多进程启动,适合把单卡脚本平滑扩展。DeepSpeed 提供 ZeRO、流水线、优化器与推理能力。框架简化入口,但不会替你决定正确并行策略,也不会自动修复数据或通信瓶颈。
调试顺序:单卡小数据过拟合;单机两卡确认数值;扩大卡数并比较全局 batch;再加 ZeRO/混合精度;最后做多机与故障恢复。一次引入所有优化会让问题难以定位。
12.8 性能诊断
先看它解决什么。 性能诊断从时间线入手:数据等待、前向、反向、通信、优化器和 checkpoint 各占多少。GPU 利用率低只是现象,不直接告诉根因。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 用 profiler 捕获稳定窗口。
- 查看 kernel 空洞与通信重叠。
- 统计数据加载和 CPU。
- 一次只改变一个瓶颈。
一个可以手算的例子。 所有 rank 在 All-Reduce 前等待同一个慢 rank,可能来自样本长度不均或硬件降频,而不是 NCCL 本身。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只看 nvidia-smi 瞬时利用率、profile 包含预热、用更大 batch 掩盖数据错误、跨节点未绑定网卡,都会误诊。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:对一次训练 step 做时间分解,提出证据支持的三项优化并复测。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
记录每步时间、数据时间、前向/反向、优化器、通信、MFU、网络带宽和显存。卡利用率低可能是数据慢、通信慢、微批过小、CPU 同步或频繁 checkpoint。某 rank 变慢会拖住所有同步 rank,需排查慢卡和数据倾斜。
12.9 练习与资料
抓住这一节的主线。 练习分布式不能只追求跑通,还要验证数值等价、样本覆盖、故障恢复和性能缩放。规模扩大前先在小集群注入错误。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 单卡与多卡 loss 对齐。
- 检查全局 batch 样本。
- kill 一个进程观察行为。
- 恢复后比较状态。
先做最小实验。 固定有效 batch 与随机性后,DDP 结果应与单卡在容差内接近;完全逐位相同通常不现实。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 多卡更快就认为正确、没有 barrier 超时、异常进程未清理、恢复只看能启动,都会遗漏。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:完成一份 DDP/FSDP 验收清单,包含正确性、吞吐、显存和恢复。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
练习:用 1/2/4 卡 DDP 保持全局 batch 不变并比较 loss;估算 ZeRO-1/2/3 各自切分哪些状态;画出 8 卡上 DP=2、TP=2、PP=2 的 rank 分组。
延伸阅读:PyTorch DDP、PyTorch FSDP、DeepSpeed ZeRO 教程。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/12_ddp_minimal.py`:torchrun 启动的最小 DDP。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 运行两卡最小 DDP,打印每个 rank 样本 id 并验证无重复覆盖。
- 为一个模型估算 DDP 与 ZeRO-1/2/3 的理论状态显存,并说明遗漏项。
- 为 8 卡单机和 2×8 卡集群分别设计并行网格并解释通信。
- 用四个 rank 的小向量手工演示四种集合通信结果。
- 比较 FP32、FP16+scaler、BF16 的损失曲线、显存和吞吐。
- 训练 20 步在第 10 步保存,比较不中断与恢复路径第 11—20 步的指标。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“DataParallel 在单进程聚合,DDP 为每个进程维护模型副本并用 All-Reduce 同步梯度”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“ZeRO/FSDP 将优化器状态、梯度乃至参数分片到各 rank,降低单卡冗余”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“Tensor Parallel 切单层矩阵,Pipeline Parallel 切层,Sequence/Context Parallel 切序列,Expert Parallel 切专家”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“集合通信是并行算法的语言:All-Reduce 汇总并复制结果,Reduce-Scatter 汇总后分片,All-Gather 收集分片,All-to-All 重新分发不同数据”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“FP16 范围小,常需 loss scaling”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“真正可续训的 checkpoint 包含模型、优化器、scheduler、scaler、随机状态、数据位置和并行拓扑信息”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第十三章 推理优化:量化、蒸馏、KV Cache 与高效服务
推理优化要围绕目标场景:离线吞吐、在线首 token、持续解码、长上下文、边缘设备各有不同瓶颈。先测 profile,再选技术。量化减少权重带宽,KV Cache 优化减少重复计算,FlashAttention 减少内存访问,连续批处理提高设备利用率,彼此解决的问题不同。

图 13-1 从模型表示、算子、缓存到调度的分层优化;每层都需质量与性能回归。
13.1 Prefill 与 Decode
先建立直觉。 Prefill 对整段输入并行计算并建立 KV Cache,通常计算密集;Decode 每步只处理新 token,却反复读取大量权重和缓存,通常更受内存带宽与调度影响。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- prefill 产生首 token 与缓存。
- decode 读取历史缓存。
- 逐步采样并追加缓存。
- 遇 EOS/长度/停止串结束。
最小例子。 长提示短回答主要压力在 prefill 和 TTFT;短提示长回答主要压力在 decode、TPOT 和缓存占用。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 用单一 tokens/s 描述两阶段、把输入 token 和输出 token 同价、忽略并发下缓存竞争,都会误判。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:对四种输入/输出长度组合记录 TTFT、TPOT 与总时间,画二维热图。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
Prefill 对整段输入并行计算,矩阵乘规模大,通常更偏计算密集;Decode 每步只生成一个 token,却要读取大量权重和 KV Cache,常偏内存带宽与调度。TTFT 主要受排队、tokenization 与 prefill 影响;TPOT 主要受 decode 影响。
优化前按输入/输出长度分桶测 TTFT、TPOT、吞吐、显存和质量。平均请求会掩盖长上下文的极端成本。
13.2 量化基础
从问题出发。 量化用较少比特表示权重或激活。核心是选择尺度、零点、粒度和校准数据,让舍入与截断误差可控。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 统计目标张量范围。
- 按 tensor/channel/group 求尺度。
- 映射到整数并裁剪。
- 反量化或用低比特 kernel 计算。
用小数据走一遍。 对称 int8 可令 q=round(x/scale),scale=max|x|/127;离群值会放大 scale,使多数小值分辨率下降。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 文件变小不等于推理变快、校准数据与真实分布不同、敏感层全部量化、只看平均精度,都会带来问题。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:手写对称 int8 量化,比较不同 group size 的重构误差。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
线性量化把浮点 $x$ 映射到整数 $q$:
$$q=\operatorname{clip}(\operatorname{round}(x/s)+z,q_{min},q_{max}),\qquad \hat{x}=s(q-z).$$
$s$ 是尺度,$z$ 是零点。对称量化通常 $z=0$;非对称量化更适合偏移分布。Per-tensor 使用一个尺度,简单但受离群值影响;Per-channel 或 group-wise 粒度更细,精度更好但元数据与 kernel 更复杂。
PYTHON
import torch
def symmetric_quantize(x: torch.Tensor, bits=8):
qmax = 2 ** (bits - 1) - 1
scale = x.abs().amax().clamp_min(1e-8) / qmax
q = torch.round(x / scale).clamp(-qmax, qmax).to(torch.int8)
return q, scale
def dequantize(q, scale):
return q.float() * scale
这是教学版 per-tensor 权重量化,不会带来真实加速,因为计算前又反量化为 FP32。性能收益需要硬件支持的低比特 kernel、打包格式和融合算子。
13.3 BNB、GPTQ、AWQ 与 GGUF
先看它解决什么。 bitsandbytes 常用于易用的运行时量化,GPTQ/AWQ 属于权重量化路线,GGUF 是 llama.cpp 生态常用文件格式与量化集合。算法、格式和执行后端不可混为一谈。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 确认目标硬件与 kernel。
- 选择权重/激活位宽。
- 准备校准数据。
- 比较质量、速度、内存和兼容。
一个可以手算的例子。 同一个 4-bit 文件在没有优化 kernel 的硬件上可能只省内存不加速,甚至因反量化开销更慢。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 只按模型大小选择方法、把 QLoRA 训练格式直接当部署格式、不同量化命名跨工具等价,都会踩坑。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为 GPU 服务与 CPU 边缘部署分别写量化选择表。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
BitsAndBytes 提供 8-bit/4-bit 线性层与 QLoRA 支持,适合快速实验。GPTQ 使用校准数据做训练后权重量化,逐步减少重构误差;AWQ 关注激活显著通道,保护重要权重。两者最终速度取决于后端 kernel 和硬件。
GGUF 是 llama.cpp 生态常用的模型文件格式,支持多种量化类型,适合 CPU/边缘推理。格式兼容不代表不同量化方案质量相同。选择时以目标硬件实测 tokens/s、首 token、内存和任务质量为准。
不要把 QLoRA 与部署量化混为一谈:QLoRA 的目标是低显存微调;部署时可选择不同的合并与量化流程。
13.4 知识蒸馏
抓住这一节的主线。 知识蒸馏让学生模型学习教师的软分布、中间特征或生成数据。温度提高后,非最大类别概率携带教师对相似性的判断。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 教师产生 logits 或答案。
- 学生同时学习硬标签与软目标。
- 调温度和损失权重。
- 在目标与回归集评估。
先做最小实验。 KL 蒸馏常乘 T² 补偿温度造成的梯度尺度变化;具体实现要核对 reduction 与 mask。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 教师错误被批量放大、合成数据缺少多样性、tokenizer 不同直接逐 token 对齐、只看学生模仿率,都会限制效果。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:在小分类任务比较无蒸馏与 logits 蒸馏,并分析错误样本。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
蒸馏用教师模型指导学生模型。分类中可组合硬标签交叉熵与温度软化后的 KL:
$$L=\lambda L_{CE}+(1-\lambda)T^2\,KL(p_T^{teacher}\|p_T^{student}).$$
语言模型还可蒸馏 logits、隐藏状态、注意力或教师生成数据。若只能调用教师 API,常采用数据蒸馏:生成高质量指令/推理样本,过滤后对学生 SFT。学生会继承教师偏差,生成数据还可能缺少真实分布多样性。
PYTHON
import torch.nn.functional as F
def distill_loss(student_logits, teacher_logits, labels, temperature=2.0,
alpha=0.5):
hard = F.cross_entropy(student_logits, labels)
soft_target = F.softmax(teacher_logits / temperature, dim=-1)
soft_logprob = F.log_softmax(student_logits / temperature, dim=-1)
soft = F.kl_div(soft_logprob, soft_target, reduction="batchmean")
return alpha * hard + (1 - alpha) * (temperature ** 2) * soft
13.5 剪枝
先把概念落到可观察对象上。 剪枝删除对输出影响较小的权重、通道、头或层。只有硬件和 kernel 支持相应稀疏结构时,参数减少才可能转化为真实加速。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 选择重要性指标。
- 按非结构化或结构化模式剪枝。
- 校准或再训练恢复。
- 在目标后端实测。
把它缩小到能逐项检查。 2:4 稀疏要求每四个权重中保留两个,规则性比任意零值更容易被专用硬件利用。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只报告稀疏率、剪掉后仍用稠密 kernel、注意力头重要性在任务间变化、无恢复训练,都会夸大收益。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:对小 MLP 做幅值剪枝与整列剪枝,比较参数、精度和真实耗时。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
非结构化剪枝把单个权重置零,只有稀疏 kernel 支持时才加速;结构化剪枝删除头、通道、FFN 单元或层,更容易获得真实速度,但影响能力更大。剪枝后通常需要恢复训练。报告参数稀疏率之外,还要报告真实硬件延迟。
13.6 FlashAttention
先建立直觉。 FlashAttention 通过分块与在线 Softmax 减少 HBM 读写,不显式保存完整注意力矩阵;它计算的是精确注意力,主要改变执行次序和内存复杂度。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- Q/K/V 分块载入片上存储。
- 局部计算分数。
- 维护在线最大值与归一化和。
- 累积输出再写回。
最小例子。 T² 分数矩阵不落到 HBM,可显著降低长序列激活内存;算术量级仍大体是二次。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 认为 FlashAttention 是稀疏近似、任何形状都同样加速、mask/dtype/kernel 不支持仍强开,都会出错。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:在支持环境比较标准 SDPA 与 Flash 后端的峰值显存和时间,并验证输出误差。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
标准注意力会把大 $T\times T$ 中间矩阵频繁写入/读取 HBM。FlashAttention 用分块和在线 Softmax,在片上 SRAM 中计算块,减少 HBM I/O,同时保持精确注意力结果。它主要改善训练和 prefill 的注意力内存访问,并不消除 $O(T^2)$ 的算术复杂度。
是否启用取决于 GPU、dtype、头维、mask 和软件版本。数值差异可能来自运算顺序,需设置合理容差测试。
13.7 KV Cache 与 PagedAttention
从问题出发。 KV Cache 保存每层历史 token 的 K/V,PagedAttention 把逻辑连续序列映射到固定大小物理块,减少预留和碎片并支持动态调度。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 新请求分配缓存块。
- prefill 写入多 token K/V。
- decode 追加新位置。
- 请求结束释放或复用块。
用小数据走一遍。 不同请求长度差异大时,预先分配最大连续空间浪费严重;分页可按增长需要追加块。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 缓存键未含模型/adapter、prefix cache 跨租户泄漏、块太小元数据多、释放不完整,都会产生问题。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:模拟可变长度请求的连续分配与分页分配,比较浪费率。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
自回归解码时,历史 token 的 K/V 不变,缓存后新一步只计算新 token 的 Q/K/V。缓存随层数、并发、上下文与 K/V 头数增长。GQA/MQA、KV 量化、滑动窗口和前缀缓存都可降低或复用成本。
PagedAttention 把缓存划分为块并维护逻辑到物理映射,类似虚拟内存思想,减少碎片并支持灵活共享。Prefix caching 对重复系统提示或长公共前缀有效;缓存键要包含模型、token 序列和相关配置。
13.8 连续批处理与投机解码
先看它解决什么。 连续批处理动态插入和移除请求;投机解码让小模型提议多个 token,再由大模型一次验证,接受连续正确前缀。二者分别优化调度和解码串行性。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 调度器按 token 预算选请求。
- draft 模型生成候选。
- target 模型并行验证。
- 按接受规则提交并继续。
一个可以手算的例子。 draft 越快且与 target 越一致,投机收益越大;频繁拒绝会浪费验证和 draft 计算。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 draft tokenizer 不一致、采样接受规则实现错误、只报理想接受率、不算额外显存,都会高估。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:用确定性 toy 分布实现投机解码,验证输出分布与直接采样一致。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
连续批处理按调度步动态组合不同请求,减少等待最慢序列的浪费。调度器需要在吞吐、公平、TTFT 和长请求饥饿之间取舍。
投机解码由小 draft 模型提出多个 token,大模型一次验证,接受一致前缀;输出分布可保持目标模型一致,但收益依赖接受率、draft 成本和硬件。若两个模型 tokenizer 或分布差异大,收益有限。
13.9 压测与回归
抓住这一节的主线。 优化必须用代表性负载做回归,质量、延迟、吞吐、显存、能耗和错误率一起报告。先 profile 再改,避免把瓶颈从 GPU 转移到排队或网络。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 冻结模型与数据版本。
- 建立未优化基线。
- 一次引入一项优化。
- 做质量和性能双门禁。
先做最小实验。 量化后 P50 更快但 P99 因某些长请求 kernel 回退变慢,不能只报平均吞吐。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 压测客户端不足、缓存命中率不真实、输出长度未固定、质量评测太小,都会给出错误结论。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:设计一张优化实验表,至少记录模型、后端、dtype、长度分布、并发、硬件和置信区间。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
建立性能矩阵:模型精度/量化格式 × 输入长度 × 输出长度 × 并发。记录质量基准、显存、TTFT、TPOT、吞吐、功耗和错误。预热后多轮测试,并报告软件、driver、硬件与采样参数。
量化质量不能只测困惑度;代码、数学、工具调用和长上下文对误差敏感程度不同。上线采用灰度和可回滚模型版本。
13.10 选择指南

图 13-2 先判断瓶颈来自 prefill、decode、KV Cache 还是模型存储,再选择技术。先把概念落到可观察对象上。 选择优化路线先识别约束:放不下、算不动、带宽不足、并发不够还是长尾过高。每种技术只对特定瓶颈有效。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- profile 定位瓶颈。
- 选择最小可逆改动。
- 在目标硬件验证 kernel。
- 保留质量回退和快速回滚。
把它缩小到能逐项检查。 显存不足可先考虑权重量化和 KV 管理;decode 带宽不足可考虑量化/并行;长上下文激活则关注 FlashAttention。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 同时上量化、蒸馏、剪枝后无法归因、追逐理论压缩率、忽略运维兼容,都会增加风险。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:为 7B 单卡、70B 多卡和 CPU 边缘三个场景给出分步优化计划。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
- 显存不足:先评估权重量化、GQA 模型、KV 量化和上下文上限。
- TTFT 高:检查队列、输入长度、prefill batching 和 tokenizer。
- TPOT 高:检查量化 kernel、张量并行通信、KV 带宽和 batch。
- CPU/边缘:测试 GGUF/llama.cpp 的多种量化与线程设置。
- 模型太大且质量可牺牲:考虑蒸馏或结构化剪枝。
练习:对随机矩阵比较 8/4-bit 量化误差;按公式估算不同 GQA 配置的 KV Cache;设计可重复的并发压测并画出吞吐—P99 曲线。
延伸阅读:FlashAttention、vLLM 文档、BitsAndBytes 官方说明、Transformers 量化选择。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/13_int8_quantization.py`:对称 int8 量化。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 对四种输入/输出长度组合记录 TTFT、TPOT 与总时间,画二维热图。
- 手写对称 int8 量化,比较不同 group size 的重构误差。
- 为 GPU 服务与 CPU 边缘部署分别写量化选择表。
- 在小分类任务比较无蒸馏与 logits 蒸馏,并分析错误样本。
- 对小 MLP 做幅值剪枝与整列剪枝,比较参数、精度和真实耗时。
- 在支持环境比较标准 SDPA 与 Flash 后端的峰值显存和时间,并验证输出误差。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“Prefill 对整段输入并行计算并建立 KV Cache,通常计算密集”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“量化用较少比特表示权重或激活”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“bitsandbytes 常用于易用的运行时量化,GPTQ/AWQ 属于权重量化路线,GGUF 是 llama.cpp 生态常用文件格式与量化集合”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“知识蒸馏让学生模型学习教师的软分布、中间特征或生成数据”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“剪枝删除对输出影响较小的权重、通道、头或层”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“FlashAttention 通过分块与在线 Softmax 减少 HBM 读写,不显式保存完整注意力矩阵”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
第十四章 面试专项:从简历证据到项目深挖
大模型岗位面试通常同时验证四件事:基础原理是否扎实,代码与算法是否能落地,项目是否真正做过,面对未知问题是否能建立可验证的分析。近期公开面经反复出现 RAG 切块/召回/重排、Agent 工具与记忆、微调数据与 LoRA、服务性能、模型结构和项目 bad case。准备时不要背孤立答案,而要建立“定义—机制—取舍—指标—故障”的表达框架。

图 14-1 项目表达建立在基础、代码和评估之上;只背术语无法承受追问。
14.1 先读岗位,而不是先刷题
先建立直觉。 岗位描述是能力假设,不是关键词清单。先判断岗位偏模型、训练、应用、推理还是平台,再把要求映射到可证明的项目证据。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 标出必须项和加分项。
- 按职责归类技术。
- 找到自己证据与缺口。
- 决定准备优先级。
最小例子。 岗位强调 RAG 评估与上线,就应准备数据、指标、故障和监控,而不是只背 Transformer 公式。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 一份简历投所有岗位、只按出现频次背题、把不会的工具写熟练,都会在追问中暴露。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:选择三个真实 JD 做能力矩阵,写出共同核心与岗位差异。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
将 JD 拆成模型算法、应用算法、平台工程、多模态或研究方向。模型算法偏训练、对齐和分布式;应用算法偏 RAG、Agent、评估与服务;平台工程偏推理引擎、调度、监控和成本。为每项要求准备一条证据:课程不算证据,代码、实验、指标、设计文档和线上结果才算。
建立技能矩阵:能解释、能手写、能调试、能设计、能量化。不会的内容如实标注学习中,不要把“调用过 API”写成“精通模型训练”。
14.2 简历:每个数字都能被复现
从问题出发。 简历的每个数字都是一个实验结论,应能回答定义、基线、数据、硬件、时间窗口、个人贡献和不确定性。项目 bullet 用问题—约束—动作—取舍—指标表达。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 给出业务和技术基线。
- 说明你亲自做的决策。
- 写指标及评估条件。
- 准备失败与局限。
用小数据走一遍。 Recall@5 从 0.69 到 0.84 必须说明相关块标注、查询数、索引版本和是否存在数据泄漏。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 数字无出处、把团队成果写成个人、只列技术栈、没有失败样例,会降低可信度。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:逐条给简历数字建立证据卡;无法复现的数字删除或改成定性描述。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
项目 bullet 推荐结构:业务问题 + 约束 + 个人动作 + 技术取舍 + 指标 + 评估条件。不要写“负责 RAG 系统,准确率提高 30%”而不说明准确率定义。
一段可追问的写法:
面向 8 万页设备手册的售后问答,负责解析、标题感知切块和检索评估。构建 360 条人工标注查询,以 BM25 为基线,引入 Dense+BM25 RRF 与 Cross-Encoder 重排,将 Recall@5 从 0.69 提升至 0.84、引用准确率从 76% 提升至 90%;通过异步召回和重排批处理将 P95 从 2.8 秒降至 1.7 秒。
准备数字的出处:评估集怎样抽样、谁标注、基线版本、置信区间、硬件和时间窗口。没有真实线上数据就明确写“离线评估”,不要暗示线上收益。
14.3 三分钟项目介绍
先看它解决什么。 三分钟介绍要先让听者理解问题和约束,再讲方案与个人决策,最后用指标和失败证明真实做过。组件清单不是项目故事。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 30 秒背景与目标。
- 30 秒数据和约束。
- 60 秒核心方案与取舍。
- 40 秒结果。
- 20 秒失败和下一步。
一个可以手算的例子。 讲混合检索时说明为何纯向量漏掉型号、怎样标注评估集、RRF 带来何种可复现提升。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 铺垫两分钟、只念架构图、不说个人贡献、结果只有‘效果很好’,都会失去重点。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:录制三次项目介绍,分别删减到 180 秒并让不了解项目的人复述。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
建议按以下顺序:
- 背景:用户是谁,痛点是什么,为什么需要大模型。
- 约束:数据、延迟、成本、隐私、更新频率。
- 方案:数据流和关键组件,为什么这样选。
- 本人贡献:亲自实现、设计和决策的部分。
- 结果:基线、指标、实验条件。
- 失败与迭代:最难 bad case、如何定位、还剩什么问题。
面试官最关心第 4—6 项。架构图能帮助表达,但不要花两分钟念组件名。
14.4 高频基础题:回答骨架

图 14-2 一个可追问的答案应包含定义、机制、取舍、证据和边界。抓住这一节的主线。 高频基础题要用定义—机制—公式或形状—取舍—验证五层回答。先给结论,再逐层展开,遇到不确定边界要明确。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 一句话定义术语。
- 画数据流或写关键公式。
- 说明为什么这样设计。
- 比较替代方案。
- 给实验或故障例子。
先做最小实验。 回答 GQA 时先说 KV 头少于 Q 头,再写缓存公式,最后讨论质量、内存和 kernel 支持。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 只背标准句、公式无变量、绝对化说‘更好’、混淆论文与具体模型实现,会被连续追问击穿。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:为每类问题写 90 秒答案,并让同伴连续追问三个‘为什么’。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
Transformer
为什么缩放点积要除以 $\sqrt{d_k}$? 假设 q/k 各维独立、均值 0、方差 1,点积方差随 $d_k$ 增长;缩放让 logits 尺度稳定,避免 Softmax 过饱和、梯度过小。进一步说明真实分布未必独立,但初始化尺度分析仍有指导意义。
Pre-Norm 与 Post-Norm? 说明归一化位置、残差路径与深层训练稳定性;不要只说“Pre-Norm 更好”,还可讨论最终归一化和模型具体实现。
MHA/GQA/MQA? 从 Q 头与 KV 头数量、KV Cache、质量和 kernel 支持比较,并写出缓存公式。
RoPE 为什么能表达相对位置? 旋转后的 q/k 点积只依赖角度差;说明它作用于 q/k,不作用于 v,并讨论长上下文外推不是改配置。
训练与微调
LoRA 为什么有效? 任务适配的权重更新常呈低内在秩;冻结 W,学习 BA。继续说明 rank/alpha、目标模块、初始化、合并与不足。
SFT 后复读? 从数据重复和模板单一、epoch/学习率、EOS、解码、标签 mask、灾难性遗忘逐层排查,用生成样例和 n-gram 重复率验证。
RAG 与微调如何选? 动态事实/引用优先 RAG,行为/格式优先 SFT;可组合。比较更新成本、可追溯、延迟和评估。
RAG
Chunk 多大? 没有固定答案。依据文档结构、Embedding 最大长度、问题粒度、召回与上下文预算,通过标注集搜索。说明 overlap 和父子块。
检索不准怎么排查? 先确认解析与标注,再看查询改写、Embedding、索引 Recall、过滤、top-k、混合检索、重排;不要直接换大模型。
怎样评估? 检索 Recall@K/MRR/nDCG,生成正确性/忠实度/引用,外加延迟成本;错误归因到组件。
Agent
记忆与上下文工程关系? 记忆是可持久状态来源,上下文工程决定每轮把哪些状态、工具、历史和证据放入有限窗口。说明写入审核、检索和遗忘。
Agent 为什么失败? 工具契约、状态、停止、权限、注入、重试与评估。给一个自己项目中的 trace 例子。
多 Agent 何时有价值? 子任务可分、并行收益高、输出可验证时;否则通信和故障成本大于收益。
对齐与推理
DPO 与 PPO? 从数据在线/离线、是否显式 RM/价值模型、实现复杂度、分布外探索和稳定性比较,并能写 DPO loss。
GRPO 的优势怎样算? 同一 prompt 多个回答的奖励做组内标准化;不需要独立价值模型。说明奖励同质和可验证奖励的限制。
FlashAttention 为什么快? IO-aware 分块减少 HBM 访问,是精确注意力;算术复杂度仍为二次。
量化精度为何下降? 尺度和舍入产生误差,离群值、粒度、校准数据、敏感层影响不同。区分真实 kernel 加速与仅存储压缩。
14.5 手撕代码题
先把概念落到可观察对象上。 手撕代码考察的是问题澄清、接口、正确性、复杂度和测试。大模型岗位常要求数值稳定与张量形状,不能只写 happy path。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 先确认输入输出与边界。
- 写最小正确实现。
- 解释时间空间复杂度。
- 主动构造极值和错误输入。
把它缩小到能逐项检查。 稳定 Softmax 先减最大值;注意力要说明 axis、mask、dtype 和形状;LRU 要说明容量为零。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 沉默写代码、用库函数绕过核心、无测试、忽略 NaN/空输入/dtype,都会丢分。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:限时手写 Softmax、RMSNorm、Attention、RoPE、RRF 和 LRU,并为每题写三个测试。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
优先练习:Softmax 数值稳定版、LayerNorm/RMSNorm、Self-Attention 与 mask、RoPE、GQA repeat_kv、Top-k/Top-p、交叉熵、LoRA Linear、DPO loss、余弦检索、RRF、LRU cache、生产者消费者、梯度累积。
写代码时先说输入输出与边界,再实现主逻辑,最后给测试。下面是稳定 Softmax:
PYTHON
import numpy as np
def softmax(x, axis=-1):
x = np.asarray(x, dtype=np.float64)
shifted = x - np.max(x, axis=axis, keepdims=True)
exp = np.exp(shifted)
return exp / np.sum(exp, axis=axis, keepdims=True)
def test_softmax():
y = softmax([[1000.0, 1001.0]])
assert np.all(np.isfinite(y))
assert np.allclose(y.sum(axis=-1), 1.0)
面试中边写边解释形状、复杂度和数值稳定性。完成后主动测极值、空输入、维度和 dtype。
14.6 算法题准备
先建立直觉。 算法准备按模式建立迁移能力:哈希、双指针、滑窗、栈、二分、树、堆、图和动态规划。目标是从约束识别状态与不变量。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 复述题目并举例。
- 先给朴素解。
- 找重复计算或单调性。
- 证明优化并测试。
最小例子。 滑动窗口只有在窗口扩张/收缩能维护目标性质时成立;包含负数的和问题未必保持单调。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 背模板不验证前提、复杂度说错、边界全交给面试官提醒、刷题后不重写,都会影响表现。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:建立错题本,每题记录识别信号、核心不变量、最小反例和一周后重写结果。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
大模型岗仍会考通用算法。按模式练习数组/哈希、双指针、滑窗、栈队列、二分、链表、树、堆、图、动态规划。目标不是刷数量,而是看到题能识别状态、证明正确、分析复杂度并写测试。
建立错题本:错误原因、正确模式、最小反例、重写日期。模拟面试限制 30—40 分钟,先澄清再编码;不要沉默十分钟后突然给答案。
14.7 系统设计题
从问题出发。 系统设计先问需求与 SLO,再估算流量、token、存储和模型资源,之后才画组件。答案要包含数据、接口、容量、安全、故障和成本。 先把名词放到一边,沿着输入、状态变化和输出走一遍;只有能指出证据落在哪一步,概念才算真正掌握。
沿数据流逐步检查:
- 澄清用户、规模和一致性。
- 估算峰值 QPS/token。
- 画主链路与异步链路。
- 设计降级、监控和回滚。
用小数据走一遍。 设计 70B 服务要讨论并行、prefill/decode、KV Cache、批处理和流量分布,而不是只说用 vLLM。 先在纸面预测结果,再让程序打印中间状态;预测与运行不一致的地方,正是需要继续追查的知识缺口。
这里最容易出现的误解。 一上来画微服务、没有数量级估算、忽略租户权限和数据删除、只谈正常路径,都会显得空泛。 出现异常时先冻结样本、配置和环境,从最靠近输入的环节开始验证;不要同时更换模型、数据和超参数。
小实验:完成企业 RAG、Agent 平台和模型服务三道设计,每道写容量估算。 提交物应包含预期、运行证据和失败记录;只有别人能按同样步骤复现,结果才具有学习价值。
典型题:“设计企业知识助手”“设计多租户 Agent 平台”“如何服务 70B 模型”。回答顺序:需求与 SLO、流量与数据、接口、架构、容量、安全、评估、故障与成本。
企业 RAG 重点谈权限、版本、引用、更新和评估;Agent 平台重点谈工具沙箱、凭据、确认、trace 和预算;模型服务重点谈 TTFT/TPOT、continuous batching、KV Cache、并行、量化和降级。
14.8 Bad case 深挖
先看它解决什么。 Bad case 深挖要讲首次异常信号、假设、证据、实验、修复和回归。面试官关注你怎样排除错误假设,而不只是最终换了什么组件。 理解的标准不是会复述定义,而是能画出数据流、预测中间结果,并设计一个让错误暴露出来的检查。
可以把实现分成以下环节:
- 冻结失败样本和版本。
- 分层构造假设。
- 一次验证一个变量。
- 修复后加入回归集。
一个可以手算的例子。 扫描 PDF 召回下降时,通过分文档类型发现 OCR 和标题丢失,修解析后恢复,而不是直接换 embedding。 这里故意不用大模型或大数据,因为小输入能把每个轴、分数和状态变化完整暴露出来。
排错时先看这些地方。 归因于‘模型不行’、没有失败数据、同时改多个组件、只报成功不报代价,都会不可信。 修复不能止于‘这次跑通’,还要把失败样本变成自动测试,防止同类问题在下一版本重新出现。
现在动手:为每个项目准备三个真实失败故事和一个仍未解决的问题。 把关键断言写进测试,并在 README 说明怎样运行、怎样判断正确、当前实现还不支持什么。
使用一张表准备每个项目的三个失败:现象、影响范围、假设、证据、实验、修复、回归。面试官可能连续问“为什么”,直到触及你的真实工作边界。
例如召回下降:先按文档类型分桶发现扫描 PDF 最差;检查 OCR 字符错误率与标题丢失;更换版面解析并保留页级标题;Recall@5 恢复;新增扫描件回归集。这个故事比“换了更好的 Embedding”更可信。
14.9 行为与协作
抓住这一节的主线。 行为题用具体情境说明决策与协作:目标冲突、信息不完整、时间压力和责任边界。STAR 只是结构,证据和反思才有价值。 遇到新术语,先找它在系统中的位置:它读取什么、保存什么、改变什么,以及失败时会留下什么信号。
真正动手时按这个顺序走:
- 交代任务与约束。
- 说明自己的判断和沟通。
- 给出结果与证据。
- 说明之后怎样改流程。
先做最小实验。 技术分歧中可讲如何定义共同指标、做小实验、记录决策,而不是说‘我说服了别人’。 把随机性固定并保留中间量,这个例子就能成为后续优化时的回归基线。
别被表面现象带偏。 把失败归因他人、故事没有个人动作、结果无法验证、每题套同一案例,都会显得准备痕迹重。 先区分定义错误、实现错误、数据问题和性能瓶颈。四类问题需要的证据不同,不能靠盲目调参混在一起处理。
验证任务:准备冲突、失败、取舍、推动和学习五个故事,每个控制在两分钟。 实验结束后写三句话:观察到了什么、这些证据支持什么结论、还有哪种解释尚未排除。
准备一次技术分歧、一次失败、一次跨团队协作、一次在约束下取舍。使用 STAR,但结果包含证据和反思。不要把所有问题归因于别人;说明你怎样改变流程,避免复发。
14.10 模拟面试清单
先把概念落到可观察对象上。 模拟面试要复现真实压力并产生反馈闭环。按岗位组合原理、代码、项目和系统设计,录音后逐项评分,而不是只数刷题数量。 这一节的重点是建立因果链。先说明问题,再看计算或流程,最后用可重复实验验证结论。
把抽象概念还原成操作:
- 建立评分量表。
- 严格计时和连续追问。
- 复盘事实错误与表达。
- 隔几天无稿重答。
把它缩小到能逐项检查。 回答正确但用了八分钟、没有先给结论,仍需改进;评分应包含结构、准确、证据和节奏。 如果这个小例子还不能解释清楚,扩大数据只会让错误更难发现。
需要特别守住的边界。 只和熟悉朋友练、提前知道题、复盘只看答案、最后一天高强度堆题,都会降低迁移。 保留完整 trace 比猜原因更重要。找到第一次偏离预期的位置,通常比分析最终错误输出更高效。
本节练习:完成至少三轮不同面试官的全流程模拟,记录可量化改进项。 除代码外,请保留一份结果说明,标明环境、随机种子、输入规模和你主动检查过的边界。
- 60 秒自我介绍,3 分钟项目,10 分钟项目深挖。
- 20 道核心原理题能够画图、写公式、说取舍。
- 10 道手撕代码可在无补全环境写出并测试。
- 2 道系统设计能估算容量与说明安全。
- 每个简历数字有来源,每项技术能说一个失败。
- 准备反问:团队目标、数据与评估、上线责任、研究/工程比例和成功标准。
14.11 经验来源与使用方式
先建立直觉。 公开面经用于发现能力维度和追问方式,不是公司固定题库。信息有样本偏差与时效性,应与岗位、官方技术栈和自身经历交叉验证。 读这一节时,先不要急着记缩写,而要不断追问:输入是什么,经过了哪些可观察变换,输出又怎样被验证。
把过程拆开看:
- 记录发布时间与岗位。
- 抽取反复出现的能力主题。
- 映射回本书章节。
- 用真实项目证据准备。
最小例子。 多篇面经提到 RAG 切块和评估,说明应会解释取舍,但不意味着背一个固定 chunk size。 例子越小,越容易手算、打印中间量并判断实现是否偏离定义。
容易踩坑。 照抄答案、泄露公司保密题、把个例当招聘标准、为匹配面经虚构经历,都会适得其反。 排错时应保存失败输入和版本,先定位最早出现异常的步骤,再决定是否调整模型或超参数。
动手任务:汇总 20 篇近期公开面经,按能力而非题目建立频次表,并标注来源。 完成后不要只保存最终输出,还要保存代码、配置、中间张量或检索结果,以及你对结果的解释。
公开面经具有样本偏差,不能据此推断每家公司固定题库;它们的价值是发现反复出现的能力维度。近期牛客面经中,项目深挖常涉及多 Agent 编排、失败重试、RAG 热更新、Embedding/Rerank 选择、切块优化、微调复读与算法题。将这些主题映射回本书对应章节,比背“标准答案”更有效。
最后的判断标准不是“背了多少题”,而是能否把一个陌生问题拆成可验证假设,能否写出正确最小实现,能否用数据解释取舍。这也是整本书希望训练的能力。
本章配套代码
下面的脚本与正文使用相同符号。建议先在代码中打印形状和中间量,再运行断言;若依赖尚未安装,至少先阅读入口函数、输入输出和测试部分。
- `examples/14_interview_drills.py`:面试代码练习。
- `examples/14_lru_cache.py`:O(1) LRU Cache。
本章端到端实验:把知识变成可复现证据
本实验不是把本章代码重新抄一遍,而是把概念、实现、测试和解释串成一个小型工程。请新建独立目录,保存 README.md、环境文件、源代码、测试、运行日志和结果图。README 至少说明任务、输入输出、运行命令、预期现象、已知限制和复现条件。
实验步骤
- 选择三个真实 JD 做能力矩阵,写出共同核心与岗位差异。
- 逐条给简历数字建立证据卡;无法复现的数字删除或改成定性描述。
- 录制三次项目介绍,分别删减到 180 秒并让不了解项目的人复述。
- 为每类问题写 90 秒答案,并让同伴连续追问三个‘为什么’。
- 限时手写 Softmax、RMSNorm、Attention、RoPE、RRF 和 LRU,并为每题写三个测试。
- 建立错题本,每题记录识别信号、核心不变量、最小反例和一周后重写结果。
每完成一步,先写下预期,再运行代码。若结果与预期不一致,不要覆盖旧日志;建立 failures.md,记录现象、假设、证据、修复和回归测试。这样得到的不是一次性 Demo,而是一份能证明你真正理解本章内容的实验档案。
验收标准
- 全新环境能够按照 README 从头运行,依赖和随机种子已记录。
- 关键函数至少有正常、边界和错误输入三类测试;涉及数值计算时检查有限值与合理误差。
- 结果包含一个基线和至少一个受控改动,能够说明变化来自哪里。
- 日志保留输入规模、耗时、内存或显存、软件版本和失败样本。
- 结论区分“实验直接证明的事实”“根据事实做出的推断”和“仍未验证的猜想”。
本章自测
- 不看正文,用自己的话解释“岗位描述是能力假设,不是关键词清单”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“简历的每个数字都是一个实验结论,应能回答定义、基线、数据、硬件、时间窗口、个人贡献和不确定性”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“三分钟介绍要先让听者理解问题和约束,再讲方案与个人决策,最后用指标和失败证明真实做过”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“高频基础题要用定义—机制—公式或形状—取舍—验证五层回答”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“手撕代码考察的是问题澄清、接口、正确性、复杂度和测试”,并给出一个可以证伪的测试。
- 不看正文,用自己的话解释“算法准备按模式建立迁移能力:哈希、双指针、滑窗、栈、二分、树、堆、图和动态规划”,并给出一个可以证伪的测试。
回答时先画图或写形状,再给结论。若只能说出术语而不能给出最小例子、边界条件和验证方法,说明这一节仍需要回到代码中重做。
附录 A 从空电脑到可复现实验环境
本附录给出一条不依赖特定云平台的环境路线。版本会变化,因此原则比命令更重要:先确定操作系统、Python、GPU 驱动和框架的兼容关系;每个项目使用隔离环境;安装后做最小健康检查;最后锁定依赖并记录硬件。
A.1 CPU、GPU 与显存分别负责什么
CPU 负责 Python 调度、数据解析、tokenization 和大量通用任务;GPU 擅长并行矩阵计算;显存保存权重、激活、梯度、优化器状态和缓存。没有 GPU 可以完成 NumPy、数据处理、API 应用和小模型实验,但本地运行数十亿参数模型会受到内存与速度限制。
粗略估算权重存储时,可用“参数量×每参数字节”。7B 模型仅 FP16 权重约需 14 GB,但真实推理还需要 KV Cache、临时 buffer 和框架开销;训练还需要梯度和优化器状态,不能用权重大小直接判断能否训练。量化文件变小也不保证目标硬件具有加速 kernel。
A.2 创建虚拟环境
BASH
python -m venv .venv
Windows PowerShell 激活:
POWERSHELL
.venv\Scripts\Activate.ps1
Linux/macOS 激活:
BASH
source .venv/bin/activate
确认 python 和 pip 指向当前环境:
BASH
python -c "import sys; print(sys.executable)"
python -m pip --version
始终优先使用 python -m pip,因为它明确调用当前 Python 对应的 pip。若 PowerShell 禁止激活脚本,应先理解组织安全策略;不要从陌生网页复制修改全局执行策略的命令。
A.3 安装 PyTorch 前先核对兼容关系
GPU 环境至少包含三层:操作系统驱动、PyTorch 包内或依赖的 CUDA runtime、实际 GPU 架构。nvidia-smi 显示的“CUDA Version”通常表示驱动可支持的最高 CUDA 版本,不等于你安装的 PyTorch build。
安装命令应从 PyTorch 官方选择器获取。安装后运行:
PYTHON
import torch
print("torch:", torch.__version__)
print("torch cuda build:", torch.version.cuda)
print("cuda available:", torch.cuda.is_available())
if torch.cuda.is_available():
print("device:", torch.cuda.get_device_name(0))
x = torch.randn(1024, 1024, device="cuda")
y = x @ x
print(y.mean().item())
若 cuda available 为 False,按顺序检查:是否安装了 CPU 版 PyTorch;驱动是否可见;当前 Python 是否是刚安装依赖的环境;远程容器是否映射 GPU;系统是否需要重启。不要在原因未明时重复安装多个 CUDA Toolkit,它们可能并不能解决 wheel 与驱动的问题。
A.4 Hugging Face 模型与缓存
下载模型前阅读模型卡、许可证、参数量、精度和上下文长度。权重、tokenizer 和配置必须属于同一版本。缓存目录可能占用大量磁盘,团队环境应明确缓存位置、清理策略和离线镜像。
对包含 trust_remote_code=True 的项目要特别谨慎:这意味着模型仓库代码会在本地执行。只有确认来源、固定 revision 并审查代码后才考虑启用。对于 checkpoint,优先使用安全序列化格式;不可信 pickle 可能在加载时执行代码。
A.5 依赖锁定与环境报告
最简单的环境快照可以用:
BASH
python -m pip freeze > requirements-lock.txt
但 pip freeze 记录的是当前环境的所有包,不一定是最小依赖。成熟项目可使用 pyproject.toml 描述直接依赖,再由锁定工具生成解析后的版本。无论采用什么工具,实验报告都应包含 Python、核心库、操作系统、CPU/GPU、驱动和启动命令。
A.6 常见环境错误清单
- ModuleNotFoundError:先打印 sys.executable,确认装包和运行使用同一环境。
- CUDA OOM:记录实际分配与保留显存,缩小单样本长度;梯度累积不能解决一条样本本身放不下。
- DLL/so 加载失败:检查架构、编译版本和依赖搜索路径,不要随意下载未知 DLL 覆盖系统文件。
- tokenizer 或模型维度不匹配:核对仓库 revision、词表大小、特殊 token 和 resize 过程。
- 多进程卡死:记录每个 rank 最后一步,检查样本数量、集合通信顺序和异常进程。
- 结果不复现:除随机种子外,还要核对数据顺序、非确定 kernel、库版本、精度与硬件。
A.7 一个合格的环境验收
环境准备完成的标准不是“安装没有报错”,而是:NumPy 示例可以运行;PyTorch 前向与反向通过;GPU 小矩阵计算通过;保存的模型能在新进程加载;版本报告已写入文件;仓库不包含密钥、缓存和大权重。之后再进入大模型下载和训练,会节省大量排错时间。
附录 B 大模型学习所需数学速查
本附录不是完整数学教材,而是把正文反复出现的符号放到同一张地图里。建议用纸笔为每个公式构造 2×2 或长度为 3 的例子,再用 NumPy 验证。
B.1 标量、向量、矩阵与张量
标量是单个数;向量是一维有序数组;矩阵有行列两个轴;张量是多轴数组。轴的语义比“几维”更重要,例如语言模型隐藏状态常写为 (B,T,D):批次、序列长度、隐藏维。
矩阵乘法 A @ B 要求 A 的最后一维等于 B 的倒数第二维。若 A 为 (m,n)、B 为 (n,p),输出为 (m,p):
C_{ij}=\sum_{k=1}^{n}A_{ik}B_{kj}
逐元素乘法不做求和,两个张量必须形状相同或满足广播规则。注意力里的 QK^T 是矩阵乘,而门控网络里的两条分支通常逐元素相乘。
B.2 点积、范数与余弦相似度
点积把两个同长度向量变成标量:
x\cdot y=\sum_i x_i y_i
L2 范数为 sqrt(sum(x_i²))。余弦相似度只比较方向:
\cos(x,y)=\frac{x\cdot y}{\|x\|_2\|y\|_2}
若向量已做 L2 归一化,点积就等于余弦相似度。向量数据库使用哪种度量必须与 embedding 模型说明和归一化方式一致。
B.3 概率、条件概率与交叉熵
概率分布的每项非负且总和为 1。条件概率 P(A|B) 表示已知 B 时 A 的概率。语言模型输出的是在前缀条件下下一个 token 的分布:
P(x_1,\ldots,x_T)=\prod_{t=1}^{T}P(x_t\mid x_{<t})
Softmax 把 logits 转成概率:
p_i=\frac{e^{z_i}}{\sum_j e^{z_j}}
数值实现应先减去最大 logit。单个正确类别 y 的交叉熵是 -log p_y;序列损失通常对有效标签取平均。Perplexity 常写为 exp(cross_entropy),但不同 tokenizer 的 token 粒度不同,不能脱离条件直接横比。
B.4 导数、偏导与链式法则
导数描述输入微小变化如何影响输出。多变量函数对每个变量的偏导组成梯度。复合函数 L=f(g(x)) 的链式法则为:
\frac{dL}{dx}=\frac{dL}{dg}\frac{dg}{dx}
反向传播就是沿计算图反向重复应用链式法则。梯度不是参数更新本身;优化器还会结合学习率、动量、二阶矩和权重衰减。
有限差分可检查解析梯度:
\frac{\partial L}{\partial w_i}\approx \frac{L(w_i+\epsilon)-L(w_i-\epsilon)}{2\epsilon}
梯度检查应使用双精度、小输入并关闭随机层;epsilon 太大有截断误差,太小受浮点舍入影响。
B.5 均值、方差与归一化
均值描述中心,方差描述离散程度:
\mu=\frac1n\sum_i x_i,\qquad \sigma^2=\frac1n\sum_i(x_i-\mu)^2
LayerNorm 对一个 token 的隐藏维做中心化与尺度归一化;RMSNorm 不减均值,只除以均方根。归一化的轴、epsilon 位置和可学习缩放必须与实现一致。
B.6 信息论直觉
事件概率越小,自信息 -log p 越大。熵是分布平均不确定性。KL 散度衡量分布 p 相对 q 的差异:
D_{KL}(p\|q)=\sum_i p_i\log\frac{p_i}{q_i}
KL 不对称,也不是严格距离。RLHF 中常用 KL 限制新策略偏离参考模型;蒸馏中可用 KL 让学生接近教师软分布。
B.7 复杂度与数量级估算
大 O 表示随变量增长的主导趋势,不等于真实耗时。注意力分数矩阵随序列长度约按 T² 增长;FFN 计算约随 T·D·Dff 增长。真实性能还受 dtype、batch、内存带宽、kernel 融合、通信和硬件利用率影响。
做系统估算时至少同时考虑:参数与状态存储、激活、KV Cache、FLOPs、读写字节、网络通信、排队和长尾。理论数量级用于提出假设,profiler 与压测用于验证。
B.8 浮点数与数值稳定性
浮点数只有有限范围和精度。FP16 范围较小,容易上溢/下溢;BF16 指数范围更大但有效尾数更少;FP32 更稳但占用和带宽更高。稳定实现常见策略包括减最大值、log-sum-exp、更高精度累积、loss scaling、梯度裁剪和 epsilon。
看到 NaN/Inf 时,先找首次出现的位置,检查输入、除零、log/exp、学习率、梯度和半精度范围。不要只在最后一层用 nan_to_num 隐藏问题。
附录 C 大模型工程术语表
术语表给出本书语境中的简明定义。它不是替代正文的“标准答案”,同一术语在论文和框架中可能有更具体含义。
- Agent:由模型参与运行时决策、可使用工具并维护状态的系统;自主程度应受权限、预算与验证约束。
- Alignment(对齐):让模型行为更符合人类或产品目标的训练与系统过程,包括 SFT、偏好优化、安全策略和评估。
- All-Gather:让所有 rank 收集到各 rank 的分片。
- All-Reduce:对各 rank 张量做求和等归约,并把结果发送给所有 rank。
- Attention Mask:在注意力分数上屏蔽不应被读取的位置,常见因果 mask 与 padding mask。
- AWQ:利用激活信息保护重要权重的权重量化方法族。
- Batch:一次并行处理的一组样本;在语言模型训练中应同时关注样本数与有效 token 数。
- BF16:具有接近 FP32 指数范围、较短尾数的 16 位浮点格式。
- BM25:基于词频、逆文档频率和长度归一化的经典稀疏检索函数。
- BPE:通过迭代合并频繁符号对学习子词词表的 tokenization 方法。
- Checkpoint:可保存并恢复训练或推理状态的快照;完整续训还需优化器、调度器、随机状态和数据位置。
- Chunk:RAG 索引与检索使用的文档片段,应保留来源、层级、页码、权限和版本。
- CLIP:通过图文对比学习建立共享表示空间的视觉语言模型。
- Continuous Batching:在生成过程中动态向执行批次插入新请求、移除已完成请求的调度方式。
- Cross-Attention:查询来自一个序列、键和值来自另一个序列的注意力。
- Cross-Encoder:把查询与候选文本联合编码并评分,常用于重排。
- CUDA OOM:GPU 显存分配失败;要区分权重、激活、缓存、临时 buffer 与碎片来源。
- Data Parallel:不同设备处理不同数据分片并同步梯度的并行方式。
- DDP:PyTorch 分布式数据并行,每个进程持有模型副本并通过集合通信同步梯度。
- Decode:自回归推理中逐 token 生成的阶段,常受内存带宽与 KV Cache 影响。
- Dense Retrieval:使用 embedding 向量进行语义近邻检索。
- DPO:使用偏好对直接优化策略相对参考模型概率差的离线偏好方法。
- Embedding:把离散对象或文本映射为连续向量的表示。
- EOS:序列结束特殊 token;训练标签、生成停止与 padding 需正确区分。
- Expert Parallel:将 MoE 专家分布到不同设备的并行方式。
- FlashAttention:通过 IO-aware 分块和在线 Softmax 减少高带宽内存读写的精确注意力实现。
- FP16:16 位浮点格式,训练时常需动态或静态 loss scaling。
- FSDP:PyTorch 全分片数据并行,可分片参数、梯度与优化器状态。
- Function Calling:模型按工具 Schema 提议函数与参数,由程序验证并决定是否执行。
- GQA:多个查询头按组共享较少的键和值头,降低 KV Cache。
- Gradient Accumulation:多次反向累积梯度后再更新,用于扩大有效 batch;不能解决单样本本身 OOM。
- GRPO:对同一 prompt 的一组回答使用组内相对奖励估计优势的策略优化方法。
- Grounding:让输出与可指认的输入、图像区域或外部证据建立对应关系。
- Hallucination:模型生成与事实、输入或证据不一致的内容;应进一步细分错误类型。
- Hybrid Retrieval:组合稀疏与稠密检索,并用 RRF 等方法融合排名。
- Inference:模型在固定权重下根据输入产生预测或生成结果的过程。
- Instruction Tuning:使用指令—回答或多轮消息数据训练模型遵循任务。
- KV Cache:生成时缓存历史 token 在各层的 K/V,避免重复计算。
- KL Divergence:衡量一个概率分布相对另一个分布差异的非对称量。
- Label Mask:用 -100 等忽略值让某些 token 不参与语言建模损失。
- LayerNorm:通常在最后一维做中心化与方差归一化,再施加可学习仿射变换。
- LoRA:用低秩矩阵表示权重增量的参数高效微调方法。
- LRU Cache:淘汰最久未使用条目的缓存策略。
- MCP:模型上下文协议,用统一 client/host/server 架构连接工具、资源等能力。
- MHA:每个查询头具有独立 K/V 头的多头注意力。
- MLA:多头潜在注意力,通过低维潜变量等设计压缩表示与缓存;具体含义依模型实现。
- MoE:混合专家网络,用路由器让 token 稀疏激活少数专家。
- MQA:所有查询头共享一组 K/V 的注意力变体。
- nDCG:考虑相关性等级和排名位置的检索指标。
- P50/P95/P99:延迟分布的分位数,比单一平均值更能反映长尾。
- PagedAttention:用分页式物理块管理逻辑 KV Cache 的机制。
- Parameter Efficient Fine-Tuning:只训练少量参数或适配器的微调方法族。
- Perplexity:交叉熵的指数形式;跨 tokenizer 比较需要谨慎。
- Pipeline Parallel:把不同模型层放在不同设备并用 microbatch 流水执行。
- PPO:使用概率比裁剪等机制限制策略更新幅度的在线强化学习算法。
- Prefill:对完整提示并行计算、建立 KV Cache 并产生首 token 的阶段。
- Prompt:提供给模型的文本或多模态输入;系统可靠性不能只依赖提示约束。
- QLoRA:在量化冻结基础权重上训练 LoRA 适配器的高效微调路线。
- Quantization:用较少比特表示权重或激活,并配合尺度与低比特 kernel。
- RAG:先检索外部证据,再将证据作为上下文用于生成的系统范式。
- Recall@K:前 K 个结果是否覆盖相关文档的召回指标。
- Reduce-Scatter:先归约,再把结果分片给不同 rank。
- Reranker:对召回候选进行更精细相关性排序的模型或规则。
- Reward Hacking:策略利用奖励函数漏洞取得高分,却没有实现真实目标。
- RMSNorm:不减均值、按均方根控制隐藏向量尺度的归一化。
- RoPE:把位置相关旋转作用于 Q/K,使点积包含相对位置信息。
- RRF:只利用名次、无需统一原始分数尺度的倒数排名融合。
- SFT:有监督微调,使用目标输出 token 的交叉熵训练模型行为。
- Softmax:把 logits 归一化为概率;实现时通常先减最大值保证稳定。
- Speculative Decoding:小模型提议多个 token,由目标模型并行验证以减少串行解码步数。
- SwiGLU:使用 SiLU 门控分支的前馈网络结构。
- Tensor Parallel:把单层矩阵运算切分到多个设备。
- Token:tokenizer 输出的基本离散单位,不一定等于字符或单词。
- Tokenizer:把文本与 token id 相互转换的规则、词表和特殊 token 集合。
- TPOT:Time Per Output Token,持续生成阶段每个输出 token 的时间。
- TTFT:Time To First Token,从请求开始到首 token 返回的时间。
- Vector Database:提供向量索引、近邻搜索和元数据过滤的存储系统。
- ViT:把图像切成 patch token 并使用 Transformer 编码的视觉架构。
- Workflow:由程序预定义执行路径的流程,与运行时自主选择步骤的 Agent 区分。
- ZeRO:通过分片优化器、梯度和参数降低数据并行冗余的技术族。
附录 D 练习、复盘与项目验收方法
本书不按时间表组织,也不要求固定学习速度。学习进度应由“能否独立完成验收”决定,而不是阅读页数。下面给出一套贯穿十四章的复盘方法。
D.1 五层掌握标准
- 能定义:不用术语循环解释术语,能说出对象解决的问题。
- 能画图:能画出输入、关键状态、输出和数据流。
- 能计算:能写形状、关键公式或复杂度数量级。
- 能实现:能完成最小 Python 代码,并通过边界测试。
- 能评估:能说明什么证据表明实现正确、何时会失败、怎样比较替代方案。
只有第一层通常只能应付选择题,无法完成项目或技术面试。每章至少选择两个主题做到第五层。
D.2 实验报告模板
MARKDOWN
# 实验名称
## 问题与假设
- 想验证什么?
- 为什么预期如此?
## 数据与环境
- 数据来源、许可、切分与版本
- Python/库/硬件/随机种子
## 方法
- 基线
- 本次唯一主要改动
- 指标及定义
## 结果
- 数值、曲线、耗时、内存或显存
- 代表性成功与失败样例
## 分析
- 证据直接支持什么?
- 哪些只是推断?
- 还有哪些替代解释?
## 回归测试与下一步
D.3 怎样设计练习
优先设计能失败的练习。比如注意力练习不只是“输出形状正确”,还应检查未来位置权重为零;RAG 不只是“能回答”,还要把目标证据从索引中移除,观察系统是否错误地凭参数记忆作答;Agent 不只是“调用工具”,还要注入未知工具、超时、空结果和权限拒绝。
每个核心函数至少覆盖:正常输入、最小/空边界、非法形状或类型、极端数值、固定随机性。涉及外部服务时,还要覆盖超时、限流、部分返回和重试幂等。
D.4 怎样读论文
第一次读论文先回答五个问题:研究问题是什么;以前方法哪里不足;作者做了什么;实验怎样支持结论;限制是什么。第二次再进入公式与实现,第三次用代码或小实验验证一项关键主张。
模型技术报告还要区分已公开事实、合理推断和未公开信息。训练数据、系统优化和评测设置常不能完全复现,不应把猜测写成确定结论。
D.5 怎样使用开源仓库
先读 README、许可证和目录,不要直接执行安装脚本。选择一个最小入口,固定 commit,查看配置和测试。运行成功后改变一个变量并预测结果;如果只能运行默认 Notebook,而不知道输入输出和失败边界,还没有真正掌握。
引用开源代码时遵守许可证和署名要求。正文可用自己的语言总结概念并链接原始来源,不要大段复制教程或书籍文本。
D.6 贯穿全书的毕业项目
建议最终完成一个“有评估的知识助手”,而不是只做聊天界面。最低要求:
- 数据摄取支持至少两种文档格式,保留来源、页码、版本和权限。
- 建立 BM25 基线、向量检索和混合检索,使用固定标注集比较。
- 回答返回引用;程序验证引用 id 与实际上下文一致。
- 服务记录 TTFT、总延迟、检索耗时、token 与错误类型。
- 至少实现一个只读工具;高风险工具必须经过权限与确认。
- 保存三类 bad case 的定位过程与回归测试。
- README 提供架构图、运行命令、数据说明、指标定义、结果与限制。
资源有限时可使用小数据、模拟模型或公开 API,但必须诚实说明哪些环节没有在本地训练或部署。项目的可信度来自证据完整,而不是组件数量。
D.7 最终自检
- 能否在白纸上画出从文本到 token、Transformer、logits 和生成的完整路径?
- 能否解释 RAG 的错误来自解析、召回、重排还是生成,并给出相应指标?
- 能否说明 LoRA/QLoRA 节省哪些状态,又没有节省哪些激活?
- 能否比较 DPO、PPO、GRPO 的数据与训练要求,而不说绝对优劣?
- 能否估算一个模型权重、KV Cache 和训练状态的数量级?
- 能否为 Agent 写出工具 Schema、权限、停止和失败测试?
- 能否用三分钟讲清一个项目的约束、个人决策、指标和 bad case?
若其中某项只能给出名词列表,就回到对应章节完成最小实验。真正的学习闭环是:发现说不清的地方,把它缩小成可运行问题,再用证据修正自己的理解。
全套教材、示例代码、参考资料全部开源免费,仓库内同时提供 Word 完整版文档,离线学习也很方便。
如果这份资料解决了你大模型学习碎片化、找不到完整学习路线的痛点,欢迎去 GitHub 仓库点 Star,你的支持是作者持续更新内容的动力!
更多推荐
所有评论(0)