1. 动机复盘当HuggingFace和OpenAI已经够用时为什么还要自己动手老实说ai-engineering-from-scratch这个标题放在今天多少有点反潮流。现在随便一个开发者pip install transformers就能在三行代码里加载一个几十B参数的模型调一个API就能做出一个像模像样的聊天机器人。在2025年这个时间点任何一个AI工程问题的默认解法都是先找个现成的库再考虑自己写。那为什么还有人在坚持从零开始做AI工程我自己的经历是这样的。半年前我接手了一个内部项目要求把一套带推理能力的对话系统嵌入到公司自己的私有化环境中。当时最大的障碍不是模型效果而是黑盒依赖我们依赖的开源推理框架跑得好好的但只要升级一个CUDA版本、换一种国产显卡整个链路就崩给你看。排查到最后你会发现你对底层发生了什么一无所知——你只知道这个函数调用返回了一个字符串但不知道为什么返回这个字符串更不知道它在什么条件下会返回错误的东西。于是我开始意识到所谓AI engineering真正值钱的能力不是把预训练模型跑起来而是在环境不配合、资源受限、文档缺失的情况下依然能把一条数据到推理的完整链路从零搭出来。这个能力只能靠亲手构建一次最小系统来获得。这就是from scratch的真正价值它不是炫技不是拒绝现代工具而是把黑盒拆成白盒的最小成本路径。这篇文章我会完整复盘一遍我自己从零构建一个带思考能力的轻量级推理模型的全过程数据从哪来、模型结构怎么写、训练怎么训、推理怎么优化、评估怎么做、部署踩了哪些坑。面向的读者是那些已经会用现成框架但想深入底层搞明白AI系统到底是怎么转起来的的开发者。如果你是纯AI研究员这篇文章偏工程如果你是纯后端工程师这篇文章偏算法但如果你想把AI工程当成一门手艺来打磨这篇文章正好卡在你的舒适区边缘。2. 从零搭建AI系统的三条主链路数据、训练、推理的最小闭环2.1 数据管线的第一步是格式检查不是写爬虫很多人一想到从零做AI第一个冲动就是去爬数据。我在这个项目上栽的第一个跟头也在这里。我花了整整三天写了一个抓取脚本从公开的数学题库、代码仓库、技术文档站点抓了几十万条文本兴冲冲地准备开始训练结果第一轮loss肠子都悔青了——因为我压根没检查数据的重复率。重复数据对训练的影响远比大多数人想象的严重。我跑了一个简单的minhash去重脚本发现这批数据里有接近三成是高度相似的模板变体同一道数学题换个数字改个名就算一条新数据同一个函数定义在不同项目里反复出现。这样的数据喂进去模型会疯狂记住文本的表层结构而非解题逻辑生成的回答看起来格式完美换个数字就翻车。这也是后来我在评估阶段发现模型伪推理的根本原因之一。所以我现在搭数据管线第一道工序永远是格式检查和质量过滤而不是继续扩充数据量。具体来说我做三件事长度过滤。去掉过短的噪音片段比如少于50个token的碎片文本和过长的异常样本比如超过2048个token的截断体。重复检测。用MinHashLSH做近似去重阈值设在0.85左右宁可错杀不可放过。格式归一化。把全角半角、换行符、缩进全部统一这一步能防止词表被无意义的空白符号撑爆。这套流程看起来毫无技术含量但它决定了后面所有环节的成败。我见过太多人花大价钱租GPU训练却从不在数据准备上花一晚上的时间这种投入产出比是严重倒挂的。2.2 训练侧最小骨架自研Transformer还是抄一个在模型结构的选择上我走了一条很多工程师会骂我脱裤子放屁的路我没有直接用transformers库里的GPT2LMHeadModel而是基于PyTorch从头实现了一个约1.2亿参数的decoder-only Transformer。原因只有一个我需要彻底搞清楚每一个张量的形状变化后面做推理优化和量化的时候才不会瞎蒙。一个最小可训练的decoder-only模型核心组件其实就五块token embedding层、位置编码、多头自注意力、前馈网络、输出投影层。其中最容易写错的是注意力部分因为维度太绕了。我用一个非常笨的办法来避免维度错误把所有张量名都带上batch和seq维度例如x: [B, T, d_model]。每一层变换之后都在代码里注释新形状这样即使错了也能很快定位。我实现的配置如下隐藏层维度 768注意力头数 12层数 8序列长度 512词表大小 30000用BPE分词器训练自有的tokenizer16000步 vocab build位置编码用可学习的绝对位置编码而不是RoPE很多人问为什么不上RoPE。说实话RoPE在外推性上确实更好但我从零实现的第一版模型图的是简单可控可学习位置编码在固定序列长度下足够用而且代码量少三分之一。这个取舍在后期做推理长度外推实验时确实成了瓶颈但从工程实现角度先跑通再换编码方式是更务实的路线。自研模型的核心收益在训练结束后才真正显现出来我可以对每一层的中间激活值做任意操作可以在任意位置插入自定义算子可以精确控制量化误差的传播路径。这些能力在跑闭源模型时想都不要想。2.3 推理侧被低估的工作量KV Cache与批处理策略如果说训练是耐力活推理优化就是技术活。最小闭环跑通之后我面临的最现实问题是这个1.2亿参数的模型单条请求推理一次要800毫秒左右完全达不到对话系统的可用标准。第一个优化就是KV Cache。没开KV Cache的时候模型每一步都要重新计算所有历史token的Key和Value矩阵时间复杂度是O(n^2)。加上KV Cache之后只计算当前token的K和V前面对应矩阵直接复用解码速度能提升一个数量级。实现上其实很简单在自注意力模块里维护一个cache字典把历史K、V存进去生成时拼接当前步的新KV即可。代码量不超过30行但需要处理的边界情况挺多比如序列长度超过预训练上限、批内不同序列长度不一致等。第二个优化是动态批处理。推理阶段一个容易踩的坑是batch size并不是越大越好。实测下来在我的消费级显卡上batch size从1提到8吞吐量确实线性增长但超过8之后显存瓶颈开始出现反而因为频繁的显存分配和释放导致单请求延迟飙升。所以我在推理服务中加了一个简单的动态窗口根据当前显存余量动态调整batch size而不是写死。这一轮优化之后单条推理延迟降到了180毫秒左右基本达到了可用水平。但我知道真正的瓶颈在后头——让模型学会先思考再回答推理时的token消耗会翻好几倍优化压力会重新压回来。3. 让模型学会思考推理模型训练路线的完整拆解3.1 思维链数据怎么造从合成到筛选的可行流程现在业内的共识是普通大模型和推理模型之间的差距很大程度上不是架构差异而是训练数据里是否包含思维链Chain of Thought。所谓推理模型本质上就是在回答之前模型被训练成先产生一段内部思考过程再给出最终答案。这个过程的实现绕不开数据。但思维链数据是稀缺的几乎没有公开的高质量中文推理数据可以直接拿来用。我的做法分三步第一步用合成方式造种子数据。针对数学题和逻辑题我写了一个分步求解器它会把一道题拆解成若干子问题逐步输出中间结论。这不是用大模型生成的而是用规则脚本按真实解题顺序写的。比如鸡兔同笼类问题脚本会先算出总脚数再根据假设全为鸡的情况算出差值最后推导出兔子数量。这种数据虽然模板化严重但它的结构是正确的可以作为训练推理能力的骨架。第二步用大模型做蒸馏和改写。我拿了一批最强的商业模型让它们对一批问题输出带完整思维过程的解答然后人工抽检质量。注意这里不是让它直接生成数据而是让它把一道题的解题思路用逐步推理的方式重新表述一遍再和我规则生成的种子数据做交叉验证。如果二者结论一致保留如果不一致丢弃。第三步清洗和去偏。这一步最容易忽略。我检查了生成的思维链里是否有打闷棍式结论也就是从上一步没法自然推出下一步逻辑的内容。另外我还把每个问题的这一道题、这 题等表述做了归一化避免模型学习到无意义的文本模式。合成、蒸馏、清洗三轮走下来我拿到了大约12万条带思维链的训练样本。数量不大但对于1.2亿参数的小模型来说这个量级足够支撑第一阶段的推理能力训练了。3.2 比RLHF更容易落地的训练策略逐步偏好优化现在一提推理模型言必称RLHF基于人类反馈的强化学习或GRPO群体相对策略优化。但对于个人开发者或者小团队来说完整的RLHF管线几乎不可行——你需要奖励模型、需要在线采样、需要大量人工反馈基础设施成本直接劝退。我采用的方案是逐步偏好优化Step-Level Preference OptimizationSLPO这个名字是我自己起的参考了DPO的思路。核心思想很简单与其对整个回答做偏好排序不如对回答中的每一步推理做偏好排序。具体实现分三步对每一条训练样本把标准思维链和模型自生成的思维链拼接成pair。用规则给pair打标如果模型自生成的某一步结论和标准步骤不一致那么标准步骤的这一步就是 preferred模型的那一步就是 dispreferred。用DPO的loss在逐步级别做优化让模型在每一步都倾向于生成正确的中间推理。这个方案的优点是不需要奖励模型不需要在线rollout可以在有限的数据上直接训练。我在实验中把DPO和SFT做了对比同样的数据量下SLPO训练的模型在数学推理benchmark上高出SFT约7个百分点关键是它学会了一种自我纠错的倾向——当某一步推理产生矛盾时模型会倾向于回溯而不是硬着头皮往下编。这个特质是我从loss曲线上看不到的但换到实际推理中非常明显。3.3 在消费级显卡上逼近推理能力的几条曲线很多人看到这里会问12万条数据、1.2亿参数就能训出推理能力说实话如果你的标准是OpenAI o3那种级别的推理那确实差了几个数量级。但如果你需要的是在垂直领域内模型能稳定地分步骤解决问题而不是胡编那这条路线是完全可行的。我在这轮实验里画了三组曲线对理解推理能力训练很有帮助第一组是SFT阶段标准微调的loss曲线。这组曲线掉得很快大概在第三个epoch就收敛了但不要高兴太早——loss低不代表推理能力强它只能说明模型学会了记住答案。第二组是DPO/SLPO阶段的偏好准确率曲线。这组曲线才是真正反映模型是否学会了步骤级推理的指标。我在训练中监控的是步骤选择准确率也就是模型在某一步推理里选中正确中间结论的频率。这个指标从初期的62%逐步爬升到了80%以上说明模型确实学到了一些可迁移的推理模式。第三组是长度外推实验曲线。训完的模型序列长度上限是512但我在测试时让它生成更长的思维链看它的表现。结果喜忧参半——模型能生成超过训练长度的内容但到了700个token之后逻辑开始涣散。这说明我的可学习位置编码方案确实限制了推理链长度后续换成RoPE是必须做的。4. 训练过程中真实的坑显存、梯度和数据偏差的三方夹击4.1 显存溢出不是容量问题是生命周期管理问题如果你以为1.2亿参数的小模型在消费级显卡上训练就能高枕无忧那就大错特错了。我第一次训练的时候batch size设成32seq len 512结果跑了不到500步就OOM。当时的直觉反应是显存不够把batch调小但这个直觉是错的。真正的问题出在PyTorch的显存分配机制上。训练过程中每个step都会产生大量中间激活值PyTorch默认的缓存分配器不一定能高效复用这块空间。我的解决方案是两招一是开torch.cuda.empty_cache()的时机把控。不要在step之间频繁调用那反而会干扰缓存复用而是隔几十个step手动调一次释放那些看似还被占用但实际已经没用的碎片空间。二是用梯度累积来弥补batch size不足。显存只够batch size为8但我想等效到32的batch那就每4个step累积一次梯度再更新参数。这个做法的副作用是BN层的统计量会偏移但我用的全attention模型没有BN完全无感。这两招组合下来同样显存撑起了4倍等效batch的训练显存峰值还降了30%左右。4.2 梯度消失之外的隐性爆炸logits量纲失控训练中另一个让我排查了一整天的怪问题loss在前1000步正常下降然后突然跳到原来的5倍再恢复这样反复震荡。一开始我以为是学习率没调好但降了学习率之后只是震荡幅度变小问题依然存在。后来我打印了每一层的梯度范数发现输出投影层的梯度范数比其他层大了两个数量级。原因在于最后一层的logits直接和词表大小挂钩而词表有30000个维度初始化的参数方差稍微大一点logits的绝对数值就会很大导致softmax后的梯度爆炸。解决方案是加了一层梯度裁剪把全局梯度范数clip到1.0另外在输出层用了更小的初始化标准差。这两个小改动加起来不到十行代码但loss曲线立刻变干净了。这个坑在我后来看开源代码时发现很多人踩过但很少有人在网上写清楚——梯度裁剪的作用不是解决梯度消失而是解决这种由维度规模引起的梯度不匹配问题。4.3 数据偏差导致的伪推理模型学会了抄答案这个坑是最隐蔽的也是对我触动最大的。我在训练完第一版推理模型后迫不及待地拿了一批新题去测试。表面效果惊艳模型会输出一大段推理过程步骤编号、小标题、总结陈词格式完美得像教科书。但当我仔仔细细把推理过程的值代入题目验证时发现它根本就是错的。问题出在数据构造上。我在生成思维链数据时规则脚本总是先把答案固定好再反推过程。这导致数据里存在强偏差正确答案的字眼比如4和推理过程之间并不是真正的因果推导关系而只是统计共现关系。模型学到的不是推理而是看到某个答案特征就生成对应套路过程的抄答案模式。修复方案是把数据构造顺序反过来先产生纯粹的推理步骤再根据步骤推导出答案而不是先有答案再造过程。另外我在训练集中混入了一些错误的思维链样本让模型学会识别推理断裂的情况而不是把所有文本都当成正确推理来模仿。这件事的教训非常深刻数据管线里的任何一个偏差都会被模型忠实地放大成伪能力。如果你的推理过程数据是从答案反推的那你的模型永远不会真正的推理它只是在表演推理。5. 评估与部署从loss下降到真能用之间隔着的事5.1 设计任务级评估集而不是只盯验证集困惑度我见过太多项目在训练结束时欢呼loss降得多低然后一到真实用户手里就原形毕露。原因很简单loss是token级别的指标它对模型是否产生了有用的推理过程几乎无感知。我后来建了一套任务级评估集每一条测试样本由三部分组成输入问题、标准答案不是标准推理过程只是最终结论、评估规则。评估规则分几类数学类提取最终答案的数值和标准答案比对。逻辑类让评测器判断模型生成的答案中的关键结论是否与标准推理一致。代码类直接执行模型生成的代码片段看是否能通过预置的单元测试。这套评估方式能直接量化推理能力new answers correct / total questions。我用这套评估集回看早期SFT模型发现它的任务级准确率只有38%而loss却表现正常。这个反差让我彻底明白训练指标和业务指标之间隔着一条鸿沟两边你都得盯着。5.2 部署时的量化、批处理与延迟调优的取舍模型训完只是开始真正的工程考验在部署环节。我的目标是在CPU上也能跑出可用的推理速度所以量化成了必选项。这里我踩了一个典型坑直接把权重从FP32转INT8结果准确率掉了近5个百分点推理质量肉眼可见地下降。后来我才意识到问题出在激活值上——对绝对值很大的异常激活值做均匀量化会产生巨大的相对误差。换用混合精度方案之后情况好多了embedding层和输出层保持FP16attention内部的Q/K/V用INT8FFN层用INT8且带per-channel的scale。这个方案牺牲了大约10%的压缩率但准确率损失控制在1%以内。部署性能调优还有两个细节值得说一是关于生成时的重复惩罚repetition penalty。推理模型生成思维链时特别喜欢绕弯子同一个论点翻来覆去说。惩罚系数设低了没用设高了会让语言变得非常怪异。我最后是把它压到1.08这个值并且在生成长度超过训练上限时自动降为1.0保证长输出时不会因为过度惩罚而自相矛盾。二是请求排队策略。推理模型因为要生成大量思维链token单请求耗时波动很大。我实现了一个基于预估token数的排队机制估算每个请求大概需要的输出长度按长度分组进入不同的推理批次避免长短请求互相影响。这个机制上线后长请求的P95延迟下降了40%。6. 对from scratch路线的重新审视哪些值得从零做哪些该抄近路走到这里整个从零构建推理模型的闭环算是跑通了。但如果让我重新评估一次我不会再像刚开始那样一切自己手写。有些环节抄近路是更明智的工程决策。值得从零实现的三个部分数据质量管控流程。这个没有现成库可以一键解决而且它是整个AI工程的致命环节必须自己掌握。推理链路的中间产物访问能力。只有自己写的模型才能在推理过程中随时插入自定义逻辑这是做可控生成、可解释AI的基础。针对特定领域的算子级优化。预训练模型是通用的但如果你要跑的是垂直场景自研模型的精简结构会带来数倍的性能优势。可以直接借用的现成组件分词器。除非词表非常特殊否则直接用BPE类的现成实现比自己训练词表快得多、稳得多。数据加载和预处理框架。与其自己写多线程DataLoader不如直接用现成的高性能数据管线。分布式训练框架。如果你不是想研究分布式系统就别碰这个深水区。我现在的态度是从零理解但不从零重复造轮子。最核心的逻辑是你必须能从头实现一遍才能在别人造好的轮子出问题时修好它但同时你也要承认轮子本身是无数前人优化的结晶硬要自己重造一遍是傲慢。最后再分享一个我个人的体会从零训练一个模型最大的收获根本不是模型本身——那个1.2亿参数的小模型在今天随便一个开源社区都能找到更好的替代品。最大的收获是从此以后任何一条AI产品报错、任何一个推理框架的诡异行为、任何一个量化后的精度损失在我眼里都不再是玄学。我能猜出它可能在哪一层出了问题然后直接去那一层验证。这种手里有地图的感觉才是from scratch真正的价值所在。