ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零手写AI工程核心:自动微分、Transformer、RAG与Agent实战

2026/10/4 13:57:48 拓冰建站 浏览量
从零手写AI工程核心:自动微分、Transformer、RAG与Agent实战 1. 为什么叫从零开始这个项目的真实定位ai-engineering-from-scratch这名字一看就带着一股倔劲。很多人第一反应是又想骗我从头学Python实际上这个项目要做的不是重学基础而是把AI工程中最容易被黑盒化的那几层全部拆开自己动手再造一遍。先说清楚我理解的AI工程到底指什么。算法工程师研究的是模型结构怎么改、损失函数怎么调、训练策略怎么设计最后交付一个权重文件。而AI工程干的是另一件事把这个权重文件变成一套真实可用的系统里面包括数据清洗与标注、检索召回、上下文拼接、模型推理加速、Agent调度的状态维护、限流与降级、线上评估与回归。这一整套东西才是业务里真正烧钱、真正容易翻车的地方。我在这个项目里给自己定的规矩很简单能自己写的绝不直接调框架。LangChain这类工具我可以看它的设计思路但最终跑在自己业务里的那套Agent流水线是自己手搓的向量检索不直接上FAISS先把HNSW的核心逻辑自己实现一遍Transformer推理不用现成模型库前向计算和KV Cache管理也自己搭。看起来很笨但恰恰是这条笨路线把我从API调用工变成了能改底层的人。这项目适合谁三类人收益最大一是想转AI工程的后端程序员你已经会写服务缺的是对模型和检索管线的底层感知二是刚入学的硕士生与其被老板催着堆实验不如把手头框架的源头理一遍三是工作中已经被工具链折磨过的人——用LangChain拼了一堆节点但不知道状态管理为什么出错调了向量库但还是召回不准这类问题只有理解底层才能根治。不适合纯小白起步至少你得先会Python和基础数据结构否则容易卡在最前面出不来。2. 项目整体架构从地基到屋顶的知识体系设计2.1 三个核心不可绕过的底层组件整个项目的知识图谱我拆成了三个不变量自动微分引擎、Transformer推理内核、Agent循环骨架。这三样东西是所有现代AI应用的基石无论上层套什么壳最终都要落到这上面。自动微分引擎解决的是怎么让机器自己算梯度Transformer推理内核解决的是一个文本输入进来多层注意力怎么流转、生成时KV为什么能缓存Agent循环骨架解决的是模型一次输出不够用怎么让它反复调用工具、维护状态、直到完成目标。这三个东西互相独立但层层依赖——没有自动微分的概念你就理解不了训练和微调不理解Transformer前向你就调不明白推理参数和显存不设计Agent骨架你就永远被困在单轮对话里做不出真正的自动化系统。我见过太多人一上来就学LangChain的抽象概念结果问到Tool Node里面的状态是谁维护的就卡壳。原因很简单你没在那个深度上自己写过一遍框架里的名词对你来说就是无根之萍。手写这三个组件等于给之后学的一切框架都打了地基。2.2 为什么常见的快速上手路线是伪学习现在网上铺天盖地的路线是Python基础 → 机器学习入门 → HuggingFace → LangChain → 做个ChatBot项目。这条路线的问题在于每一步都在用但每一步都没懂。调几行HuggingFace的pipeline确实能跑出结果可一旦要修改模型结构做工程化适配你连改哪里都不知道。另外一个更大的误区是重论文轻代码。很多科班出身的人能画出Transformer架构图却写不对一次完整的前向计算不知道padding mask和causal mask怎么合并。纸上谈兵的危害比不学更大因为你会误以为自己会了。这个项目的设计原则就是每个知识块必须配一个可运行的手写实现把抽象概念变成调试过的代码才算过。为什么顺序很重要因为学习曲线里的前置依赖决定你走得远不远。不先写自动微分引擎直接手写Transformer你连梯度从哪来都不知道不先掌握Transformer前向直接做推理部署出了精度问题你会以为是量化导致的但实际是position encoding实现错了。后面我会按顺序给出每个阶段的毕业标准方便自己对照进度。2.3 模块编排学习曲线怎么铺才算合理实际编排里我把完整路线分成八个阶段每个阶段都有明确的交付物而不是含糊的学一下数学。第一阶段是数学基础重点只有两个向量/矩阵的运算直觉以及链式法则。不需要啃完整本统计教材用到什么补什么。第二阶段是手写自动微分引擎交付一个支持四则运算和ReLU的Tensor类能对表达式自动求梯度。第三阶段是用numpy手写两层MLP在MNIST上跑通交付训练代码和loss曲线。到这里为止地基已经稳了。第四阶段是手写Transformer block包括多头注意力、前馈网络、层归一化在玩具数据集上验证前向输出形状和梯度能流回第五阶段在这个基础上做KV Cache管理和推理量化第六阶段是自建RAG检索层包括文档分块、向量索引、重排序第七阶段是手写一个轻量Agent框架支持工具注册、多轮调用、记忆管理第八阶段是部署监控把前面写的服务打包加上日志、指标和基础的可观测性。这条路线走完你的收获不是会调框架而是AI应用常见的坑你基本都在最低层踩过一遍以后出了问题能快速定位是哪一层。后面几章我会把最核心的几个模块详细拆开讲包括实现要点和常见的反直觉细节。3. 核心模块拆解与实操要点3.1 手写反向传播引擎自动微分到底怎么自动很多人以为反向传播是PyTorch发明的其实核心就是高等数学里的链式法则框架只是把它自动化了。手写这个引擎主要是理解计算图这个概念——每个Tensor记录自己是怎么算出来的反向时沿着图往回走逐层乘上梯度。我实现的最小版本核心是一个Tensor类class Tensor: def __init__(self, data, requires_gradFalse, depends_on(), local_grad()): self.data data self.requires_grad requires_grad self.grad None self.depends_on depends_on # 父节点列表 self.local_grad local_grad # 本操作的局部梯度函数 def backward(self, gradNone): if grad is None: grad np.ones_like(self.data) if self.grad is None: self.grad grad else: self.grad grad for parent, lg in zip(self.depends_on, self.local_grad): parent.backward(lg(grad))四个核心算子就够了add、mul、matmul、relu。add的局部梯度是复制一份mul是取另一个操作数matmul是左乘右边转置relu是小于0处置零。你只需要把每个算子的局部梯度写对反向传播就自动跑通了。这里有一条很关键的测试技巧写完之后一定要用数值梯度校验。就是用(f(xeps) - f(x-eps)) / (2*eps)估算梯度再和你的自动梯度对比误差在1e-6以内才算对。这个习惯救了我很多次——有一个bug是mul算子的局部梯度写反了数值梯度一测就暴露了光靠肉眼根本看不出来。注意初学者最容易犯的错误是忘记在梯度的上处理分支节点也就是一个节点被两个父节点共用的场景。梯度必须累加而不是覆盖否则多分支网络全部训练不出来。3.2 从零实现Transformer注意力机制的三个计算陷阱手写Transformer最让人崩溃的往往不是结构而是三个反直觉的细节。第一个是softmax溢出。注意力分数是Q乘K再除以根号d当dk比较大、输入又极端时exp(大数)会直接爆成inf。标准做法是先减去最大值再求exp这个trick几乎每个库都有但新手手写时最容易漏。第二个陷阱是attention mask的形状和位置偏移问题。训练时的padding mask要把无效位置的分数置为负无穷解码时的causal mask要保证当前位置只能看到前面。很多人在合并这两个mask时搞错——Padding mask作用于key侧的位置causal mask作用于query侧的位置两者的广播维度不一样一不留神就会写错形状。第三个是KV Cache的缓存失效逻辑。推理阶段我们会把历史token的K和V存下来避免重复计算。但缓存的前提是K和V的batch维度、头数维度要和当前请求对齐。一旦改变batch size或者换了一条新的对话流缓存必须清理否则就会把上一段对话的内容拼接进来生成质量断崖下跌。我早期做流式接口时就在这里踩过大坑被线上用户投诉过。一个简化但结构完整的前向实现大概是def attention(q, k, v, maskNone): dk q.shape[-1] scores np.matmul(q, k.transpose(0, 1, 3, 2)) / np.sqrt(dk) if mask is not None: scores np.where(mask, scores, -1e9) weights softmax(scores) return np.matmul(weights, v)别小看这十几行代码整个推理引擎最耗时的部分就是这里。优化的时候要往内存布局和cache locality方向想后面部署章节会展开。3.3 自建RAG检索层为什么不能全局暴力扫描RAG的核心是先检索、再生成但检索这两个字的内涵比大多数人想的要深。很多人的第一版RAG就是embedding全部丢进向量库然后top-k召回拼上下文。这种做法在小语料库上能跑通数据一多就完蛋因为暴力扫描的耗时是线性的百万级文档必然拖垮接口延迟。我项目里要求手写的核心是索引结构。工业界最常用的近似最近邻索引是HNSW分层可导航小世界图。它的思路很朴素构建多层图上层图稀疏、跳跃步长大下层图稠密、定位精确。搜索时先在上层快速找到大致区域再逐层下沉精细查找。手写的价值在于你亲自实现了图的构建、邻居选择、搜索链表维护这些步骤后才能真正看懂参数选择——为什么efConstruction大一些召回更好但建索引更慢为什么M太大内存会爆炸这些参数调优问题再也不会是玄学。下面这个表是我实测的几类索引方案对比方案召回精度查询延迟10万条内存占用实现难度暴力扫描100%约120ms最低极低倒排索引BERT中等约15ms低低HNSW手写约95%约3ms中高FAISS调库约95%约2ms中低手写HNSW的高实现难度其实是最大收获因为你会理解为什么业界最终选了近似方案而不是精确检索——没有人能接受120ms的检索延迟叠加在生成延迟上。另外检索完一定要加重排序。向量召回的前几名很可能是语义相似但实际无关的噪声用交叉编码器cross-encoder把query和文档逐字交互打分能显著提升送入大模型的上下文质量。重排序模型不需要很大哪怕是一个3亿参数的小模型都能让最终回答的质量上一个大台阶。3.4 轻量级Agent框架循环调用与记忆管理的取舍Agent框架手写的核心不在工具调用本身而在状态管理。模型每输出一次结果不一定是最终答案可能是一次工具调用的参数。这时候你的系统要负责解析模型输出、执行工具、把结果拼回对话上下文、再次送入模型、持续这个循环直到模型说结束了。我实现的最小Agent循环是这样的while True: response model_generate(messages, tools_schema) if response.get(finish): # 模型本轮输出的是最终答案 return response[content] # 否则视为工具调用 tool_name response[tool] args response[arguments] result execute_tool(tool_name, args) messages.append({ role: function, name: tool_name, content: result })看起来简单陷阱在细节模型输出的JSON参数经常不合法你需要写一个容错解析器而不是直接json.loads否则一次格式错误整个循环就死了。另外要设置最大轮数防止模型陷入死循环空耗token。我用的是15轮上限超过就强制终止并返回当前内容。记忆管理是另一个关键取舍。对话窗口的上下文长度有限你到底保留哪些历史信息两个方案最常用滑动窗口法只保留最近N轮对话摘要压缩法超过阈值时把前面的对话用模型压缩成摘要。我实测下来短任务用滑动窗口足够了又快又稳长文档分析任务必须用摘要压缩否则窗口被历史对话占满新内容进不来。在这个项目里我是先实现滑动窗口版本跑通后再加摘要压缩两者切换时你才能真正感受到Token预算问题的真实性。4. 实操路上避不开的坑训练资源、调试与工具链4.1 没有A100怎么办单卡消费级显卡的极限玩法这是整个项目里被问得最多的问题我只有一块4070能学吗我的答案是不仅能而且很多关键体验反而更深刻。没有A100不代表什么都做不了关键是学会裁剪问题——用最小的模型验证最核心的机制。我推荐两条路。第一条是纯CPU训练跑微型模型比如GPT-2级别降到10层、隐层256维在几万条数据上训练理解transformer的训练动态完全够用一个batch几秒能跑完调试迭代极快。第二条是用消费级显卡做LoRA微调不要去动全参数。一张12GB显存的卡配合QLoRA的4bit量化能跑7B模型的微调代价是训练速度和显存都刚刚好压在边缘。下面是我实测的显存参考表如果你的卡低于对应规格就得把模型再缩小显存可选玩法6GB2B模型的LoRA微调、小模型推理8GB3B模型量化推理、2B微调12GB7B模型量化推理、7B QLoRA微调16GB7B全参数微调的边缘、14B量化推理24GB14B QLoRA微调、本地部署更宽裕另外强烈建议配合免费的云端算力平台练手——Colab和Kaggle都提供免费的GPU时长虽然不能长时间跑但对于验证一个想法来说已经足够。我自己很多实验就是在Colab上跑通的消费级显卡的痛点不在于能不能跑而在于跑多快学会用小模型验证想法、再上大模型才是真正省时间的工程技巧。4.2 常见的三个调试噩梦及排查方法AI工程里因为各种奇怪原因产生的bug其实有相当一部分是无心的形状错误和数据吞掉了梯度。我自己在项目里遇到过三个典型的调试噩梦写出来给你排雷。第一个噩梦是训练loss直接变成NaN。排查顺序是先查学习率是否过大试着降到1e-5看能不能恢复再查输入数据是否包含NaN或Inf用np.isnan(x).sum()逐层检查最后查loss里有没有log(0)的场景在clamp里加个极小值能解决。我遇到过最隐蔽的一次是数据归一化时除的是一个标准差为0的列直接产出了一堆NaN这个问题花了我一下午才定位。第二个噩梦是梯度消失带来的模型学到一半不学了。训练曲线突然变平不是模型的错很可能是ReLU的负数区间把梯度杀光了。可以试着把ReLU换成LeakyReLU或者检查一下有没有在初始化时把权重全置成0了。手写反向传播引擎时这个问题最容易暴露因为你每层都看得见调起来反而比框架黑盒快。第三个噩梦是复现性问题。上次跑出来85%准确率这次同样的代码只有83%。原因多半出在随机种子——不光要固定Python的random和numpy的seed还要固定CUDA的seed和PyTorch的dataloader shuffle顺序。另外一个小坑是BatchNorm在训练和推理时的行为不同eval模式下如果你忘了切模式指标会莫名其妙地上不去。把这个检查项做完你的复现性至少提升一半。注意踩坑后一定要把定位过程写成文档。这个项目里我手工建了一份事故档案每次把现象、排查步骤、根因、修复方法记下来。后面做大型系统时这份档案就是最快的排障手册比任何教程都管用。4.3 工具链清单不同阶段用什么最顺手很多人在工具选择上纠结很久其实核心原则很简单训练阶段选社区生态最丰富的推理阶段选性能最合格的检索阶段选你了解底层原理的。训练阶段我最终留在HuggingFace Transformers加Peft的组合。虽然项目规则是手写核心但真正跑实验的时候还是要踩在巨人肩膀上——Trainer封装好了断点续训、混合精度、日志上报省下来的时间拿去改模型结构更值。如果你自己想控制每个训练step手写训练循环也不难但要自己处理AMP GradScaler和梯度裁剪工作量不小。我的建议是跑通一个实验用HuggingFace研究某个具体机制上手写训练循环两边都熟练以后你才能真正判断什么时候该用框架。推理阶段vLLM和llama.cpp二选一。vLLM的优势是吞吐量高适合在线服务并发场景llama.cpp的优势是CPU上也能跑适合本地调试和小规模部署。我自己是在手写Transformer前向后再用vLLM做部署对比能非常直观地看到为什么PagedAttention能省显存。检索和向量库方面FAISS和hnswlib都够用。但我的建议是先用hnswlib自己调一遍参数了解熟悉了之后再上FAISS因为FAISS的功能更全但抽象层级也更高直接上手容易被它的接口误导。向量和关系型混合查询场景用pgvector这种PostgreSQL插件能省一个中间件。日志和监控别一上来上重型系统用Python标准库的logging记录结构化日志配合一个简单的prometheus端点足够你发现大多数问题。5. 学习路线参考不同背景的人该怎么切入5.1 转行的后端程序员最短可行路径如果你是后端程序员有扎实的Python和系统设计功底缺的是模型底层概念我建议你跳过所有花哨的数学推导直接按这个顺序走向量和矩阵运算复习两周 → 手写自动微分引擎三周 → 用numpy手写MLP两周 → 手写Transformer block四周 → 做一个小RAG项目六周。总计四个月左右每天投入两小时就能跑完。这个路径对你来说重点是把模型当系统看模型是一个有状态的计算图推理时要管理显存生成时要管理上下文窗口。你后端的老本行——服务框架、并发控制、错误处理——在这里反而是优势AI系统和传统系统的边界会越来越模糊。5.2 有算法背景的人直接跳到工程化阶段如果你已经能读懂Transformer论文、微调过模型那前面的自动微分和手写Transformer对你来说太基础了。直接切入工程化的四个模块KV Cache和推理优化、检索管线和索引参数调优、Agent状态设计与容错、部署监控与线上评估。算法背景的人最容易犯的错是知识断层——《Attention is All You Need》读到滚瓜烂熟但不知道生产级推理要处理连续批处理、动态shape、显存碎片。补齐工程视角的关键是亲手压一次延迟把你的模型用vLLM部署起来写个压测脚本看看并发从1到32时延迟和显存怎么变化大部分工程直觉是在这种压力测试里长出来的。5.3 时间投入与里程碑目标不同时间投入适合设计不同的节奏。每天能投入4小时以上的人两个月可以走完全部八个阶段每天只有1小时的人战线拉到六个月重点放在前四个阶段后面四个月用真实项目带动。我自己感受最深的里程碑是手写Transformer前向跑通的那一刻。虽然输出是乱码loss在缓慢下降但你能看到attention weight在可视化中是逐渐集中到语法相关词的——那个瞬间抽象的概念才算真正长在了身体里。后面学任何框架你都会带着我知道这里面是什么的底气去读源码这种感觉是from-scratch路线独有的奖励。6. 最后分享一点我的个人体会这个项目做到中后期我最大的变化不是代码能力变强了而是面对线上事故的心态变了。以前模型输出幻觉、检索结果不对、Agent调用卡死我的第一反应是换个框架试试现在第一反应变成了先判断问题在哪一层——是embedding质量的问题还是索引召回参数的问题还是生成阶段上下文拼错了还是Agent状态机没收敛。能快速定位到层修复方案就八九不离十了。我从这个项目里总结出两条经验觉得比技术细节本身更值得分享。第一条是如果你的目标是做AI应用别只在提示词工程层面打磨。提示词是最后5%的优化空间而检索质量、上下文管理、工具链路可靠性才是那95%的工程底盘。底盘稳了提示词怎么调都是锦上添花底盘不稳提示词写得再花哨也守不住线上效果。第二条是手写一遍不等于什么都自己造轮子。框架和库是站在巨人肩膀上的工具但前提是你得知道它们解决了什么问题、在什么场景下会失效。我见过两个极端一种人只会调库出了怪问题连排查方向都没有另一种人什么都想自己写结果一个检索系统写三个月还达不到生产可用。这个项目的平衡点是核心机制必须手写过一遍生产组件尽量用成熟方案。如果你也打算开一个类似的from-scratch项目我建议你给自己定一个硬性截止日期并选一个真实可用的场景作为终极目标——比如跑通一个本地知识库问答系统或者做一个自动整理周报的小Agent。有真实目标牵引着才不至于在前面的基础阶段耗尽热情。跟代码较劲本身是有趣的但能亲手把完整的东西从无到有搭出来那种成就感才最扎扎实实。