Lecture 2 · 面向构建者的 LLM · LLMs for Builders

Engineering AI Agents · Lecture 02。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:Stanford CS329Z Engineering AI Agents(Fall 2026),Lecture 2 · Mon Sep 28 · LLMs for Builders。阅读材料:Schluntz & Zhang, Building Effective Agents(Anthropic, 2024);补充阅读:Rajasekaran et al., Effective Context Engineering for AI Agents(Anthropic, 2025)。 依据说明:本讲以原课第 2 讲的公开 slides 为主线(什么是语言模型、架构、训练、推理、给智能体用的 LLM 五部分),结合两篇阅读材料,以及 slides 引用的论文(Transformer、线性注意力、RULER、InstructGPT、DeepSeekMath、SWE-smith、推测解码、Large Language Monkeys、Self-Consistency、SGLang、DSPy 等;凡是引用数字的都核对过原文)。全部实验和其中的”替身”(n-gram 模型、纯 Python 注意力、会写 JSON 的模拟模型、长时程任务)是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture02/(只用 Python 标准库;7 个脚本 + 5 个模块;不需要 API key;最慢的脚本约 8 秒) 预计阅读时间:4–5 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

slides 给这一讲定了两个目标:理解并解释训练和推理现代 LLM 时的核心设计决策;综合地看这些决策怎样带来局限,而这些局限又怎样影响应用使用 LLM 的方式。翻译成构建者的语言,学完这一讲你应该能够:

  1. 说清楚”语言模型”在数学上是什么(一个序列上的概率分布,用链式法则分解),为什么实践中总用 log 概率,以及”生成式”意味着什么。
  2. 解释贪心、温度、top-k、top-p、束搜索各自做了什么;并且能从智能体的角度推理它们的后果:每步一点点的”尾巴”怎样在多步里累积,温度怎样同时影响 pass@k、pass^k 和多数投票。
  3. 画出 decoder-only Transformer 的一层,解释因果掩码、位置编码、为什么上下文越长越贵(n²),以及线性注意力用一个固定大小的状态换来了什么、失去了什么。
  4. 讲出从预训练(交叉熵、数据流水线、去重、配比)到后训练(指令微调、RLHF、RLVR、面向智能体的训练)的流水线,以及每一步怎样塑造了你手里这个模型的脾气。
  5. 解释 prefill 和 decode 为什么快慢不同、KV cache 和 batch 为什么重要、为什么输出 token 更贵;推测解码为什么能”无损加速”;测试时扩展什么时候有用(有没有验证器)。
  6. 用三种方法拿到结构化输出(解析、解析 + 校验 + 重试、约束解码),知道各自怎样失败;会用 DSPy 式的签名和小分类器作为替代。
  7. 做上下文工程:知道上下文里装了什么、为什么不能”全塞进去”、怎样删(清理、笔记、压缩、子智能体),以及”放在哪”怎样决定了前缀缓存能省多少钱。

0.2 先修与本讲范围 Prerequisites & scope

slides 开头画了一张”LLM 技术栈”:最底层是 LLM 工程(分词器、架构、预训练、后训练、RL、scaling law、分布式计算、GPU kernel——CS336 的范围),中间是 LLM 适配(in-context learning、微调、LoRA、蒸馏、RLHF / DPO——CS224N 的范围),最上层是 LLM 应用(聊天机器人、RAG、智能体……——CS329Z 的范围)。原课的说法是:

我们会在技术栈的最高层工作,但理解一点它们的工作原理仍然很重要……

这一讲就是这个”一点”。它不会教你训练一个 LLM,而是回答:底层的哪些设计决策,会在你搭智能体的时候冒出来? 比如:为什么同一个提示词两次结果不同(解码);为什么上下文越长越贵、越不可靠(注意力);为什么模型会自信地编造(训练目标);为什么”让它写 JSON”会出错(解码 + 训练);为什么缓存能省 90% 的钱(推理)。

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
第 1 讲:LM 调用的约定(消息进、字符串出)、智能体循环、pass@k 与 pass^k检索、分块、重排(第 3 讲)
Python 基础;高中的概率(条件概率、乘法法则)和对数工具调用协议、MCP(第 4 讲;协议细节见 前置讲 P2)
矩阵乘法是什么(会算 2×2 的就够了)DSPy 的模块和优化器(第 5 讲)、提示词与权重优化(第 9 讲)
并发、缓存、成本账本的概念(前置讲 P3)记忆与压缩的系统设计(第 7 讲)、多智能体(第 8 讲)

0.3 本讲地图 Map

§1  什么是语言模型:链式法则、log 概率、生成式 = 可以采样                        ← 代码 01
§2  解码:贪心、温度、top-k、top-p、束搜索;解码对智能体意味着什么                 ← 代码 02
§3  架构:decoder-only Transformer、因果掩码、位置、n²、线性注意力与混合            ← 代码 03
§4  训练:交叉熵、预训练数据(去重、配比)、中期训练、后训练(SFT、RLHF、RLVR)      ← 代码 01
§5  推理:prefill / decode、KV cache、屋顶线、推测解码、测试时扩展                  ← 代码 04、02
§6  结构化输入输出:解析、重试、约束解码、DSPy 签名、分类器                          ← 代码 05
§7  上下文工程:上下文里有什么、context rot、怎样删、放在哪、两篇阅读材料          ← 代码 06
§8  用真实模型试一试                                                              ← 代码 07(可选)
§9  小结   §10 常见误区   §11 练习   §12 延伸阅读   §13 下一讲预告

0.4 记号与术语 Notation & terms

记号 / 术语意思
token、词表 V模型读写的最小单位(真实模型是子词,本讲的代码用整词);词表大小也记作 V
x<tx_{<t}第 t 个 token 之前的所有 token(“前文”)
P(xt∣x<t)P(x_t \mid x_{<t})给定前文,下一个 token 的概率分布——语言模型每一步算的就是它
logitsoftmax 之前的分数;Pi=ezi/∑jezjP_i = e^{z_i} / \sum_j e^{z_j}
交叉熵 / 损失(nats)−1N∑tlog⁡P(xt∣x<t)-\frac{1}{N}\sum_t \log P(x_t \mid x_{<t});困惑度 perplexity = e损失e^{\text{损失}}
温度 T把 logit 除以 T 再 softmax;T = 0 定义为贪心
n上下文长度(token 数);d 是向量维度
prefill / decode读入提示词(可并行)/ 一个一个地生成(只能串行)
TTFT / TPOT首 token 延迟 / 之后每个输出 token 的间隔
pass@k / pass^kk 次里至少一次对 / k 次全对(第 1 讲 §6.2)

0.5 本讲的”替身” · The stand-ins

真实 LLM 没法在笔记本上拆开看,而且每次调用要花钱、结果不可复现。本讲的代码用四个能逐项核对的替身:

替身它和真东西一样的地方不一样的地方
n-gram 语言模型(lm_lab.py),在两个领域上训练:生成的”课程通知”(485 句)和第 1 讲的虚构百科(543 句)都是”给前文、出下一个 token 的分布”;链式法则、交叉熵、困惑度、所有解码方法、推测解码都完全一样只看前 2–4 个 token,靠数次数;不会推理、不会泛化到没见过的组合
纯 Python 注意力(attention.py),随机权重Q/K/V、因果掩码、位置编码、softmax、线性注意力的数学完全一样;乘加次数是实测的没训练过,所以”谁看谁”没有语言学意义;只演示结构上的性质
会写 JSON 的模拟模型(structured.py)接口和行为模式像真实模型:大多数时候写干净的 JSON,偶尔加开场白、代码围栏、单引号……每种毛病出现的概率是我们设定的旋钮
长时程任务(context_lab.py),在虚构百科上跑 36 步token、前缀缓存的成本、“哪些事实还在上下文里”都是机械地算出来的模拟读者不会被长上下文干扰(真实模型会,见 §7.2)

和第 1 讲同样的提醒:替身能演示机制,不能证明关于真实模型的结论。凡是数字由旋钮决定的地方,下文都会明说。


1. 什么是语言模型 · What is a language model

1.1 一个概率分布

直觉。语言模型就是一台”续写机”:给它一段开头,它告诉你接下来每个可能的 token 有多大概率。把这个能力反复使用,它就能给一整句话打分,也能写出一整句话。

定义。一个语言模型是 token 序列上的概率分布。用乘法法则(链式法则)把一整句话的概率拆开:

P(x1,x2,…,xn)=∏t=1nP(xt∣x1,…,xt−1)P(x_1, x_2, \dots, x_n) = \prod_{t=1}^{n} P(x_t \mid x_1, \dots, x_{t-1})

slides 的写法是:一个 token 序列的概率 = 从第一个到最后一个 token、“当前 token 在前文条件下的概率”的连乘。每一项都是同一个问题:“看了前文,下一个 token 是这个的概率是多少?“——这也是模型每一步真正在算的东西。

在我们的三元模型上算一遍(代码 01 演示 1;三元模型只看前 2 个 token,句末还有一个”到此结束”的 </s>):

链式法则的一项概率累乘log 累加
P(‘The’ | 句首)0.17131.71×10⁻¹−1.764
P(‘class’ | The)0.36126.19×10⁻²−2.783
P(‘was’ | The class)0.77794.81×10⁻²−3.034
P(‘about’ | class was)0.68773.31×10⁻²−3.408
P(‘agents’ | was about)0.07282.41×10⁻³−6.029
P(’.’ | about agents)0.17674.26×10⁻⁴−7.762
P(’</s>’ | agents .)0.90943.87×10⁻⁴−7.857

一句 6 个词的话,概率已经只有 3.9×10⁻⁴。注意哪一项最”贵”:‘agents’ 只有 7.3%,因为 “The class was about” 后面可以接很多东西。

1.2 为什么总是用 log 概率

slides 在这页下面加了一行小字:“实践中我们通常把概率的 log 加起来,为了数值稳定。“代码 01 把 60 句百科(522 个 token)当成一段话:

  • 直接连乘:0.0——浮点数下溢了。真实概率约 10⁻⁴⁵²,而 float64 能表示的最小正数约 5×10⁻³²⁴。
  • log 相加:−1041.5,毫无问题。

所以你在 API 里看到的永远是 logprobs(log 概率),训练时最小化的是 log 概率的负数(§4.1),束搜索比较的是 log 概率之和(§2.3)。

1.3 生成式 vs 判别式:所以可以采样

slides 用一个”知识检查”问:按上面方式建模的语言模型是生成式还是判别式?——生成式。判别式模型(逻辑回归、SVM)学的是”给定输入,属于哪一类”;生成式模型(朴素贝叶斯、扩散模型、语言模型)学的是数据本身的分布。slides 提醒,记号 P(xt∣x<t)P(x_t \mid x_{<t}) 长得像”给定输入预测输出”,容易让人以为是判别式的;但把所有项乘起来,它定义的是整个序列的分布。

