写给客户端工程师的 AI 底层原理
INTERACTIVE COURSE 不只读懂,动手把整套原理跑一遍 开始学习 →写在前面
这篇文档面向客户端/前端工程师。你不需要有任何 AI 背景,只需要会写代码。每一章分两层:通俗版讲清”这东西是干嘛的”,让你下次别人问起时能真的答出来;进阶版讲原理和 why,不这么设计会怎样。
序章:一次 API 调用里发生了什么
在把模型内部拆开之前,先看一遍从”调 API”到”拿到回复”这条链路上,模型到底做了几件事。有了这张全景图,后面的章节就是往这七步里各自填肉。
let reply = await llm.chat(prompt: "今天天气怎么样?")
按下回车之后,大概会有七步:
- Tokenizer 分词:把字符串切成一串 token(子词)。“今天天气怎么样?“大约切成 6–8 个 token。字节数变整数 ID,模型只认整数。
- Embedding 查表:每个 token ID 到一个大矩阵里查出对应的向量(比如 4096 维浮点数)。文字从此变成数字。
- 加位置编码:向量本身不带顺序信息,用 RoPE 或正弦编码把”你排第几位”这条信息注入进去。
- 过 N 层 Transformer Block:每层做两件事——Self-Attention(每个词看上下文更新自己)+ FFN(非线性特征提取)。堆 32、64、甚至 100 多层。
- Softmax + 采样:最后一层输出一个”下一个 token 是什么”的概率分布。用某种采样策略(贪心 / Top-k / Temperature)从里面选一个。
- 自回归循环:把刚采出的这个 token 拼回输入尾巴,回到第 3 步继续算,一个词一个词吐。直到吐出
<EOS>或达到 max_tokens。 - De-tokenize:把生成的 token ID 序列还原成人看得懂的字符串,返回给客户端。
两个阶段影响你能感知到的两个指标:
- Prefill(第 1–4 步一次性处理完整个输入)→ 决定 TTFT(首字延迟)
- Decode(第 5–6 步一个个吐 token)→ 决定 TPS(吐字速度)
这两个阶段的瓶颈完全不同,后面第六章会详细讲。
关于 Sampling 和 Temperature:第 5 步里选下一个 token 的方式,就是你在 API 里能调的旋钮。temperature=0 通常接近每次选概率最高的候选,但不同服务端实现、数值精度和并发路径仍可能带来差异;温度升高会把概率分布拉平,让低概率候选更容易被选中。翻译、代码补全这类要稳的场景通常用低温,写故事、头脑风暴可以适当提高。
上面讲的是”用”——推理时这 7 步怎么串起来。下一节讲”造”——这堆参数到底是怎么学出来的。
先读这一节:模型是怎么”学会”的
通俗版
先说 token 是什么
序章说”字符串切成一串 token”,那 token 到底是什么?不是字,也不是词,是子词——比字大、比词小的中间粒度。
主流方案叫 BPE(Byte Pair Encoding):先把语料里所有字节对的出现频率统计一遍,最高频的合并成一个 token,重复几万轮,最后得到一个固定词表。常见词”Apple”可能是一个 token,罕见词”tokenization”也可能被切成 token + ization 两个 token。中文、英文和代码会被怎样切分,取决于具体模型使用的 tokenizer;同一段文字换一个 tokenizer,token 数也可能不同。
为什么不直接用”字”或”英文单词”?字太细,一句话切出上百个 token,浪费上下文长度;英文单词太粗,遇到没见过的词(人名、专业术语、拼错的字)就没法处理。BPE 是折中——常用词整块,生僻词自动拆成常见片段,词表大小和覆盖率平衡得最好。
工程上直接的影响:你算 API 费用、估上下文用量,都是按 token 数算的,不是按字符数。128K context 能装多少文字,必须用目标模型的 tokenizer 对真实内容测量,不能靠固定的”汉字数 × 比例”换算。
先说最重要的一件事:模型本质上是一堆数字。不是一组人工编写的规则,也不是能逐条查阅的知识库——而是一个巨大的参数列表。7B 代表约 70 亿参数,70B 代表约 700 亿参数;很多闭源模型并不公开准确参数量,不该拿传闻数字当成事实。你可以把语言模型理解成一个函数:输入一串 token,输出下一个 token 的概率分布。训练,就是让这个函数的预测逐渐变准。
来具体走一遍这个过程,看看模型是怎么学会”今天天气真好”这句话的。
第一步:初始化
模型刚被创建出来,所有参数都是随机数——就像你新建了一个 float[] 数组然后没有初始化,里面是垃圾值。这时候把”今天天气真”喂给模型,它输出的概率分布也是乱的,可能它认为下一个字最有可能是”鸡”,概率 3.2%;“好”排在第 847 位,概率 0.001%。
第二步:计算 Loss
Loss(损失)就是一个数字,表示”模型猜得有多错”。正确答案是”好”,模型却把”好”排在第 847 位——这个差距被量化成一个数。具体用的是交叉熵:正确答案的概率越低,Loss 越高。你可以把 Loss 理解成”罚款金额”,答得越离谱,罚得越多。
第三步:反向传播
这是最关键的一步。我们知道 Loss 是多少,但 70 亿个参数里,哪些导致了这次错误?每个参数各自”应该往哪个方向调整”?
反向传播做的事情就是:对每一个参数,算出”如果这个参数增加一点点,Loss 会变大还是变小”——这个”变化方向”就是梯度。梯度就是每个参数的调整建议单,告诉你”这个参数调小一点点,Loss 就会降低一点点”。
第四步:更新参数
把所有参数按照梯度指示的方向,各自移动一小步(这个步长叫学习率,通常是 0.0001 这个量级)。然后回到第一步,重新喂数据,重新算 Loss,重新反向传播,重新更新。
这个循环做多少次,没有统一答案。预训练通常面对海量 token,数据会被分批送入模型,训练轮数取决于数据配比、去重方式、模型规模和算力预算。每一批数据都会产生 Loss 和梯度,参数就在大量更新中逐步形成语言与世界规律。
为什么反复做这件事,模型就会变聪明?
想象你教一个新员工校对文章,每次他改错了你就告诉他哪里不对,然后他调整自己的判断标准。刚开始他乱猜,慢慢地他开始学到规律:句子后面接什么词更自然、什么词不能跟着什么词用。但 LLM 学的不只是语法,因为训练数据里有新闻、有教材、有代码、有对话、有论文——“下一个 token 最可能是什么”这个问题,要答得准,模型必须理解世界是什么样的。模型学准下一个 token 是任务,副产品就是它得懂世界。
所以一个只做”下一个 token 预测”任务的模型,最后居然会写代码、会解数学题、会聊天。想预测准,就得懂。
进阶版
反向传播为什么能工作
链式法则的直觉是这样的:Loss 是怎么算出来的?是模型最后一层的输出经过 softmax 得到概率,概率和正确答案算交叉熵得到 Loss。最后一层的输出,是由倒数第二层的输出经过一个矩阵乘法得到的。倒数第二层的输出,又是由更前面一层算出来的……整个模型是一个嵌套的函数组合:Loss = f₁(f₂(f₃(...(input)...)))。
链式法则说:对于嵌套函数,你想知道最开始某个输入对最终输出有多大影响,可以把每一层的”影响比例”乘起来。就像供应链里某个零件价格变动对最终产品售价的影响,把每一个中间环节的”放大倍数”连乘就行了。反向传播就是从 Loss 往回走,一层一层地把梯度传回去,GPU 并行地算每个参数的梯度,最后一次性更新。
训练和推理的本质区别
训练时,你需要记住正向传播的所有中间结果(每一层的激活值),因为反向传播要用它们来算梯度。一个 7B 模型训练时的显存占用,往往是推理时的 4-8 倍,根子就在这。
推理时不需要保留任何中间结果,算完一层扔掉,只留最终输出。所以同样一个模型,推理能跑在消费级显卡上,训练却得上 A100 集群——不是模型变了,是计算图的保留策略完全不同。PyTorch 里体现在 with torch.no_grad(): 这个上下文管理器:告诉框架不要建计算图,不要记中间值,省掉大量显存。
预训练 vs 微调 vs RLHF
预训练做的是上面说的那件事:给模型喂海量文本,让它学会”预测下一个 token”。这阶段结束后,模型学会了大量世界知识,但它不知道怎么”听话”——你问它”帮我写一封邮件”,它可能接着续写出几百封格式各异的邮件,因为它学到的是”文本续写”,不是”指令执行”。
Instruction Tuning(指令微调)解决的就是这个问题:用”问题-回答”格式的数据继续训练,数据量少得多(几万到几百万条),但模式很固定,模型学会了”当输入是问题时,输出应该是回答”。base 模型和 instruct 模型行为差距那么大,根子就在这里——参数量相同,训练目标不一样。
RLHF(人类反馈强化学习)是在 Instruction Tuning 之后再做的一步,解决的是”模型知道怎么回答,但不知道怎么回答得让人满意”这个问题。具体做法是:让人类标注员对模型的多个输出排序,用这个排序训练一个”打分模型”(Reward Model),然后用强化学习让语言模型的输出尽量获得高分。ChatGPT 能”听你的话”、不乱说话,主要靠 RLHF 这一步。
这三步可以类比客户端开发:预训练是把整个 SDK 编译好;Instruction Tuning 是在 SDK 基础上开发应用层逻辑;RLHF 是做用户测试然后迭代产品——每一层都依赖上一层,但解决的是不同层次的问题。
为什么 base 模型和 instruct 模型行为差距那么大?
你可能见过同一个模型有两个版本:Llama-3-8B(base)和 Llama-3-8B-Instruct(instruct)。参数数量完全一样,模型结构完全一样,但你和 base 模型说”帮我写一封邮件”,它会把这句话当成”一段文本的开头”去续写——写出几十封格式各异的邮件,甚至还会续写出”用户:帮我写一封邮件 助手:好的,以下是三个版本……用户2:帮我写……”这样的训练数据格式。而 instruct 版本听到同样的话,就知道要帮你写一封邮件,然后写好交给你。
差距全在训练目标上:base 模型只学过”续写”,instruct 模型额外学过”执行指令”和”让人满意”。权重里装的东西不一样,行为自然截然不同。所以部署时一定要用 instruct 版本,别用 base。
第一章:向量(Embedding)是什么
通俗版
先来回答一个最基础的问题:模型吃的是数字,但文字不是数字,怎么办?
最直接的想法是用 ASCII 码——“好”的 UTF-8 编码是 0xE5A5BD,给模型一个整数不就行了?问题是这样没有任何语义信息。“好”和”棒”在语义上非常接近,但它们的 UTF-8 编码对应的数字,和”好”与”坏”的数字距离差不多——模型根本无从判断这两个字在意思上有什么关系。
你用一个数字表示一个词,你能表达的信息只有”排序”——数字 1 比数字 2 小。但语义关系远不止”大小”,还有相似程度、属于哪个类别、是名词还是动词、带正面情绪还是负面情绪……一个数字表达不了这些。
所以我们用多个数字——一个词变成一个向量,比如 512 个浮点数。你可以把这 512 个数字理解成一个词在 512 个不同”维度”上的坐标,每个维度隐约对应着某种语义属性(这些维度不是人为设计的,是模型自己学出来的,所以不能直白地说”第 3 个维度是情绪”,但它确实在做类似的事)。
用地图类比:一个地点在地图上需要两个坐标(经度和纬度)才能定位,因为地理空间是二维的。语义空间是高维的,你需要更多坐标才能”定位”一个词的含义。
为什么语义相近的词,向量会自动靠近?
这不是人工设计的,是训练出来的。想想模型是怎么训练的:预测下一个词。“今天天气真好”和”今天天气真棒”都是正确的句子——在”今天天气真”这个上下文后面,“好”和”棒”都是合理的预测。因为这两个词在大量相似上下文里都出现过,模型为了能同时预测对这两个词,就会把它们的向量放到相似的位置。相似的上下文喂出相似的向量,这个规律从数据里自动长出来。
“国王 - 男人 + 女人 ≈ 女王”这个经典案例说明了一件更惊人的事:向量的方向是有含义的。从”国王”到”女王”的方向向量,和从”男人”到”女人”的方向向量,几乎是平行的——这意味着”性别”这个概念被编码成了一个一致的方向。这套几何结构是模型从数百亿句话里自己长出来的,没人手把手教过。
进阶版
Embedding 层在模型里的位置
Embedding 是模型的第一层,负责把 token ID(一个整数)变成向量(一组浮点数)。整个 Embedding 层本质上是一个查找表:一个 [vocab_size, embedding_dim] 的矩阵,比如 [32000, 4096]——32000 个词,每个词对应一行 4096 个浮点数。每次输入一个 token ID,就返回矩阵里对应的那一行。
这个矩阵的参数也是通过训练学出来的,不是固定的。意味着 Embedding 不是一个静态词典,而是整个模型训练过程中的一部分——模型在学”怎么预测下一个词”的同时,也在不断调整每个词的向量表示,让整个系统的预测越来越准。查找表的每一行都在被梯度更新。
Word2Vec vs LLM Embedding 的本质区别
Word2Vec(2013 年)是早期的词向量方法:每个词有且只有一个固定向量。“苹果”这个词,不管是”苹果公司”还是”苹果好吃”,它的向量永远一样。
现代 LLM 里已经不存在”静态词向量”了。你在 Embedding 层查到的向量,只是这个 token 的初始表示,它会被后续的每一层 Transformer 不断修改,最终输出的向量已经完全不是 Embedding 层的那个了。同一个词”苹果”,在”苹果公司发布了新品”这句话里,经过 32 层 Transformer 之后,它的最终向量和在”妈妈买了一个苹果”这句话里是不同的——上下文已经把它塑造成了不同的表示。这是 Contextual Embedding,是 Transformer 架构的核心能力。
和 CGPoint/CGVector 的类比
表面上很像:CGPoint 是二维坐标,Embedding 向量是高维坐标,都是用数字描述”位置”。但有一个根本差异:CGPoint 的坐标系是人定义的(x 是水平方向,y 是垂直方向,单位是像素),每个维度含义明确。
Embedding 向量的每个维度是模型自己学出来的,没有人可以告诉你”第 137 个维度是什么意思”。更准确的类比是:想象一个 512 维的空间,里面的坐标轴是模型根据”预测效果最优化”这个目标自动确定的,没有固定方向,只有相对关系。你能做的操作不是”测量 x 方向的距离”,而是”计算两个向量有多相似”(点积或余弦相似度)。
这一点很重要:向量的维度不可解释,但向量之间的距离和方向可以用来推断语义关系。你不能问”这个词的第 3 维是什么意思”,但你可以问”这两个词在语义空间里距离多近”——这是 Embedding 唯一允许的操作,也是它和普通坐标系最根本的区别。
第二章:Self-Attention(自注意力机制)
通俗版
先说 Attention 要解决什么问题
考虑这句话:“猫坐在垫子上,它看起来很舒服。”
“它”指的是什么?你一眼就知道是猫。但模型怎么知道?模型拿到的是一串 token,每个 token 刚进来时只有自己的向量,完全不知道上下文。如果模型在处理”它”这个词时,只看”它”自己的向量,它永远不知道”它”指向什么——因为”它”本身没有具体含义,含义全在上下文里。
Attention 就是用来解决这件事的:让每个词在计算自己的最终表示时,能够”看”整个句子里其他词,并决定”我需要从哪些词借多少信息过来”。
用一个完整例子走一遍
句子:“猫追了狗,它跑掉了。“(假设”它”在这里指狗)
模型处理到”它”这个 token 的时候,要搞清楚它指什么。Attention 的做法是:
- 把”它”变成一个查询(Q)——“我在找什么?”
- 把句子里所有其他词(猫、追、了、狗……)都变成键(K)——“我这里能提供什么?”
- “它”的 Q 和每个词的 K 做点积,得到一个相似度分数——比如”它”和”狗”的分数是 4.2,“它”和”追”的分数是 0.3,“它”和”猫”的分数是 1.1
- 对这些分数做 softmax,变成概率分布:比如”狗”0.65,“猫”0.22,其他词加起来 0.13。Softmax 干的事一句话就能说清:把一堆可正可负、可大可小的分数,压成一组加起来等于 1、原本大的还是大的正数——之后就能当权重用了。全文之后再看到 softmax,说的都是这一步。
- 把每个词的**值(V)**按这个概率加权平均
最终”它”的向量 = 0.65 × 狗的V向量 + 0.22 × 猫的V向量 + 0.13 × 其他词的V向量
结果是:“它”的向量被”狗”的语义信息大量渗透,变成了一个”它≈狗”的向量表示。 后续层在处理”跑掉了”时,用这个已经知道”它是狗”的向量,就能做出更准确的判断。
为什么要有 Q、K、V 三个角色而不是一个?
因为”我想找什么”和”我能提供什么”是两件事。同一个词,作为被查询对象时需要暴露”我是什么”(K),作为查询发起者时需要表达”我在找什么”(Q),作为被选中提供信息时需要传递”我的具体内容”(V)。
类比一下:K 像书的目录项标题,Q 像你在图书馆查的关键词,V 像书本身的内容。你用关键词匹配目录标题,选中之后读的是书的内容——目录标题和书的内容可以不一样(一本书的标题是”网络编程”,但内容包含了 socket、TCP、HTTP 等),这就是 K 和 V 分离的意义。
为什么 Q 和 K 也不能合成一个
有人会追问:既然 K 是”我能提供什么”,Q 是”我在找什么”,那能不能就用同一个向量既当 K 又当 Q?答案是不行,因为关系是有方向的。
举个例子:“工程师”和”报销单”之间存在关联——工程师会填报销单。但反过来”报销单”关注的东西里,“工程师”未必排在最前面,报销单可能更关注”金额""日期""审批人”。也就是说,A→B 的注意力权重和 B→A 的注意力权重不该对称。如果 Q=K,两个词之间的点积就永远对称,模型学不到”关系有方向”这件事。
Q 和 K 用两套不同的矩阵,就是给”我看向谁”和”谁看我”这两个方向留出各自的表示空间。同一个词作为”发起查询”和作为”被查询”,可以给出完全不一样的向量。这是三个矩阵而不是两个的核心理由。
Q、K、V 是怎么算出来的
每个词进这一层的时候,手上只有一个 embedding 向量。模型给每一层都准备了三套可学的矩阵 W_Q、W_K、W_V,把这个向量分别乘一遍,就得到这个词在这一层里的 Q、K、V。三套矩阵在训练里各自朝不同方向长——W_Q 学”该拿什么去问”,W_K 学”该拿什么去应答”,W_V 学”该把什么信息拿出来”。
Q/K/V 不是词的固有属性,是模型每一层临时算出来的三个”角色扮演”。换一层,同一个词又得到全新的一套 Q、K、V——这也是为什么 Transformer 要堆几十层:每层从不同角度重新审视一次上下文。
Causal Mask 是什么
生成文字时,模型只能看到已经生成的内容,不能”偷看”未来。用一张表来理解,假设有 4 个 token”今|天|天|气”,每行表示”这个 token 能看到哪些 token”:
今 天 天 气
今 [ ✓ ✗ ✗ ✗ ]
天 [ ✓ ✓ ✗ ✗ ]
天 [ ✓ ✓ ✓ ✗ ]
气 [ ✓ ✓ ✓ ✓ ]
“今”只能看自己;“气”能看所有人。被打 ✗ 的位置,在计算时强制把分数设为 -∞,经过 softmax 之后变成概率 0,等于完全看不见。
FFN 是干什么的
每个 Transformer Block 里,Attention 之后还跟着一个 FFN(前馈网络)。
Attention 做的是”信息聚合”——我从别的词那里借来了信息,更新了自己的向量,但这个更新是各词向量的加权平均,是线性操作,表达能力有上限。
FFN 做的是”特征提取”——用非线性激活函数(ReLU/GELU)对 Attention 输出的向量进一步变换。非线性意味着”如果某个维度超过某个阈值才激活,否则置零”,这让模型能表达”如果 A 且 B,则 C”这样的组合语义。
Attention 管词与词的关联,FFN 管非线性组合,缺一层网络就废。
一个 Transformer 模型里,大概 1/3 的参数在 Attention,2/3 的参数在 FFN。
进阶版
为什么除以 √d_k
Self-Attention 的计算是 Q·Kᵀ / √d_k。为什么要除这个数?
考虑向量维度 d_k = 64 的情况。Q 和 K 都是 64 维向量,每个维度的值大概是均值 0、标准差 1 的随机数。两个向量做点积,就是把 64 个维度的乘积加起来——每个维度贡献的方差是 1,64 个加起来标准差是 √64 = 8。
如果不缩放,点积的数值会随维度增大而暴涨。很大的数值输入 softmax 之后,会导致梯度消失:softmax 在极端值附近的梯度趋近于零,注意力分布会变成 one-hot(几乎全部权重集中在一个词上),模型失去了”综合多个词的信息”的能力,训练极不稳定。
除以 √d_k 之后,点积的标准差变回 1,softmax 在平坦区域工作,梯度正常。这是统计学推导出来的工程选择。
KV Cache 的内存占用计算
KV Cache 是推理加速的关键优化:生成第 N 个 token 时,前 N-1 个 token 的 K、V 向量已经算过了,缓存下来就不用重算。但这个缓存有多大?
以 Llama 3.1 70B 为例(80 层、64 个 Query 头、8 个 KV 头、head dimension 128,KV 使用 FP16,batch size 为 1):
- 每层每个 token 的 KV 大小:2(K 和 V)× 8 个 KV 头 × 128 维 × 2 字节 = 4096 字节 = 4 KiB
- 80 层合计:80 × 4 KiB = 320 KiB / token
- 128K context:128,000 × 320 KiB ≈ 39.1 GiB
这还没有算模型权重、运行时缓冲、batch 放大和推理引擎额外开销。KV Cache 是长上下文推理的重要显存瓶颈,但不能脱离模型结构、精度和 batch 单独报一个固定数字。
O(N²) 在实际工程里意味着什么
Self-Attention 的计算复杂度是 O(N²):每个 token 都要和所有其他 token 计算注意力分数。
- 1K token:1,000² = 100 万次计算
- 8K token:8,000² = 6,400 万次计算(64× 更多)
- 128K token:128,000² = 163 亿次计算(163,000× 更多)
从 1K 到 128K,context 长度涨了 128 倍,Attention 的计算量涨了 128² = 16,384 倍。128K context 的推理为什么比 4K 慢得多、贵得多,就是这条平方关系撑着。业界研究线性注意力,就是想替掉 O(N²) 的 Softmax Attention。
FFN 内部长什么样
通俗版说了 FFN 是干什么的,这里补一下它的形状。一层 FFN 其实就是两次线性变换夹一次非线性:向量先被一个大矩阵抬到 4 倍宽(比如 4096 → 16384),过一次 GELU 激活,再被另一个矩阵压回原来的宽度。“先抬高再压回”听着绕,但模型的知识和记忆主要就存在这两块大矩阵的权重里——Attention 只管调度信息,装东西的是 FFN。
一个 70B 模型里,FFN 大概占 2/3 参数,也就是四五百亿参数全在这。业界近两年推的 MoE(专家混合),本质就是把 FFN 拆成很多个”专家 FFN”,每次只激活其中两三个——参数总量不变,每一步实际算的只有其中一小块,推理成本就下来了。DeepSeek-V3、Mixtral 用的都是这套。
第三章:多头注意力、位置编码、GQA
通俗版
多头(Multi-Head):为什么单头不够
考虑这句话:“苹果公司发布了新的苹果手机。”
你在理解这句话时同时在做好几件事:要搞清楚”苹果公司”和”苹果手机”是两个不同概念(语义消歧),同时要搞清楚”发布了”的主语是”苹果公司”(语法关系),同时还要知道”新的”修饰的是”手机”(依存关系)。这些理解任务同时进行,互不干扰。
单头注意力是一个”注意力通道”,每次只能学到一种权重分布——类比你只有一个注意力资源,却要同时盯着语义、语法、指代三件事,做不到。
多头注意力就是开 N 份并行的注意力计算。Head 1 可能专门在学语义关系,Head 2 在学语法依存,Head 3 在学指代关系。每个头独立计算自己的 Q/K/V,得到自己的注意力矩阵,最后把所有头的输出拼接起来再过一个线性变换。
有人会问计算量不就翻倍了吗?实际没有。多头的做法是把原来的向量维度”分头”——如果原来是 512 维用 8 个头,每个头只处理 64 维。总计算量和单头 512 维差不多,但模型能同时从多个角度理解语言。
位置编码:Self-Attention 为什么不感知顺序
Self-Attention 计算的核心是矩阵乘法,参与运算的向量是一个集合,不是有序数组。如果把输入序列的顺序打乱,矩阵乘法的结果只是对应行重排,计算本身完全不受影响——模型感知不到”你”排在第一位还是第三位。
所以 Transformer 必须人工把位置信息注入进去。
最原始的做法用正弦波和余弦波叠加来给位置编号。直觉类似二进制计数:低位快速变化,高位缓慢变化,多个不同频率的波叠加,让每个位置的”波形组合”都唯一。这和二进制里用不同权重的位来表示不同数字是一个道理——用 4 位二进制,0000 到 1111 能表示 16 个不同的数;用 N 个不同频率的波叠加,N 个位置都有唯一的”频率指纹”。问题是这是绝对位置编码——模型学到的是”位置 1 长这样,位置 2 长这样”,推理时遇到没见过的超长位置,就会懵掉。
RoPE(旋转位置编码)的核心改进是:从绝对位置变成相对位置。它不告诉模型”你在第几位”,而是告诉模型”你和另一个词之间差了几位”。具体操作是在做点积之前,根据位置对 Q 和 K 分别做旋转变换,旋转角度取决于位置。旋转的妙处在于:两个向量点积之后,结果只和它们的相对旋转角度(也就是相对位置差)有关,与各自绝对位置无关。
这对长上下文扩展很重要:模型学到的是相对位置关系,而不只是某个绝对位置编号。但“8K 训练直接推到 128K”并不会自动成立,通常还要配合 RoPE scaling、继续训练或其他长上下文方法,并重新验证长距离质量。
GQA:显存和质量之间的工程权衡
先算一笔账。假设你有一个模型:32 个注意力头,每个头维度 128,上下文 128K,FP16。KV Cache 每层需要:
32 头 × 128 维 × 128000 位置 × 2 字节 × 2(K 和 V)≈ 2 GB / 每层
如果模型有 32 层,总共 64 GB,还没算模型权重本身。长上下文推理为什么那么贵,答案就在这。
三种方案的对比:
- MHA(多头注意力):每个头有独立的 K 和 V,表达能力最强,但 KV Cache 最大
- MQA(多查询注意力):所有 Query 头共享一组 K/V;相对 32 个独立 KV 头的 MHA,理论 KV Cache 可缩小到约 1/32,但实际质量变化取决于模型和训练方式
- GQA(分组查询注意力):把 32 个头分成若干组,组内共享 KV,组间独立。折中方案——比 MHA 省显存,比 MQA 质量好
Llama 3、Qwen、Mistral 选 GQA 而不选 MQA,因为 MQA 在长对话和复杂推理上质量掉得明显。GQA 只掉一点质量,就能省下大量显存。
进阶版
多头计算是否真正并行
各头之间完全独立,可以真正并行计算。PyTorch 的底层 CUDA kernel 通常会把多头打包成一个大矩阵乘法,利用 GPU 的 Tensor Core 批量处理,比”启动 N 个独立线程”的并行更高效,是向量化批量计算。头与头之间唯一有顺序依赖的是最后的拼接和线性投影,计算量可以忽略不计。
RoPE 的工程实现
RoPE 的旋转变换是把每个向量的相邻两维当作一个复数平面,按位置做复数乘法(相当于在二维平面上旋转)。整个操作没有新增可学习参数,是纯计算变换,对推理速度的影响极小。
外推能力的来源在于:相对位置 m-n 对应的旋转角差值,在训练集内短序列上出现过。只要模型学会了”距离 k 的相对关系”,即使绝对位置超出训练范围,相对关系依然成立。实践中还会用 NTK-aware scaling、YaRN 这类频率插值方法对高频分量做进一步调整,让更长的外推更稳定——Qwen、DeepSeek 的 128K/1M 长上下文都是这条路。
第四章:多模态(ViT/CLIP)
通俗版
模型”看图”和人看图的根本区别
人看一张图,是先整体感知构图,再把注意力集中到感兴趣的区域,然后结合语义理解”这是一只猫坐在椅子上”。
模型”看”一张图,是先把图片切成一个个方块(patch),每个方块变成一串数字,然后把这一串数字当成一个”词”输进去,交给 Transformer 来处理。
以标准 ViT 的教学口径来说,一张 1024×1024 的图片,如果每个 patch 是 16×16 像素,就会切出 64×64 = 4096 个 patch。每个 patch 是 16×16×3(RGB)= 768 个像素值,经过线性投影变成一个向量。真实多模态模型还可能对这些视觉表示做重采样或压缩,所以 4096 是原始 patch 数,不是所有模型最终送进 LLM 的固定 token 数。
为什么不直接把像素值传进去
你可能会想:何必这么麻烦,直接把三百多万个像素值传进去不就好了?
第一,计算量爆炸。Transformer 的注意力计算是 O(N²),300 万个 token 的注意力矩阵是 300 万 × 300 万,根本算不了。
第二,相邻像素之间的信息密度很低。图片里,相邻的几个像素几乎肯定是一个颜色,信息高度冗余。Patch 切割是粗粒度的信息聚合,把”一小块区域”的信息压缩进一个向量,丢掉冗余像素,保留局部纹理、边缘、颜色分布这些有意义的特征。就像你不会把 RAW 格式的每个比特都传给服务器,而是先 JPEG 压缩一下。
工程上必须知道的成本陷阱
在不做后续压缩的教学模型里,一张 1024×1024 图片会产生 4096 个 patch 表示,几乎吃满 4K 上下文窗口。真实图像 API 的视觉 token 规则由具体模型决定,成本应按模型文档或返回用量核对。
视频理解更贵:30 秒视频按 1fps 降采样是 30 帧图片。若仍沿用上面的未压缩教学口径,就是 30 × 4096 = 122,880 个 patch 表示。因此真实视频模型通常会在时间和空间维度继续采样、压缩,不能把原始 patch 数直接当成 API token 账单。
在上述未压缩的教学模型里,分辨率从 1024×1024 降到 512×512、patch 尺寸不变时,patch 数会降到四分之一。真实模型是否同比降低费用与延迟,还要看它的视觉编码和计费规则。
CLIP:让图片和文字”说同一种语言”
ViT 把图片变成向量,文字也有向量,但这两个向量空间是各自独立训练的,互相不认识。CLIP 的做法是:用大量图文配对数据(图片和它的描述文字),训练时强制让描述同一件事物的图文向量靠近,不匹配的推远。
训练完之后,猫的图片向量和”猫”这个字的向量在空间里落在相近位置,图文就”对齐”了。你拿一张新图片,不需要预先定义类别,直接和各种文字描述的向量比相似度,谁最近就是什么——这叫零样本分类(zero-shot)。
图片向量到底怎么进大模型:两种融合范式
CLIP 只解决了”图和文能对齐”,但没解决”图片信息怎么参与 LLM 生成”。真正的多模态大模型(GPT-4V、Claude、Gemini)要把图片当成上下文一部分,让模型基于图片来回答问题。业界主要有两条路:
-
早期融合(LLaVA 派):图片过 ViT 得到一串 token,通过一个小投影层对齐到 LLM 的词向量空间,然后直接和文字 token 拼在一起送进 LLM。就像把图片”翻译”成 LLM 能读的”外语词”,之后 LLM 用同一套注意力处理图文。实现简单、训练成本低,但图片 token 会挤占宝贵的上下文长度。
-
交叉融合(Flamingo / Q-Former 派):图片 token 不进主流。LLM 该怎么走还怎么走,只是每隔几层塞一个 Cross-Attention 层,让文字 token 通过 Cross-Attention “扭头去看”图片 token。图片 token 数量可以先用 Q-Former 压缩到几十个”查询向量”,大幅省 context。Flamingo 用这套支持了多图交错输入。
音频也是同样的思路:Whisper 把语音每 80ms 切一帧、算 Mel 频谱、编码成向量,最后也变成一串”音频 token”接进 LLM。所有模态最终都得走”变成 token → 进 Transformer”这条路。
进阶版
ViT 的思路转变
CNN 处理图片的方式是用小窗口(卷积核)在图片上滑动,每次只看局部。要看到全局关系,需要堆叠多层卷积,信息才能从局部逐层传播到全局。这种设计有归纳偏置——它假设图片的局部特征比全局特征更重要,相邻像素的关系比远距离像素的关系更重要。这个假设对自然图片通常成立,所以 CNN 在数据量有限时表现很好。
ViT 的做法是直接把 patch 序列扔给 Transformer,Transformer 的注意力机制天然是全局的——每个 patch 在第一层就可以和所有其他 patch 做注意力计算,没有局部优先的假设。代价是 ViT 没有任何先验假设,需要从数据里自己发现”哪些位置的信息更相关”这条规律,所以需要大量数据才能学好。这也是为什么 ViT 是随着 CLIP 这种大规模预训练数据集的出现才真正起来的——数据少 CNN 更稳,数据够多之后 ViT 才能发挥出来。
CLIP 对比学习的训练细节
一个 batch 里有 N 张图片和对应的 N 段文字描述。CLIP 把每张图片和每段文字都编码成向量,构成一个 N×N 的相似度矩阵。对角线是正样本(图片 i 对应文字 i),非对角线是负样本。损失函数强制对角线相似度最高,其余最低。
batch size 越大,任务越难,模型学到的表示越细致。实际训练时 batch size 会开到 32768 甚至更大,使得每张图片要从三万多个候选里区分出自己对应的文字,逼着模型学到非常精细的语义表示。
第五章:深度思考模型(o1/R1)
通俗版
“退回去”到底是什么意思
普通模型(GPT-4/Claude 3.5)生成文字就像用钢笔直接写——每写一个字就是最终答案,第一步判断错了,后面只能沿着错误方向走下去。
深度思考模型(o1/R1)的做法是:先打草稿,草稿可以随时撕掉重来。在给你最终答案之前,模型会生成一大段”内部独白”(通常是 <think> 标签里的内容)。在里面尝试一条推理路径,算到一半发现不对,就说”等等,这样不行”,退回去从另一个角度试。
这和代码里的回溯搜索很像:
func solve(_ state: State) -> State? {
if isSolution(state) { return state }
for nextState in generateCandidates(state) {
if isPromising(nextState) {
if let result = solve(nextState) { return result }
}
}
return nil // 这条路走不通,回溯
}
区别在于:代码里的回溯有明确状态和分支;推理模型的内部过程通常不会完整公开。外部 test-time scaling 可以生成多条候选,再用规则、测试、投票或学习到的评分器筛选;模型内部也可能学会延长推理、检查和修正。下面的”多路径 + 评估器”只是一种帮助理解算力与质量关系的教学模型,不代表所有 o1/R1 类模型都在推理时运行同一棵搜索树。
为什么烧更多算力能让答案更准确
对于一道可验证的数学或代码题,增加推理 token、生成更多候选并做验证,通常能扩大搜索覆盖,但收益取决于候选是否真正多样、验证器是否可靠,以及额外计算有没有用在正确位置。
关键在于你得有办法判断哪条路径更可信。这个信号可以来自测试、规则、投票、结果验证器或 Reward Model;它们都可能犯错,所以”采样更多”不自动等于”答案更准”。
所以深度思考模型是拿算力换准确率——多试几条路径,选好的那条。层数固定的模型深度是死的,用推理长度换推理深度。业界给这个思路起了个名字叫 Test-Time Compute——过去几年 Scaling Law 都在讲”训练时堆算力”(Train-time),o1/R1 之后大家发现”推理时堆算力”也能持续换到质量提升,是一次范式转移。
常见误区:CoT ≠ o1 的深度思考
在 prompt 里写”请一步一步思考”(Chain-of-Thought,CoT)和 o1 的深度思考不是一回事。前者是你手动提示模型,任何模型都能用,但模型自身没有学会”如果走错了怎么退回来”。o1 是通过强化学习,把”会思考、会试错、会退回重来”这个能力直接训练进了模型权重里,不需要你写任何 prompt。
用客户端术语:CoT 像是”用户手动写自动化脚本”,o1 像是”App 内置自动化引擎”。
什么问题适合深度思考,什么不适合
适合的:数学题、代码逻辑推理、逻辑谜题、复杂多步骤规划——尤其是有测试、规则或可验证结果的任务。
不适合的:写文案、写故事、翻译、简单信息查询、对延迟敏感的场景——深度思考模型的首字延迟很长,thinking 过程可能生成几千到几万个 token。
进阶版
Reward Model 是怎么训练的
标注员看同一个问题的两个不同回答(Answer A 和 Answer B),选更好的那个。重复这个过程几十万到上百万次,得到大量(问题、好答案、差答案)三元组,用来训练一个分类模型——预测”给定问题和回答,人类会觉得这个回答有多好”,输出一个分数。
RLHF 的完整训练流程
- 预训练:海量文本做下一词预测,学到语言知识和世界知识。模型只知道”续写文本”
- SFT(有监督微调):用人工精心写好的对话数据(“用户问X,助手应该回答Y”)做微调,让模型从”续写文本”变成”回答问题”
- RM 训练:用人类偏好数据训练奖励模型(独立的小网络)
- PPO(近端策略优化):让主模型生成回答,Reward Model 打分,根据分数更新主模型参数。同时用 KL 散度约束,防止模型为了刷高分走极端
蒸馏和 Fine-tuning 的本质区别
普通 Fine-tuning 是:给模型看正确答案,让它拟合”这个问题的答案是 XXX”。
蒸馏是:用大模型(teacher)对每个问题生成一个概率分布(比如”答案是’苹果’的概率 0.7,‘水果’的概率 0.2……”),让小模型(student)拟合这整个概率分布,而不只是拟合最高概率的那个词。
区别在于:概率分布里包含了 teacher 对所有可能答案的”置信度”,苹果旁边的高概率”水果”暗示着 teacher 知道这两个词在语义上很近。这是一种知识传递,student 学到的是 teacher 的”思维方式”,而不只是 teacher 的”答案”。
白盒蒸馏 vs 黑盒蒸馏
上面讲的是白盒蒸馏——student 能看到 teacher 的完整概率分布甚至中间层激活,学的信息最丰富。前提是你手里有 teacher 的权重。
黑盒蒸馏是另一种打法:teacher 只给最终文本回答(比如 GPT-4 的 API 输出),student 拿这些高质量问答对做监督微调。信息比白盒少(只有 top-1 答案,没有概率分布),但胜在不用 teacher 权重。开源社区早期蒸国外闭源模型基本走的这条,也因此引发过”用输出蒸馏是否合规”的争议。
第六章:端侧部署——把大模型塞进手机
通俗版
量化:把参数”降分辨率”
把模型从 FP16 量化到 INT4,字节数缩小 4 倍,但问题不只是体积。
FP16 表示的是一个浮点数,范围覆盖极大,精度在大数和小数之间自动调节。INT4 只有 16 个可能的值(-8 到 7),把原来连续的权重强行映射到这 16 个离散值上,必然有信息损失。
类比是把一张 16 位灰度图压缩成 4 色灰度图——你把所有中间灰色都强制映射到”深灰、中灰、浅灰、白”四种颜色,精细纹理全部丢失。
量化损失的感知因任务而异:对于简单问答,几乎感知不到;对于精确数值计算或长上下文,INT4 的误差可能累积放大,导致明显质量下降。
LoRA:轻量补丁,按需切换
不需要在手机上存一整个专用模型。手机里放一个通用基础模型,每个应用场景(写代码/翻译/总结)只需要下载一个几十 MB 的”补丁文件”(LoRA)。打开代码 App,加载代码 LoRA;切换到翻译 App,换上翻译 LoRA。基础模型不动,按需切换能力。
LoRA 的核心想法是:大模型的全量微调太贵,但如果相信”微调只需要在原始权重上做一个低秩的更新”,可以用两个小矩阵的乘积来近似这个更新。
原始权重矩阵是 4096×4096 = 16M 个参数。LoRA 加了两个旁路矩阵:A 是 4096×8,B 是 8×4096,总共约 65K 个参数。推理时把 BA 加到原始权重上,效果接近全量微调但参数量缩小了 200 多倍。
这和 iOS 里的 Category Extension 很像——原始类不动,给它打一个 patch 扩展功能,可以独立分发、热切换。
TTFT vs TPS:两个不同的瓶颈
从发送请求到第一个字出现的时间叫 TTFT(Time to First Token,首字延迟);之后字弹出来的速度叫 TPS(Tokens Per Second,吐字速度)。
这两个指标背后是两个不同的瓶颈:
-
TTFT 取决于 Prefill 阶段:一次性并行处理整个输入 prompt,受限于计算能力(GPU 算力)
-
TPS 取决于 Decode 阶段:逐个生成 token,每步都要读一遍 KV Cache,受限于内存带宽
Decode 为什么经常卡在带宽上? 每吐一个 token,推理引擎都要读取大量模型权重,并访问不断增长的 KV Cache。batch 较小时,矩阵运算很难把 GPU 算力完全吃满,数据搬运往往先成为瓶颈。具体 TPS 仍取决于量化、batch、并行方式、内核实现和硬件,不能只用显存带宽除一次数据量得到固定答案。量化和 MoE 会减少每步实际读取的权重,投机解码则尝试让一次大模型验证确认多个候选 token,它们都在改善每个输出 token 的有效成本。
优化方向完全不同:想降 TTFT 要优化计算并行度,想提 TPS 要优化内存带宽利用率。
投机解码:小模型猜,大模型验
用一个轻量小模型(1B)先快速猜 5 个候选 token,大模型(70B)一次性验证这 5 个 token 对不对。验对的全部接受,第一个不对的回退,从那个位置重新采样。
关键是:大模型可以并行验证一段候选 token,比自己逐个 Decode 更容易利用硬件。最终能加速多少,取决于草稿长度、接受率、batch、模型大小和推理内核;候选大量被拒绝时,草稿成本反而会抵消收益。
进阶版
几种量化方案的工程特点
GPTQ:逐层量化,每量化一个权重,就用 Hessian 矩阵的信息补偿其他权重,把误差分散出去。质量比简单四舍五入好很多,但量化过程本身需要 GPU 跑几个小时。适合”量化一次,反复推理”的离线场景。
AWQ(Activation-aware Weight Quantization):重要的权重(乘以激活后影响大的那些)更精细地量化,不重要的更粗。通过对重要权重做缩放,让它们在量化时损失更小。
GGUF:llama.cpp 的格式,支持从 Q2 到 Q8 的各种精度,面向 Mac/PC 端侧部署。在 M 系列 Mac 上利用统一内存架构,CPU 和 GPU 共享内存,GGUF 模型可以非常高效地运行。
投机解码的精确性保证
target model 在第 k 个位置不接受 draft token 时,会从 (target_prob - draft_prob) 的校正分布里采样一个新 token。这确保最终生成的 token 序列的概率分布,和完全由 target model 逐 token 生成完全一样——投机解码是精确等价的,不是近似。这也是它区别于别的加速方案的地方。
主流端侧推理引擎选型
| 引擎 | 场景 | 特点 |
|---|---|---|
| llama.cpp / GGUF | 桌面/服务端 | CPU 优化极致,量化方案最全 |
| MLC-LLM | iOS/Android/Web | 跨端编译,WebGPU 支持 |
| Core ML | Apple 生态 | 调用 ANE 神经引擎,Swift 原生 |
| MNN / TNN | 移动端 | 国产硬件适配好 |
第七章:Mamba——内存恒定的替代方案
通俗版
O(N²) 有多严重,用数字感受一下
Transformer 注意力计算复杂度是 O(N²)。
- 1K token:100 万次运算
- 1M token:1 万亿次运算
从 1K 到 1M,token 数量乘以 1000,但计算量乘以了 100 万。
标准实现的训练内存也会遇到 O(N²) 的注意力矩阵——反向传播需要保留相关中间量。128K context 的单个 FP16 注意力矩阵按 128K × 128K × 2 字节估算约为 32 GB。FlashAttention 一类实现可以通过分块计算避免完整落下这张矩阵、显著降低实际显存,但不会改变全局注意力两两配对的计算规模。
Mamba 的”固定大小状态”
Mamba 的思路:维护一个固定大小的”压缩状态”,新信息来了就更新这个状态,旧信息被选择性地”遗忘”。就像你记读书笔记,不把原文抄下来,而是不断更新你的理解摘要——摘要永远是固定大小的,不管原书有多厚。
结果:处理长度为 N 的整段序列时,总计算量和训练内存随 N 线性增长;做自回归推理时,每一步只维护固定大小的递推状态,不需要像 KV Cache 那样随历史长度继续增长。也就是 整段处理 O(N),单步推理状态 O(1),不能写成总计算量与上下文长度无关。
关键创新叫”选择性”:丢什么、留什么,不是固定的,而是由当前输入动态决定。模型会学到:这个信息很重要,要强写入状态;那个信息是噪声,让它快速衰减。LSTM 的门控同样依赖当前输入和历史状态;Mamba 的区别不在于“只有它会按输入决定遗忘”,而在于把输入相关的选择机制放进状态空间模型,并配合硬件友好的并行扫描,在长序列上兼顾表达能力和吞吐。
它丢弃了什么
状态大小固定,意味着早期的具体细节会逐渐被”压缩”,最终消失。如果你在一段 100K token 的文档里,第 1 个 token 是一个重要的名字,Mamba 需要把这个名字压缩保存在固定大小的状态里,同时还要处理后续 99999 个 token。在足够长的序列后,这个名字可能已经被后来的信息稀释掉了。
而 Transformer 用 KV Cache,理论上每个历史 token 的精确信息都在。这是 Mamba 和 Transformer 在”精确细节检索”能力上的根本差距。
还没完全替代 Transformer
目前主流趋势是”混合架构”——大部分层用 Mamba 处理局部信息,少数关键层用 Attention 做精确的长距离检索(比如 Jamba:约 80-90% 的层用 Mamba,10-20% 用 Attention)。两者互补,而不是完全替代。
进阶版
SSM 的递推公式直觉
状态空间模型的工作方式:维护一个内部状态向量 h,每次读入一个新 token x,按以下规则更新:
- 新状态 = A × 旧状态 + B × 当前输入(A 控制旧信息保留多少,B 控制新信息写入多少)
- 输出 = C × 新状态(C 控制从状态里读出什么信息)
Mamba 的”选择性”体现在:A、B、C 这三个矩阵不是固定的,而是由当前输入 x 经过一个小网络计算出来的,让模型可以根据输入内容动态调整”记忆策略”。
端侧的优势
端侧最大的约束是内存。Transformer 的 KV Cache 随对话长度线性增长——一个 100 轮的长对话,KV Cache 可能占满整个设备内存预算。Mamba 的状态大小固定,第 100 轮和第 1 轮占的内存完全一样。端侧长期对话助手这类场景特别吃这套。
截至 2026 年初,端侧主流模型仍然是 Transformer 架构,但 Falcon Mamba 1B、Zamba2-2.7B 等已经在测试 Mamba 端侧适配,只是量化工具链和 Metal/NPU 优化还不如 Transformer 成熟。
第八章:Attention Is All You Need 论文真正说了什么
通俗版
绝大多数人对这篇 2017 年论文的印象就是”哦,Attention 嘛”。但读完前面七章再来看,会发现有三件事和你以为的不一样。
第一:它是一篇翻译论文
任务是英译德、英译法,刷翻译排行榜。原始架构是完整的”双塔结构”——左边 Encoder 读懂原文,右边 Decoder 吐出译文。现在你用的 ChatGPT 是把右塔(Decoder)单独拿出来用的,是”半台车”,不是论文里的完整结构。
第二:论文里有三种 Attention,不是一种
- Encoder 内部的 Self-Attention:双向的,每个词能看前后文
- Decoder 内部的 Masked Self-Attention:带 Causal Mask,只能看已生成的词,不能偷看后面
- Cross-Attention(交叉注意力):Decoder 扭头去看 Encoder 的输出
Cross-Attention 是翻译能工作的关键,但前面几章完全没提。它的工作方式是:Decoder 生成每个词时,把当前的生成状态作为 Q,把 Encoder 的输出作为 K 和 V,做一次注意力计算,查询”我现在要生成的这个词,应该关注原文的哪些部分”。
用翻译”我爱北京天安门”为例:Encoder 读完整句中文输出 5 个向量,Decoder 已经生成了”I love”,现在要生成第三个词。Cross-Attention 让当前状态(Q)和原文 5 个词的 K 向量计算相似度,发现”天安门”这个词分数最高,于是从”天安门”对应的 V 向量里吸收信息,生成”Tiananmen”。每生成一个词,都重新做一次这个查询。
第三:最大卖点是”快”,不是”准”
论文标题”Attention Is All You Need”的潜台词是”把 RNN/LSTM 全扔了”。RNN 必须串行处理(第一个词处理完才能处理第二个),没法并行;Transformer 所有词同时处理,可以充分利用 GPU 并行计算能力。训练时间从”几周”变成”8 张 GPU 跑 3.5 天”,还顺手把翻译质量刷到了当年 SOTA。它能掀翻整个领域,靠的是并行——Attention 是拿来实现并行的工具。
BERT / GPT / T5 的分家
原始双塔拆开之后,走了三条路:
- 只留 Encoder(BERT):双向 Self-Attention,每个词能看前后文。擅长”理解”——读一段话,做分类、问答、实体识别。
- 只留 Decoder(GPT/LLaMA):有 Causal Mask,只能看前面的词。擅长”生成”——给一个开头,续写下去。
- 保留双塔(T5/BART):Encoder 读原文,Decoder 生成目标文本,中间通过 Cross-Attention 连接。擅长”转换”——翻译、摘要、改写。
这不是谁更先进,是任务决定架构。Causal Mask 的有无从根本上决定了模型”能看到什么信息”,进而影响了它学到的表示类型。
为什么 Causal Mask 这个细节会决定能力边界?
Encoder(没有 Mask,双向):处理”苹果”这个词时,它既能看到左边的”发布”,也能看到右边的”手机”。上下文是完整的,所以 Encoder 的向量表示质量更高——它知道”苹果”在这里是科技公司,不是水果。高质量的表示适合做分类、问答这类”需要完整理解”的任务。但 Encoder 没有”续写”能力,因为它不是设计来逐步生成的。
Decoder(有 Causal Mask,单向):处理”苹果”时只能看到左边,不知道右边是”手机”还是”好吃”。信息是残缺的,但这个残缺是刻意的——正是这个限制,逼着模型学会”在只知道前文的情况下预测下一个词”,也就是生成能力。反过来,正因为在训练时每个词都只看到残缺的上下文,它的向量表示不如 Encoder 完整。
所以这两类模型的能力差距,本质上是训练时”能看到多少信息”的差距,不是架构大小的差距。
进阶版
残差连接(Residual Connection)
原始 Transformer 完全没提 Residual Connection 的重要性,但它是 Transformer 能堆叠到上百层的关键。
每一层的输出是 output = input + f(input) 而不是 output = f(input)。
关键在于:如果 f(input) 趋近于 0(这一层没学到东西),output 就等于 input,信号原封不动流过去。梯度也可以无衰减地流回去——通过加法,梯度不会被乘法截断,这使得 100 层的网络也能稳定训练。
类比 iOS 里的 CALayer 叠加:如果某一层的 alpha=0(什么都不渲染),下面的内容直接透出来。残差连接就是给每一层加了一条”透明通道”,让信号即使在这一层没有学到任何东西的情况下,也能原封不动地传到下一层。
Layer Norm 通常在残差之后,把每一层的输出归一化到相似的数值范围,防止不同层之间的数值分布差异过大导致训练不稳定。两者配合,是 Transformer 能堆叠到上百层的工程基础。
Masked Decoder Self-Attention 的实现细节
Causal Mask 在实现上是一个上三角矩阵,diagonal 以下是 0,以上是 -∞。在计算 softmax 之前,把注意力分数矩阵和这个 mask 相加,未来位置的分数变成 -∞,softmax 之后趋近于 0,等效于当前 token 对未来 token 的注意力权重为零。
这个实现的优势是:虽然逻辑上每个 token 只能看左边,但矩阵乘法在硬件上依然批量并行完成,mask 只是在分数层面把不合法的位置屏蔽,不影响并行计算效率——这是训练时 Transformer 比 RNN 快得多的根本原因。
第九章:客户端调用侧的四件事
前八章都在讲模型内部。这一章讲客户端在 API 层能看得见、能调得动的四件事——不懂机制照样能调,但踩坑时你会不知道自己踩在哪。
采样参数:Temperature / Top-k / Top-p 到底在调什么
模型每一步输出的并不是一个 token,而是整个词表上的概率分布——几万个 token 各有一个概率。到底吐哪个字,是”采样”这一步决定的。
- Temperature(温度):过 softmax 前把分数除以一个 T。T=0.1 时高分更高、低分更低,分布变尖,每次几乎都选同一个字,输出稳定;T=1.5 时分布变平,冷门 token 也有机会被选中,输出发散。业务上要复现代码/JSON → 调到 0;要写故事、发散思路 → 调到 0.8–1.2。
- Top-k:只从概率最高的 k 个候选里采样。k=50 就是只有前 50 名有资格被抽中,挡住长尾里的怪词。
- Top-p(核采样):从”累加概率达到 p”的最小候选集里采样。p=0.9 就是凑到 90% 概率覆盖的那几个词里选。相比 Top-k,Top-p 会根据分布形状动态收放——分布尖时候选少,分布平时候选多。
三个参数通常组合使用。需要注意:不同推理引擎对 temperature=0、top-p 和 seed 的处理并不完全一致;即使固定 seed,跨硬件、跨批次或跨服务版本也未必逐 token 相同。严格复现要同时锁定模型、推理引擎、采样参数和运行环境。
Function Call 本质上是模型吐 JSON,客户端自己去调
客户端把一堆函数 schema(名字、参数、描述)注册给模型,模型在生成时会吐一段特殊格式的 JSON,说”我要调用 get_weather(city='北京')”。模型本身什么都没执行,它只是被训练成会在合适的时机吐这种 JSON 字符串出来。真正的执行是客户端拿到 JSON、解析出函数名和参数、自己去调本地或后端服务,然后把结果拼回消息历史,再喂给模型下一轮。
所以 Function Call 的可靠性只取决于两件事:模型训练时见过多少这样的调用样本、你的 schema 描述写得够不够清楚。参数描述含糊、类型不明确,模型就会瞎猜。
幻觉从哪里来
模型的目标是”说得像”,从头到尾都不是”说真话”。它在做的事本质是”在这段上下文之后,哪个 token 出现的概率最高”——概率最高的不一定是事实。当模型对某件事没见过、见得少、或者记忆模糊时,它不会停下来说”我不知道”,因为它没被训练成这样。它会输出一个”看起来像那么回事”的答案:语法通顺、格式正确、语气自信,内容是编的。
工程上能压幻觉的手段就三条:
- 给它上下文(RAG):把真实答案先塞进 prompt 里,让它照着说
- 让它调工具(Function Call):需要事实的地方去查真实数据,别让模型自己回忆
- 让它先想再答(CoT / o1 那套,第五章讲过):把答案的推导过程展开,减少”张口就来”
别指望换个模型幻觉就没了——这是概率生成的机制病,不是某个模型的 bug。
System Prompt 为什么”有效”
System Prompt 本质就是排在最前面、优先级最高的一段上下文,没什么魔法开关。它有效的原因很朴素:
- 位置在最前:Attention 里,后面每一个 token 都要看它一遍,影响所有后续生成
- 训练时被特殊标记:主流模型训练时,system 消息会带一个特殊 token 或角色标签,模型学到”这段是设定,得遵守”
- 风格锚定:模型输出会向 prompt 的语气、术语、格式靠拢——你 system 里用严肃口吻,它输出也严肃;写”用大白话回答”,它就少讲术语
局限也在这:System Prompt 不是硬约束,只是概率上的引导。用户消息里如果反复强推另一种风格,模型可能就跟着漂了。真要硬约束(比如禁止某类内容),得靠后处理,别只靠 prompt。
流式输出(Streaming)为什么天生就能做
顺带说一件天天用但很少细想的事:API 为什么能”一个字一个字往外吐”?
模型本来就是自回归生成的——一次算一个 token,算完立刻拿得到。HTTP 层用 Server-Sent Events(SSE)或者分块传输,服务端每算出一个 token 就 flush 一次推给客户端,不等整段生成完。流式本身就是这套自回归推理的自然延伸,没啥新东西。客户端要做的是解析 SSE 事件流、增量拼接、增量渲染——iOS 上用 URLSession 的 delegate 回调,Web 上用 EventSource 或 fetch + ReadableStream。
流式的好处不只是”体验好”,还有:可以在生成中途取消(省钱省算力)、可以边生成边做敏感词检测、可以边生成边切 UI 状态(比如工具调用中的转圈)。
附录:工程师速查
术语速查
| 概念 | 一句话 |
|---|---|
| Token | 模型处理的最小文字单位;字符到 token 的比例取决于具体 tokenizer |
| Tokenizer / BPE | 把字符串切成子词的分词器,主流用字节对合并(BPE) |
| Embedding | 把词变成向量(一串数字),语义相近的词向量相近 |
| Self-Attention | 让每个词”看看”上下文,更新自己的含义 |
| Transformer Block | Attention + FFN,是模型的一层,通常叠 N 层 |
| KV Cache | 缓存已算过的 K/V 向量,避免重复计算,但占显存 |
| Prefill | 一次性处理完整个输入 prompt,决定首字延迟 |
| Decode | 逐个生成 token,决定吐字速度 |
| TTFT | Time to First Token,首字延迟 |
| TPS | Tokens Per Second,吐字速度 |
| Sampling | 从概率分布里选下一个 token 的策略(贪心/Top-k/Top-p) |
| Temperature | 采样温度,0 = 每次相同,>1 = 更放飞 |
| 量化 INT4 | 把每个参数压缩到 4 位,体积缩小 4 倍,精度略损 |
| LoRA | 轻量”补丁”,插拔式给模型添加特定能力 |
| MoE | 专家混合,每个 token 只激活一部分参数(Mixtral / DeepSeek-V3 都是) |
| CoT | Chain-of-Thought,在 prompt 里让模型一步步思考 |
| Test-Time Compute | 用推理阶段花更多算力换更好答案,是 o1/R1 一派的核心思路 |
| RoPE | 旋转位置编码,主流大模型的位置编码方案,支持长上下文外推 |
| YaRN / NTK-aware | 对 RoPE 做频率插值,把训练时的短上下文外推到更长 |
| GQA | 多个 Q 共享少量 KV,降低 KV Cache 显存开销 |
| Residual Connection | 残差连接,让深层网络能稳定训练的关键机制 |
| Cross-Attention | Decoder 查询 Encoder 输出,翻译/多模态融合的核心 |
| 蒸馏 | 小模型学大模型的概率分布(而非只学答案),传递”思维方式” |
参数量 → 显存换算表(推理,仅权重)
选模型跑端侧/私有部署时最先要算的一件事:这个模型能不能塞进我的显存/内存里。
| 参数量 | FP16(2 字节/参数) | INT8(1 字节/参数) | INT4(0.5 字节/参数) |
|---|---|---|---|
| 1.5B | 3 GB | 1.5 GB | 0.75 GB |
| 7B | 14 GB | 7 GB | 3.5 GB |
| 13B | 26 GB | 13 GB | 6.5 GB |
| 70B | 140 GB | 70 GB | 35 GB |
| 671B(DeepSeek-V3) | 1342 GB | 671 GB | 336 GB |
注意:这只是理想权重占用,实际推理还要加量化元数据、KV Cache、运行时缓冲和系统余量。70B INT4 的理想权重就约 32.6 GiB,32GB 统一内存设备无法把它当成稳定可用配置;24GB 显存能否运行 13B INT4 或 7B FP16,也要连同上下文长度和推理引擎开销一起测。
量化损失参考表
| 精度 | 体积(相对 FP16) | 典型质量损失 |
|---|---|---|
| FP16 | 100% | 基准 |
| INT8 | 50% | < 1% |
| INT4 | 25% | 1%–3%(长上下文会累积放大) |
| INT2 | 12.5% | 明显下降,多数场景不建议 |