AI 底层原理
用 AI 写代码的人,多半都遇到过这种事:
同一个需求,昨天一次就对;今天同样的提示词再问一次,它改了你的文件名,还顺手加了一个你从没提过的配置项。
你多半以为模型"时灵时不灵"。
其实它从头到尾都没变笨,也没变聪明。它只是按照它自己的方式在工作,而这个方式,恰好不是我们习惯的"确定性"。
上一篇里我们说,AI 的底层是玩概率。这一篇我们把这句话拆开来看:概率是怎么从"一堆文字"变成"一段代码"的?中间到底发生了什么?为什么同样的输入会跑出不同的结果?知道了这些,我们日常该怎么跟它打交道。
1、为什么值得花十分钟看底层
这一章的目标很明确:不要求你记住任何一个公式。公式只是配图。真正的价值是——当 AI 出问题时,你能分清它是"没听懂",还是"没抽到那一下"。
NOTE
下面这个实验完全跑在浏览器里:它不联网、不调用任何商业模型、也不加载真实权重。它用一组极小的合成参数,把"分块 → 表示 → 注意力 → 概率 → 生成"这条链路真的跑一遍,让你看见中间的数值怎么变。用它理解机制,不要用它去评价真实模型的能力。
这个实验有 12 步,每一步对应真实推理链路上的一个环节。你可以边读边点:
现在,我们跟着它往下走。
2、文字要先变成"模型能吃的东西"
2.1 分块:AI 眼里的字,不是你眼里的字
模型读不懂"变量"这两个汉字。它读到的是一串编号。
这个编号叫 token(词元)。所谓分块,就是把一段连续文本切成一小块一小块,每一块换成一个 ID。
这里有个新人几乎一定会踩的坑:token 既不等于一个汉字,也不等于一个英文单词。它是介于两者之间的"子词"碎片,具体怎么切,取决于模型自己的 tokenizer。
我们可以用一组粗略的经验值,来体会不同内容的"贵贱":
看这张表,你会立刻理解两件事。
第一,中文比英文"贵"。同样意思的一段话,中文消耗的 token 往往明显更多。"你好世界"这 4 个汉字,在很多 tokenizer 下就是 4 个 token;而 "hello world" 大约 2 个。
第二,代码比散文"贵"。标识符短、标点多、缩进密集,一个在散文里很完整的英文单词,放进代码里常被切成好几块。
WARNING
还有一个更反直觉的事实:tokenizer 是会变的。以 Claude 为例,4.7 及更新的模型换用了新的 tokenizer,同样的文本会比旧模型多出大约 30% 的 token。所以"我上次数过是这么多"这种经验,隔几个版本就可能失效——精确数字永远要用官方的计数接口。
这也顺带解释了另一个经典困惑:为什么 AI 读一篇 10 kB 的网页,大概要吃掉 2500 个 token?因为计费单位是 token,不是字节。
到第 1 步,你可以改改输入框里的文字,切成英文、中文、Emoji,看看块数怎么变。
2.2 向量:模型读的是数字,不是文字
拿到 ID 之后,模型会去一张巨大的表里查,把 ID 换成一串数字。这串数字叫向量(embedding)。
这一步很多人会想不通:怎么把"你好"变成 0.3、-0.7 这样的东西?
打个比方。你要给一箱衣服贴标签,不能写"这是件短袖",你得写"尺码:L;颜色:白;季节:夏"。一个词也一样,它没法被直接贴上"人类/代码/形容词"这种标签,但它可以被拆成一组"特征值"——哪些数值高,说明这个符号在训练数据里更常跟某一类用法一起出现。
所以模型并不"认识"一个词,它只是知道这个词长得像哪些词。
到第 2 步,你会看到这些数字变成了一排色块。
WARNING
但不要试图解释这些数字的具体含义。真实模型的向量有几百上千维,是训练自己长出来的,没有任何一个人规定"第 7 维代表语法"。演示里缩到 4 维,只是为了让你能看见它变化;看懂色块的大小,不等于看懂了语义。
2.3 位置:同样的词,换个位置就是另一回事
"我去"和"去我",字完全一样,意思完全不同。
注意力机制本身是不知道顺序的——它就是一堆加权求和,谁在前面谁在后面它并不关心。所以顺序信息必须额外注入。
最经典的做法是位置编码:给每个位置生成一组数字,和词向量逐维相加。这样,"第 3 个位置上的'我'"就有了属于自己的表示。
到第 3 步,点一下"交换前两个位置",你会看到同一个符号在两个位置上的输入不一样了。
NOTE
演示用的是四维正弦位置编码,这是原始 Transformer 论文里的经典写法。现代模型大多改用旋转位置编码(RoPE)之类的方案,不一定是直接相加,但**"位置必须被单独编码"这件事是一致的**。
3、注意力:模型怎么"翻阅"你给的资料
到这一步,模型手里已经是一排数字了。接下来它要回答一个问题:我当前位置,该看谁?
3.1 三个角色:提问、报索引、给内容
注意力机制的核心,是给每个位置准备三份材料:
- Query(查询):我想找什么
- Key(键):我这里有什么
- Value(值):我真正的内容
怎么理解?想象你去图书馆查资料。你脑子里有一个问题(Query),每本书脊上贴着一个标签(Key),而书里才是正文(Value)。
做法是:拿你的问题去和每本书的标签做一次比对,越匹配,得分越高;然后按得分高低,从每本书的正文里各取一点,按比例混合起来,凑成你要的答案。
数学上就是三步:
- 打分:Query 和所有 Key 做点积,得到一个相关度分数
- 归一化:把分数过一遍 softmax,变成加起来等于 1 的权重
- 加权求和:按这个权重,把所有 Value 混合起来
写成公式,就是我们熟悉的那一行:
Attention(Q, K, V) = softmax(QKᵀ / √d) · V那个 √d 是干什么的?就是个稳压器。维度越高,点积的数值越容易炸掉,除一下 √d 让它回到可控范围,否则后面的 softmax 会全部塌到 0 或 1,训练直接崩掉。
到第 4 步,你可以点开得分矩阵,验证某一次点积算得对不对;也可以切换"查询位置",看权重怎么跟着变。
3.2 不偷看未来:为什么模型能一个字一个字地写
上面这套机制有个问题:如果生成第 5 个字时,它能看到第 6 个字,那答案就等于抄了。
所以解码器会加一层因果掩码:预测当前位置时,不允许读取它右边的任何位置。实现上,就是把这些位置的分数在 softmax 之前设成负无穷,权重直接变成 0。
WARNING
注意,这不是"给未来的位置打一个低分"。低分意味着"稍微参考一下",而掩码是"完全不许看,出局"。这是两件完全不同的事。
到第 5 步,演示会给出"移除因果掩码"的对照按钮,你可以亲手把未来位置放开看看。
3.3 多头、前馈、残差:一句话带过
真实模型还会做三件事:
- 多头:用几组不同的投影各算一遍注意力,关注点可以不一样,再拼起来
- 前馈网络:对每个位置单独做一次非线性变换(4 维 → 8 维 → 4 维这样的形状)
- 残差与归一化:把原始输入和变换结果相加保留下来,再把数值范围拉回来,避免层数一多就数值爆炸
这些细节你现在不用记。但有一条必须记住,因为它是很多人误用的根源:
WARNING
注意力权重高,不等于这就是模型做决定的原因。 权重是"聚合信息时各占多少比例"的中间量,不是一份可读的因果解释。
所以别在团队里说"这个头是事实头""那个头是语法头"——这类命名听着专业,实际上是把自己的猜测包装成了结论。
3.4 顺手回答一个老问题:为什么"上下文越长越差"
这一节要专门讲,因为它大概是新人最高频的困惑。
2023 年斯坦福团队做过一个很干净的实验:把一段关键信息放在长上下文的不同位置,看模型能不能找出来。结果是一条 U 形曲线——信息在开头和结尾时表现最好,在正中间时明显变差。
后续的研究反复验证了这条曲线,并且把它和人类记忆的首因效应、近因效应对上了号:我们自己回忆一份会议纪要时,开头和最后说的部分,也确实记得最牢。
所以"上下文越长越差"不是玄学,它在机制里有明确的位置。
推论很实用:
- 最重要的约束、验收条件,放在提示词的开头或结尾,别埋在中间
- 上下文越长,模型要分配的"注意力"越薄,你那条关键要求被稀释的概率就越高
- 与其把所有资料一股脑塞进去,不如只给这一件事真正需要的那几份
顺带说清楚一件事:模型的上下文窗口还在持续变大,现在已经有百万 token 级的了。但能装下和能用好是两回事——窗口是仓库容量,注意力才是你实际拿到多少。
4、从分数到文字:同一个问题,为什么会有不同答案
这是全文最实用的一段。
4.1 logit 不是概率
模型算完最后一步,会给词表里的每个候选打一个分,这叫 logit。
logit 有两个反直觉的特点:
- 它可以是负数
- 它加起来不等于 1
所以 logit 不是概率。它更像"考试得分":考了 80 分,不代表你有八成把握会选这个答案,只是说它比那个 59 分的选项更值得考虑。
真正的概率是 softmax 之后的结果,它把一堆分数变成加起来等于 1 的分布。
WARNING
两个常见误解:其一,得分是负的候选,概率并不为零;其二,最大的候选概率也未必接近 100%。当模型"不确定"的时候,它内部其实是"60% 像 A,25% 像 B,15% 像 C",而不是"就是 A"。
到第 9 步,你可以选中任意一个候选,拖动滑杆改它的 logit,然后看后面整排概率怎么重排;也可以点"恢复网络得分"退回去。
4.2 温度:只改变概率的"形状",不改变"知识"
温度(temperature) 是最有必要理解的一个参数。它做的事非常单一:
pᵢ = softmax(zᵢ / T)就是把分数先除以一个数,再去 softmax。就这么一步。
- T 很小(比如 0.2):分布被拉尖,最大概率更接近 1,输出更稳定、更保守
- T 很大(比如 1.8):分布被抹平,长尾词也拿到了机会,输出更发散
- T 趋近 0:等价于永远选最高分,也就是贪心解码,同一个输入每次输出完全一样
到这里必须划一条线:
WARNING
温度不是准确率开关,它是一个随机性旋钮。
调低温度不会让模型"知道更多"。候选集合从头到尾一个都没变过——它只是让模型更倾向于挑那个本来就排第一的候选。
很多人排查问题时第一反应是"把温度调低一点试试",这通常是在用错误的方式解决错误的问题。模型答错,大概率是候选里根本就没有正确答案(它压根不知道,或者你的上下文不足以让它推出来),这时候调温度没有任何用。
那实际该怎么设?
现在主流 API 的默认 temperature 大致在 0.7 ~ 1.0,top-p 保持 1.0,而且不少官方文档会明确建议:调一个就够了,不要两个一起动。
按场景这么分:
到第 10 步,拖动温度滑杆,实时看分布怎么变;再点"抽样 100 次",看有限次抽样的频率和理论概率差多少。
4.3 top-p:另一种改法,思路完全不同
还有一个常被和温度混为一谈的参数:top-p(也叫 nucleus sampling)。
它的做法不是"重新分配每个候选的权重",而是直接删候选:按概率从高到低累加,保留累计概率刚过 p 的那一小撮,剩下的长尾全部剔除,然后在这批候选里重新归一化。
一句话对比:
- 温度:给每个候选重新分配权重,一个都不删
- top-p:把靠后的候选整个删掉
所以 top-p = 0.9 的意思是"只保留最可能的那 90% 概率质量"。它的好处是自适应——模型有把握的时候候选本来就少,犹豫的时候候选自然就多。而低 top-p 的风险是,可能把候选砍到只剩一两个。
这也正是它们不该一起调的原因:两个机制叠加,你没法预测最终分布。
4.4 自回归:它是一个词一个词往前拱的
最后把镜头拉远一点。
模型生成的方式是自回归(autoregression):
前缀 → 预测下一个词元 → 把这个词元接到前缀后面 → 用新前缀再预测下一个这解释了很多日常现象。
为什么同一个提示词会给出不同答案? 因为每一步都有"从分布里抽一个"的动作。第一步抽得不一样,后面所有的条件都跟着变了。这不是不稳定,这是它的设计。
为什么改一个词会全盘皆变? 因为前缀变了,后面每一步的分布都跟着变。生成是一条会分叉的链,不是一次性的整体计算。
为什么"多跑几次"有时真的有用? 因为你在不同的分支上采样,等于把"最可能的那一条路径"扩展成了"好几条可能的路径"。前提是你能判断哪条更好——这就又回到了人的判断力上。
到第 11 步,你可以手选一个候选追加进去,然后撤销上轮、换一个候选,观察下一轮的分布是怎么跟着变的。
5、KV Cache:为什么长对话会变贵、变慢
这一节解释一个你一定有体感的现象:同一个会话刚开始时回复飞快,聊到后面明显变慢。
5.1 缓存了什么
按 3.1 那套注意力算法,生成第 1000 个 token 时,前面 999 个 token 的 Key 和 Value 都要重新算一遍——明明结果每次都一样。
这就是 KV Cache 要解决的问题:把算过的 K/V 存下来,只为新位置计算新的 K/V。
NOTE
为什么缓存是安全的?因为因果掩码的存在,历史位置的 K/V 永远不会因为后面新增内容而改变。所以缓存纯粹是一次优化,不是一个质量上的妥协。缓存路径和完整重算的结果应该完全一致——到第 12 步,你可以亲手比对两条路径的 logits。
5.2 但缓存不是免费
省掉的是"重复计算",省不掉的是"读取"。
每生成一个新 token,模型都要把历史上所有的 K/V 从显存里读出来,送到当前查询面前。所以上下文越长,每一步要搬的数据就越多。
这就是为什么长上下文推理是显存带宽瓶颈,而不是算力瓶颈:
- prefill(预填充):把你输入的提示词一次性并行处理完,算力瓶颈,耗时随输入长度大致线性增长
- decode(解码):一个一个地吐字,显存带宽瓶颈,现代 GPU 上大约每秒几十到一百多个 token
缓存要占多少显存?可以直接算。以 Llama 3 70B 为例:
80 层 × 8 个 KV 头 × 128 维 × 2(K 和 V)× 2 字节(FP16)≈ 320 KB / token上下文到 10 万 token,光是缓存本身就要 32 GB 显存。这就是长上下文为什么是显存问题,而不只是速度问题。
还有一个容易被忽略的账单事实:输出 token 的单价通常是输入的 3 ~ 5 倍。所以"让它少写废话"不只是省时间,是真的省钱。
5.3 缓存也能变成一种优化手段
主流 API 现在都支持提示词缓存(prompt caching):把一段前缀存下来,后续请求直接读。
以 Claude 为例,价格规则很干净:
- 缓存命中读取:基础输入价的 0.1 倍(省 90%)
- 5 分钟缓存写入:基础输入价的 1.25 倍
- 1 小时缓存写入:基础输入价的 2 倍
算一下就知道:5 分钟档写入一次,只要命中一次就回本;1 小时档要命中两次才回本。
这背后有一条很重要的工程常识:缓存是按前缀匹配的。
所以正确的做法是:
系统提示 → 工具定义 → 稳定的项目背景 → 用户本次的问题把会变的东西(当前时间、用户 ID、动态检索结果)统统放到最后面。如果你在前面塞了会变的内容,后面全部作废,缓存命中率直接归零——而这个变化在部署 diff 里是完全看不出来的。
WARNING
具体倍率各家会调整,换模型也可能变。上面是当前的数字,用之前请以官方定价页为准。但**"稳定前缀放前面、动态内容放后面"这个原则不会变**。
6、这些原理,能变成什么动作
知道了机制,我们才能把它翻译成日常操作。
6.1 给出"相关"的上下文,而不是"全部"上下文
机制:注意力要分配,长上下文两头强、中间弱。
动作:只给这一件事真正需要的材料。宁可多问一句"这段代码的调用方在哪",也不要一次贴进整个仓库。
6.2 把最重要的要求放在两头
机制:U 形注意力曲线。
动作:目标、约束、验收条件,写在提示词的开头和结尾;中间那段留给背景资料。
6.3 别用温度当调试手段
机制:温度只改概率形状,不改候选集合。
动作:结果不对,先看它有没有把正确方案放进候选里。没放进去,调温度救不回来;放进去了只是排第二,才轮到采样参数。
6.4 要求"可验证",而不是"看起来对"
机制:最大候选概率也未必接近 1。模型内部是"60% 像 A",它给你的却常常是笃定的 A。
动作:在提示词里明确要求它跑测试、给依据、标注哪些已验证、哪些仍然未知。把它从"答题者"改成"要交作业的人"。
6.5 对成本有感觉
机制:中文和代码更费 token,输出更贵,长对话更慢。
动作:长会话定期开新窗口(既省钱,也避免上下文互相污染);把稳定内容放前面吃缓存;让模型少写废话。
下面这个模板可以直接抄走:
请根据这段函数与失败用例,先定位遗漏的输入条件,再提出最小修复。
不要用降低温度代替错误分析,也不要补猜未提供的业务规则;
修改后运行对应测试,说明哪些结果已验证、哪些仍未知。它的每一句都在对应上面的机制:"先定位再修改"对应别急着采信;"不要用降低温度代替错误分析"对应温度不是知识开关;"不要补猜未提供的业务规则"对应候选里没有的东西,不会因为你的提示而出现;"说明哪些已验证"对应用外部检查代替自我感觉。
7、最后
理解原理的意义,不是给模型贴标签。
不是为了证明"AI 其实就是一只概率鹦鹉",也不是为了证明"AI 其实很可靠"。这两种结论,都帮不了你写代码。
它真正的用处是:让你能判断,什么时候该相信它、什么时候必须自己去验,以及一个具体动作该往哪个方向使劲。
回到那个最初的场景:同样的需求,昨天一次就对,今天又改了你的文件名。
现在你知道了——那不是模型时灵时不灵,而是它在一条概率的链上往前拱,而那条链上每一环都有分叉。你要做的是把分叉收窄:上下文给准、要求写清、结果让它自己验。
这和上一篇的结论是一回事:AI 编程把门槛降到了地板,但天花板捅到了大气层。地板那部分,它一个人就能走完;天花板那部分,永远需要有人站在上面看着。