生成式的直接后果:可以从中采样。代码 01 从 “The class was about” 往后采样了 4 次:

The class was about attention on evaluation Margrove is a city in the Reed River, upstream from Korport ...
The class was about data and retrieval.
The class was about optimization on Friday at 9am Today the class covers evaluation and benchmarks.
The class was about safety before Thursday Ostrell now lives in Wynhaven.

有通顺的,也有”串领域”的(课程通知里冒出百科的城市名)——三元模型只看前两个词,看不到整句话在说什么。真实 LLM 的上下文窗口是几十万 token,但”每一步从分布里挑一个 token、接到后面、再算下一步”的过程完全一样。

1.4 token 和分词器

真实模型的 token 是子词(BPE 一类的分词器):常见词一个 token,罕见词拆成几块,数字和代码常常被拆得很碎。对构建者来说有三点要记住:

  1. 计费和上下文长度都按 token 算,而不同语言、不同内容的”每个字多少 token”差别很大(第 1 讲的 count_tokens 只是一个近似)。
  2. 模型看到的是 token,不是字符——数一个单词里有几个字母、精确地处理长数字,对它比你想象的难。
  3. 分词器是模型的一部分:同一段文字,换一个模型,token 数和切法都会变;缓存、成本估算都要用对应模型的分词器(或 API 报告的真实用量)。

本讲的代码按整词切分(lm_lab.tokenize,和第 1 讲 llm.count_tokens 的切法一样),好处是每个 token 都看得懂。

1.5 构建者视角:提示词就是”条件”

整门课里你写的每一个提示词——系统提示、工具说明、few-shot 示例、检索到的文档——在模型眼里都只是 x<tx_{<t} 的一部分,是那个条件概率的条件。上下文工程(§7)、RAG(第 3 讲)、工具调用(第 4 讲)本质上都在回答同一个问题:往条件里放什么,能让我们想要的续写概率最大?


2. 解码:从分布里挑 token · Decoding

2.1 下一个 token 的分布

slides 展示了一个真实模型在 “The class was about” 之后的分布:‘to’ 0.3171、‘as’ 0.0491、‘the’ 0.036……;接着在 “was about to” 之后:‘be’ 0.1501、‘begin’ 0.0855……然后指出:“我们不一定总要选最可能的下一个 token。有不同的采样(解码)策略。”

我们的三元模型在同一个位置给出(代码 01):‘how’ 0.114、‘attention’ 0.082、‘decoding’ 0.082、‘agents’ 0.073、‘data’ 0.072……分布比真实模型平——它只学过几百句课程通知。

2.2 贪心、温度、top-k、top-p

  • 贪心(greedy):每步取概率最大的那个。确定、可复现,但单调,而且容易复读(§2.4)。
  • 温度(temperature):slides 的说法是”把所有概率除以某个温度 T 再重新归一化”;更准确地说是把 logit 除以 T(等价于 pi→pi1/Tp_i \to p_i^{1/T} 再归一化): PT(i)=exp⁡(zi/T)∑jexp⁡(zj/T)P_T(i) = \frac{\exp(z_i / T)}{\sum_j \exp(z_j / T)} T < 1 让分布更尖,T > 1 让分布更平,T → 0 退化成贪心。
  • top-k:只保留概率最大的 k 个 token,再归一化后采样。
  • top-p(nucleus sampling):按概率从大到小累加,保留”刚好凑够 p”的最小集合,再归一化后采样(Holtzman et al., 2020)。

代码 02 演示 1 在 “The class was about” 上调温度(“尾巴” = 在 T = 1 时概率不到 0.1% 的 354 个 token):

温度 T最大概率熵(nats)等效候选数 e熵e^{熵}top-p = 0.9 留几个尾巴的总质量
0.250.4601.856.370.0000
0.50.2182.5212.4120.0003
1.00.1143.2024.4160.0543
1.50.0684.2670.71420.2591
2.00.0415.02151.22420.4669

两点值得注意。第一,温度把概率从头部搬到尾部:T = 1.5 时,那几百个”本来几乎不可能”的 token 加起来占了 26%。第二,top-k 和 top-p 的区别在分布形状变化时才显出来:

上下文最可能的 tokentop-k = 5top-p = 0.9top-p = 0.99
‘Office hours are’(尖)‘on’ 0.90551177
‘Adaeze Corard studied under’(平:几十个人名)‘Pita’ 0.140524179

top-k 在尖分布上多留了 4 个本不该考虑的词,在平分布上又砍掉了大量合理的人名;top-p = 0.9 自适应地留 1 个或 24 个——这就是 nucleus sampling 被提出的理由。但 top-p = 0.99 在尖分布上放进了 177 个”平滑出来的尾巴”:阈值不是越接近 1 越安全。

2.3 束搜索

**束搜索(beam search)**同时保留 b 条”到目前为止总 log 概率最高”的候选,每步把每条都扩展一个 token,再留下最好的 b 条。b = 1 就是贪心。动机:贪心只看眼前,可能错过”先走一步不太好、后面好得多”的路。代价:每步算 b 次分布,时间和内存大约是贪心的 b 倍(slides:“束搜索的时间和内存复杂度更高”)。

代码 02 演示 3 在 8 个提示词上比较(括号里是总 log 概率):

提示词贪心束搜索 b = 4,α = 0
The class was abouthow to evaluate agents. (−5.22)同左
It was founded in2017 by Selin Quillow in Selholt. (−8.38)Room 100. (−4.43)
Its main rival isa city in Pellucia. (−5.86)Brencrest. (−3.88)

8 个里有 2 个,束搜索找到了概率严格更高的句子——但 “It was founded in Room 100.” 概率更高,却答非所问。原因:每多一个 token 总 log 概率只会更低,所以未归一化的束搜索偏爱短句;按长度归一(打分 = 总 log 概率 / 长度^α,α = 1)能纠正一部分。更深的教训是:“模型认为最可能的句子”不等于”最好的句子”。这也是开放式生成很少用束搜索、而翻译这类”要最可能答案”的短输出更常用它的原因。

2.4 质量与多样性;复读

代码 02 演示 2 用六种设置对 8 个提示词各生成 200 次:

解码平均 log P / tokendistinct-2不同输出数长文本复读(强迫写 40 个 token)
贪心−0.860.031840/40
T = 0.7−1.110.3471557/40
T = 1.0−1.740.4191757/40
T = 1.5−4.180.6791990/40
top-k = 5−1.090.26913534/40
top-p = 0.9−1.240.3671547/40

读法:“平均 log P / token”是模型给自己写的东西打的分,可以当作”像不像模型认为的正常文字”;distinct-2(不重复的二元片段比例)和”不同输出数”衡量多样性。两者此消彼长。T = 1.5 写出了这样的句子:“The class was about context engineering studied under Bram Lingate. Ostrsby known for work on tidal turbines. ecology.”——多样,但已经是胡话。

复读是贪心的典型病:一旦回到同一个上下文,就会走同一条路,于是陷入循环(Holtzman et al. 2020 把这类现象叫 neural text degeneration,并据此提出了 nucleus sampling)。top-k = 5 也常复读(34/40):每步只在 5 个候选里挑,很容易走回老路。top-p = 0.9 在多样性、复读率和质量之间取得了不错的平衡——这是它成为常用默认值的原因。

slides 的总结:每种采样策略有不同的取舍;温度和 top-p / top-k 帮助多样性;策略取决于任务——数学和代码更可预测(用低温度),创意写作可能受益于更高的多样性;策略还取决于预算(束搜索更贵)。

2.5 解码对智能体意味着什么

上面都在说”一句话”。智能体关心的是”一条几十步的轨迹”和”同一个任务跑很多次”。这里有两个第 1 讲埋下的伏笔。

