目录
1. 核心机制:自回归下一个 Token 预测
2. Token:模型理解文本的最小单位
2.1 Token 是什么
2.2 Token 的常见形态
2.3 为什么需要 Token
2.4 解决“词汇爆炸”问题
2.5 词表(Vocabulary)
2.6 Byte-fallback:零 UNK 的幕后功臣
3. 如何选出下一个 Token:从 Logits 到采样
3.1 采样策略
3.2 终止符
4. 推理工程:Prefill、Decode 与 KV Cache
4.1 输入 Token 和输出 Token
4.2 致命伤一:算力闲置
4.3 致命伤二:显存带宽被榨干
4.4 致命伤三:KV Cache 占用显存
4.5 工程优化
5. 多轮对话:无状态模型如何“记住”上下文
5.1 LLM 本质是无状态的
5.2 多轮对话靠应用层维护
5.3 为什么需要多轮对话
5.4 核心瓶颈:上下文窗口
6. 多轮对话的优化:摘要、滑动窗口、RAG 与剪枝
6.1 对话摘要
6.2 滑动窗口
6.3 RAG:检索增强生成
6.4 KV Cache 与提示词缓存
6.5 上下文剪枝
6.6 工程建议
大语言模型(LLM)看起来能对话、能推理、能写代码,但底层生成机制极其简单:给定前文,预测下一个 Token。完整回答并不是一次性写出来的,而是“逐字预测、文字接龙”地自回归生成。
本文从五个层面展开:
LLM 的核心生成机制:自回归下一个 Token 预测;
Token、Tokenizer 与词表:文本如何变成数字;
如何选出下一个 Token:Logits、Softmax 与采样策略;
推理工程:Prefill、Decode、KV Cache 与输出 Token 为什么贵;
多轮对话:无状态模型如何“记住”上下文,P(x1,x2,…,xT)=t=1∏TP(xt∣x<t)以及摘要、滑动窗口、RAG、剪枝等优化方案。
这个过程叫自回归生成(Autoregressive Generation)。
DeepSeek 默认终止符是
<|end▁of▁sentence|>,不同模型终止符不同。
1. 核心机制:自回归下一个 Token 预测
LLM 本质上是一个条件概率模型:
其中:
模型输出的是词表中所有 Token 的概率分布;
通过采样策略选出一个 Token,拼接到前文后面,继续预测下一个。
这就是自回归生成(Autoregressive Generation):
用自己刚刚生成的结果,作为预测下一个结果的依据,像滚雪球一样,一步一步往外推。
伪代码可以写成:
context = tokenizer.encode(prompt) while True: logits = model(context) # 计算词表中每个 token 的分数 probs = softmax(logits / temperature) # 转成概率分布 next_token = sample(probs, top_k, top_p) # 采样 if next_token == eos_token: break context.append(next_token) return tokenizer.decode(context)举个例子:
用户输入:
中国的首都是哪里?
模型可能依次生成:
中国 → 的 → 首都 → 是 → 北京 → 。
当然,实际切分不一定按词,而是按 Token。每次生成一个 Token 后,模型会把它拼回上下文,再预测下一个,直到遇到终止符。
不同模型的终止符不同。例如 DeepSeek 默认终止符是:
<|end▁of▁sentence|>2. Token:模型理解文本的最小单位
2.1 Token 是什么
Token(词元)是 LLM 理解和处理文本的最小基本单位。
人类阅读时,大脑会自动断句:
我爱大模型开发
我 / 爱 / 大模型 / 开发
LLM 也需要把文本切分成一个个 Token,这个过程叫分词(Tokenization)。
不同的人断句方式不同,不同模型厂商的切词方式也不同。同一句话,在不同 Tokenizer 下可能切出不同的 Token。
2.2 Token 的常见形态
| 类型 | 示例 | 说明 |
|---|---|---|
| 完整单词 | Hello | 可能是 1 个 Token |
| 子词 | un + believe + able | 一个单词拆成多个 Token |
| 标点 | ,.! | 通常各占 1 个 Token |
| 中文 | 我、爱、大模型 | 一个汉字通常 1 到 2 个 Token,视词频而定 |
LLM 不看完整句子,而是把句子拆成 Token 来“理解”和“生成”。
2.3 为什么需要 Token
计算机只能处理数字和数学运算,无法直接理解文本。Token 是连接“人类语言”和“模型数字世界”的桥梁。
第一步:切分编号。
unhappiness → ["un", "happi", "ness"] → [432, 891, 256]此时计算机拿到的是纯数字[432, 891, 256]。但注意:数字 891 并不比 432 “更大”或“更开心”,它只是索引。
第二步:把离散 ID 映射成高维向量。
"happy" → [0.8, 0.9, -0.1, ...] "glad" → [0.79, 0.88, -0.09, ...]语义越近,向量在空间中距离越近。un的向量带有“否定”方向,与happy组合后,会把语义推向sad附近。
2.4 解决“词汇爆炸”问题
如果按字母为单位:
词表很小,但粒度太细;
unhappiness会被拆成 11 个字符;每个字符几乎没有独立语义;
序列变长,计算量按平方增长,效率极低。
如果按单词为单位:
词表会爆炸;
英语有几十万单词,还有大量派生词;
遇到新造词如
cryptocurrency,模型只能当成未知。
主流方案是子词分词,如 BPE、WordPiece、Unigram、SentencePiece,把文本切分成介于字和词之间的子词单元。
2.5 词表(Vocabulary)
词表是模型预定义的映射字典,包含模型“认识”的所有 Token。
| ID | Token |
|---|---|
| 0 | ! |
| 1 | # |
| ... | ... |
词表定义模型能“看见”和“写出”的所有基本单位。模型输出 ID,再通过词表映射回文本。
常见词表大小:
GPT-2:50,257;
Llama 2:32,000;
Llama 3:128,256。
模型不能生成词表外的 Token ID。例如词表大小是 50,257,它永远不可能输出编号 50,258 的 Token。
但模型可以组合词表内的 Token,拼出词表里没有的“词”。例如:
un + believe + able → unbelievable如果遇到词表无法表示的字符,比如某些 Emoji,就会触发字节级回退(Byte-fallback)。
2.6 Byte-fallback:零 UNK 的幕后功臣
以 😊 为例,其 UTF-8 编码是 4 个字节:
text
0xF0 0x9F 0x98 0x8A
传统 Tokenizer 如果词表里没有 😊,会抛出[UNK],模型完全不知道它代表什么。
Byte-fallback 会把 4 个字节拆开,映射到词表里的专用字节 Token:
<byte_0xF0> <byte_0x9F> <byte_0x98> <byte_0x8A>这样模型不会报错,也不会丢弃字符。正常高频词仍然用大 Token 保证效率,极端情况下降级到字节模式保证鲁棒性。
3. 如何选出下一个 Token:从 Logits 到采样
LLM 决定下一个 Token 的核心流程是:
基于前文上下文,计算词表中所有 Token 的分数(Logits);
通过 Softmax 转成概率分布;
通过采样策略选出一个 Token;
拼接到上下文,循环往复。
这就是自回归生成。
3.1 采样策略
| 策略 | 说明 | 特点 |
|---|---|---|
| 贪婪搜索 | 每次选概率最大的 Token | 确定性强,但容易重复、呆板 |
| Beam Search | 保留多个候选序列 | 适合翻译等任务,开放生成中多样性差 |
| 温度采样 | 用温度 TT 调整分布平滑度 | TT 低更确定,TT 高更随机 |
| Top-k | 只保留概率最高的 k 个 Token | 过滤长尾 |
| Top-p | 保留累积概率达到 p 的最小集合 | 又称 Nucleus Sampling |
| 重复惩罚 | 降低已出现 Token 的概率 | 减少复读 |
3.2 终止符
模型生成到终止符时停止。常见终止符:
<|endoftext|> <|eot_id|> <|end▁of▁sentence|>不同模型终止符不同,工程上需要正确配置。
4. 推理工程:Prefill、Decode 与 KV Cache
4.1 输入 Token 和输出 Token
| 类型 | 内容 | 计费 |
|---|---|---|
| 输入 Token | 用户提示词、系统提示、联网搜索结果、历史对话 | 按输入 token 计价 |
| 输出 Token | 模型生成的完整回复 | 按输出 token 计价 |
为什么输出 Token 更贵?核心有三个原因。
4.2 致命伤一:算力闲置
输入阶段叫Prefill:
把 1000 个 Token 打包成一个大矩阵;
GPU 的 Tensor Core 可以满载并行计算;
算得多,但 0.1 秒就干完了;
占用硬件时间极短。
输出阶段叫Decode:
每次只算 1 个 Token 的 Q;
但为了算这 1 个 Q,要把缓存里的 1000 个 K 搬出来做点积;
GPU 计算单元大量时间在等数据搬运;
生成 1000 个输出 Token 需要串行跑 1000 步;
总耗时可能是输入阶段的 10 倍以上。
类比:
输入像满载货车走高速:费油但跑得快;
输出像市区堵车走走停停:不费油但耗时极长。
4.3 致命伤二:显存带宽被榨干
输出阶段瓶颈从“计算速度”变成“显存搬运速度”,即Memory Bound。
| 阶段 | 瓶颈 | 硬件资源 |
|---|---|---|
| 输入 Prefill | 计算密集 | 算力 |
| 输出 Decode | 内存带宽密集 | HBM 带宽 |
为了算一个新 Token,要把缓存里几千个 K、V 向量从显存搬到缓存。显存带宽是 GPU 最稀缺、最贵的资源之一。例如 H100 带宽 3.35TB/s,输出阶段疯狂搬运数据,带宽容易打满。
4.4 致命伤三:KV Cache 占用显存
Transformer 解码时,自注意力需要 K 和 V。每生成一个新 Token,就要把该 Token 的 K、V 追加进显存,直到对话结束。
生成第 1 个 Token:缓存 1 份 K/V 生成第 2 个 Token:缓存 2 份 K/V ... 生成 1000 个 Token:缓存 1000 份 K/V对于云厂商,显存就是钱。一块 A100 只有 80GB 显存,如果一个用户生成超长文本占了 40GB,这张卡能同时服务的用户数就急剧减少。
4.5 工程优化
KV Cache:缓存历史 K/V,避免重复计算;
MQA / GQA:减少 KV 头数,降低 KV Cache 大小;
PagedAttention:像操作系统虚拟内存一样分页管理 KV Cache,减少碎片;
Continuous Batching:连续批处理不同请求的 Decode 阶段,提高吞吐;
KV Cache 量化:用低精度存储 K/V,降低显存占用;
投机采样:用小模型草拟多个 Token,大模型并行验证,加速 Decode。
5. 多轮对话:无状态模型如何“记住”上下文
5.1 LLM 本质是无状态的
LLM 本身是无状态(Stateless)的。处理每条指令时,默认不知道你之前说过什么。
你问 DeepSeek:
我叫小明,我今年 6 岁了,你叫什么?
它回复后,你打开一个新会话,它并不知道你是谁。因为大模型没有记忆,只是在做概率预测。
5.2 多轮对话靠应用层维护
所谓“多轮对话”,其实是应用层把历史对话记录连同当前新问题一起,重新打包发送给模型。
messages = [ {"role": "system", "content": "你是一个助手。"}, {"role": "user", "content": "我叫小明,我今年6岁了,你叫什么?"}, {"role": "assistant", "content": "你好小明,我是AI助手。"}, {"role": "user", "content": "我叫什么,我明年几岁了?"} ]模型看到完整 messages 后,就能回答:
你叫小明,明年 7 岁了。
这种能力就是多轮对话带来的。
5.3 为什么需要多轮对话
上下文依赖
代词“它”“那个”需要前文才能理解。复杂问题逐步澄清
用户往往不会一次提供全部信息,而是多轮互动逐步完善需求。纠错与对齐
模型第一轮可能出错,多轮对话给用户“指正”的机会。
5.4 核心瓶颈:上下文窗口
大模型的“记忆”完全依赖输入文本,即上下文窗口。这个窗口有固定长度上限,例如 32K tokens。一旦对话历史超出限制,最早的信息就会被遗忘或截断。
用户:我对花生过敏,以后推荐食物时请避开花生。 助手:明白了,我会记住您对花生过敏。 ... 多轮之后 ... 用户:有没有适合下午茶的蛋糕配方? 助手:推荐花生酱玛芬蛋糕:低筋面粉、花生酱、鸡蛋、牛奶、糖、泡打粉。 用户:???我一开始就说了我对花生过敏!这就是上下文窗口溢出导致关键信息丢失。
6. 多轮对话的优化:摘要、滑动窗口、RAG 与剪枝
6.1 对话摘要
超过长度后,让模型把旧对话压缩成简短摘要。
摘要:用户对花生过敏。用户是烘焙新手,已尝试白面包,烤箱为30升。 用户询问过戚风蛋糕塌陷的原因,正在寻找简单的下午茶蛋糕配方(不能含花生)。后续对话带上摘要,而不是全量历史。
优点:节省 Token。
缺点:可能丢失细节,需要保留关键实体。
6.2 滑动窗口
只保留最近 N 轮对话,超过就移除最早的消息。
优点:实现简单。
缺点:可能丢失早期关键信息,比如“花生过敏”。
6.3 RAG:检索增强生成
把历史对话或外部知识存入向量数据库,按需检索相关片段,而非全量输入。
流程:
当前消息 → 检索器 → 向量库 → 相关片段 → 拼接当前消息 → 模型生成常用向量数据库:FAISS、Milvus、Chroma、Weaviate。
优点:可以保留长期关键信息。
缺点:检索质量依赖 Embedding 和召回策略。
6.4 KV Cache 与提示词缓存
KV Cache 缓存已计算的历史 K/V,每轮只算新内容。
["今", "天", "天", "气"] 生成“真”:缓存前 4 个 token 的计算信息。 生成“不”:取出之前 5 个结果,和新算的拼接。 生成“错”:取出之前 6 个结果,和新算的拼接。几乎所有生产级服务都会用 KV Cache 或提示词缓存(Prompt Caching)来降低延迟和成本。
6.5 上下文剪枝
移除历史中对当前生成无用的 Token。
剪枝前:
[1] 小明:帮我推荐几本好看的科幻小说,要短篇的。 [2] AI:《你一生的故事》《呼吸》《献给阿尔吉侬的花束》都不错。 [3] 小明:谢谢!对了,你吃午饭了吗? [4] AI:我不吃饭,不过你可以去食堂看看。 [5] 小明:好吧。我现在要写物理作业,是关于“动量守恒”的。 ...剪枝后:
[5] 小明:我现在要写物理作业,是关于“动量守恒”的。 [6] AI:动量守恒的关键是系统不受外力或合外力为零。你有具体题目吗? ...优点:减少 Token。
缺点:可能剪掉后续需要的信息,需谨慎。
6.6 工程建议
设计多轮对话系统时,可以按 Token 预算分配上下文:
| 模块 | 建议 |
|---|---|
| 系统提示 | 固定关键约束,如“用户对花生过敏” |
| 历史摘要 | 压缩旧对话,保留实体和意图 |
| 检索片段 | RAG 召回相关历史或知识 |
| 最近历史 | 保留最近 3-5 轮,保证连贯 |
| 当前问题 | 必须完整保留 |
触发策略:
当历史 Token 超过阈值时,自动生成摘要;
关键信息置顶到系统提示;
使用 KV Cache / Prompt Caching 降低重复计算;
对长对话使用 RAG,而不是全量拼接。