01 大模型基础
做 Agent 开发不需要会训练模型,但需要知道模型大致是怎么工作的,否则很多现象解释不了:为什么 Agent 的输入 token 是输出的几十倍、为什么同一个问题每次回答不一样、为什么缓存能省钱、为什么上下文越长越慢、为什么模型会一本正经地编造。AI 应用岗面试里,这些基础题通常是一面的开场。
这篇要讲清楚:token 是什么;Transformer 和自注意力用大白话怎么讲;模型怎么一个 token 一个 token 地生成;KV cache 为什么存在;上下文窗口;temperature 和 top_p 到底改了什么;首 token 延迟和吞吐;怎么计费;推理模型是什么;怎么选模型;为什么会有幻觉;预训练、SFT、RLHF、微调、LoRA 分别是什么。不推公式,但每个概念都给一个能跑的小实验或项目里的真实数据。
怎么读:
前置:00 学习路线与面试地图。这是系列里的第一篇知识篇,不需要其他前置。
第一部分 速记页
| 问题 | 一句话答案 |
|---|---|
| token 是什么 | 模型处理文本的最小单位,一个英文单词可能是一到几个 token,一个汉字可能是一个或几个,取决于分词器 |
| 大模型本质上在做什么 | 根据前面所有 token,预测下一个 token 的概率分布,从中选一个,接到后面,重复 |
| Transformer | 2017 年提出的神经网络结构,核心是自注意力,现在的大模型基本都基于它 |
| 自注意力 | 处理每个 token 时,看一遍前面所有 token,按相关程度加权汇总信息 |
| 自回归生成 | 一次只生成一个 token,每个新 token 都依赖之前生成的所有 token |
| 预填充 vs 解码 | 预填充一次性处理全部输入;解码逐个生成输出 token |
| KV cache | 把已经算过的 token 的中间结果(K、V 向量)存起来,生成下一个 token 时不用重算 |
| 上下文窗口 | 一次请求里输入加输出最多能有多少 token |
| temperature | 调整概率分布的”尖锐程度”:低了更确定,高了更随机 |
| top_p | 只在累积概率达到 p 的那几个候选里抽样,砍掉长尾 |
| 首 token 延迟(TTFT) | 发请求到收到第一个 token 的时间,主要受输入长度影响 |
| 计费 | 按输入 token、输出 token 分别计价,输出通常更贵;缓存命中的输入很便宜 |
| Agent 为什么输入远多于输出 | 每一步都重发完整历史;我项目 288 次运行输入是输出的 41.6 倍 |
| 推理模型 | 回答前先生成一段思考过程,思考的 token 也按输出计费 |
| 幻觉 | 模型生成看起来合理但错误的内容;训练和评测机制奖励”猜”而不是”说不知道” |
| 预训练 | 在海量文本上学习预测下一个 token,得到基础模型 |
| SFT | 用人工写的”问题—好回答”数据继续训练,让模型学会按指令回答 |
| RLHF | 用人对回答的偏好训练奖励模型,再用强化学习优化,让回答更符合人的偏好 |
| LoRA | 冻结原模型参数,只训练很小的附加矩阵,大幅降低微调成本 |
| 应用开发该不该微调 | 大多数情况先用提示词、RAG、工具;知识会变用 RAG,要稳定改变输出风格格式再考虑微调 |
第二部分 易混对照
| 容易混的两个 | 区别 | 一句话记法 |
|---|---|---|
| token vs 字符 | token 是分词器切出的单位,和字符数、词数都不一一对应 | 按 token 计费,不按字 |
| 参数 vs 上下文 | 参数是模型训练好的固定权重;上下文是每次请求临时给的输入 | 脑子 vs 手边的资料 |
| 预填充 vs 解码 | 预填充并行处理所有输入 token;解码一次一个 token 串行生成 | 读题 vs 写答案 |
| KV cache vs 提示词缓存 | KV cache 是一次请求内部的计算复用;提示词缓存是跨请求复用相同前缀的计算结果 | 同一次 vs 下一次 |
| temperature vs top_p | temperature 改变整个分布的形状;top_p 截掉低概率的尾巴 | 调陡峭程度 vs 砍尾巴 |
| temperature=0 vs 完全确定 | 0 通常等于每次选概率最大的,但服务端的实现和计算误差仍可能让结果不同 | 近似确定 |
| TTFT vs 吞吐 | TTFT 是等第一个字多久;吞吐是每秒生成多少 token | 开口快 vs 说得快 |
| 推理模型 vs 思维链提示 | 推理模型经过训练会自己先思考;思维链提示是在提示词里要求”一步步想” | 天生 vs 被要求 |
| 幻觉 vs 知识过时 | 幻觉是编造;过时是训练数据截止后的事不知道 | 瞎说 vs 没听说 |
| 微调 vs RAG | 微调改模型参数,适合改行为和格式;RAG 在请求时提供资料,适合会变的知识 | 改脑子 vs 给资料 |
| SFT vs RLHF | SFT 学”标准答案”;RLHF 学”哪个回答更好” | 照抄范文 vs 学评分标准 |
| 全参数微调 vs LoRA | 前者更新全部参数;后者只训练附加的小矩阵 | 重装修 vs 贴墙纸 |
第三部分 面试口述稿
3.1 “大模型是怎么生成文本的?”
大模型本质上在做一件事:给定前面所有的 token,预测下一个 token 的概率分布。
生成的时候分两个阶段。第一个阶段叫预填充,把输入的全部 token 一次性送进模型,计算出每个 token 的中间表示,这一步可以并行,但输入越长越慢,决定了等第一个字要多久。第二个阶段叫解码,模型根据前面所有内容算出下一个 token 的概率,按采样参数选一个,接到末尾,再算下一个,一次只能出一个 token,这就是自回归。
为了让解码不那么慢,推理时会用 KV cache:每个 token 在注意力计算里要算出 K 和 V 两组向量,算过的存起来,生成新 token 时只算新 token 的,前面的直接复用。
这个过程解释了 Agent 开发里很多现象。比如 Agent 每一步都要把完整历史重新发给模型,输入 token 会远大于输出,我项目 288 次运行输入是输出的 41.6 倍;再比如提示词缓存,就是把相同前缀的计算结果跨请求复用,所以稳定的系统提示词要放在最前面。
3.2 “temperature 和 top_p 有什么区别?”
模型最后一层对每个候选 token 输出一个分数,经过 softmax 变成概率。temperature 是在 softmax 之前把分数除以一个数:小于 1 会让高分的更突出,分布更尖,结果更确定;大于 1 会让分布更平,低概率的词更容易被选中。
top_p 是另一种控制:把候选按概率从高到低排,累加到 p 为止,只在这几个里面抽样,后面的长尾全部砍掉。它的好处是候选数量随分布自动变化,模型很确定的时候可能只剩一两个候选,不确定的时候保留多一些。
我自己用标准库算过一个例子:五个候选词,temperature 0.2 时第一名的概率是 0.993,1.0 时是 0.629,2.0 时降到 0.431;top_p 设 0.9 时,前三个累积到 0.964,第四第五个被砍掉。
实际开发里,要求稳定输出的场景,比如工具调用、信息抽取、Agent 的决策,温度调低;写作、头脑风暴调高一些。一般建议两个参数只调一个。
3.3 “为什么大模型会有幻觉?怎么缓解?”
从原理上说,模型是在预测”看起来最合理的下一个 token”,它没有一个单独的机制去核实事实是不是真的。训练数据里很少出现、或者根本没有的知识,模型也会按语言规律生成一个像样的答案。
2025 年 9 月有一篇论文叫 Why Language Models Hallucinate,专门讲这个问题,核心观点是训练和评测机制在奖励”猜”而不是”承认不确定”:评测按对错打分,说”不知道”得零分,猜一个还有可能蒙对,所以模型被优化成倾向于猜。
应用层面缓解的办法:给资料,用 RAG 让模型基于检索到的内容回答,并要求标注出处;允许并鼓励拒答,提示词里明确”资料里没有就说不知道”;能用程序验证的就验证,我项目里不信模型说”编译成功”,由程序独立检查产物;关键结论做引用核对。
3.4 “预训练、SFT、RLHF、微调、LoRA 分别是什么?”
预训练是在海量文本上训练模型预测下一个 token,得到的基础模型会续写,但不一定会按指令回答。
SFT 是监督微调,用人工写的”指令加好回答”继续训练,模型学会听指令。RLHF 是基于人类反馈的强化学习:让人给多个回答排序,训练一个奖励模型来模拟人的偏好,再用强化学习让模型的回答得分更高。InstructGPT 那篇论文就是这三步,他们发现经过这样训练的 13 亿参数模型,回答比 1750 亿参数的 GPT-3 更受人偏好。
微调泛指在已有模型上用自己的数据继续训练。全参数微调成本高,LoRA 是常用的省钱办法:冻结原模型,只在每层旁边加两个很小的矩阵来训练。LoRA 论文里和 GPT-3 1750 亿参数的全量微调比,可训练参数少了一万倍,显存需求降到三分之一。
做应用开发时我的判断顺序是:先提示词,再 RAG 和工具,最后才考虑微调。知识会变、要给出处的用 RAG;要稳定地改变输出格式、风格,或者让小模型学会特定任务来降成本,再考虑微调。
3.5 “你怎么选模型?”
看几个维度:任务效果,用自己的评测集测,不只看公开榜单;工具调用能力,Agent 场景特别重要;上下文长度;价格,尤其输入价格和缓存价格,因为 Agent 输入远大于输出;延迟和并发限额;是否支持需要的功能,比如严格的结构化输出、推理模式;数据合规和能不能私有化部署。
我项目用的是 DeepSeek 的 deepseek-flash,主要原因是便宜,评测要跑几百次运行、几千万 token,按价格页现在的非高峰价,未命中缓存的输入每百万 token 0.15 美元。这个选择的局限是我没有换模型跑同一套评测,所以不知道结论在别的模型上是否成立。另外评测中途遇到过余额不足返回 402 导致崩溃,这也提醒选模型要考虑额度和降级。
第四部分 逐个详解
4.1 token:模型看到的不是字
先说为什么会有 token 这种切法。 让模型读文字,最直觉的两种切法都有毛病。按整词切:词表要事先定死,训练时没见过的词(新起的库名、拼错的词、少见的人名)只能统一换成一个”未知词”符号,模型等于看不到它。按单个字符切:什么词都能表示,但一句话会变成很长的序列,模型要多算很多步,而且单个字母本身几乎不带意思。
2015 年,爱丁堡大学的 Rico Sennrich、Barry Haddow、Alexandra Birch 做神经机器翻译时,碰到的正是罕见词翻不出来的问题。他们借用了一种 1990 年代的数据压缩算法 BPE(Byte Pair Encoding,字节对编码)的思路:从单个字符开始,反复把语料里最常挨在一起的两个片段合并成一个新片段,直到词表达到预定大小。这样常见词整个是一个单位,罕见词被拆成几个常见片段,不会再有”未知词”(论文 Neural Machine Translation of Rare Words with Subword Units)。现在大模型的分词器基本都是这个思路的变体,OpenAI 的 GPT-2 把合并的起点从字符换成了字节,任何语言、任何符号都能切。
| 切法 | 好处 | 卡在哪 |
|---|---|---|
| 按整词 | 序列短,一个单位就是一个词 | 词表外的词只能变成”未知词” |
| 按字符 | 什么都能表示 | 序列太长,单个字符没什么意思 |
| 子词(BPE 一类) | 常见词完整、罕见词拆开,没有”未知词” | 切出来的单位和字、词都不一一对应,所以 token 数只能用分词器算 |
token(词元):分词器把文本切成的最小单位,模型输入输出都以 token 为单位,计费和上下文长度也按 token 算。
- 常见英文单词可能是 1 个 token,长词或罕见词会被切成几段
- 中文一个汉字可能是 1 个或多个 token,取决于分词器
- 代码、JSON、日志里的符号、空格、换行也都占 token
- 不同模型的分词器不同,同一段文字在不同模型上 token 数不一样
06 篇有一个用 tiktoken 看文字怎么被切的实验,这里不重复。记住两点:估算成本要用对应模型的分词器或 API 返回的 usage 字段;日志、报错、代码这类内容 token 密度高,Agent 的工具结果往往是 token 大户。
4.2 Transformer 和自注意力:大白话版
先说为什么会有 Transformer。 2017 年以前,处理文字这类序列的主流是循环神经网络(RNN)和它的改进版 LSTM:从第一个词开始,一个词一个词往后读,每读一个就更新一次内部的”记忆”。这有两个硬伤。一是必须按顺序算,第 100 个词要等前 99 个都算完,GPU 核心再多也没法并行,训练大模型很慢。二是句子一长,开头的信息要经过几十上百次传递才到结尾,容易被冲淡,翻译长句时结尾的”它”常常对不上开头的名词。2014 年 Bahdanau 等人在翻译模型里加了”注意力”,让模型生成每个词时可以回头直接看原文的所有位置,效果好了很多,但主体还是循环结构,按顺序算的问题没解决。
2017 年 6 月,Google 的 Ashish Vaswani 等八位作者发表了下面这篇论文。他们做的也是机器翻译,但干脆把循环结构整个拿掉,只留注意力:每个词直接和其他所有词算相关程度,所有位置可以同时算。论文报告英德翻译比当时最好的结果高 2 个多 BLEU 分,训练只用了 8 张 GPU 跑 3.5 天。
| 怎么读一句话 | 能不能并行训练 | 相隔很远的两个词 | |
|---|---|---|---|
| RNN / LSTM | 一个接一个读 | 不能 | 要一步步传过去,容易丢 |
| RNN + 注意力 | 仍然逐个读,但生成时能回看原文 | 不能 | 好了很多 |
| Transformer | 所有位置同时互相看 | 能 | 一步直达 |
Transformer:2017 年论文 Attention Is All You Need 提出的网络结构。论文摘要里的说法是完全基于注意力机制,去掉了之前序列模型常用的循环和卷积结构,更容易并行、训练更快。现在的大语言模型基本都是在它的基础上发展来的。
不看公式,理解三件事就够了:
1. 每个 token 先变成一串数字(向量)。 这串数字表示这个 token 的含义和它所在的位置。
2. 自注意力(self-attention):处理每个 token 时,回头看前面所有 token,决定该从谁那里取多少信息。
比方:读到”zlib 编译失败,它缺头文件”里的”它”,你会回头找”它”指的是谁。自注意力做的就是这件事,只不过是用数字算出来的:
flowchart LR
Q["当前 token「它」<br/>生成一个 Query 向量<br/>(我在找什么)"] --> S{和前面每个 token 的<br/>Key 向量算相似度}
K1["zlib 的 Key"] --> S
K2["编译 的 Key"] --> S
K3["失败 的 Key"] --> S
S --> W[softmax 变成权重<br/>加起来等于 1]
W --> M["按权重汇总各 token 的 Value 向量<br/>得到「它」的新表示"]
- Query(查询):当前 token “想找什么”
- Key(键):每个 token “我是什么”,用来被匹配
- Value(值):每个 token “能提供的信息”
- 相似度越高,权重越大,从那个 token 取的信息越多
3. 这样的层叠很多层。 每层都做一次注意力和一些计算,越往上表示越抽象。最后一层对词表里每个 token 给出一个分数,分数经过 softmax 变成”下一个 token 是它”的概率。
因果(causal)注意力:生成式模型里,每个 token 只能看它前面的 token,不能看后面的,因为生成时后面的还不存在。
为什么这些和应用开发有关? 每个 token 都要和前面所有 token 算相似度,输入越长,计算量增长得越快。这是长上下文更慢、更贵的原因之一,也和 06 篇讲的”上下文越长越不可靠”有关系。
4.3 动手实验 1:注意力权重和 KV cache 省了什么
用标准库算一个玩具版的注意力权重,再数一数 KV cache 省掉了多少计算。保存为 attention.py。
import math
TOKENS = ["zlib", "编译", "失败", ",", "它", "缺", "头文件"]
KEYS = {
"zlib": [0.9, 0.1, 0.8], "编译": [0.2, 0.9, 0.1], "失败": [0.1, 0.8, 0.3],
",": [0.0, 0.1, 0.0], "它": [0.5, 0.2, 0.5], "缺": [0.3, 0.4, 0.2], "头文件": [0.7, 0.2, 0.6],
}
QUERY_IT = [1.0, 0.1, 0.9]
def attention_weights(query, tokens):
scale = math.sqrt(len(query))
scores = [sum(q * k for q, k in zip(query, KEYS[t])) / scale for t in tokens]
top = max(scores)
exps = [math.exp(s - top) for s in scores]
total = sum(exps)
return [e / total for e in exps]
visible = TOKENS[:5]
for token, w in zip(visible, attention_weights(QUERY_IT, visible)):
print(f"'它' 看 '{token}':{w:.2f} {'#' * round(w * 40)}")
print()
prompt, new = 2000, 300
no_cache = sum(prompt + i for i in range(new))
with_cache = prompt + new
print(f"输入 {prompt} token,生成 {new} token")
print(f"不用 KV cache:要算 {no_cache:,} 次 K/V")
print(f"用 KV cache: 要算 {with_cache:,} 次 K/V,少 {no_cache / with_cache:.0f} 倍")
实际输出(Python 3.14,只用标准库):
'它' 看 'zlib':0.33 #############
'它' 看 '编译':0.16 ######
'它' 看 '失败':0.16 #######
'它' 看 ',':0.13 #####
'它' 看 '它':0.22 #########
输入 2000 token,生成 300 token
不用 KV cache:要算 644,850 次 K/V
用 KV cache: 要算 2,300 次 K/V,少 280 倍
逐段讲。
数据。 KEYS 里每个 token 的 Key 向量、QUERY_IT 是”它”的 Query 向量,都是手工编的三维数字,只为演示计算过程。真实模型里向量有几千维,由训练得到,含义也不是人能直接读懂的。这里故意让”zlib”和”头文件”的第一、三维较大,“它”的 Query 也在这两维较大,模拟”它”在找一个名词性的东西。
attention_weights。
zip(query, KEYS[t]):把两个列表按位置配对,[(1.0, 0.9), (0.1, 0.1), (0.9, 0.8)]sum(q * k for q, k in ...):对应位置相乘再相加,这叫点积(dot product),两个向量方向越接近,点积越大,用来衡量相似度/ scale:除以维度的平方根。Transformer 论文里的做法,防止维度高时点积太大,softmax 之后权重过于集中math.exp(s - top):softmax 的计算:每个分数取指数,再除以总和,得到加起来等于 1 的权重。先减去最大值top是一个防止数值溢出的常用技巧,不影响结果visible = TOKENS[:5]:切片,取前 5 个。生成”它”的时候,它只能看到自己和前面的 token(因果注意力),看不到后面的”缺""头文件”
KV cache 计数。
- 不用缓存:生成第 i 个新 token 时,要把”输入 + 已生成的 i 个 token”全部重新算一遍 K、V。
sum(prompt + i for i in range(new))就是 300 步加起来的总数 - 用缓存:输入的 2000 个 token 在预填充时算一次;之后每生成一个 token 只算这一个新的,共 300 次
- 这里只数 K、V 向量的计算次数,没算注意力本身(新 token 仍然要和前面所有缓存的 K 做相似度),所以”280 倍”不是真实的整体加速比,只说明重复计算被省掉了多少
看结果。
- “它”对”zlib”的权重最大(0.33),说明在这组编造的向量下,“它”主要从”zlib”那里取信息
- 所有权重加起来是 1,每个 token 都分到一点,这是 softmax 的性质
- KV cache 的代价是显存:每个 token 的 K、V 都要存着,上下文越长占得越多。这也是长上下文请求贵的一个原因
KV cache 和提示词缓存的关系:KV cache 是一次请求内部复用;提示词缓存(06 篇)是把某个前缀的 KV 结果跨请求存下来,下次请求开头完全相同时直接复用,省掉预填充的计算,所以命中的输入 token 能便宜很多(DeepSeek deepseek-flash 非高峰价命中 0.003 美元、未命中 0.15 美元每百万 token)。
自己改一改:
- 把
QUERY_IT改成[0.1, 1.0, 0.2](更像在找动词),看权重变化 - 把
visible改成全部 7 个 token,这相当于允许”看到未来”,想一想生成时为什么不能这样 - 把
prompt改成 50000(长上下文),new保持 300,看两种计数的变化
4.4 自回归生成:预填充和解码
自回归(autoregressive):每次只生成一个 token,生成的 token 接到输入末尾,作为生成下一个 token 的依据。
sequenceDiagram
participant C as 客户端
participant M as 模型服务
C->>M: 输入 2000 个 token
Note over M: 预填充(prefill)<br/>2000 个 token 并行计算<br/>存下每个 token 的 K、V
M-->>C: 第 1 个输出 token
Note over C,M: 到这里的时间 = 首 token 延迟
loop 解码(decode),每次一个
Note over M: 只算新 token 的 K、V<br/>和前面所有缓存的 K 做注意力
M-->>C: 下一个 token
end
M-->>C: 结束(生成了结束符或到达长度上限)
| 阶段 | 做什么 | 快慢取决于 | 对应的计费 |
|---|---|---|---|
| 预填充 | 一次性处理全部输入 | 输入长度;缓存命中的前缀可以跳过 | 输入 token |
| 解码 | 逐个生成 | 输出长度、模型大小、服务负载 | 输出 token |
为什么输出通常比输入贵? 预填充可以并行,一大段输入在 GPU 上一次算完;解码必须串行,每个输出 token 都要单独走一遍模型。同样的算力,生成输出更费时间。
什么时候停? 模型生成了表示结束的特殊 token;或者达到了请求设定的最大输出长度;或者模型决定调用工具(API 返回的停止原因会不同,02 篇讲 finish_reason)。
这对 Agent 意味着什么?
flowchart LR
S1["第 1 步<br/>输入:系统提示 + 任务<br/>3000 token"] --> S2["第 2 步<br/>输入:上面全部 + 第 1 步的调用和结果<br/>4500 token"]
S2 --> S3["第 3 步<br/>输入:上面全部 + 第 2 步<br/>6000 token"]
S3 --> SN["……第 n 步<br/>输入越来越长"]
模型本身不记得上一次请求,每一步都要把完整历史重新发一遍,每一步都要重新预填充(没命中缓存的部分)。于是:
- 输入 token 远大于输出:我项目第一版评测 baseline 一轮输入 1,375,134 token、输出 49,840 token,约 27 倍;
runs.db全部 288 次运行输入 48,704,845、输出 1,169,783,约 41.6 倍 - 步数越多,每一步越慢、越贵,15 篇的估算实验里 6 步到 40 步无缓存成本差了约 27 倍
- 提示词缓存对 Agent 特别重要,因为每一步的前缀和上一步几乎完全一样
4.5 上下文窗口
上下文窗口(context window):一次请求里模型能处理的 token 总数上限,包括输入和输出。
以写这篇时核对的 DeepSeek 价格页为例,deepseek-flash 上下文长度 100 万 token,最大输出 38.4 万 token。
窗口大不等于可以随便塞:
| 问题 | 原因 |
|---|---|
| 贵 | 按输入 token 计费,每一步都重发 |
| 慢 | 预填充时间随输入增长,首 token 延迟变长 |
| 不可靠 | 06 篇讲过的 Lost in the Middle、Context Rot:内容越多,模型越容易忽略中间的信息 |
所以 06 篇的上下文工程要解决的,不是”装不装得下”,而是”放什么进去才最有效”。我项目里 read_file 分页、命令输出截断,都是在控制每次放进上下文的量。
4.6 采样参数:temperature 和 top_p
模型最后一层给每个候选 token 一个分数,这个分数叫 logit。logit 经过 softmax 变成概率,再按某种规则从中选一个。
动手实验 2:五个候选的下一个 token,看不同参数下的概率。保存为 sampling.py。
import math
import random
from collections import Counter
LOGITS = {"cmake": 4.0, "make": 3.0, "ninja": 2.2, "ls": 1.0, "rm": -1.0}
def softmax(logits, temperature):
scaled = {t: v / temperature for t, v in logits.items()}
top = max(scaled.values())
exps = {t: math.exp(v - top) for t, v in scaled.items()}
total = sum(exps.values())
return {t: e / total for t, e in exps.items()}
def top_p(probs, p):
kept, acc = {}, 0.0
for token, prob in sorted(probs.items(), key=lambda kv: -kv[1]):
kept[token] = prob
acc += prob
if acc >= p:
break
total = sum(kept.values())
return {t: v / total for t, v in kept.items()}
def show(title, probs):
print(f"{title:<18}" + " ".join(f"{t}={probs.get(t, 0):.3f}" for t in LOGITS))
for t in (0.2, 1.0, 2.0):
show(f"T={t}", softmax(LOGITS, t))
show("T=1.0, top_p=0.9", top_p(softmax(LOGITS, 1.0), 0.9))
rng = random.Random(0)
probs = softmax(LOGITS, 1.0)
draws = Counter(rng.choices(list(probs), weights=list(probs.values()), k=1000))
print("T=1.0 抽 1000 次 ", dict(draws))
实际输出(Python 3.14,只用标准库):
T=0.2 cmake=0.993 make=0.007 ninja=0.000 ls=0.000 rm=0.000
T=1.0 cmake=0.629 make=0.231 ninja=0.104 ls=0.031 rm=0.004
T=2.0 cmake=0.431 make=0.262 ninja=0.175 ls=0.096 rm=0.035
T=1.0, top_p=0.9 cmake=0.652 make=0.240 ninja=0.108 ls=0.000 rm=0.000
T=1.0 抽 1000 次 {'make': 232, 'cmake': 629, 'ninja': 105, 'ls': 31, 'rm': 3}
逐段讲。
LOGITS:假设模型在”下一步执行什么命令”这个位置给五个候选的分数。数字是编的softmax(logits, temperature):{t: v / temperature for ...}:字典推导式,每个分数除以温度- 减去最大值再取
math.exp,然后除以总和,得到概率(4.3 节讲过这个防溢出技巧)
top_p(probs, p):sorted(probs.items(), key=lambda kv: -kv[1]):按概率从大到小排。kv是(token, 概率)元组,kv[1]取概率,加负号实现倒序- 依次放进
kept,累加概率acc,一旦达到p就break跳出循环 - 最后把留下来的概率重新归一化,让它们加起来还是 1
probs.get(t, 0):被砍掉的候选在字典里没有,get的第二个参数是找不到时的默认值rng.choices(候选列表, weights=权重, k=1000):按权重有放回地抽 1000 次。random.Random(0)固定种子,结果可复现Counter(...):数每个候选出现了几次
看结果。
- 温度越低,分布越尖:T=0.2 时
cmake占 99.3%,几乎总是选它;T=2.0 时降到 43.1%,rm也有 3.5% 的机会 - 温度除的是 logit,不是概率:分数差距被放大或缩小,所以低温时第一名”赢者通吃”
- top_p=0.9 砍掉了尾巴:
cmake+make= 0.860 还不到 0.9,加上ninja到 0.964,停止。ls和rm被排除,再高的随机性也不会选出rm - 抽样结果和概率吻合:1000 次里
cmake629 次,和 0.629 一致;rm抽到了 3 次。在 Agent 里,这意味着同一个任务每次运行路径可能不同,这就是 09 篇强调每个配置要跑多轮的原因
贪心解码(greedy decoding):每次直接选概率最大的,相当于温度趋近于 0。很多 API 里 temperature=0 就是这个效果,但由于服务端的批处理和浮点计算,结果也不保证每次完全相同。
top_p 的来源:Holtzman 等人 2019 年的论文 The Curious Case of Neural Text Degeneration(ICLR 2020)提出了 Nucleus Sampling(核采样),就是 top_p。论文指出以似然最大为目标的解码会产生平淡、重复的文本,而截掉概率分布里不可靠的长尾再抽样,能兼顾多样性和质量。
实际怎么设:
| 场景 | 建议 |
|---|---|
| 工具调用、结构化抽取、Agent 决策 | 低温度,结果更稳定 |
| 分类、判断 | 低温度 |
| 写作、创意、生成多个候选 | 适当提高 |
| 同时调温度和 top_p | 一般只调一个,两个都动效果不好预测 |
| 推理模型 | 很多厂商对推理模式限制或忽略采样参数,以文档为准 |
自己改一改:
- 把
LOGITS里cmake改成 3.1(和make很接近),再看 T=0.2 的结果。模型”犹豫”时低温也不一定稳 - 把 top_p 改成 0.5,看剩几个候选
- 把随机种子从 0 改成 1,看抽样次数的变化
4.7 延迟和吞吐
| 指标 | 含义 | 主要影响因素 |
|---|---|---|
| 首 token 延迟(TTFT,time to first token) | 发请求到收到第一个 token | 输入长度、缓存命中、排队 |
| 每 token 时间(TPOT,time per output token) | 相邻两个输出 token 的间隔 | 模型大小、服务负载 |
| 总耗时 | TTFT + 输出 token 数 × TPOT | 输出长度影响最大 |
| 吞吐(throughput) | 单位时间生成的 token 数 | 服务端批处理、硬件 |
用户体感主要看 TTFT,所以要流式输出(02、14 篇):第一个 token 一出来就显示,不用等全部生成完。
Agent 的延迟要乘以步数。10 篇算过我项目:268 次成功运行里工具耗时占 52%,剩下的部分平均每步约 1.9 秒(包含模型调用、启动探测和验收,是模型耗时的上限)。
降延迟的手段:减少输入长度(截断、压缩);提高缓存命中;限制输出长度(让模型简洁、结构化输出);小模型处理简单步骤;并行调用;减少步数。
4.8 计费
按 token 计费,输入和输出分开计价,缓存命中的输入另有低价。以写这篇时核对的 DeepSeek 价格页为例(每百万 token,美元):
| 模型 | 输入(命中缓存) | 输入(未命中) | 输出 |
|---|---|---|---|
deepseek-flash 非高峰 | 0.003 | 0.15 | 0.6 |
deepseek-flash 高峰 | 0.006 | 0.3 | 1.2 |
deepseek-v4-pro 非高峰 | 0.022 | 0.66 | 1.98 |
价格页说明高峰时段是周一到周五 UTC 01:00–04:00 和 06:00–10:00,非高峰价格是高峰的一半。价格经常变化,引用时以官方页面为准。
看出几件事:
- 输出单价是未命中输入的 4 倍,但 Agent 的输入量是输出的几十倍,Agent 的钱主要花在输入上
- 命中缓存的输入价格是未命中的 1/50,缓存命中率对 Agent 成本影响巨大(15 篇估算:40 步时有无缓存差 4 倍多)
- 同一个模型,高峰和非高峰差一倍,批量评测这类不急的任务可以安排在非高峰
计算方法:API 返回的 usage 字段里有输入、输出、缓存命中的 token 数(02 篇讲各家字段名),乘以对应单价。10 篇有完整的成本看板设计。
一个真实的教训:我项目 LangGraph 对比评测进行到第 3 轮 re2 第 28 步时,DeepSeek 返回 402(余额不足),接下来 4 个项目每个都是第 1 步就崩溃。计费不只是算钱,还要监控余额和预算。
4.9 推理模型
先说为什么会有推理模型。 2022 年 1 月,Google 的 Jason Wei 等人发现,在提示词的示例里写出一步步的解题过程,模型也会照着先写步骤再给答案,数学应用题的正确率明显上升,这就是思维链提示(05 篇讲 ReAct 时会细讲)。但这是靠提示词引出来的:每次都要给示例或加一句”一步步想”,模型也没专门练过怎么想。2024 年 9 月 12 日 OpenAI 发布 o1,在 Learning to reason with LLMs 里说,他们用大规模强化学习训练模型使用自己的思维链,而且思考时间越长,效果越好。2025 年 1 月 DeepSeek 发布 DeepSeek-R1 并公开论文,讲了用强化学习训练推理能力的做法,其中 R1-Zero 没有先用人工写的推理过程做监督微调。之后各家陆续推出了自己的推理模式。
推理模型(reasoning model):在给出最终回答之前,先生成一段思考过程(有的厂商叫 thinking),再给出答案。思考过程让模型在数学、代码、多步推理类问题上表现更好。
| 普通模式 | 推理模式 | |
|---|---|---|
| 输出 | 直接回答 | 思考过程 + 回答 |
| 计费 | 输出 token | 思考 token 通常也按输出计费 |
| 延迟 | 较低 | 较高,思考越长越慢 |
| 适合 | 简单问答、格式转换、工具调用的简单决策 | 复杂推理、规划、疑难排查 |
和思维链提示的区别:思维链(Chain-of-Thought)是在提示词里要求模型”一步步想”,05 篇引用过相关论文;推理模型是经过训练,自己就会先思考。
现在很多模型两种模式都支持。 核对 DeepSeek 价格页时,deepseek-flash 和 deepseek-v4-pro 都标注支持思考和非思考两种模式,默认是思考模式。10 篇讲的 OpenTelemetry 约定里也专门有 gen_ai.usage.reasoning.output_tokens 字段记录推理用掉的输出 token。
在 Agent 里怎么用:推理模式能提升复杂决策的质量,但每一步都多一段思考输出,步数多的 Agent 成本和延迟会明显上升。常见做法是:规划、疑难排查的步骤用推理模式,简单的工具调用步骤不用;或者用评测集对比两种模式的效果和成本再决定。我项目没有记录是否开启了思考模式和思考 token,这是记录上的一个缺口。
4.10 怎么选模型
| 维度 | 看什么 | 怎么判断 |
|---|---|---|
| 任务效果 | 你的任务上做得好不好 | 用自己的评测集测,公开榜单只作参考 |
| 工具调用 | 选对工具、参数格式对、多轮稳定 | 04 篇的 BFCL 等榜单 + 自己的评测 |
| 上下文长度 | 窗口多大、长上下文下效果是否下降 | 按实际需要的长度测 |
| 价格 | 输入、输出、缓存命中价格 | 按自己的输入输出比算,Agent 重点看输入和缓存 |
| 延迟 | TTFT、每 token 时间 | 实测,不同时段不同 |
| 并发和限额 | 每分钟请求数、token 数、并发数 | 价格页或控制台;deepseek-flash 价格页标注并发上限 2500 |
| 功能 | 结构化输出、严格模式、推理模式、多模态 | 查文档 |
| 合规 | 数据是否用于训练、能否私有化部署、数据存储地域 | 公司要求 |
不要只用一个模型的思路:简单步骤用便宜模型,复杂步骤用强模型;主模型不可用时切换到备用模型(02 篇)。前提是两个模型都在同一套评测集上测过。
4.11 幻觉
幻觉(hallucination):模型生成看起来合理、但实际错误或没有依据的内容。
为什么会有? 模型学的是”文本的规律”,生成时选”最像是接下来会出现的 token”,没有独立的事实核查机制。2025 年 9 月 Kalai、Nachum、Vempala 的论文 Why Language Models Hallucinate 给出了一个解释:训练和评测机制在奖励猜测,而不是奖励承认不确定。评测只看对错时,回答”不知道”一定得零分,猜一个还有机会得分,于是模型被优化得倾向于自信地猜。
常见类型:
| 类型 | 例子 |
|---|---|
| 编造事实 | 不存在的论文、API 参数、配置项 |
| 张冠李戴 | 把 A 库的用法说成 B 库的 |
| 不忠于资料 | 给了资料,但回答里加入了资料里没有的内容 |
| 自述不实 | Agent 说”编译成功了”,实际没有 |
应用层怎么缓解:
| 手段 | 说明 | 本系列 |
|---|---|---|
| 给资料 | RAG,基于检索内容回答 | 08 |
| 要求出处并校验 | 标注片段编号,程序检查存在,核对是否支持结论 | 08、15 |
| 允许拒答 | 提示词明确”资料里没有就说不知道”;检索分数低时直接拒答 | 03、08 |
| 用外部结果验证 | 能跑测试、能查数据库的,就不信模型自述 | 05、09 |
| 工具代替记忆 | 查询实时数据而不是让模型回忆 | 04 |
| 低温度 | 减少随机性,但不能消除幻觉 | 本篇 4.6 |
最后一类”自述不实”对 Agent 最危险。 我项目的独立验收就是为此设计的,而且验收自己也错过:旧标准只看退出码,模型和验收一起”认为”成功了,实际上一个目标都没编译。
4.12 模型是怎么训练出来的
应用开发不需要会训练,但要能说清楚几个阶段分别做了什么,以及什么时候应该考虑微调。
flowchart LR
PT["预训练<br/>海量文本<br/>预测下一个 token"] --> BASE[基础模型<br/>会续写 不一定听指令]
BASE --> SFT["监督微调 SFT<br/>人写的指令 + 好回答"]
SFT --> RM["训练奖励模型<br/>人对多个回答排序"]
RM --> RL["强化学习<br/>让回答得分更高"]
RL --> CHAT[对话模型<br/>听指令 更符合偏好]
CHAT -.-> FT["你的微调<br/>全参数或 LoRA"]
| 阶段 | 做什么 | 数据 |
|---|---|---|
| 预训练 | 学习预测下一个 token,获得语言能力和知识 | 海量网页、书籍、代码 |
| SFT(监督微调,supervised fine-tuning) | 学会按指令回答 | 人工写的”指令—回答”对 |
| 奖励模型 | 学会判断哪个回答更好 | 人对同一问题多个回答的排序 |
| 强化学习 | 优化模型,让奖励模型打分更高 | 奖励模型的打分 |
先说为什么会有 RLHF。 预训练出来的模型只会续写:你问”怎么交叉编译 zlib”,它可能接着编出几个类似的问题,因为网页上一个问题后面常常跟着别的问题。SFT 用范文教它回答,好了一些,但好回答很难全靠写范文穷举:同一个问题有很多种好答法,而”哪个更有帮助、哪个在胡说”,人一对比就看得出来,却很难写成规则。2017 年,OpenAI 和 DeepMind 的 Paul Christiano 等人在 Deep reinforcement learning from human preferences 里提出:让人在两段行为里挑更好的那段,用这些选择训练一个奖励模型,再用强化学习去优化。他们当时训练的是游戏和机器人模拟任务里的智能体,人只需要对不到 1% 的交互给反馈。2022 年 3 月 OpenAI 的 InstructGPT 把这套方法用到了语言模型上,同年 11 月上线的 ChatGPT 用的也是这一类方法。
后两步合起来叫 RLHF(基于人类反馈的强化学习)。InstructGPT 论文描述的就是 SFT、奖励模型、强化学习这三步,论文摘要里的结果是:13 亿参数的 InstructGPT,输出比 1750 亿参数的 GPT-3 更受人偏好,参数少了 100 倍。
先说为什么会有 LoRA。 全参数微调要更新模型的每一个参数,训练时参数、梯度、优化器状态都要放进显存;更麻烦的是每微调一个任务就多出一份和原模型一样大的副本。LoRA 论文拿 GPT-3 举例:每个任务都部署一个 1750 亿参数的独立实例,成本高得没法接受。2021 年 6 月,微软的 Edward Hu 等人提出 LoRA,思路是原模型一个参数都不动,只学一个很小的”修正量”。
LoRA(低秩适配,Low-Rank Adaptation):LoRA 论文提出冻结预训练模型的参数,只在 Transformer 每一层加入可训练的低秩分解矩阵。摘要里和用 Adam 全量微调 GPT-3 175B 相比:可训练参数减少 10000 倍,GPU 显存需求减少到三分之一,而且推理时不增加延迟。
“低秩分解矩阵”用大白话说:原来一个很大的权重矩阵不动,旁边加两个很”瘦”的小矩阵,它们相乘的结果作为对原矩阵的修正。要训练的只是这两个小矩阵。
应用开发里什么时候考虑微调:
flowchart TD
Q1{问题是什么} -->|模型不知道某些知识<br/>或知识经常变| RAG[RAG / 工具查询]
Q1 -->|不按要求的格式输出| P[先改提示词<br/>结构化输出 / 严格模式]
Q1 -->|任务做得不好| E[先确认评测集<br/>改提示词 加示例 加工具]
P -->|还不行| FT
E -->|提示词到头了<br/>有足够的高质量数据| FT[考虑微调<br/>通常用 LoRA]
Q1 -->|大模型能做好但太贵太慢| D[用大模型的结果<br/>微调小模型]
不适合用微调解决的:注入新知识(容易记不牢、没法给出处、知识更新要重新训练)。适合的:稳定的输出格式和风格、特定领域的任务模式、把大模型的能力迁移到便宜的小模型上。
我项目没有做任何微调。按上面这张图判断:交叉编译的经验会随项目和依赖版本变化、需要能追溯来源,更适合放在知识库里;而且几百次运行记录的规模也不够做微调。
第五部分 对照项目
| 本篇知识点 | 项目里的位置 | 做到了什么 | 没做到或可以改进的 |
|---|---|---|---|
| 输入远大于输出 | runs.db、REPORT 成本表 | 统计出 27 倍(第一版)、41.6 倍(全部 288 次) | — |
| 模型选择 | build_agent.MODEL = deepseek-flash | 便宜,能支撑几百次评测运行 | 没有换模型跑同一评测 |
| 采样随机性 | 每配置跑 3 轮 | 看到同项目步数波动(标准差平均 2.5 到 3.4 步) | 没显式设温度,也没记录 |
| 上下文控制 | read_file 分页、输出截断 | 控制单次放进上下文的量 | 没有历史压缩 |
| 提示词缓存 | 系统提示词固定在最前 | 前缀稳定 | 没记录缓存命中,不知道实际命中率 |
| 推理模式 | — | — | 没记录是否开启、思考 token 用了多少 |
| 计费和余额 | REPORT 记录 402 崩溃 | 事后能查到原因 | 没有余额监控和告警 |
| 幻觉(自述不实) | build_agent.verify | 不信模型说”成功”,程序独立验收 | 验收自身错过两次 |
| 微调 | — | 判断不需要,经验放知识库 | — |
第六部分 追问清单
| 你刚讲完 | 下一个追问 | 回答方向 |
|---|---|---|
| token | 中文和英文哪个更费 token | 取决于分词器;估算要用对应模型的分词器或 usage |
| 自注意力 | Q、K、V 是什么 | 查询、键、值;Q 和 K 算相似度得权重,按权重汇总 V |
| 自注意力 | 为什么长上下文慢 | 每个 token 要和前面所有 token 计算;预填充时间增长 |
| KV cache | 代价是什么 | 显存;上下文越长占用越多 |
| KV cache | 和提示词缓存什么关系 | 前者一次请求内复用,后者跨请求复用相同前缀 |
| 自回归 | 为什么输出比输入贵 | 预填充并行、解码串行 |
| Agent 成本 | 为什么输入是输出几十倍 | 每步重发完整历史;项目 41.6 倍 |
| temperature | 设成 0 就完全确定吗 | 通常贪心选最大,但服务端计算仍可能不同 |
| top_p | 和 top_k 的区别 | top_k 固定保留 k 个;top_p 按累积概率动态决定个数 |
| 延迟 | 用户觉得慢怎么办 | 流式输出改善 TTFT 体感;减输入、提缓存命中、减步数 |
| 推理模型 | Agent 里要不要开 | 复杂规划和排查开;简单步骤不开;用评测对比成本和效果 |
| 幻觉 | 为什么模型会自信地编 | 训练和评测奖励猜测;没有事实核查机制 |
| 幻觉 | RAG 能消除幻觉吗 | 能减少,不能消除;还要引用校验、允许拒答 |
| RLHF | 和 SFT 有什么区别 | SFT 学标准答案;RLHF 学偏好排序 |
| LoRA | 为什么省资源 | 冻结原参数,只训练低秩小矩阵;推理不增加延迟 |
| 微调 | 你项目为什么不微调 | 经验变化快、要出处,适合知识库;数据量不够 |
| 选模型 | 你怎么选的 | 价格为主;没做多模型对比是局限 |
第七部分 闭卷自测
1. 用自己的话说清楚大模型生成一段回答的过程,包括预填充、解码和 KV cache。
答案
预填充:把全部输入 token 一次性并行送入模型,算出每个 token 的中间结果并把 K、V 缓存下来,然后得到第一个输出 token。解码:每次根据前面所有内容预测下一个 token 的概率分布,按采样参数选一个接到末尾,只为这个新 token 计算 K、V,并和缓存的 K、V 做注意力,重复直到结束符、长度上限或工具调用。
2. 自注意力里 Query、Key、Value 分别起什么作用?为什么生成时要用因果注意力?
答案
Query 表示当前 token 在找什么;Key 表示每个 token 是什么,用来和 Query 算相似度;Value 是每个 token 能提供的信息,按相似度得到的权重汇总。生成时后面的 token 还不存在,训练时也要模拟这一点,所以每个 token 只能看前面的。
3. 实验 1 里不用 KV cache 要算 644,850 次 K/V,用了只要 2,300 次。这两个数怎么算出来的?“少 280 倍”是整体加速比吗?
答案
不用缓存:生成第 i 个 token 时重算”2000 + i”个 token 的 K/V,i 从 0 到 299 求和,2000×300 + (0+…+299) = 644,850。用缓存:输入算一次 2000,之后每个新 token 算 1 次,共 2300。不是整体加速比,只数了 K/V 的计算次数,新 token 和所有缓存 K 做注意力的计算没算进去。
4. 为什么 Agent 的输入 token 远大于输出?我项目的数字是多少?这对成本优化意味着什么?
答案
模型不记得上一次请求,每一步都要重发系统提示词、任务和完整历史,工具结果还很长。第一版 baseline 约 27 倍,全部 288 次运行约 41.6 倍。优化重点在输入:提高缓存命中(稳定前缀)、截断工具结果、压缩历史、减少步数。
5. 实验 2 里 temperature 从 0.2 到 2.0,第一名的概率怎么变?top_p=0.9 为什么保留了三个候选?
答案
0.993 → 0.629 → 0.431,温度越高分布越平。按概率排序累加:0.629 + 0.231 = 0.860 < 0.9,再加 0.104 = 0.964 ≥ 0.9,停止,保留 cmake、make、ninja,ls 和 rm 被砍掉,剩下三个重新归一化。
6. temperature=0 能保证每次输出完全相同吗?这对评测有什么启示?
答案
不能保证,通常近似贪心选最大,但服务端批处理和浮点计算可能导致不同结果;Agent 多步累积后路径差异更大。所以评测每个配置要跑多轮,看平均和波动。
7. 首 token 延迟和每 token 时间分别受什么影响?用户觉得慢时先做什么?
答案
首 token 延迟主要受输入长度、缓存命中、排队影响;每 token 时间受模型大小、服务负载影响。先做流式输出改善体感,再减少输入、提高缓存命中、限制输出长度,Agent 还要减少步数。
8. 按 DeepSeek 价格页,deepseek-flash 非高峰的命中输入、未命中输入、输出价格分别是多少?从中能得出 Agent 成本的哪两个结论?
答案
每百万 token 0.003、0.15、0.6 美元。结论:Agent 输入量是输出的几十倍,钱主要花在输入上;命中缓存价格是未命中的 1/50,缓存命中率影响巨大。(另外高峰价翻倍,批量任务可放非高峰。)
9. 推理模型和思维链提示有什么区别?在 Agent 里怎么权衡是否开启?
答案
推理模型经过训练,会自己在回答前生成思考过程;思维链提示是在提示词里要求模型一步步想。推理模式提升复杂决策质量,但思考 token 按输出计费、增加延迟,步数多时成本明显上升。复杂规划和排查步骤开,简单工具调用不开,用评测集对比效果和成本。
10. 按 Why Language Models Hallucinate 的解释,幻觉为什么会产生?应用层有哪些缓解手段?
答案
训练和评测机制奖励猜测而不是承认不确定:只看对错时说”不知道”必得零分,猜还可能得分。缓解:RAG 给资料、要求出处并校验、允许拒答、用外部结果验证(不信模型自述)、用工具查询代替回忆、低温度。
11. 预训练、SFT、RLHF 各自用什么数据、学到什么?InstructGPT 的关键结果是什么?
答案
预训练用海量文本学预测下一个 token,得到会续写的基础模型;SFT 用人工写的指令-回答对学会按指令回答;RLHF 用人对回答的排序训练奖励模型,再用强化学习让回答更符合偏好。InstructGPT 的 13 亿参数模型输出比 1750 亿参数 GPT-3 更受偏好,参数少 100 倍。
12. LoRA 为什么省资源?什么问题适合用微调、什么问题不适合?
答案
冻结原模型参数,只训练每层加入的低秩小矩阵;论文里相比 GPT-3 175B 全量微调,可训练参数少 10000 倍、显存降到三分之一,推理不增加延迟。适合:稳定的输出格式和风格、特定任务模式、把大模型能力迁移到小模型降成本。不适合:注入会变的知识、需要出处的知识,这类用 RAG。
延伸阅读
- Attention Is All You Need(Vaswani 等,2017)— Transformer 原始论文
- The Curious Case of Neural Text Degeneration(Holtzman 等,ICLR 2020)— Nucleus Sampling(top_p)
- Training language models to follow instructions with human feedback(InstructGPT,2022)— SFT、奖励模型、RLHF
- LoRA: Low-Rank Adaptation of Large Language Models(2021)
- Why Language Models Hallucinate(Kalai、Nachum、Vempala,2025)
- DeepSeek 模型与价格 — 上下文长度、思考模式、分时价格
下一篇:02 模型 API 工程——知道模型怎么工作之后,看怎么稳定、省钱地调用它:流式、结构化输出、超时重试、限流、降级。