(1) 尾巴在多步里累积。 设想模型写到 Action: 之后,下一个 token 的分布是:三个合法工具名(search / lookup / finish)共占 97%,剩下 3% 散在 60 个”不是工具名”的 token 上(拼错的名字、多余的空格、“Sure”、”```”……)。这是一个示意分布(sampling_lab.ACTION_DIST),但结构很典型。每步的出错概率是 ε,n 步里至少出错一次的概率是 1−(1−ε)n1 - (1 - \varepsilon)^n(代码 02 演示 4):

解码每步写出非法动作10 步30 步100 步
贪心0%0%0%0%
T = 0.70.24%2.3%6.8%21.1%
T = 1.03.00%26.3%59.9%95.2%
T = 1.0, top-p 0.950%0%0%0%
T = 1.0, top-p 0.992.06%18.8%46.4%87.5%
T = 1.311.8%71.5%97.7%100%

每步 3%,听上去很小;30 步之后接近六成的轨迹至少踩一次坑。这就是第 1 讲 §6.1”错误会累积”在解码层面的版本。三条工程对策:降温、用 top-p 砍掉尾巴(注意 0.99 砍不干净:合法 token 一共只有 97%)、以及在 harness 里做格式校验 + 让模型重试(第 1 讲 agent.py 的 malformed 处理),或者干脆用约束解码让非法 token 根本不可能出现(§6)。

(2) 温度怎样同时影响 pass@k、pass^k 和多数投票。 第 1 讲 §6.2 讲过:pass@k(k 次里至少一次对)衡量能力,pass^k(k 次全对)衡量可靠性。温度对两者的作用方向正好相反。为了能精确计算,代码 02 演示 5 用了一个”推理路径”模型(sampling_lab.py):每道题有 6 条推理路径,每条有一个 logit、通向一个答案;温度作用在路径上;答案的概率是所有通向它的路径概率之和。题目分四类(比例是我们设定的:40% / 20% / 25% / 15%):

  • easy:最可能的路径通向正确答案,正确答案也是总概率最大的答案。
  • trap:最可能的单条路径通向错误答案,但通向正确答案的路径加起来最多。
  • hard:正确答案有 8%–35% 的概率,但不是最多的。
  • impossible:没有任何路径通向正确答案。
解码pass@1pass@5pass@20pass^5多数票@5多数票@21多数票@∞
贪心(T = 0)42.2%42.2%42.2%42.2%42.2%42.2%42.2%
T = 0.345.3%63.7%73.8%31.0%44.0%42.1%42.0%
T = 0.747.1%72.6%82.6%23.3%49.0%49.4%46.8%
T = 1.047.4%74.8%84.0%20.6%50.5%53.2%60.0%
T = 1.547.5%76.2%84.5%18.6%52.3%56.2%60.0%
T = 3.047.4%76.9%84.7%17.0%53.2%57.8%60.2%

按题型拆开(T = 1.0):easy 题贪心 100%、单次采样 82%、pass^5 只有 50%;trap 题贪心 0%、多数票@∞ 100%;hard 题多数票@∞ 0%、pass@20 96%;impossible 题全是 0。

三个结论,每一个都比具体数字重要:

  1. pass@k 随温度上升,pass^k 随温度下降。 想”多试几次总能碰对”(有验证器时,§5.5),就要多样性;想”每次都对”(用户面前的产品),就要低温度。同一个温度旋钮,两个指标朝相反方向走。
  2. 多数投票需要”适中”的温度。T = 0.3 时样本几乎都走同一条最可能的路径,trap 题救不回来;T ≥ 1 时全部救回。trap 题正是 self-consistency(Wang et al., 2023)的直觉:“最可能的一条推理路径”不等于”最可能的答案”——把通向同一个答案的路径加起来(对推理路径求和 / 边缘化),比只走一条更准。论文报告在 GSM8K 上比贪心的 CoT 提高 17.9 个百分点。
  3. 多数投票救不了 hard 题(正确答案根本不是众数——投票甚至把贪心碰巧答对的那几道也投没了),只有”多采样 + 一个能认出正确答案的验证器”能救(§5.5)。impossible 题什么都救不了:采样只能在模型已有的分布里找,不能创造模型没有的能力。

一个简化要说清楚:真实模型的温度作用在每个 token 上,不完全等于”路径层面”的温度;这里为了能精确计算,把一整条推理路径当作一个整体来调温度。定性结论不变。

还有一个现实约束:本讲义默认的真实模型 claude-opus-5-5 不接受 temperature 参数(第 1 讲 §9)。在这种 API 上,“调成贪心”不是一个选项;一致性只能靠更确定的提示词、缓存相同的请求、或者多采样再投票来管理(代码 07 演示 3 让你亲眼看看)。


3. 语言模型的架构 · Architecture

3.1 简史

slides 列了一条线:n-gram 模型(2010 年代以前)→ 第一个神经语言模型(Bengio et al., 2003)→ 循环神经网络语言模型(Mikolov et al., 2010)→ LSTM(1997 年发明,2014 年前后在语言建模和 seq2seq 里流行)→ Transformer(Vaswani et al., 2017, Attention Is All You Need)。我们的 n-gram 替身就在这条线的起点。

从 n-gram 到神经网络,变的是”P(xt∣x<t)P(x_t \mid x_{<t}) 怎么算”:n-gram 只能查表(见过这个上下文才有数),神经网络用参数把相似的上下文映射到相似的表示,于是能泛化到没见过的组合(§4.1 会看到 n-gram 在这一点上有多吃力)。

3.2 decoder-only Transformer 的一层

slides 用十几页图一步一步地搭出了一层,并在开始前警告:“在这里请特别注意! 如果在这里跟丢了,这一讲剩下的很多内容会非常难懂。“我们按同样的顺序走一遍(attention.causal_attention 是对应的代码):

  1. 嵌入(embedding):每个 token 查表得到一个 d 维向量。
  2. 位置嵌入(positional embedding):加上一个表示”第几个位置”的向量(§3.3 解释为什么必须加)。
  3. 层归一化(layer norm):把每个向量的尺度调整到稳定的范围。
  4. Q、K、V:每个位置的向量分别乘三个矩阵 WQ,WK,WVW_Q, W_K, W_V,得到 query(我想找什么)、key(我是什么)、value(我带着什么信息)。
  5. 打分:位置 t 对位置 i 的分数是 qt⋅kiq_t \cdot k_i,所有分数排成矩阵 QK⊤QK^\top。
  6. 因果掩码(causal mask):未来位置(i > t)的分数设为 −∞——预测下一个 token 时不许偷看答案。
  7. 缩放:除以 dk\sqrt{d_k},免得维度高时分数太大、softmax 太尖。
  8. softmax:每一行变成加起来等于 1 的权重。
  9. 加权求和:权重乘 V,得到每个位置的输出。合起来: Attention(Q,K,V)=softmax ⁣(QK⊤dk+M)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^\top}{\sqrt{d_k}} + M\right) V
  10. 输出投影 WOW_O:投回模型维度。
  11. 残差连接(residual):把输入加回来——这一层只需要学”在原来基础上改什么”。
  12. 多头(multi-head):用多组 WQ,WK,WVW_Q, W_K, W_V 并行地做上面的事,各自关注不同的关系,再拼接、投影。
  13. 前馈网络(feed-forward):每个位置各自过一个两层的全连接网络(再加一次残差)。
  14. 叠 N 层,最后由 LM head 把每个位置的向量投到词表大小,softmax 得到 P(xt+1∣x≤t)P(x_{t+1} \mid x_{\le t})。

代码 03 演示 1 打印了一个随机初始化的注意力头在 “The class was about agents” 上的权重矩阵:每一行加起来是 1,右上三角全是 0(因果掩码)。权重本身没有语言学意义——训练会把 WQW_Q、WKW_K 调成让有用的位置得到高权重。

3.3 因果掩码与位置

代码 03 演示 2 做了两个小实验:

  • 把最后一个词换掉,前 4 个位置的输出变化:有因果掩码时恰好为 0;没有掩码(双向注意力,像 BERT)时变化 0.59。这正是 decoder 能一次并行算出所有位置预测的原因:位置 t 的预测只依赖 t 之前的 token,训练时把整句话喂进去,每个位置同时得到一个”预测下一个 token”的训练信号,而不会泄露答案。推理时它带来了 KV cache(§5.1)。
  • 打乱前 4 个词的顺序,最后一个位置的输出变化:不加位置编码时恰好为 0;加了正弦位置编码后变化 0.11。注意力是对一个集合加权求和,本身看不出顺序——“dog bites man”和”man bites dog”对它来说一样。位置信息必须加进去。

3.4 注意力 vs 循环:为什么 Transformer 赢了

slides 引用了原论文的表 1(每层的计算量、必须串行的操作数、任意两个位置之间的最长路径):

层类型每层计算量串行操作数最长路径
自注意力O(n2⋅d)O(n^2 \cdot d)O(1)O(1)O(1)O(1)
循环(RNN)O(n⋅d2)O(n \cdot d^2)O(n)O(n)O(n)O(n)

slides 的两句评语抓住了要点:“O(1) 的串行操作数对 GPU 上的并行训练是巨大的优势!“——RNN 必须一个 token 一个 token 地算,Transformer 一层里所有位置同时算;以及 “O(n²) 意味着长上下文变得昂贵。” 代码 03 演示 3 实测了乘加次数(d = 16):n 从 32 翻倍到 256,softmax 注意力每次 ×3.1、×3.5、×3.7(越来越接近 ×4,n² 项逐渐占主导),线性注意力每次正好 ×2.00。外推到 d = 128:n = 10 万时,单层单头的 softmax 注意力约 2.6×10¹² 次乘加,是线性注意力的约 780 倍。

3.5 线性注意力:用一个固定大小的状态换掉 KV cache

slides 的推导非常干净:

  1. “去掉 softmax”——于是一切都是线性的。
  2. 线性运算可以换结合顺序:(QK⊤)V=Q(K⊤V)(QK^\top)V = Q(K^\top V)。先算 K⊤VK^\top V(一个 d×d 的矩阵),而不是 QK⊤QK^\top(一个 n×n 的矩阵)。
  3. 加上因果性,K⊤VK^\top V 变成一个随时间累积的状态: St=St−1+vtkt⊤,ot=St qtS_t = S_{t-1} + v_t k_t^\top, \qquad o_t = S_t\, q_t 每来一个 token,状态更新一次;输出只需要当前状态。这就是 Katharopoulos et al.(2020)的标题:Transformers are RNNs。

于是每步的计算和内存都是常数 d2d^2,和已经读了多少 token 无关(attention.linear_attention_recurrent;自检确认它和”矩阵写法”的结果只差 2×10⁻¹⁶)。slides 的结论:“线性注意力快得多、内存占用更好……但是,它在更长的上下文上更吃力。”

为什么会吃力? 一个 d×d 的状态能”记住”的东西是有限的。代码 03 演示 4 做了一个联想回忆(associative recall)实验:写入 n 个 (key, value) 对(d = 32 维随机单位向量),再拿其中一个 key 去问,看能不能取回对应的 value。比较三种”记忆”:

nsoftmax 存的数线性存的数softmax线性·新线性·旧delta·新delta·旧
85121,024100%100%100%100%100%
322,0481,024100%98%100%91%53%
644,0961,024100%86%84%95%8%
1288,1921,024100%50%53%92%0%
25616,3841,024100%20%17%92%0%

(“新”= 最后写入的 16 条,“旧”= 最先写入的 16 条。)

  • softmax 注意力永远 100%:它把每条记录都原样存着(KV cache),读的时候用”软 argmax”挑出匹配的那条。代价是存储随 n 增长。
  • 普通线性注意力把所有记录叠加进一个 32×32 的矩阵。取回时 v^=vj(kj⋅kj)+∑i≠jvi(ki⋅kj)\hat v = v_j (k_j \cdot k_j) + \sum_{i \ne j} v_i (k_i \cdot k_j):第一项是信号,第二项是”串扰”。n 超过 d 以后串扰淹没信号,新旧记录一起变模糊。
  • delta 规则(slides 图里的”清掉不重要的记忆”;DeltaNet 一类模型的更新方式):写之前先看这个 key 上现在记着什么,只写差值:S←S+β(v−Sk)k⊤S \leftarrow S + \beta (v - S k) k^\top。它始终记得最近写入的内容(九成以上),但 n 超过 d 以后,早期的记录几乎全被覆盖——像一个容量固定、会遗忘的记忆。

对智能体的含义很直接:“在一大段上下文里精确找回很久以前的一个细节”——第一轮对话里用户说过的约束、几十步之前读过的一个文件名——正是纯线性注意力的弱项。slides 引用的 RULER(Hsieh et al., 2024)发现,很多声称支持长上下文的模型在”大海捞针”之外的更难的长上下文任务上,性能随长度明显下降。

3.6 关于上下文长度

slides 专门有一页:“Transformer 和线性注意力本身都没有强制一个严格的上下文长度……除了模型处理位置嵌入的方式。实践中,模型需要学会使用位置嵌入,所以我们受限于训练时用过的上下文长度。“代码 03 演示 2 已经说明位置信息是”加进去”的;模型只在训练见过的范围内学会了怎样解读它。超出这个范围,行为就不可预测了(这也是为什么各种”位置插值”技术要专门去延长上下文)。标称的上下文长度是”能塞进去多少”,不是”能用好多少”——§7.2 还会回到这一点。

3.7 混合架构,以及这一切为什么和智能体有关

slides 把两者并排比较:

全 softmax 注意力线性注意力
上下文长度上 O(n2)O(n^2)O(n)O(n)
所有 token 都能回看所有之前的 token新 token 只和一个压缩的状态交互
有利于长上下文长上下文更难
KV cache(内存)随 n 增长状态的内存是常数

结论是”实践中人们把全局注意力和线性注意力混合起来”(slides 举的例子是 Kimi K3)。代码 03 演示 5 算了一笔账:一个 32 层的模型在 n = 128k 时,每生成一个 token 的注意力乘加——32 层全注意力约 1.07×10⁹;8 层全注意力 + 24 层线性约 2.69×10⁸(降到约四分之一);全部线性约 1.05×10⁶,但会丢掉远处的细节。留下少数全注意力层,就保住了”精确回看任意位置”的能力。2025 年起不少开源模型(例如 Qwen3-Next、Kimi Linear)都采用了”约 3 层线性 + 1 层全注意力”一类的比例。

slides 用四句话回答”这一切为什么对智能体重要”:

  1. 智能体要处理极长的上下文(几十步的轨迹、整个代码仓库)。
  2. 推理效率是构建智能体系统时的一个设计考量(§5)。
  3. 了解底层架构能帮你为应用挑对模型——同样标称 128k 上下文的两个模型,长上下文召回可能差别很大,要自己测。
  4. 围绕你所用模型的长处和短处来设计 harness 和脚手架,会让你成为更好的智能体工程师。

4. 训练:从预训练到后训练 · Training

4.1 损失:交叉熵

训练目标只有一句话:让模型给训练数据里真实出现的下一个 token 尽量高的概率。对每个位置,损失是

Lt=−∑w∈V1[w=xt]⋅log⁡P(w∣x<t)=−log⁡P(xt∣x<t)\mathcal{L}_t = -\sum_{w \in V} \mathbb{1}[w = x_t] \cdot \log P(w \mid x_{<t}) = -\log P(x_t \mid x_{<t})

slides 把这个式子拆开标注:“对词表里的每个 token”、“这个 token 是数据里的那个时为 1,否则为 0”、“这个 token 的 log 概率”——于是求和里只剩下真实 token 那一项。slides 的例子:训练数据是 “The class was about how agents work……”,模型在 “The class was about” 之后给 ‘how’ 的概率是 0.0124,损失就是 −log⁡(0.0124)-\log(0.0124)。(slides 的数值 1.907 用的是以 10 为底的对数;训练时一般用自然对数,约 4.39——两者只差一个常数倍,不影响优化。)

在我们的模型上算一遍(代码 01 演示 2):句子 “Homework 2 is due on Friday at noon .” 每个 token 的损失从 0.095(’</s>‘,几乎确定)到 3.252(‘Homework’,句首很多可能);平均 1.191 nats,困惑度 e1.191=3.29e^{1.191} = 3.29——模型平均像是在 3.3 个等可能的选项里挑。

“看得更远”不一定更好。 n 元模型的 n 就是它能看多远。代码 01 比较了 n = 1 到 5(训练集 vs 留出测试集的平均损失):

n不平滑·训练不平滑·测试add-0.01·测试插值·训练插值·测试
14.629∞(16 / 2083 个 token 概率为 0)4.6884.6334.657
21.360∞(123 个)1.8501.5651.858
30.956∞(205 个)2.1001.1351.638
40.813∞(234 个)2.5390.9391.603
50.735∞(270 个)2.9860.8501.615
  • 不平滑(数次数就是让训练损失最小的解——最大似然):n 越大训练损失越低,但测试集上总有”训练时没见过的组合”,模型断言它们概率为 0,损失是无穷大。
  • add-k 平滑在 n = 2 最好,n 再大反而变差:上下文越长,每种上下文见过的次数越少,估计越不可靠——过拟合。
  • 插值(把长短上下文的估计按权重混合)在 n = 4 附近最好。

这张表解释了为什么需要神经网络:Transformer 用共享参数把相似的上下文映射到相似的表示,于是能对几千个 token 长、从没见过的上下文给出合理的预测——但它最小化的是同一个交叉熵。

构建者视角:训练目标是”像训练数据那样续写”,不是”说真话”。模型不知道答案时,“编一个看起来合理的”往往比”说不知道”更符合这个目标——这是幻觉的一个根源(第 1 讲 §5.1),也是后训练要花大力气纠正的东西(§4.4)。

4.2 预训练数据:抽取、过滤、去重

slides 引用 DCLM(Li et al., 2024)的流水线:文本抽取(从 HTML 里取出正文)→ 基于规则的过滤 → 去重 → 语言识别 → 质量过滤(用一个分类器给网页打分)→ 最终的训练集。DCLM 用这条流水线做出的数据集训练的 7B 模型,在 MMLU(5-shot)上达到 64%。slides 还贴了几段 The Pile(Gao et al., 2020,800 GB)里的原始样本:动漫论坛的帖子、Fortran 手册、“注册后查看全文”的样板文字、烹饪学校的报道交织在一起——这就是”互联网规模的数据”本来的样子,流水线的每一步都在和这种噪声搏斗。

为什么要去重? 代码 01 演示 3 往训练数据里塞了同一句话 50 次(“The final exam is on Friday in Room 300 .”):

重复次数P(这句话)P(Friday | quiz … on)贪心续写 ‘The quiz on agents is on’课程测试损失
05.7×10⁻¹⁵0.206Friday at 10am.1.151
13.3×10⁻⁶0.216Friday at 10am.1.151
501.9×10⁻³0.460Friday in Room 300.1.162

两件事:模型把高频重复的文字背了下来(概率涨了近三个数量级);而且这种扭曲会串到别的句子——问小测验在哪天,它也答 “Friday in Room 300”。测试损失只差 0.011,在总体指标上几乎看不出来。真实的预训练里,重复的样板文字、被大量转载的文章、数据集泄露进训练集的 benchmark 题目,都会造成同样的问题(第 12 讲会讲 benchmark 污染)。

4.3 数据配比与中期训练

训练数据由很多来源组成(网页、代码、数学、书、论文……),每种来源占多少是一个独立的、影响巨大的旋钮(slides 引用了 Olmix 这类专门研究配比的工作)。代码 01 演示 4 固定 600 句训练数据,只改”课程通知”的占比 w:

w00.050.100.250.500.750.901.0
课程测试损失7.0672.9722.1801.4021.2771.1401.1101.097
百科测试损失2.0822.0872.1822.1892.3592.8913.5576.906

只放 5% 的课程数据,课程测试损失就从 7.07 降到 2.97,之后收益递减;两个领域此消彼长。**中期训练(midtraining)**是同一个旋钮换了个时机:在预训练的后期,把目标领域(数学、代码、智能体轨迹)的比例调高。slides 引用的几项 2025–2026 年的工作都指向同一个方向:把更多目标领域的数据混进”预训练”阶段,往往比只在最后微调更有帮助。

4.4 后训练:指令微调、人类偏好、可验证奖励

预训练得到的模型会”续写”,但不会”听话”。后训练把它变成一个助手:

  • 指令微调(instruction finetuning / SFT):在”指令 → 好的回答”这样的数据上继续训练。slides 引用的 FLAN 系列工作(Chung et al., 2022)在 1.8K 个任务上做指令微调,Flan-PaLM 540B 平均比 PaLM 540B 高 9.4 个百分点。
  • 从人类偏好中学习(RLHF):InstructGPT(Ouyang et al., 2022)的三步——收集示范做 SFT;让人类对多个回答排序、训练一个奖励模型;用强化学习优化策略。论文最出名的结果:人类评估者更喜欢 1.3B 参数的 InstructGPT 的输出,而不是 175B 的 GPT-3。
  • 人类反馈的问题(slides 引自 CS329X):标注者可能不可靠;每个人的偏好不同;会给模型优化带来坏的激励——谄媚(sycophancy,顺着用户说)和故作权威(authoritativeness,听起来很确定);数据本身的来源可能不合伦理。
  • 可验证奖励的强化学习(RLVR):把奖励换成能自动验证的东西——单元测试、Lean 证明、精确匹配、系统状态……对数学和代码特别好用(slides 引用 DeepSeekMath 的 GRPO;DeepSeekMath 7B 在 MATH 上达到 51.7%,不借助外部工具和投票)。但 slides 提醒:“要小心验证器的设计”——模型会钻验证器的空子(奖励篡改,第 1 讲 §6.5)。

4.5 面向智能体的训练

最后一步是专门为智能体收集训练数据。slides 的例子是 SWE-smith(Yang et al., 2025):自动地在真实代码仓库里”制造 bug”(让原本通过的测试失败),得到大量”修 bug”任务实例(来自 128 个 GitHub 仓库的 5 万个),再让智能体去解、收集成功的轨迹,用来训练。用这些轨迹训练的 SWE-agent-LM-32B 在 SWE-bench Verified 上达到 40.2% pass@1。第 10–11 讲会专门讲”智能体需要什么数据”。

4.6 构建者视角:训练决策带来的脾气

把 §4 和你搭智能体时会遇到的现象对上:

训练上的决策你会看到的现象对策(哪一讲)
目标是”像数据那样续写”不知道时会编;对训练数据之后的事一无所知(知识截止)检索(第 3 讲)、工具(第 4 讲)、允许说”不知道”
数据里的重复与配比擅长常见的格式和领域,对罕见格式、罕见语言更差用模型”见惯了”的格式(§7.6 引的 Anthropic 建议)
后训练学到的对话 / 工具调用格式偏离原生格式时更容易出错用 API 原生的工具调用格式,或者足够清楚的文本协议(第 4 讲)
人类偏好的副作用谄媚:用户一反驳就改口;回答听起来很确定评估时专门测(第 13 讲);不要用”你确定吗”当验证器
RLVR 与验证器在可验证的任务上很强;会钻验证器的空子把评估器当成需要防御的对象(第 12–14 讲)

5. 推理:一个 token 是怎样生成出来的 · Inference

5.1 prefill 与 decode;KV cache

一次 API 调用在服务器上分两个阶段:

  • prefill:把整个提示词一次性送进模型。因为有因果掩码(§3.3),所有位置可以并行计算,同时把每个位置每一层的 K、V 存下来——这就是 KV cache。
  • decode:一个一个地生成。每生成一个 token,只需要为这一个新 token 算 q、k、v,然后和缓存里所有的 K、V 做注意力。它只能串行:第 t+1 个 token 要等第 t 个生成完。

为什么能缓存?因为因果掩码保证过去位置的 k、v 永远不会因为后面的 token 而改变。代码 04 演示 1 在一个单层单头的小模型上实测(提示词 64 个 token,再生成 64 个):

方式乘加次数
每步从头重算(没有缓存)24,513,536
prefill(读 64 个提示词 token,填缓存)115,712
decode(生成 64 个 token,用缓存)246,784

两种方式生成的 64 个输出逐元素完全相同(差为 0),有缓存时少算 68 倍。KV cache 是纯粹的优化——代价是内存:缓存随上下文线性增长。还有一个对构建者特别重要的推论:改动上下文前面的任何一个 token,它后面的缓存就全部作废,要重新 prefill。§7 的”按顺序往后加”就是从这里来的。

5.2 GPU 与屋顶线:decode 在等内存

slides 用屋顶线模型(roofline)解释 prefill 和 decode 的不同。一块 GPU 有两个上限:算力(每秒多少次浮点运算)和显存带宽(每秒能从显存读多少字节)。一段计算的算术强度 = 运算次数 / 读的字节数。强度低于”屋脊点”(算力 ÷ 带宽)时,GPU 在等内存(带宽受限);高于它时,才是在等算力(算力受限)。

代码 04 演示 2 按公式算了一个 8B 参数、BF16 的模型在一块 H100 级别 GPU(约 989 TFLOPS、3.35 TB/s)上的理想上限(真实系统只能达到其中一部分;KV cache 按 32 层、8 个 KV 头、头维 128 计,每个 token 128 KiB——和 Llama 3 8B 的配置相同)。屋脊点约 295 FLOP/字节:

  • prefill:一次处理 n 个 token,权重只读一遍,算术强度约等于 n。n = 1,000 时已是算力受限,理想耗时约 16 ms;n = 10,000 时约 162 ms。首 token 延迟(TTFT)主要由提示词长度决定。
  • decode:每步只为每个请求算 1 个 token,却要把 16 GB 权重整个读一遍。batch = 1 时算术强度约 1,远低于屋脊点——GPU 大部分时间在等内存。理想上限约每秒 206 个 token。

把请求凑成一批(batch)就能”读一次权重服务很多请求”:

batch每步耗时(TPOT)吞吐(token/s)KV cache 占用
14.9 ms2060.3 GB
85.4 ms1,4812.1 GB
327.3 ms4,3958.4 GB
12814.8 ms8,65333.6 GB
25624.8 ms10,31967.1 GB

(每个请求已有 2,000 token 的上下文。)b 从 1 到 8,吞吐涨了 7.2 倍、每个 token 的延迟几乎不变;但每个请求的 KV cache 也要读、要存,b = 256 时 KV cache 已是权重的四倍多,吞吐只比 b = 128 多 19%。这就是长上下文贵、显存紧的原因(vLLM 的 PagedAttention 专门管理这块内存)。

构建者要记住的三个指标:TTFT(首 token 延迟,主要看提示词长度)、TPOT(之后每个 token 的间隔,看 decode 和服务器负载)、吞吐。也由此理解定价:输出 token 通常比输入 token 贵好几倍(例如每百万 token 输入 3 美元、输出 15 美元),因为 decode 用硬件用得很”浪费”;而被缓存命中的输入 token 便宜得多(§7.4),因为它连 prefill 都省了。

5.3 推测解码:小模型打草稿,大模型验证

decode 慢在”一次只能出一个 token”,可验证一串现成的 token 可以像 prefill 一样一次并行做完。推测解码(Leviathan et al., 2023;Chen et al., 2023)利用了这一点:

  1. 小模型(draft)便宜地连续猜 γ 个 token;
  2. 大模型(target)一次前向,算出这 γ+1 个位置上自己的分布 p;
  3. 从左到右检查每个猜测 x:以概率 min⁡(1,p(x)/q(x))\min(1, p(x)/q(x)) 接受;一旦拒绝,就从”剩余分布” norm(max⁡(0,p−q))\text{norm}(\max(0, p - q)) 里重新抽一个,丢掉后面的猜测;
  4. γ 个全被接受,就从 p 免费多抽一个。

这个接受规则在数学上保证:输出的分布和只用大模型逐个采样完全一样——它是纯粹的加速。代码 04 演示 3 用二元模型当草稿、四元模型当目标:

领域γ草稿被接受的比例每次大模型调用产出的 token估计加速(草稿成本 = 5%)
百科(模板化)1 / 2 / 4 / 869% / 63% / 44% / 32%1.62 / 1.96 / 2.33 / 2.47×1.54 / ×1.79 / ×1.98 / ×1.94
课程通知1 / 2 / 4 / 866% / 52% / 39% / 27%1.56 / 1.83 / 2.15 / 2.21×1.48 / ×1.67 / ×1.83 / ×1.75

γ 越大,每次大模型调用产出的 token 越多,但越往后越难猜对:存在一个最合适的 γ。模板化的百科更好猜——slides 说”有些领域比别的更可预测”。无损的验证:对 “Office hours are on” 的下一个 token 各采样 3,000 次,和四元模型精确分布的总变差距离,直接用四元模型采样是 0.044(纯抽样噪声),推测解码是 0.047,只用二元模型是 0.477。论文里的真实加速:Leviathan et al. 在 T5-XXL 上报告 2–3 倍,Chen et al. 在 Chinchilla 70B 上报告 2–2.5 倍。

5.4 测试时扩展:多想一会儿、多试几次

“推理时多花算力换准确率”有两条路。

第一条:一次想得更久。 从 chain-of-thought 提示(Wei et al., 2022)到专门训练的推理模型。slides 的例子:一件 80 美元的夹克先涨价 25%、再在新价格上打 25% 的折,最后多少钱?传统 LLM 直接算出 80 × 1.25 = 100、100 × 0.75 = 75;推理模型会先冒出”加 25% 再减 25% 好像抵消了,是 80 美元”,接着”等等……抵消要求两个百分比用同一个基数,这里折扣作用在涨价之后的价格上”,然后算出 75 美元并自己检查一遍。能自我纠错的长推理是 RL 训练(§4.4)出来的。

各家 API 都提供了”推理强度”档位:例如 GPT-OSS 在系统提示里写 “Reasoning: low / medium / high”;本讲义默认的 Claude 模型用 effort 参数(low / medium / high / xhigh / max,见 lecture01/real_llm.py)。slides 介绍了两种训练出”可控强度”的做法:在 RL 的奖励里惩罚”答错且超出预算”的回答(低档位的惩罚系数更大);或者用不同档位的教师模型做多教师蒸馏。对构建者的含义:推理强度是一个和温度一样的旋钮——简单的分类、抽取用低档,难的规划、调试用高档;它直接影响延迟和输出 token 的成本。

第二条:多试几次。

  • 有验证器时:采样 k 次,只要有一次通过验证就算成功(pass@k / “覆盖率”)。Large Language Monkeys(Brown et al., 2024)发现覆盖率随采样数稳定增长:在 SWE-bench Lite 上,单次采样解决 15.9%,采样 250 次解决 56%,超过了当时单次采样的最好成绩 43%。
  • 没有验证器时:只能多数投票(self-consistency)。

代码 02 演示 6 在 §2.5 的推理路径题上(T = 1)比较了两者:

k141664256
覆盖率 pass@k(有完美验证器)47.4%72.0%83.3%85.0%85.0%
多数票@k(没有验证器)47.6%49.8%52.5%55.2%–
多数票的极限 k → ∞60.0%

有验证器时,覆盖率一路涨到”不是 impossible 的题”的比例;没有验证器时,多数投票的上限是”正确答案是众数”的题的比例。工程含义:先问”我能不能便宜地验证一个答案”——单元测试、精确匹配、执行结果、schema 校验。能,就值得多采样;不能,多采样的收益很有限(第 9 讲会讲怎样按难度分配推理算力)。


6. 结构化输入输出 · Structured I/O

6.1 为什么需要一份”合同”

slides 用 BAIR 那张复合 AI 系统的图提出动机:系统里的组件之间要通信,就需要一份共同的合同。下游代码要的是 {"name": ..., "email": ..., "intent": ...} 这样的对象,LLM 写的却是文字。怎样可靠地从文字拿到对象?

本节的任务取自 slides 里 DSPy 的例子:从一条短消息里抽取联系人信息——名字(字符串)、邮箱(字符串或 null)、意图(“meeting” / “intro” / “follow-up” 之一)。代码 05 生成了 200 条带标准答案的消息,其中 25 条的意图不在 schema 里(改期、取消)——真实系统里,schema 漏掉某些情况是常态。

模拟模型(structured.ToyExtractor)的行为分两层:内容(名字和邮箱读对的概率 0.97,意图读对 0.9;意图在 schema 之外时,80% 写出它自己的词,比如 “reschedule”)和写作习惯(开场白 12%、```json 围栏 10%、单引号 4%、尾逗号 3%;没有邮箱时写 Python 的 None 12%、写字符串 “N/A” 8%)。这些概率都是旋钮。

