llm-anatomy 是一个教学导向的开源项目:不调库,把 LLM 的每个零件亲手造一遍——搞清楚模型为什么长这样,而不只是怎么调它。
它和 credis 是这个博客的两根支柱:credis 拆"计算机怎么工作"(系统底层),这个系列拆"模型为什么长这样"(模型底层)。这是连载第一篇。
会说 KV cache 的人很多,说得清它省了哪步计算的很少
2026 年做 Agent 的工程师,技术栈大概长这样:prompt 写得炉火纯青,编排框架换过三套,MCP 工具接了一排,各家模型的参数表倒背如流。我自己每天也这么干活,这没什么不好。
但前阵子和一个同行聊推理成本,他说他们"开了 KV cache 省钱"。我随口问了一句:它省的是哪一步计算?空气安静了几秒。这个词他天天用,简历上也写了,但说不出它缓存的是什么、为什么 Q 不用缓存。老实说,两年前的我也答不上来。
这不是个例,这是行业常态:模型能力越强、API 越好用,“会调用"和"懂原理"之间的鸿沟就越宽——因为跨过去的必要性看起来越来越低。 不懂原理照样能交付,真的。但一旦你要做技术选型、要读新模型的 technical report(MLA 是什么?DeepSeek 为什么能把价格打下来?)、要判断某个推理优化对自己的场景有没有用,停在编排层的人就只能听别人转述结论。这就是"懂原理"和"会调用"的分水岭——不在会不会用工具,在被追问第二个"为什么"的时候还站不站得住。
不服的话,先自测三个问题:
- KV cache 缓存的到底是什么?为什么 Q 不需要缓存?
- FlashAttention 省的是浮点运算,还是别的什么?
- LoRA 的可训练参数凭什么能压到全量的千分之几?
三个都能不打磕巴答上来,这个系列你可以只看目录;答不上来也完全不丢人——这个连载存在的意义,就是让它们在你手里从黑话变成常识。
所以我开这个连载:不调库,把 LLM 的每个零件亲手造一遍。 反向传播自己写,注意力自己写,Transformer 自己搭,LoRA 自己挂。造完你会发现,那些吓人的名词底下没有魔法,只有一小撮想法在反复变奏。
主线:每一代架构都在修上一代的一个 bug
先上结论:过去三十年的序列建模史,说人话就是一部 bug 修复史。 每一代架构的诞生都不是灵光一现,而是被上一代的某个具体 bug 逼出来的。把这条链捋顺,整张知识地图的骨架就有了——剩下的全是细节。
这也是我不按"名词表"来讲的原因:知识点是压缩包,演化史才是解压密码。
先看全图再逐段拆:主干自上而下是六代架构的修 bug 链,右侧分叉是 LLM 时代的推理优化全家桶,底部那条绿链是微调范式——右下角的章号标记了每一块会在本系列哪一章被你亲手造出来。

