LLM 和 Agent 的核心术语和原理 讲过,LLM 的本质就一句话:根据已有的文本,预测下一个 token 出现的概率。
比如输入「今天天气」,模型算出「真」的概率 30%,「不」的概率 20%,选一个拼到文本末尾,再拿新文本预测下一个,如此循环,直到输出一个表示「说完了」的特殊 token。所以模型的一次「回答」,其实是若干次「预测下一个 token」。
这里的「已有文本」就是 上下文是什么?token 怎么计费? 讲过的 messages 数组,它会被拼接成一大段文本送进模型。
这个解释足够理解 LLM 的本质了,但它具体是怎么预测的呢?本文就用通俗的语言把这个过程拆开,不需要很深的数学背景,你就能理解这些问题的答案:
- 这个 30% 概率到底是怎么算出来的?模型内部发生了什么?
- 为什么输出 token 都比输入 token 贵?
- 为什么缓存命中的输入 token 便宜很多?
开始之前,还得把训练和推理这两个词分清楚。
训练是造模型的过程:拿海量文本反复做文字接龙,猜错了就调整模型内部的参数,产物是一大堆固定下来的数字,包括词表、embedding 矩阵和每一层的权重,打包在一起就是你能下载到的模型文件。
推理则是使用模型的过程:拿这堆已经固定的参数,对你的输入算一遍,得到下一个 token。你每调用一次 API,都是在做一次推理。
两者最关键的区别是参数动不动:训练时参数一直在被修改,推理时参数是只读的。所以模型不会因为跟你聊过什么就发生改变,对话历史全靠 messages 每次重新发给它。
本文讲的是推理,即你输入问题,LLM 生成回复的这个流程。训练那边的完整流程,包括预训练、SFT、RLHF 这些环节,后面会单独展开。
文字怎么变成向量
词表是怎么造出来的
模型不认识文字,只认识数字,所以第一步是把文本切成 token,每个 token 对应词表里的一个数字索引,类似这样:
Qwen3 词表(节选)
the -> 1782
hello -> 14990
今天 -> 100644
天气 -> 104307
...在模型眼中,一段文本就是若干 token 序列,或者说数字序列。
比如开头那句「今天天气」,在 Qwen3 里正好切成 今天 和 天气 两个 token,模型实际收到的就是 100644 和 104307 这两个数字。
这个词表不是人工编的,而是从训练语料里统计出来的,用的算法叫 BPE(Byte Pair Encoding,字节对编码)。
训练语料是成篇的自然语言文本,不过在统计之前有一步预处理:按空格和标点把文本切成一个个词,空格并到后面那个词上。比如 the cat sat on the mat 会变成这样(Ġ 代表空格):
the | Ġcat | Ġsat | Ġon | Ġthe | Ġmat切完之后统计每个词出现了多少次,就得到一张「词 -> 频次」的表,这才是 BPE 真正的输入。
接下来就是按照固定的规则反复统计、合并。一开始词表里只有最基础的单个字符 a, b, c, ...,每个词都是一串散字符的组合。
然后统计所有词,看哪两个相邻的片段一起出现得最频繁,就把这一对合并成一个新片段。合并出来的新片段会参与下一轮统计,所以片段会逐轮变长。
假设一份迷你语料统计下来只有 4 个词,为了看着清楚,这里省略了词首的空格标记:
语料:cat(10 次) cats(5 次) hat(12 次) hats(4 次)
起点:c a t / c a t s / h a t / h a t s频次具体是这么算的:把每个词内部所有相邻的组合都列出来,一个词出现多少次,它内部的每个组合就算多少次,最后在整个语料范围内汇总。
比如 a + t 这个组合,cat、cats、hat、hats 四个词里都有,所以它的频次是 10 + 5 + 12 + 4 = 31 次。而 h + a 只在 hat、hats 里有,频次是 12 + 4 = 16 次。全部数完,a + t 的 31 次是最高的,第一轮就合并它。
照这个规则往下合并,前四轮是这样:
第 1 轮:a + t 出现 31 次(10+5+12+4),合并成 at
c at / c at s / h at / h at s
第 2 轮:h + at 出现 16 次(12+4),合并成 hat
c at / c at s / hat / hat s
第 3 轮:c + at 出现 15 次(10+5),合并成 cat
cat / cat s / hat / hat s
第 4 轮:cat + s 出现 5 次,合并成 cats
cat / cats / hat / hat s注意第 2 轮合并的是 h + at,其中 at 是第 1 轮的产物,它作为一个整体参与了新一轮的统计。
下面这个面板就是这份迷你语料的完整合并过程,你可以一轮轮点下去,看每轮的频次统计、被合并的组合,以及 merges.txt 和 vocab.json 里多出来的东西:
走到最后,四个词都变成了完整的单词,各占一个 token,语料里再没有相邻组合可以合并了。真实的训练就是在巨大的语料库中这样合并十几万轮。
每合并一次会留下两样东西:一条「这两个片段可以合并」的规则,和一个新 token 的词表条目。训练结束时,前者按顺序存进 merges.txt 文件,后者存进 vocab.json 文件。
后面的文章 看懂 Hugging Face 和模型文件 会介绍如何查看和下载开源模型,这两个文件就在模型目录里。这里我们以 Qwen3-0.6B 为例,看看这两个文件长什么样子。
merges.txt 一行一条合并规则,按训练时学到的先后顺序排列。看开头几行:
Ġ Ġ
ĠĠ ĠĠ
i n
Ġ t
ĠĠĠĠ ĠĠĠĠ
e r开头的全是语料里最高频的组合:连续空格(训练语料里有大量代码,缩进就是成串的空格),还有 in、er 这种英文里最常见的字母对。这个文件总共有 151,387 条规则。
vocab.json 就是词表,存储每个 token 和对应的索引,这里节选几条:
{
"Ġthe": 279,
"the": 1782,
"Ġworld": 1879,
"Hello": 9707,
"hello": 14990
}注意 the 和 Ġthe(带词首空格)、hello 和 Hello(大小写不同)是不同的 token,各有各的编号。
Qwen3 的词表最终有 151,936 个 token,就是这款模型能区分的 token 种类数上限。
tokenizer 怎么切分文字
词表造好之后,每次调用模型,tokenizer 就用它来处理你的输入。
第一步和训练时一样,先按空格和标点把文本切成一个个词,然后进行切分。
这里有个容易误解的地方:tokenizer 并不是拿着词表去文本中做字符串匹配,而是反过来,先把词打散成一个个单字符,再照着 merges.txt 里的规则一步步合并回去,直到没有规则可用为止。最后剩下几块,就是几个 token。
拿 hello 走一遍。它会用到 merges.txt 里的 4 条规则,这几条散落在文件的不同位置:
...
e l (第 46 条)
...
l o (第 130 条)
...
el lo (第 4536 条)
...
h ello (第 14735 条)
...从散字符开始,把这 4 条规则依次套用上去:
起点:h | e | l | l | o
用 e l -> h | el | l | o
用 l o -> h | el | lo
用 el lo -> h | ello
用 h ello -> hello可以看到并不是按照 hello 从左到右进行合并,而是从上往下扫 merges.txt,看哪条合并规则靠前就先合并哪些字符。排位靠前意味着训练时学得早,在语料里出现得更频繁,所以要先合并。
比如相邻的组合有 h + e、e + l、l + l、l + o 四个,它们在 merges.txt 规则表里都能查到,排位分别是 128、46、399、130。e + l 的 46 最靠前,所以第一步合并的是它,而不是最左边的 h + e。
最后 hello 合并成了一个整体(token),查 vocab.json 得到编号 14990。「常见单词整词占一个 token」就是这么来的。
再看一个完整的句子 The weather is unbelievably nice today,按照上述规则进行拆分、合并,得到这样的 token 序列:
tokens: The | Ġweather | Ġis | Ġunbelie | vably | Ġnice | Ġtoday
ids: 785 | 9104 | 374 | 38937 | 88134 | 6419 | 3351weather、nice、today 都通过 merges.txt 中的规则合并成了整体。但是 unbelievably 合并到 Ġunbelie 和 vably 就停住了,因为 merges.txt 中没有这两个词的合并规则,没法再往下合并。
为啥没有它俩的合并规则呢?只能说明在训练语料中 unbelievably 属于低频词,生成词表的步骤中并没有创建对应的规则。
所以「低频长词被拆成几段」并不是 tokenizer 主动去拆它,只是合并到某一步没有规则可用,剩下几块就是几个 token。
注意 vably 前面没有 Ġ,说明它和前一个 token 在原文里是连着的。靠这个标记,token 序列能无损还原出原始文本。
明白了分词的原理,就知道为什么有些模型处理中文更费 token 了:
如果训练语料中英文占大头,那么英文单词的总体词频就高,merges.txt 中就有更多针对英文的合并规则,常见英文单词就能合并成独立 token;中文的总词频低,merges.txt 中的合并规则就少,一个汉字往往要占 1 到 2 个 token。
当然,这取决于训练语料,不同模型差异很大。比如 Qwen / DeepSeek 这类中文语料充足的模型,很多常见中文词汇也有可能只占一个 token,处理中文就比较省 token。
embedding 矩阵
切分完,一句话变成了一串 token 编号序列,但编号本身没有含义,hello 是 14990 号还是 42 号都无所谓。
所以第二步是查表。模型里有一张 151,936 行、1,024 列的大表,叫 embedding 矩阵:每个 token 编号对应表里的一行,取出来就是一个 1,024 维的向量(一串 1,024 个数字)。
其中行数 151,936 就是词表的大小(可识别 token 的数量),列数 1024 是模型的固有参数,一般来说列数越大就能存储越多的细节,模型能力就越强。
你可以把这个向量理解为 token 的「语义坐标」,这张表的每个数字都是训练出来的,训练的结果是:含义相近的 token,向量在空间里的位置也相近。
比如「猫」和「狗」的向量离得近,「猫」和「微积分」离得远。
那这张表是怎么训练出来的呢?
刚建表的时候,里面填的全是随机数,此时「猫」和「狗」的向量毫无关系。训练时模型反复做文字接龙:给它一段真实文本,让它预测下一个 token,预测错了就纠正,把相关向量朝着「让正确答案概率更高」的方向微调一点点。
海量语料这样过一遍,那些经常出现在相似位置的词,比如「我养了一只 ___」里的「猫」和「狗」,向量就被推到了相近的位置。所以「语义相近的向量也相近」并不是设计出来的,而是训练的副产品。
到这里,「今天天气」这半句话变成了几个 1,024 维向量,可以送进模型主体了。
向量怎么变成下一个 token
多头注意力机制
查表得到的向量有个问题:它是固定的,不管上下文。
比如「苹果」这个 token 的向量在 embedding 表中是固定的,但是在「我吃了个苹果」和「我买了个苹果手机」里含义完全不同,光靠查表区分不出来。
注意力机制(attention) 就是解决这个问题的:让每个 token 去「看」它前面的所有 token,把相关的信息吸收进来,更新自己的向量,然后再尝试预测下一个 token。这样就能充分参考前面的所有信息,给出合理的预测了。
具体怎么个看法?每个 token 的向量会衍生出三个向量:
- (Query,查询):我在找什么信息。
- (Key,键):我有什么特征。
- (Value,值):我实际能提供的信息。
有点像搜索引擎: 好比是每个网页的标题和关键词, 好比是网页的正文, 好比是你输入的搜索词。
一个 token 拿自己的 ,和前面每个 token 的 算匹配度;匹配度高的,就多吸收一点它的 。把吸收来的信息加权汇总,这个 token 就得到了一个融合上下文的新向量。
比如在「我买了个苹果手机」里,最后一个 token 是「手机」,它的 和前面「苹果」的 匹配度很高,「苹果」的信息被吸收进「手机」的向量里,模型自然就知道这整句话在说苹果公司的一款产品,而不是一种水果。
类似的,在「我吃了个苹果」里,最后一个 token「苹果」的 和前面「吃」的 匹配度很高,「吃」的信息被吸收进「苹果」的向量里,模型自然就知道这句话在说一种水果而不是苹果公司。
这里有个关键细节:生成任务里的注意力是单向的,每个 token 只看它前面的 token,不看后面的。这叫因果注意力(causal attention),因为预测下一个 token 的时候,「后面」还不存在。
单向注意力带来一个重要性质:一个 token 的 和 算出来之后就永远不会变。因为它们只由这个 token 自己和它前面的内容决定,后面再新增多少 token 都影响不到它。先记住这个性质,本文后半篇讲的省钱手段全靠它。
真实的模型还要更复杂一些。前面说每个 token 衍生出一组 、、,实际上模型会同时算好几组。
因为一个 token 和前文的关系有很多种,可能是语法上的搭配,可能是指代同一个东西,也可能只是位置挨得近,一组 、、 只能表达一种关系,所以让好几组各看各的,最后把它们吸收到的信息汇总起来。
每一组就叫一个注意力头(attention head),多个头并行计算的这套设计就叫多头注意力(Multi-Head Attention),是 Transformer 架构的标配。Qwen3-0.6B 有 16 个头,每个头处理的都是 128 维的向量。
16 个头各做一遍语义吸收,结果再汇总处理,这一整套计算合起来叫做一层。
模型会把这样的层叠起来反复计算,Qwen3-0.6B 叠了 28 层:第一层输出的向量作为第二层的输入,再做一遍同样的计算,如此重复 28 次。每层都有自己独立的 16 个头和参数,关注的侧重点也不同,所以向量逐层吸收到的上下文信息越来越丰富。
给整个词表打分
走完全部 28 层之后,最后一个 token 的向量已经凝聚了整句话的信息,模型用它来预测下一个 token。
预测的方式简单粗暴:拿这个向量和词表里 151,936 个 token 挨个算匹配度,得到 151,936 个分数。这些分数有个专门的名字,叫 logits。
这些分数还不是概率,可能有正有负、加起来也不等于 1。最后用 softmax 函数归一化:给每个分数取指数,再除以所有指数的总和:
其中 是第 个 token 的分数, 是它最终的概率, 是词表大小。这样运算之后得到的 151,936 个 都是正数,且所有概率加起来正好等于 1。
到这里,针对整句话下一个 token 的概率分布才真正出现。比方说「今天天气」这段话,得到的概率分布可能是「真」30%,「不」20% 等等。
从概率分布到输出
有了概率分布,选哪个 token 输出?
最省事的做法是永远选概率最大的那个,这叫贪心解码(greedy decoding)。但实践中发现它的输出很死板,还容易陷入同一句话反复循环的怪圈。
所以实际的做法是按概率抽签:「真」有 30% 的机会被抽中,「不」有 20%。这就是为什么同一个问题,模型每次的回答都不太一样。
抽签的随机程度可以调,这就是 temperature 参数。它的实现只有一步:做 softmax 之前,先把所有分数除以 temperature。
小于 1 时,分数之间的差距被除法放大,高分 token 的概率更突出,输出更稳定; 大于 1 时,差距被抹平,低分 token 也有了出头机会,输出更多样。
等于 0 是个特例,数学上除以零没有定义,所以推理框架的约定是:temperature 设为 0 就直接走贪心解码,每次都选概率最大的 token。LLM 和 Agent 的核心术语和原理 说编程场景要把温度设为 0,底层含义就是这个。
另一个常见参数 top-p 是给抽签划范围的:把 token 按概率从高到低排,从最高的开始累加,加到 (比如 0.95)就停,后面的长尾全部丢弃,在保留的集合里重新归一化再抽签。它防的是小概率的怪 token 被运气抽中。类似的 top-k 更直接,只保留概率最高的前 个。
这些参数不用你从零调,开源模型发布时一般都带着一套调好的默认值。比如后面的教程会带你 在本地运行大模型,用 ollama show qwen3:0.6b 就能看到这个模型的默认采样参数:
Parameters
repeat_penalty 1
stop "<|im_start|>"
stop "<|im_end|>"
temperature 0.6
top_k 20
top_p 0.95下面这个面板就让你亲手调这几个参数,初始值就是上面这套默认值。里面的数据是我在本机跑 qwen3:0.6b 实测的:输入「今天天气」,把模型给出的概率最高的 20 个 token 抓下来。
灰色底条是模型实测的原始概率,蓝色条是当前参数下的抽签概率,拖动 temperature 就能看出分布被拉尖还是被抹平。
点「抽一个」是真的按当前概率抽签一次,抽中的 token 会拼到 prompt 后面。多抽几次,你就看到了同一个分布抽出不同结果的过程:
预填充和解码
现在完整的生成流程有了:切 token、查表、过 28 层权重网络、给词表打分、softmax 计算概率、抽签,得到一个新 token,拼回文本末尾,再来一轮。
注意这个循环的一个特点:它没法并行。第 2 个输出 token 的计算依赖第 1 个输出 token 的结果,必须等它生成完才能开始。生成 500 个 token 的回答,就要老老实实串行跑 500 轮。
输入侧完全不同。你发来的 prompt 可能有一万个 token,但它们是现成的文本,不用一个一个生成,每个 token 的 、、 和注意力可以铺开在 GPU 上一次性算完。GPU 最擅长的就是这种大矩阵运算,算力能被充分利用。
业界给这两个阶段起了名字:处理输入的阶段叫 prefill(预填充),逐个生成输出的阶段叫 decode(解码)。
decode 阶段还存在另一种浪费,跟 GPU 的工作方式有关。
模型权重加载之后是常驻显存的,但显存里的数据没法直接参与运算,必须先搬进 GPU 的计算单元。所以每算一层就要搬一层的权重,28 层走完,相当于把全部权重从显存完整搬了一趟。
问题在于,这趟搬运的量是固定的,跟这一轮要算多少个 token 无关。
prefill 一次处理上万个 token,搬一趟权重就可以处理上万个 token 的计算,很划算;decode 每轮只算一个 token,却要搬同样一趟。结果就是 GPU 大部分时间不是在计算,而是在等数据搬运,算力大量闲置。NVIDIA 的 一篇技术博客 对这两个阶段的性能特征有详细分析。
上下文是什么?token 怎么计费? 讲计费时说过「输入可以并行处理,输出只能串行生成」,指的就是 prefill 和 decode 的差别。同样数量的 token,decode 占用 GPU 的时间远比 prefill 长,所以输出 token 的单价更贵。
KV cache
decode 阶段还藏着一个更隐蔽的性能问题:
生成第 101 个 token 时,注意力需要用到前 100 个 token 的 和 ;生成第 102 个 token 时,又需要前 101 个的 和 。难道每一轮都把前文所有 token 的 、 从头算一遍?
当然不用。前面讲因果注意力时说过那个性质:一个 token 的 和 算出来就永远不会变。既然不会变,算一次存下来就行了。
这块存 、 的内存就叫 KV cache。有了它,每一轮 decode 只需要计算新 token 自己的 、、,把新的 、 追加进缓存,前文的 、 直接从缓存里读。
你可能会问:为什么只缓存 和 ,不缓存 ?因为注意力的计算方式是拿「当前 token 的 」去匹配「前文所有 token 的 和 」,历史 token 的 在后续轮次里根本用不到,用完一次就可以扔了。
没有 KV cache,每一轮的计算量会随着上下文变长而暴涨;有了它,每轮只算增量,所以 Ollama、vLLM 这些推理引擎和各家云端服务全都默认开启。
但 KV cache 的代价是内存,我们来算笔账,Qwen3-0.6B 的 config.json 里有几个关键数字:
前面讲过它有 28 层,每层 16 个注意力头,每个头的 、 各是 128 维。不过 、 并不是每个头都单独算一份:每两个头共享一组,所以每层实际只有 8 组要缓存。这个共享设计叫分组查询注意力(GQA),目的就是压缩 KV cache。
缓存一个 token 需要: 和 两份,乘 28 层,乘 8 组,乘 128 维,共 57,344 个数字。按 FP16 精度每个数字 2 字节,一共 114,688 字节,约 115KB。
单个 token 不起眼,但乘上上下文长度就吓人了:32,768 个 token 的上下文,KV cache 就要占 115KB × 32,768 ≈ 3.7GB。
这也解释了「上下文越长,内存占用越多」:KV cache 的大小和上下文长度成正比,长上下文吃掉的就是这块内存。
缓存命中为什么便宜
多轮对话的实质是每一轮都把完整的对话历史重发一遍,相邻两次请求的 messages 前缀完全相同。
从服务端的视角看,上一轮请求做 prefill 时,已经为这些前缀 token 算过一遍 、 了,它们就躺在 KV cache 里。如果请求结束后不把它们丢掉,而是保存起来,下一轮请求进来时检测到前缀一致,就直接把存好的 、 加载回来接着用,前缀部分的 prefill 整个跳过,只计算新增的部分。
这就是 Prompt Cache 的真身:一块跨请求保存的 KV cache。
为什么必须前缀完全一致才能命中?还是因果注意力那个性质:每个 token 的 、 只由它自己和它前面的内容决定。前缀一模一样,算出来的 、 就一模一样,可以放心复用;但只要中间改动一个字,从那个位置往后所有 token 的 、 就全变了,缓存立刻作废。
这也是为什么 Agent 的设计原则是「保持 messages 前缀稳定,只往末尾追加」。中途改系统提示词、往历史中间插消息,都会让缓存大段失效。
再看价格。命中缓存的 token 不消耗 GPU 算力,服务商的成本从「计算」变成了「存储和读取」,这两者的成本差着数量级。
DeepSeek 干脆把这块缓存放到磁盘阵列上(官方叫 Context Caching on Disk),磁盘存储便宜,所以命中价格能比未命中便宜一个数量级以上。
Anthropic 的 缓存定价 也能对上了:缓存读取按基础输入价的 0.1 倍计费,省掉的是计算;缓存写入按 1.25 倍(保存 5 分钟)或 2 倍(保存 1 小时)计费,因为 、 要实打实占用服务器的存储资源,存得越久越贵。
顺便说一句,缓存命中除了省钱还提速:命中了前缀缓存的 prefill 计算,第一个字能更快蹦出来。
比如你用 Claude Code / Codex 新开会话时,第一条消息通常响应比较慢,之后再追问,就会快很多。
总结
把整条链路串起来:文字被 tokenizer 切成 token 编号,查 embedding 表变成向量;几十层计算让每个 token 吸收前文的信息;最后对全词表打分,softmax 把分数变成概率,按 temperature 和 top-p 的设置抽签选出下一个 token,拼回文本末尾,进入下一轮循环。
推理分 prefill 和 decode 两个阶段:前者并行处理输入,后者串行生成输出,这就是输出 token 更贵的原因。
KV cache 则是串起后半篇的关键概念:因果注意力保证每个 token 的 、 算完不变,所以 decode 不用重算前文;长上下文的内存大头是它;Prompt Cache 的本质是跨请求复用它。搞懂这一个概念,API 价格表上的各种数字就都能对上了。
下一篇 用 Ollama 在本地运行大模型,我们把一个开源小模型下载到自己的电脑上,亲手把这条推理链路跑起来。