6.2 三种做法

  • A. 提示词里要求 JSON,直接 json.loads。 最简单,任何 API 都能用。
  • B. 宽松解析 + 校验 + 重试。 先从回复里抠出 {...}(去掉开场白和围栏),json.loads 不行就试 Python 的字面量语法(ast.literal_eval:只解析字面量、不执行代码,对不可信输入是安全的;千万不要用 eval);再按 schema 校验;失败就把具体的错误说明发回去,让模型重写。
  • C. 约束解码(constrained decoding)。 slides:“强迫语言模型输出某种特定的结构。“做法是把 schema 编译成一个状态机:每一步只允许语法允许的 token,其余 token 的概率直接置零(slides 引用的 SGLang 用有限状态机实现这一点)。structured.grammar_for 把我们的 schema 编译成 18 个位置的规则:{、"name"、:、空格、<字符串>、,、空格、"email"……其中 15 个位置是唯一确定的字面量——这些位置根本不需要问模型,直接写上(SGLang 的”压缩状态机” / jump-forward)。

6.3 实验:各自怎样失败

代码 05 演示 1 先看同一条消息(没有邮箱)的几种写法:

概率回复json.loads宽松解析 + 严格 schema
0.515{"name": "Ada", "email": null, "intent": "follow-up"}通过通过
0.077{"name": "Ada", "email": None, ...}失败通过
0.070Sure! Here is the JSON:⏎{...}失败通过
0.057```json⏎{...}⏎```失败通过
0.052{..., "email": "N/A", ...}通过schema 不通过
0.021{'name': 'Ada', 'email': null, ...}失败解析失败
0.016{..., "intent": "follow-up",}失败解析失败

“N/A” 是合法的 JSON——任何解析器都接受,只有 schema 给邮箱加了格式限制才拦得住。

演示 2 在 200 条消息上比较五种做法(A/B 按温度 1 整段采样;C 在合法 token 里按温度 1 采样,可比):

做法第一次就是合法 JSON对静默错误被标记平均调用
A 直接 json.loads,不重试67%44.5%14.0%41.5%1.00
A’ 直接 json.loads,最多重试 2 次70%74.5%16.5%9.0%1.55
B 宽松解析 + 校验,最多重试 2 次71%78.5%15.5%6.0%1.23
C 约束解码(严格 schema)100%76.5%23.5%0.0%1.00
C’ 约束解码(邮箱不限格式)100%70.5%29.5%0.0%1.00

(“静默错误” = 通过了校验但内容是错的;“被标记” = 系统知道这条失败了。)

读这张表:

  1. 直接解析很脆弱:三分之一的回复连 json.loads 都过不了。重试救回大部分,代价是更多调用;宽松解析第一次就能解析更多回复,所以调用更少、“对”的比例更高。
  2. 约束解码永远合法、只调用一次,而且更快:18 个 token 里只有 3 个需要模型做选择。
  3. 但约束解码的”被标记”永远是 0。 它不可能产生格式错误,于是也不可能报告”模型想说的东西 schema 里没有”。
  4. schema 没写的,它帮不上忙:邮箱不限格式时(C’),11 条没有邮箱的消息被写成了 “N/A”。

演示 3 专门看那 25 条 schema 之外的消息:

做法被标记输出的意图(条数)
B 宽松解析 + 重试12follow-up 4, meeting 9
C 约束解码0follow-up 4, intro 2, meeting 19
C 约束解码(schema 加上 “other”)0follow-up 1, meeting 2, other 22
B 宽松解析 + 重试(schema 加上 “other”)0follow-up 1, meeting 1, other 23

做法 B 只标记了 12 条:模型三次都写 “reschedule” 时,校验失败,错误说明写得清清楚楚——“‘intent’ must be one of [‘meeting’, ‘intro’, ‘follow-up’], got “reschedule""——人一看就知道 schema 漏了情况。另一半在某次重试时写出了一个合法值就被接受了:重试直到通过校验,本身也会把”不知道”洗成”错的”(真实模型读到”必须是三者之一”的错误说明后,只会更倾向于挑一个)。约束解码连这一半的信号都没有:模型想写 “reschedule”,语法不允许,它就在合法值里挑了一个——静默地。给 schema 一个出口(“other” / “unknown”)之后,两种做法都能正确地说”这不是三类之一”。

结论:约束解码保证格式,不保证内容;它让 schema 成为唯一的真相,所以 schema 必须覆盖所有真实情况,包括”都不是”。还有一个现实约束:约束解码要能控制解码器——自己部署的开源模型可以(SGLang、vLLM、Outlines 等),托管的 API 要看服务商是否提供”结构化输出”或”严格的工具调用”功能。做法 B 则对任何 API 都可用。实践中两者常常组合:能约束就约束格式,再做内容校验(例如邮箱真的出现在原文里吗?)。

有研究讨论过”格式限制会不会损害推理质量”(例如 Tam et al., 2024 Let Me Speak Freely?),也有后续工作认为差异主要来自提示词写法。一个稳妥的做法是:需要推理的地方先让模型自由地想,再把结论填进结构里——Anthropic 的 Building Effective Agents 也建议”给模型足够的 token 去思考,不要让它一开始就把自己写进死角”(§7.6)。

6.4 DSPy 签名:从”写提示词”到”声明要什么”

slides 展示了 DSPy 的签名(signature):你只声明输入字段、输出字段和类型——

class ExtractContact(dspy.Signature):
    """Extract contact info."""
    message: str = dspy.InputField()
    name: str = dspy.OutputField()
    email: Optional[str] = dspy.OutputField()
    intent: Literal["meeting", "intro", "follow-up"] = dspy.OutputField()

——框架把它渲染成提示词。slides “揭开了盖子”,展示了渲染出来的样子:每个字段用 [[ ## 字段名 ## ]] 单独占一段,并附上类型说明。代码 05 演示 4 用 structured.render_signature 仿写了这个格式,再用 parse_fields 按标记切分回复:规范的回复、带开场白和 None 的回复都能正确解析,漏了字段能精确报出 “missing field ‘intent’”。

这种格式的好处:值不需要放进 JSON 的引号里,模型不用处理转义(多行代码、带引号的文字放进 JSON 很容易出错);坏处是它仍然是”解析 + 校验”这条路。更重要的是思想上的转变:提示词是从签名生成的,不是手写的——于是框架可以自动地改写、优化它(第 5 讲从零实现一个迷你 DSPy,第 9 讲讲提示词优化)。

6.5 约束模型的另一种方式:小分类器

slides 的最后两种”约束”:分类器(在嵌入上训练一个小的分类头,输出天然只能是几个类别之一)和所谓的 “System One” 模型(大致是”选项可调的分类器”,用来做快速的决策)。代码 05 演示 5 从零训练了一个朴素贝叶斯(400 条标注数据)只做意图分类:在这个模板化的玩具数据上准确率 100%,一次 LLM 调用都不用;LLM + 宽松解析 + 重试(schema 含 “other”)的意图准确率是 90%(由模拟模型的”读对率”旋钮决定)。真实数据更杂,但取舍是一样的:小分类器便宜、快、不会有格式错误,但需要标注数据、类别是死的;LLM 零样本、能同时抽取和分类、能说”都不是”。一个决策如果只是”从固定几类里选”,先想想要不要用 LLM。

6.6 工具调用就是结构化输出

slides 用 Toolformer(Schick et al., 2023)收尾:模型在文字里插入 [Calculator(400 / 1400) → 0.29] 这样的 API 调用。今天的”函数调用 / 工具调用”本质上是同一件事——模型输出一个”工具名 + 符合 JSON Schema 的参数”的结构化对象,harness 解析、校验、执行。本节的所有教训(格式会坏、要校验、重试会”屈服”、schema 要覆盖所有情况、约束解码只保证格式)在第 4 讲会原封不动地再出现一次。


7. 上下文工程 · Context engineering

7.1 从”提示词工程”到”上下文工程”

Anthropic 的 Effective Context Engineering for AI Agents(2025)开篇就说:提示词工程关心”怎样写好一段指令”,上下文工程关心”在每一次调用时,怎样在有限的上下文里放进最合适的那一组 token”——系统提示、工具、示例、消息历史、外部数据,全都算。对智能体来说后者更关键,因为智能体在循环里运行,每一步都会产生更多可能相关的信息,必须周期性地决定留什么、删什么。

文章的核心比喻:上下文是一种有限的注意力预算。LLM 和人一样,信息越多越难聚焦;Transformer 里 n 个 token 之间有 n² 对关系要处理(§3.4),而且训练数据里短序列比长序列多得多,模型对长距离依赖的”经验”更少。所以:找到最小的一组高信号的 token,让期望的结果最可能发生。

slides 列出了智能体上下文里的东西:系统提示、消息历史、记忆、工具调用结果、附件。

7.2 为什么不能”全塞进去”:context rot

slides 的标题是 “Why not just put everything? Context Rot”,引用了 OOLONG(2025)等评估长上下文推理和聚合能力的工作。Chroma 的一份技术报告(Context Rot, 2025)系统地测了很多模型:随着输入 token 增加,模型准确地使用上下文信息的能力下降——即使任务本身没有变难。再加上 RULER(§3.5)和”lost in the middle”(Liu et al., 2024:放在长上下文中间的信息最容易被忽略;第 3 讲细讲),结论一致:标称的上下文窗口是”能塞进去多少”,不是”能用好多少”。

代码 06 演示 1 先看第 1 讲 ReAct 智能体(60 道 2–3 跳题)最后一次调用时上下文里装着什么:

组成部分平均 token占比
系统提示(规则 + 动作说明 + 1 个示例)29760%
题目112%
模型自己写的 Thought / Action8718%
工具结果 Observation9820%

平均每道题调用 4.7 次;最后一次调用的上下文约 532 token,而整条轨迹累计发送了约 2,019 token(3.8 倍)——每一步都重发整个历史,n 步的总量 ∝ n²。短任务里系统提示占大头;步数一多,工具结果会成为主要部分。

7.3 实用原则

slides 强调”构建上下文没有唯一的方法,这是你在 HW1 里要自己实验的东西”,然后给了几条实用的建议;Anthropic 的文章展开得更细。合在一起:

系统提示:找到合适的”高度”。 两种失败:一种是把复杂、脆弱的 if-else 逻辑硬写进提示词;另一种是只给模糊的高层指导、以为模型和你有同样的背景。好的系统提示在两者之间:具体到能引导行为,又灵活到给模型留出判断的空间。用 XML 标签或 Markdown 标题分节(背景、指令、工具指南、输出格式)。追求”能完整描述期望行为的最小信息集”——最小不等于最短。

工具:避免臃肿。 slides:“有时候几个工具就够了。Pi 只有 read、write、edit 和 bash 四个。“Anthropic 的文章说最常见的失败之一是工具集臃肿——功能重叠、让模型不知道该用哪个:“如果一个人类工程师都不能明确地说出某个情况下该用哪个工具,就别指望 AI 智能体能做得更好。“每个工具的说明每次调用都要重发(演示 1 里系统提示占 60%),工具越多越贵。

示例:少而典型。 与其在提示词里塞一长串边界情况,不如挑几个多样的、典型的例子。

检索:用的时候再取(just-in-time)。 slides:“用检索找回最相关的记忆或文件——下一讲细讲。“Anthropic 的文章更进一步:智能体可以只在上下文里放轻量的标识(文件路径、存好的查询、链接),需要时再用工具加载。Claude Code 是一个混合的例子:CLAUDE.md 一开始就放进上下文,其余文件用 glob、grep 之类的工具按需查找。代价是按需检索比预先加载慢,而且需要好的工具和指引。

按顺序往后加:不要让 KV cache 失效。 slides 专门画了一页:如果在上下文前面插入或修改了一点东西(比如开头多了一个 “Actually”),它后面所有位置的位置嵌入都变了,整段 KV cache 作废。动态的内容放在最后。

用评估来决定。 slides:“你可以搭 benchmark / eval 来测试你的改动。“上下文的每一个选择——放不放示例、工具说明写多长、什么时候压缩——都应该用数据而不是直觉来定(第 12–13 讲)。

长时程任务的三种技术(Anthropic 的文章):

  1. 压缩(compaction):上下文快满时,让模型把历史总结成一段,用总结开启新的上下文。Claude Code 的做法是保留架构决策、未解决的 bug、实现细节,丢掉冗余的工具输出,再附上最近访问的 5 个文件。最轻量的一种压缩是清理旧的工具结果:深埋在历史里的工具原始输出,智能体很少需要再看。slides 指出压缩的难点:“到需要压缩时,上下文已经非常长了。新的决定是:新上下文里留什么。留太多,很快又要压缩;丢太多,可能丢掉完成任务所需的关键信息。”
  2. 结构化笔记(note-taking):智能体定期把要点写到上下文之外(一个 NOTES.md、一个待办列表),需要时再读回来。文章举的例子是 Claude 玩宝可梦:在几千步里记着训练进度、地图、战斗策略。
  3. 子智能体(sub-agent):主智能体做规划,把聚焦的子任务交给子智能体,每个子智能体在自己干净的上下文里可能用掉几万 token,但只交回一段 1,000–2,000 token 的浓缩总结。

7.4 实验:36 步的长任务,9 种策略

代码 06 演示 2 用第 1 讲的虚构百科造了一个长任务:“这 12 家公司里,哪一家的总部城市建城最早?“每家公司 3 步(search 公司 → search 城市 → lookup settled),共 36 次工具调用,最后比较 12 个年份。动作序列是写死的(我们要比较的是上下文策略,不是决策能力)。成本用相对价格计算:输入 1、写缓存 1.25、读缓存 0.1、输出 5(比例取自 Anthropic 的公开定价)。前缀缓存的模型:每次请求如果和之前某次请求有公共前缀,这部分按读缓存价计。20 个随机实例的平均:

策略调用峰值上下文总输入 token缓存命中成本(无缓存)成本(前缀缓存)可见事实答对
全部保留(基线)371,92438,49995%42,73010,28712.0/1220/20
全部保留 + 系统提示开头放时间戳371,93738,9801%43,21152,41712.0/1220/20
清理旧工具结果(只留最近 6 条)371,44230,86069%35,09118,1692.2/120/20
清理 + 笔记写在助手消息里371,55232,84069%37,67119,66912.0/1220/20
同上,但攒够 12 条才清一次371,64935,46990%40,30012,25812.0/1220/20
清理 + 笔记块放在系统提示里371,54632,81252%37,04325,60212.0/1220/20
压缩(超过 1,200 token 就总结)381,25224,50492%29,0459,27912.0/1220/20
压缩(有损:每个事实 90% 保留)381,25224,42692%28,9419,23911.3/1220/20
子智能体(每家公司一个,只回一行)6146714,89586%18,6767,62112.0/1220/20

逐行读:

  1. 前缀缓存是最大的一个旋钮。 同样”全部保留”,有缓存的成本只有没缓存的 24%:每一步的请求都是上一步的请求再往后加一点,前面的部分全部命中。
  2. 一个时间戳就能让缓存完全失效。 放在系统提示开头、每次都变,命中率掉到 1%,成本反而比完全不用缓存还高(每次都按写缓存的 1.25 倍付费)。动态内容要放在最后。
  3. 只清理旧工具结果:省了 token,丢了答案。 12 个年份最后只剩 2.2 个看得见,一题都答不对。而且每次清理都改动了更早的消息、打破缓存,有缓存时比全部保留还贵(18,169 vs 10,287)。
  4. 笔记找回了事实;放在哪决定了成本。 把学到的事实写进自己的消息里(“Note: X: HQ Y, settled Z.”),清理工具结果也不丢答案。每步都清理仍然频繁打破缓存;攒够一批再清理,成本接近全部保留(12,258)。笔记块放进系统提示,每加一条就打破整段缓存(25,602)。顺带一提,Anthropic 的 context editing 功能里有一个参数专门规定”每次至少清掉多少 token”,就是为了让打破缓存这件事值得。
  5. 压缩:峰值上下文最小的单智能体策略,多花 1 次总结调用,有缓存时成本(9,279)比全部保留还低。有损压缩平均丢掉 0.7 个事实;这 20 题碰巧没有丢到”答案那家”,但丢到就必错——总结时保留什么,是压缩策略的全部难点。
  6. 子智能体调用次数最多(61 次),但每个上下文都很小,总 token 和成本都最低;主智能体只看到 12 行结论。代价是协调成本和信息在交接中丢失的风险(第 8 讲)。

这张表有一个我们必须说清楚的盲点:我们的模拟读者不会被长上下文干扰,所以”全部保留”在这里永远答对。真实模型会(§7.2):在几十万 token 的上下文里,“全部保留”既贵(即使有缓存,也要付一次完整的 prefill 和读缓存的钱)又可能答错。所以真实系统里,压缩、笔记、子智能体的价值比这张表显示的还要大;而它们的风险(丢信息)是一样的。

演示 3 逐次打印了缓存命中率:全部保留从第 2 次调用的 75% 一路升到九成以上;时间戳每次只命中最前面的几个 token;每步清理从第 8 次调用起掉到 36%,之后慢慢回升;批量清理只在真正清理的那一步(第 19 次)掉到 20%,其余时候和全部保留一样。

7.5 另一种思路:递归语言模型

slides 最后介绍了一种”不同的哲学”:递归语言模型(Recursive Language Models, RLM;Zhang, Kraska & Khattab, 2025)。与其想办法把长输入塞进(或压缩进)上下文,不如根本不把它放进上下文:长文本作为一个变量放在一个编程环境(REPL)里,模型写代码去查看、搜索、切分它,并对切出来的片段递归地调用自己(或另一个模型),再把结果汇总。这把”上下文管理”从一个提示词问题变成了一个程序问题——和第 7 讲(记忆)、第 8 讲(多智能体)的思路一脉相承。

7.6 阅读材料:Building Effective Agents

原课给这一讲配的主阅读是 Anthropic 的 Building Effective Agents(Schluntz & Zhang, 2024)。它和本讲的关系是:本讲讲”LLM 这个零件的脾气”,这篇讲”怎样用这个零件搭系统”。要点:

  • **增强的 LLM(augmented LLM)**是基本构件:一个 LLM 加上检索、工具和记忆。
  • workflow vs agent:workflow 是用预先写好的代码路径编排 LLM 和工具;agent 是 LLM 动态地决定自己的流程和工具使用(第 1 讲 §4 的定义就来自这里)。先找最简单的方案,只在需要时才增加复杂度;智能体用延迟和成本换取更好的任务表现。
  • 五种 workflow 模式:提示链、路由、并行化(分段 / 投票)、orchestrator–workers、evaluator–optimizer(第 6 讲逐一实现)。
  • 关于框架:建议先直接用 LLM API,很多模式几行代码就能实现;用框架时要理解底层代码(这正是这门课”先从零造一遍”的理由)。
  • 三条原则:保持简单;透明地展示智能体的规划步骤;精心设计智能体–计算机接口(ACI)——像写给初级工程师的文档那样写工具说明,反复测试模型怎样用工具,用”防呆”(poka-yoke)的设计让参数难以用错。附录里的例子:给 SWE-bench 智能体的工具改成必须用绝对路径之后,模型就不再在切换目录后用错相对路径了;他们说花在优化工具上的时间比优化整体提示词还多。
  • 关于格式(和 §6 呼应):给模型足够的 token 先”想”,不要让它一开始就把自己写进死角;让格式贴近模型在互联网文本里自然见过的样子;避免格式上的额外负担——比如要求它精确地数出几千行代码的行数,或者把代码放进 JSON 里(换行和引号都要转义)。

8. 用真实模型试一试 · Try it on a real model

代码 07(可选)把 §6 的联系人抽取交给真实的 Claude:JSON 做法(宽松解析 + 校验 + 重试)和 DSPy 式字段标记做法各跑一遍,并对 schema 之外的意图特别留意——模型是写了一个 schema 外的词(被标记),还是挑了一个合法值(静默错误)?重试时会不会”屈服”?然后把同一个问题问 5 遍,看看在不能调温度的 API 上,开放式问题和有唯一答案的问题的一致性有什么不同。

pip install anthropic
python3 07_real_llm.py --n 8

--toy 不联网,用模拟模型跑同一段代码,用来检查脚本本身。真实模型每次运行的结果都可能不同;如果你看到真实模型的格式错误比我们的玩具少得多、但 schema 外的意图依然会被”塞”进合法值,那正是本讲想让你注意到的:格式问题可以靠工程解决,内容问题要靠 schema 设计和评估。


9. 小结 · Summary

  1. 语言模型 = 序列上的概率分布,用链式法则拆成”下一个 token 的条件概率”;实践中用 log 概率。它是生成式的,所以可以采样;提示词就是那个”条件”。
  2. 解码决定了从分布里挑什么:温度把概率在头部和尾部之间搬运,top-p 自适应地砍尾巴,贪心确定但会复读,束搜索找到的”最可能”不一定”最好”。对智能体:每步一点点的尾巴在多步里累积成可观的失败率;温度让 pass@k 上升、pass^k 下降;多数投票要适中的温度,而且只能救”正确答案是众数”的题。
  3. 架构:decoder-only Transformer 用因果掩码换来并行训练和 KV cache;位置信息是加进去、学出来的,所以上下文长度受训练限制;注意力的代价是 n²。线性注意力用固定大小的状态换常数代价,但状态记不住太多东西(叠加会模糊,delta 规则会遗忘);实践中混合两者。
  4. 训练:交叉熵让模型”像数据那样续写”而不是”说真话”;数据的去重和配比决定了模型擅长什么、会背什么;后训练(SFT、RLHF、RLVR、面向智能体的训练)塑造了它听话、谄媚、会用工具、会钻验证器空子等脾气。
  5. 推理:prefill 快、decode 慢(等内存);KV cache 和 batch 是效率的关键,也是输出 token 更贵、长上下文更贵的原因;推测解码无损加速;测试时扩展在有验证器时最有效。
  6. 结构化输出:直接解析脆弱;解析 + 校验 + 重试通用但会”屈服”;约束解码保证格式、更快,但会静默地把 schema 外的内容塞进合法值——schema 必须覆盖所有情况。签名和小分类器是两种换思路的做法。
  7. 上下文工程:上下文是有限的注意力预算,越长越贵、越不可靠。删(清理、笔记、压缩、子智能体)要保住后面要用的事实;放(前缀缓存)要按顺序往后加,动态内容放最后。一切都要用评估来定。

10. 常见误区 · Common pitfalls

  • “temperature = 0 就完全确定了。” 在很多 API 上近似如此,但服务器端的批处理、浮点运算顺序、模型版本更新都可能让结果变化;有的模型根本不接受 temperature。需要可复现时,缓存请求和结果(前置讲 P3)。
  • “温度越低越好 / 越高越好。” 取决于你要的是 pass^k(产品的稳定性)还是 pass@k(配合验证器的搜索),以及要不要做多数投票(需要适中的温度)。
  • “top-p 设成 0.99 更安全。” 在尖的分布上,0.99 可能恰好把大量尾巴放回来(代码 02 演示 1、4)。
  • “上下文窗口是 200k,那就把 200k 都用上。” 标称长度 ≠ 能用好的长度(context rot);而且每步重发、n² 的代价都在。
  • “清理旧工具结果总能省钱。” 有前缀缓存时,频繁改动前面的消息会让缓存失效,可能更贵;而且会丢掉以后要用的事实(代码 06)。
  • “约束解码解决了结构化输出的问题。” 它只解决格式;schema 外的内容会被静默地塞进合法值。
  • “重试直到通过校验就行了。” 重试会让模型”屈服”成一个合法但错误的值;重试次数用完时要标记,而不是悄悄接受最后一个。
  • “多采样总能提高准确率。” 没有验证器时,多数投票的上限是”正确答案是众数”的比例;对 impossible 的题毫无作用。
  • “模型说得很确定,所以大概是对的。” 后训练(人类偏好)会奖励”听起来权威”;确定的语气不是正确的证据。
  • “把动态信息(日期、用户名、笔记)放在系统提示开头很自然。” 它会让前缀缓存全部失效(代码 06:成本比不用缓存还高)。

11. 练习 · Exercises

练习 1(链式法则):用代码 01 的三元模型,“The class was about agents .” 的概率是 3.87×10⁻⁴。不运行代码,“The class was about memory .” 的概率会更高还是更低?你需要知道哪两个数?

参考答案 前四项(The、class、was、about)完全相同,差别只在 P('memory' | was about) 和 P('.' | about memory)、P('</s>' | memory .)。所以需要比较 P('agents' | was about) = 0.0728 和 P('memory' | was about),以及后面两项。'memory' 不在代码 01 打印的前 8 名里(第 8 名 'tool' 是 0.0501),所以 P('memory' | was about) < 0.0501 < 0.0728;如果后两项差不多,整句概率会更低。可以用 `lm.sequence_probs(tokenize("The class was about memory ."))` 验证。

练习 2(温度):一个分布是 {A: 0.6, B: 0.3, C: 0.1}。分别算 T = 0.5 和 T = 2 之后的分布。哪个 token 的概率变化最大?

参考答案 p^(1/T) 再归一化。T = 0.5:平方后 0.36、0.09、0.01,和 0.46 → A 0.783、B 0.196、C 0.022。T = 2:开方后 0.775、0.548、0.316,和 1.639 → A 0.473、B 0.334、C 0.193。C 的相对变化最大(从 0.022 到 0.193,差近 9 倍):温度主要是在搬运**尾巴**的概率。

练习 3(尾巴的累积):一个智能体平均 40 步完成任务,每步有 1% 的概率写出格式错误的动作。(a) 一条轨迹至少出一次格式错误的概率?(b) 如果 harness 遇到格式错误就让模型重试,每次重试独立地有 1% 的概率再错,“连续 3 次格式错误就放弃”的规则下,因为格式错误而失败的轨迹比例大约是多少?

参考答案 (a) 1 − 0.99⁴⁰ ≈ 33%。(b) 一步失败需要连续 3 次错:0.01³ = 10⁻⁶;40 步里至少一次:约 40 × 10⁻⁶ = 0.004%。重试把"一次出错"从"整条轨迹失败"变成了"多花一次调用"(约 40 × 0.01 = 0.4 次额外调用)。这就是第 1 讲 agent.py 处理 malformed 的意义。

练习 4(pass@k 与 pass^k):某道题模型单次答对的概率在 T = 0.2 时是 0.9,在 T = 1.0 时是 0.6。分别算两种温度下的 pass@5 和 pass^5。你的产品是”用户问一次、必须答对”,选哪个温度?如果你有一个完美的单元测试来挑答案呢?

参考答案 T = 0.2:pass@5 = 1 − 0.1⁵ ≈ 1.0000,pass^5 = 0.9⁵ = 0.59。T = 1.0:pass@5 = 1 − 0.4⁵ = 0.99,pass^5 = 0.6⁵ = 0.078。"问一次必须答对"看单次准确率和一致性,选低温度。有完美验证器时,两者的 pass@5 都接近 1——这道题上温度不重要;但对于"低温度时模型根本走不到正确答案"的题(代码 02 的 hard 题),高温度 + 验证器才能救。

练习 5(多数投票):一道题有三条推理路径:路径 1 的概率 0.4、答案 X;路径 2 和 3 的概率各 0.3、答案都是 Y(正确)。贪心解码给出什么?温度 1 下采样 101 次多数投票,大约给出什么?T → 0 呢?

参考答案 贪心走最可能的路径 1,答 X(错)。T = 1 时答案分布是 X 0.4、Y 0.6,101 次投票几乎必然 Y(对)——这就是 self-consistency。T → 0 时路径分布集中到路径 1,所有样本都答 X,投票也救不回来(代码 02 演示 5 里 T = 0.3 的那一行)。

练习 6(KV cache 与上下文顺序):你的智能体每一步的请求是 [系统提示(2,000 token), 用户档案(500 token,每步可能更新), 对话历史(逐步增长)]。用户档案每 3 步更新一次。怎样重排能让前缀缓存命中更多?估算一下差别。

参考答案 把用户档案移到对话历史**之后**(或者作为一条新消息追加,而不是改写原来的那条)。原顺序下,每次档案更新都让"档案 + 全部历史"失效,只有系统提示的 2,000 token 能命中;而且档案一变,后面的历史全部要重新写缓存。重排后,系统提示 + 历史始终命中,只有末尾的档案和新内容需要写缓存。历史越长,差别越大——代码 06 里"笔记块放在系统提示里"(25,602)vs "笔记写在消息里"(19,669)就是这个效应。

练习 7(约束解码):你用约束解码让模型输出 {"sentiment": "positive" | "negative"}。一条输入是 “The service was fine, I guess.”。会发生什么?怎样修改 schema 或流程,让下游知道这种情况?

参考答案 模型可能想说 "neutral" 或 "mixed",但语法只允许两个值,它会挑一个——静默地。修法:给 schema 加 "neutral" / "unknown" 出口;或者让模型先输出一个自由文本的理由再填结构;或者额外要求输出一个置信度字段并对低置信度的结果做人工复核;或者用"解析 + 校验"的做法让 schema 外的值暴露出来(但要注意重试会"屈服")。

练习 8(上下文策略):代码 06 里”清理旧工具结果(只留最近 6 条)“一题都答不对。不加笔记,只改 keep_last,要留多少条才能保证答对?成本会怎样?

参考答案 答案那家公司可能是 12 家里的任何一家,它的年份出现在该公司的第 3 条工具结果里。要保证最后还看得见所有年份,就要保留全部 36 条工具结果——等于不清理。只要不是全留,总有一些实例的答案在被清理的部分里。这说明"清理"必须配合"把要用的东西提炼出来"(笔记、压缩),单独使用只适合"旧结果以后真的不会再用"的任务(比如 Claude Code 里早已处理完的文件内容)。可以改 `run_policy(..., keep_last=k)` 验证。

练习 9(推测解码):代码 04 里 γ = 8 比 γ = 4 的估计加速更低。用”接受率”解释为什么;如果草稿模型的成本从 5% 降到 1%,最优的 γ 会变大还是变小?

参考答案 第 i 个猜测只有在前 i − 1 个都被接受时才有用;越往后被接受的概率越低(γ = 8 时接受率只有约 30%),多猜的那几个大多被丢掉,却仍要付草稿模型的成本。草稿越便宜,多猜的代价越小,最优 γ 会变大。可以把 `cost = st["target_calls"] + 0.05 * st["draft_calls"]` 里的 0.05 改成 0.01 验证。

练习 10(开放题):你要搭一个读邮件、给邮件分优先级(高 / 中 / 低)并写摘要的系统(原课 HW1 的第一部分)。结合本讲,列出你在解码、结构化输出和上下文上各会做哪两个决定,以及你会怎样用评估验证它们。

参考答案(之一) 解码:分类用低温度(要的是一致性,pass^k);摘要可以稍高但不必高。结构化输出:优先级是固定三类,可以考虑小分类器或约束解码,但 schema 要有"无法判断"的出口;摘要是自由文本,不需要约束。上下文:系统提示里放分类标准和 2–3 个典型例子(而不是一长串规则),按"系统提示 → 例子 → 邮件"的顺序以便缓存命中;长邮件线程先截断或总结。评估:准备一份带人工标注的小测试集,比较不同提示词 / 解码设置的准确率和一致性(同一封邮件跑 5 次结果是否相同),并记录成本和延迟。

12. 延伸阅读 · Further reading

本讲的阅读材料

  • Schluntz & Zhang, Building Effective Agents, Anthropic Engineering, 2024.
  • Rajasekaran et al., Effective Context Engineering for AI Agents, Anthropic Engineering, 2025.

语言模型与解码

  • Holtzman et al., The Curious Case of Neural Text Degeneration, ICLR 2020(nucleus sampling)。
  • Hugging Face, Decoding Strategies in Large Language Models(slides 推荐的入门博客);Hugging Face LLM Course 的分词器一章。
  • Wang et al., Self-Consistency Improves Chain of Thought Reasoning in Language Models, ICLR 2023.

架构

  • Vaswani et al., Attention Is All You Need, NeurIPS 2017.
  • Katharopoulos et al., Transformers are RNNs: Fast Autoregressive Transformers with Linear Attention, ICML 2020.
  • Yang et al., Parallelizing Linear Transformers with the Delta Rule over Sequence Length, NeurIPS 2024;Songlin Yang 的博客系列 DeltaNet Explained。
  • Hsieh et al., RULER: What’s the Real Context Size of Your Long-Context Language Models?, COLM 2024.

训练

  • Li et al., DataComp-LM: In Search of the Next Generation of Training Sets for Language Models, 2024.
  • Chung et al., Scaling Instruction-Finetuned Language Models, 2022.
  • Ouyang et al., Training Language Models to Follow Instructions with Human Feedback, NeurIPS 2022.
  • Shao et al., DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models, 2024.
  • Yang et al., SWE-smith: Scaling Data for Software Engineering Agents, NeurIPS D&B 2025.

推理

  • Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM), SOSP 2023.
  • Leviathan, Kalman & Matias, Fast Inference from Transformers via Speculative Decoding, ICML 2023;Chen et al., Accelerating Large Language Model Decoding with Speculative Sampling, 2023.
  • Brown et al., Large Language Monkeys: Scaling Inference Compute with Repeated Sampling, 2024.

结构化输出与上下文

  • Zheng et al., SGLang: Efficient Execution of Structured Language Model Programs, NeurIPS 2024.
  • Khattab et al., DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines, ICLR 2024(第 5 讲的阅读材料)。
  • Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools, NeurIPS 2023.
  • Hong, Troynikov & Huber, Context Rot: How Increasing Input Tokens Impacts LLM Performance, Chroma, 2025.
  • Zhang, Kraska & Khattab, Recursive Language Models, 2025.

13. 下一讲预告 · Next lecture

第 3 讲 检索增强生成(RAG):本讲 §7.3 说”用检索找回最相关的记忆或文件——下一讲细讲”。下一讲回答:为什么 LLM 需要检索(知识截止、长尾、幻觉、可溯源);一条 RAG 流水线由哪几步组成(索引、检索、阅读);文档怎样切块(固定大小、重叠、按结构、上下文化检索、late chunking、RAPTOR);怎样检索(从零实现 BM25、稠密检索、混合检索与 RRF、cross-encoder 重排、ColBERT 的 late interaction);怎样评估检索器(Hit@k、MRR、nDCG、Recall);检索怎样变成智能体的一个动作(Self-RAG、CRAG、Search-R1,以及多跳检索);以及 RAG 在检索端、阅读端、循环端分别会怎样坏掉。配套代码会从零写一个检索库,在一家虚构公司的内部文档上把这些方法一一比较——这也是原课 HW1 要你从零实现 BM25 和多跳 RAG 的部分。

← 返回课程目录