RNN:一个优雅的想法,两个致命的 bug
想法很美:句子是序列,那就用同一套权重沿时间轴滚动,每读一个词更新一次"记忆”。理论上多长的句子都能处理,参数量还不随句长增长。
但它出厂就带着两个 bug:
- 串行:第 t 步必须等第 t-1 步算完才能开工。GPU 有上万个核心,RNN 让它们排队看一个人干活。
- 梯度消失:训练时误差要沿时间轴往回传,每回传一步就乘一次权重矩阵。几十步连乘下来,句首的信号衰减到接近零——“我在法国长大……所以我说一口流利的(?)“这种长距离依赖,它学不动。
顺带一提,“2026 年还要不要学 RNN"这个问题值得单开一篇番外——到时候我用三条真实训练曲线说话,不抛情怀。
LSTM / GRU:门控修了梯度,没修串行
LSTM(1997)的解法是给信息修一条高速公路:cell state 走加法通道,梯度不再被迫连乘;再配三个门(遗忘/输入/输出)决定什么该记、什么该忘——严格考据的话,1997 原版只有两个门,遗忘门是 2000 年才补上的,这里按现代通行版讲。GRU(2014)是它的精简版,两个门办同样的事。远距离依赖终于能学了。
但看清楚:门控修的是梯度消失,串行这个 bug 一个字没动。 训练照旧一个时间步一个时间步地爬,序列越长越慢。这颗雷一直埋到 2017 年才炸。
Seq2Seq:打开了新任务,也造出了信息瓶颈
2014 年,机器翻译需要"输入一句话、输出另一句话”,两边长度还不一样。Seq2Seq 给出的架构是:encoder 把原句从头读到尾,压成一个固定长度的向量;decoder 再从这个向量出发,把译文一个词一个词吐出来。
新 bug 就藏在"固定长度"四个字里:不管原句是 5 个词还是 50 个词,都得塞进同一个几百维的向量。 好比让你读完一整本书,只许留一张便签,然后照着便签复述全书——短句还凑合,长句的开头早被挤丢了。当年的实验曲线也确实如此:句子一变长,翻译质量断崖式下跌。
Bahdanau Attention:为翻译而生的"回头看”
修法直白得可爱:既然一个向量装不下整句话,那就别压了——decoder 每吐一个词,都回头看一遍 encoder 的全部隐状态,按相关程度加权取用。翻译到 “love” 的时候,注意力自然聚在"爱"上。这个机制在 2014 年的原始论文里叫"对齐”(alignment),它就是给翻译修的,没想过要统治世界。
但它顺手引入了一个改变一切的范式:用一个打分函数衡量"我现在要生成的位置"和"输入的每个位置"有多相关。 这个打分函数,是后面十年所有故事的主角。
Transformer:革的是 RNN 的命
2017 年那篇论文的标题常被当成鸡汤金句,其实它是一句挑衅——“Attention is All You Need"的完整潜台词是:注意力已经够用了,RNN 可以滚了。
逻辑很硬:既然 attention 能在任意两个位置之间建立直连,那 RNN 沿时间轴滚动的那套"记忆"还留着干嘛?全扔。让序列里每个位置同时和所有位置互相打分(self-attention),全部位置并行计算——RNN 那个与生俱来的串行 bug,在这里被连根修掉。训练吞吐起飞,堆数据堆参数的 scaling 路线从此才走得通。
代价当然有,新 bug 两个:注意力要两两打分,计算和显存都是 O(n²);扔掉 RNN 的同时也扔掉了天然的先后顺序,位置信息得靠"外挂”(位置编码)补回来。这两个 bug,撑起了下一个时代的全部优化。
LLM 时代:所有花哨名词都在救同一条公式
缩放点积注意力就一行:softmax(QK^T / √d) · V。2019 年之后你听到的高频名词,几乎全是在优化这条公式的不同环节:
- KV cache:自回归生成时,历史 token 的 K 和 V 根本不会变,缓存下来别重算——省的是每生成一个新 token 就把全部历史重投影一遍的矩阵乘。Q 为什么不用缓存?因为旧位置的 query 再也不会被用到,每步只有当前 token 在提问。
- RoPE:位置外挂 2.0。把位置写进 Q、K 的旋转角度里,两个向量一点积,相对位置自动浮现,长上下文外推也更友好。
- GQA:KV cache 自己成了显存大户,那就让每组 Q 头共享一份 K/V(组数少于 Q 头数),缓存体积直接砍到几分之一。
- MLA:更狠,把 K/V 压成一个低秩隐向量存着,用时再展开。DeepSeek 能把 API 价格打穿地板,这是主力功臣之一。
- FlashAttention:不改数学,改访存。分块计算让那个 n² 的注意力矩阵根本不在显存里落地——省的不是浮点运算,是 GPU 内存带宽。
一句话:这些名词不是并列的知识点,是同一条公式的补丁列表。 逐个背名词永远背不出这个结构;把公式亲手推一遍,它们自己就串起来了。
微调范式:同一条逻辑的最后一环
连"怎么用模型"本身也走在这条修 bug 链上:BERT 时代的标准做法是全量微调——整个模型跟着下游任务再训一遍,2018-2019 年大家真就这么干 → 冻结主干、只训一个分类头,是训不起全量的穷人方案(便宜,但表达力也跟着打折)→ LoRA 用低秩旁路把可训练参数打到千分之几(比冻结头能打,又远比全量便宜)→ 到今天连卡都不用买,把数据交给 API 微调(因为对多数团队来说连 LoRA 的运维都是负担)。第 4 章我们把这条链放在同一个任务上真刀真枪跑一遍:冻结头和 LoRA 实跑,API 那条算笔账。
一张表收拢整条链
| 架构 | 修了上一代的什么 bug | 自己引入/遗留的新 bug |
|---|---|---|
| RNN | —(开山) | 串行;梯度消失 |
| LSTM / GRU | 门控修梯度消失 | 串行原封不动 |
| Seq2Seq | 让"句到句"任务成为可能 | 整句压进一个向量的信息瓶颈 |
| Bahdanau Attention | 对齐修瓶颈(为翻译而生) | 底座仍是 RNN,仍然串行 |
| Transformer | self-attention 修串行 | O(n²);位置信息要靠外挂 |
| LLM 时代 | RoPE/KV cache/GQA/MLA/FlashAttention 各修公式一个环节 | 显存与成本的军备竞赛 |
| 微调范式 | 全量→冻结头→LoRA→API,逐代修"贵" | 可控性与数据主权交给了平台 |
看懂这张表,你就拿到了这个系列的全部剧情大纲。 接下来四章,就是把表里每一行拆开,亲手造一遍。
和 credis 的关系:两根支柱
熟悉这个博客的读者知道我在连载 credis——用纯 C 手写迷你 Redis,拆的是"计算机到底怎么工作":内存、指针、socket、系统调用。
llm-anatomy 是另一根支柱,拆的是"模型为什么长这样":梯度、注意力、架构演化、微调范式。一个向下拆机器,一个向上拆模型;两条线互不依赖,可以并行追。 拆完这两样,从系统调用到 self-attention,你的技术栈里就没有整段整段的黑箱了。
顺便说清楚这个系列适合谁:
- 适合:每天调模型 API、但被第二个"为什么"问倒过的工程师;想建立模型底层直觉的转行者和学生;读过论文、却从没亲手写过一次反向传播的人。
- 不适合:想学 prompt 技巧的(全系列一行 prompt 都不教);想要 SOTA 训练配方的(我们造的是教学件,不是生产件);数学洁癖患者(推导以直觉优先,严格证明请出门左转教科书)。
路线图:五章 + 番外,封顶
先把丑话说前面:这个系列 5 章加番外封顶,不会越写越膨胀。 教学连载最常见的死法就是第 8 章开始注水、第 12 章不了了之。地图已经画完,路线如下:
| 章 | 内容 |
|---|---|
| 00 | 知识地图(本篇) |
| 01 | 纯 numpy 手写 mini-autograd:反向传播不是魔法(+优化器推导链:指数加权平均→动量→RMSProp→Adam) |
| 02 | 手撕注意力:三种打分函数,以及 KV cache/FlashAttention/MLA 都在救这一条公式 |
| 03 | 手写 Transformer,然后换件成 LLaMA:RMSNorm/RoPE/GQA/pre-norm 逐件替换 |
| 04 | NSP 之死与微调范式变迁:全量→冻结头→LoRA,两条实跑 + API 算账 |
| 番外候选 | ①2026 还要不要学 RNN(三曲线实验)②fasttext vs LLM 三笔账 |
两篇番外不占正传编号,写不写、先写哪篇,到时候看评论区投票。
每一章你将亲手造出什么(注意,是造出,不是"了解"):
- 01:一个百来行的 mini-autograd 引擎——能对任意计算图自动求导,并用它从零训出一个真的在收敛的小神经网络;Adam 不是抄四行公式,是沿着推导链看着它自己长出来。
- 02:一个 numpy 版注意力层——加性、乘性、缩放点积三种打分函数逐一实现、同场对比;再亲手数一遍 KV cache 到底省掉了哪几次矩阵乘。
- 03:一个能在小语料上学会说人话的 mini-Transformer;然后像换汽车零件一样,LayerNorm 换 RMSNorm、位置编码换 RoPE、MHA 换 GQA、post-norm 换 pre-norm——换完,你手里就是一个 mini-LLaMA。
- 04:同一个任务上实跑冻结头与手写 LoRA 的真实对比数字(API 这条只算账不实跑);顺便讲清楚 NSP 这个曾经的"标配预训练任务"是怎么被整个行业悄悄埋掉的。
每章都会写成独立可读,你可以只挑感兴趣的看;但按顺序读,你会看到每个零件是被什么问题逼出来的——这正是那条"修 bug 链"的意义所在。
全系列约定
- numpy 起步,PyTorch 第 3 章才登场。 autograd 和注意力先用裸 numpy 造——“底下没有魔法"这件事,只有亲手写过才真的信。等你自己的 autograd 能跑通,再看
loss.backward()就是老朋友了。 - uv 管环境。
uv sync一条命令拉起全部依赖,没有 conda 玄学,没有"我这边能跑”。 - 每章对应仓库一个目录。 代码即讲义,目录即章节,跟着 repo 走就是跟着连载走。
- 所有实验数字都真实跑出来。 文中每条曲线、每个对比数字,仓库里都有对应脚本,你可以在自己机器上复现出同一个结论——数值允许浮动,趋势不会骗人。
试一试
| |
uv sync 能跑通,环境就绪,第 0 章就毕业了——本章是地图,没有代码作业,但把环境立起来这件事永远值得提前做:等第 1 章开工时,你只需要专心对付梯度。
下一章预告
第 1 章从最底层开工:纯 numpy 手写 mini-autograd。 你大概每天都在敲 loss.backward(),但它背后到底发生了什么?剧透一句:反向传播不是魔法,只是链式法则加一本记账本。写完这一百多行,你会顺手把 SGD 一路升级成 Adam——不是背公式,而是沿着"指数加权平均→动量→RMSProp→Adam"这条推导链,看每一步分别在修谁的 bug。是的,优化器的历史也是一部 bug 修复史。
仓库在 github.com/KerroKapple/llm-anatomy,每章一个目录,watch 起来跟着走。有问题欢迎开 issue 讨论。
