去年年初我开始动手做“ai-engineering-from-scratch”这个项目的时候身边不少朋友都劝我别自找麻烦大模型接口都现成的云平台一键微调也多的是为什么非要从零开始训练一个语言模型我的理由其实很简单——我需要一套自己完全能控制每一个环节的AI工程能力而不是一个只能调参的黑盒。这个项目走下来严格来说它不是一个单次实验而是一条从零搭建语言模型、再升级到推理模型、最后部署成可用服务的完整实践路线。如果你也想知道一个LLM是怎么被“造”出来的数据是怎么流进模型的训练过程中那些报错和玄学问题是怎么被逐个攻破的那这篇内容应该能帮你省下不少走弯路的时间。核心就三件事数据、训练、部署顺序不能乱。1. 从零开始之前这个项目到底想解决什么问题1.1 为什么我坚持亲手构建而不是直接调现成接口先说一个很现实的问题既然如此多成熟的大语言模型都开源了为什么还要从零开始训练我自己的判断是直接调用现成接口和亲手构建一个模型两者解决的问题根本不在同一个维度。调接口解决的是“怎么用模型”从零构建解决的是“模型内部发生了什么”。举个例子你给模型输入一段提示词它吐出一段文字中间经历了分词、Token ID映射、位置编码、注意力计算、多层Transformer堆叠、采样策略……这些环节任何一个出了问题模型输出的质量都会受影响。如果不亲手过一遍你很难在线上服务出问题时快速定位病根——是数据污染是上下文窗口处理出错还是采样参数设置不当我倾向于把“ai-engineering-from-scratch”理解为三个递进的层次复现一个能跑的语言模型、理解训练管线里的每个细节、在此基础上扩展推理能力。做完前两层你才拥有第三层的判断力。就像学做饭先亲手把洗菜、切菜、火候都摸一遍再去看米其林菜谱才能真正看懂那些步骤为什么关键。1.2 目标拆解先做一个能“说话”的底座再谈“推理”从零开始做AI工程最怕的就是一开始就想要一个无所不能的系统。我在项目启动前把目标拆成了三个里程碑每个里程碑都有明确的验收标准第一里程碑训练出一个参数量不大但行为正常的语言模型输入任意 Prompt 都能产出通顺的续写文本。第二里程碑通过指令微调与思维链数据让模型具备初步的多步推理能力比如数学应用题和逻辑判断题。第三里程碑将模型部署为可调用的推理服务并跑通“收集 Badcase - 构造数据 - 增量训练 - 回归评估”的迭代闭环。这三个里程碑分别对应 AI 工程中“模型能力构建”“模型能力增强”“模型工程化落地”三个核心环节。我特别想提醒一点第一里程碑没有完成之前不要碰第二里程碑。推理能力是建立在语言建模能力之上的底座模型的续写能力如果还不稳定你做再多思维链数据模型学到的也只是表面套路而不是真正的推理路径。2. 环境与工程基座把本机改造成可复现的AI实验室2.1 硬件、驱动与Python环境的务实组合“从零开始”这四个字很多时候是从装环境开始的。网上关于训练大模型的教程很多但很少有人认真讲讲环境搭建这件事的坑。我自己的实践是不要一上来就用多卡分布式先用单卡把整个流程跑通。分布式训练解决的是“规模”问题不是“正确性”问题。模型代码本身有 Bug 时多卡只会把 Bug 放大不会帮你修掉它。我最初使用的是一块 24GB 显存的消费级显卡训练一个 1 亿到 3 亿参数的小模型完全够用。Linux 系统、NVIDIA 驱动、CUDA、PyTorch这一条链路的版本匹配一定要谨慎。我的建议是直接用 PyTorch 官方提供的 CUDA 版本安装命令不要自己单独装 CUDA Toolkit 再装 PyTorch否则很容易出现版本不一致导致算子加载失败。我当时的环境组合大概是这样的# Python 3.11 PyTorch 2.x CUDA 12.x conda create -n ai-eng python3.11 -y conda activate ai-eng pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install transformers datasets tokenizers accelerate wandb这个地方有一个非常容易被忽略的细节PyTorch 的 CUDA 版本要和显卡驱动支持的 CUDA 版本兼容。驱动太老而 PyTorch 太新程序会在调用 CUDA 算子时直接报“CUDA driver version is insufficient”。排查这类问题有一个简单命令跑一下python -c import torch; print(torch.cuda.is_available())如果输出 True说明环境基本没问题。2.2 代码结构、依赖锁定与实验追踪第一天就该做对很多人做 AI 实验的习惯是打开一个 Jupyter Notebook从上到下堆代码跑通了就完事。这个习惯在一两次小实验时没什么问题但一旦训练时间变长、实验次数变多你就会发现根本分不清“这次结果是用哪份代码、哪份数据、哪个超参数跑出来的”。所以我在项目第一天就搭好了工程骨架。推荐的代码结构是按功能模块拆分而不是按文件类型拆分。我当时用的是这个结构ai-engineering-from-scratch/ configs/ # 实验配置YAML 格式 data/ # 数据处理与 Tokenizer 训练脚本 models/ # 模型结构定义 training/ # 训练循环、评估脚本 inference/ # 推理服务的封装 scripts/ # 一键启动脚本实验配置一定要和代码分离。我见过太多人把超参数硬编码在训练脚本里换一组参数就要改动代码最后也无法追踪哪个配置对应哪个模型。正确做法是使用 YAML 或 JSON 配置文件每次实验记录一份配置副本。哪怕不用专业的实验管理平台也要养成“配置即代码”的习惯。实验追踪方面我推荐从第一轮训练起就使用 wandb 或 TensorBoard。别嫌麻烦训练曲线是你判断模型状态最直观的工具。损失函数的下降趋势、训练集与验证集的差距、学习率的变化轨迹这些都是后续排查问题的第一手证据。没有曲线记录你就像闭着眼睛开车。3. 数据工程AI工程里最容易被低估的硬仗3.1 语料获取、清洗与去重脏数据会让训练白费如果只说一句大实话模型训练里 80% 的时间花在数据上20% 的时间花在训练上。数据工程的优先级永远高于模型结构设计因为语言模型的本质是“从数据分布中学习规律”数据分布一塌糊涂模型学出来的东西就不可能好。我当时的目标是训练一个中文为主的底座模型语料来源以公开的开源数据集为主例如 Hugging Face 上一批高质量的中文语料。获取原始语料只是第一步后面清洗链路才是真正的体力活。我分成了四个环节格式清洗把 HTML 标签、Markdown 语法符号、异常空白字符统一剔除或转换。内容过滤用规则和简单的分类器过滤掉低质量内容比如纯广告文本、重复的短语、无意义的乱码。去重这一步最关键。我用了 MinHash 加 LSH 的近似去重方案因为完全精确去重在亿级文本上是不可行的。去重做不好训练数据里同样的句子反复出现模型就容易产生“复读机”现象也就是大家常说的记忆过强、泛化不足。隐私过滤移除明显的手机号、身份证号等个人敏感信息。这不是可选步骤是必须做的合规底线。做数据清洗时我踩过一个印象深刻的坑有一段中文语料我把所有 HTML 标签全去掉之后忘了合并被拆散的段落导致同一句话被截成两半连续出现几万次。模型训练中期出现严重的重复生成问题我排查了很久才定位到是这个清洗步骤的疏漏。从那以后我养成了一个习惯每次清洗完数据先人工抽样几百条肉眼过一遍再进入训练流程。3.2 BPE分词与样本打包把文本变成模型能吃的张量语言模型不认识字符它认识的是 Token ID。把文本切成 Token 的方法主流选择是 BPEByte Pair Encoding。BPE 的核心逻辑并不复杂从一个字符级别的词表开始反复把出现频率最高的相邻 Token 对合并成一个新 Token直到词表达到预设大小。通过这种方式常见的词会被拆成较少的 Token罕见的词会被拆成多个子词从而兼顾词表大小和表示能力。词表大小怎么定我当时的做法是设成 32K训练语料是 20GB 左右的中文文本。中文和英文有一个显著差异中文字符本身信息密度高词表太大容易让模型在嵌入层浪费参数词表太小又会把常见词拆得过于零碎。对于中小规模模型来说16K 到 48K 是一个比较合理的区间32K 是一个在多数场景下表现稳定的默认值。分词器训练完成后下一步是把文本序列打包成固定长度的训练样本。这里要特别注意一个细节不要简单地按固定长度截断每一段文本因为一段文本可能到 4000 Token 就结束了下一条又从 0 开始会造成大量填充浪费。正确做法是先把所有文本拼接成一个长流再按目标序列长度切分成连续的块。我用的序列长度是 512这个长度在训练效率和上下文能力之间比较均衡。每个样本保留完整的 Token ID 序列语言模型自回归预测下一个 Token这就是最基本的训练目标。4. 从零训练一个语言模型核心管线与实战参数4.1 架构选型先拥抱标准Decoder再谈魔改从零训练语言模型首选架构一定是标准的 Decoder-only Transformer也就是 GPT 系列采用的架构。原因不是它最先进而是它被验证得最充分、实现资料最多、出问题最容易排查。魔改架构是“先跑通再优化”的事绝不应该是第一步。标准的 Decoder-only 模型包含这几个核心组件Token 嵌入层、位置编码现代主流是 RoPE也就是旋转位置编码、多层 Transformer Block每层含 Masked Multi-Head Self-Attention 和 Feed-Forward Network、LayerNorm、输出映射层。我使用的是 12 层 Transformer Block隐层维度 7688 个注意力头参数量大约 1 亿出头。这里给一个实用的参数量估算公式方便你设计模型时心里有数。对标准的 GPT 式架构在隐层维度 (d)、层数 (L) 的情况下参数量近似为[ \text{params} \approx L \times (12 \times d^2 13 \times d) ]比如我用的配置 (d768, L12)算出来大约 87M再加上词表嵌入层约 32K×76825M 的参数总参数在 1.1 亿左右。这个量级在单卡 24GB 上训练压力很小适合作为从零项目的起步配置。但“参数少”不等于“随便训”。架构里任何一个组件写错了模型都能用某种方式“硬学”下去最后表现为损失降不下来或者输出完全崩溃。我建议每一个子模块都要写单元测试输入一批随机张量确认输出的形状和数值范围符合预期再开始训练。4.2 优化器、学习率与训练循环每一步都算数训练循环看起来就是“正向传播、算损失、反向传播、更新参数”四步但每个步骤里的细节都直接影响最终效果。优化器我选用 AdamW。相比经典的 AdamAdamW 把权重衰减从梯度更新中解耦出来对大语言模型的训练更稳定。我的超参数设置是lr3e-4beta10.9beta20.95weight_decay0.1。权重衰减设成 0.1 看起来很大但在大模型场景里是常见选择它能让模型依赖更少的参数完成任务从而提升泛化能力。学习率调度策略我用的是“预热 余弦退火”。具体来说前 500 步学习率从接近 0 线性升高到峰值 (3 \times 10^{-4})之后按余弦曲线逐步衰减最低衰减到峰值的 10% 左右。为什么必须预热因为在训练初期模型参数还是随机的梯度方向噪声很大如果一开始就用大学习率很容易让参数震荡到不理想的区域。预热相当于给模型一个“慢启动”过程让梯度方向稳定后再加大步幅。训练的另外几个关键参数批大小我用的是动态批大小每个 Batch 内累积 0.5M Token也就是序列长度 512、Batch Size 为 32 时每次更新大约处理 16K 个样本。你不需要一开始就追求极大 Batch重点是小模型配相对大的学习率。梯度裁剪max_grad_norm1.0。这个参数一定要加尤其训练初期模型不稳定时能避免梯度爆炸直接毁掉训练。损失函数标准交叉熵损失对因果语言建模来说就是让模型在每个位置都尽量正确地预测下一个 Token。4.3 显存优化三板斧裁剪、重计算与混合精度单卡训练小模型时显存通常还够用但只要模型规模再往上走显存立刻成为硬约束。我在这轮项目里实践了几种显存优化手段实测下来最有用的三件套第一梯度累积。如果显卡显存不够装下目标 Batch Size那就把一个大的 Batch 拆成多个小的 Micro-Batch分多次累计梯度最后统一更新一次参数。比如目标 Batch Size 是 32显存只装得下 8那就每个 Micro-Batch 用 8 跑 4 次梯度累积后再更新。它不改变数学结果只是用时间换显存。第二激活重计算Activation Checkpointing。Transformer 在前向传播时需要保存每一层的中间激活值用于反向传播这部分显存消耗非常大。激活重计算的思路是前向传播时不保存中间激活只在反向传播需要时重新计算一遍。代价是训练时间增加约 20% 到 30%但显存消耗可以大幅下降。需要动态调整。第三混合精度训练。我用的是 BF16它的指数位和 FP32 相同动态范围更大在训练稳定性上比 FP16 更好尤其适合语言模型训练。启用混合精度后模型权重和梯度可以用低精度存储而优化器状态保持高精度显存占用大约能省一半。我最终的训练配置大致是单卡 24GB1.1 亿参数模型Batch Size 32序列长度 512BF16 混合精度梯度累积 4 步激活重计算开启。整体训练了约 100 亿 Token 的数据耗时几天损失曲线从最初的 9 左右一路降到 2.5 附近。这个损失值不算低但对于一个小模型来说已经算正常水平了。5. 从“会接话”到“会推理”推理模型的构建路线5.1 推理能力从哪来先理清SFT与RL的分工底座模型训练完成后它的能力是“续写”。你给它一段数学题的题干它可以继续写下去但它并不知道应该先算什么、再算什么只会顺着概率往下蒙。要想让它具备多步推理能力需要第二个阶段的训练。最近一年开源社区对推理模型的研究热度很高技术报告普遍指向两条路线。一条是监督微调SFT路线构造包含“思维链”的指令数据让模型模仿完整的推理过程。另一条是强化学习RL路线让模型在探索中自己发现推理策略通过奖励信号强化正确的推理路径。我的实践结论是SFT 和 RL 不是二选一而是接力关系。先用 SFT 教会模型“推理应该长什么样”再用 RL 让推理能力进一步泛化。对于从零项目来说SFT 的性价比远高于 RL因为 RL 对基础设施的要求高一个量级你需要精心设计奖励函数、需要大量交互采样、还需要处理奖励欺骗等问题。我先踏踏实实把 SFT 路线走通RL 作为后续扩展项。5.2 代码与思维链数据让模型学会“先把思路写下来”思维链数据的核心思想是把模型中间的推理过程显式地写出来而不是只给出最终答案。举个例子一道数学题的标准监督数据应该是这样问题一个商店以每个 8 元的价格购进 100 个杯子以每个 12 元的价格售出 80 个剩余的打 7 折出售。问总利润是多少 答案首先计算售出 80 个的收入80 × 12 960 元。剩余 20 个打 7 折出售每个价格为 12 × 0.7 8.4 元收入为 20 × 8.4 168 元。总收入为 960 168 1128 元。总成本为 100 × 8 800 元。总利润为 1128 - 800 328 元。因此总利润为 328 元。可以看到关键变化在于“先计算”“剩余”“总成本”这些中间步骤被明确写了出来。模型在监督微调时学到的不仅是输入和输出的映射更是一套“分步思考再作答”的生成模式。代码数据在推理能力训练里的价值容易被低估。代码本身就是一种严密的推理过程变量定义、条件分支、循环、函数调用每一步都是有逻辑依据的。让模型在大规模代码数据上继续预训练或微调能强化它对结构化逻辑的建模能力。我实际观察到的结果是模型在代码数据上的表现提升之后它在逻辑推理类任务上的准确率也有明显提升。数据配比方面我采用的是一个混合方案思维链指令数据 50%、代码数据 30%、普通对话数据 20%。这里的思路是让模型既学会推理路径也保持对话的自然度和泛化能力不至于变成一台只会做题的机器。5.3 推理模型怎么评估别只盯着跑分推理模型的评估比底座模型麻烦得多。底座模型看 Perplexity 就行但推理模型要评估的是“回答对不对”以及“推理过程合不合理”。我在项目里搭建了三个维度的评估。第一是数学类基准测试。我选了几组公开的数学题数据集设置了统一的评测脚本统计最终答案的正确率。这类指标最客观但注意不要用训练集里的题目来评测否则数据污染会让分数变得毫无意义。第二是代码执行验证。让模型生成代码然后在沙箱环境里实际运行看它能否通过预设的测试用例。这是比数学题更硬的评估方式因为代码对就是对、错就是错没有模棱两可的中间状态。第三是人工 Badcase 分析。模型在基准测试上分数高不代表在真实场景里好用。我会定期抽取模型在真实对话里的失败案例逐个分析失败模式——是逻辑推理跳步了是计算粗心了还是指令理解偏了。这些人工分析最终会反哺到数据构造环节形成迭代闭环。6. 训练中的坑问题排查与调试实录6.1 损失不降、梯度爆炸、显存OOM最常踩的三个坑做这个项目的过程里我至少遇到过几十次训练异常但高频问题基本可以归成三类。损失不降是最让人焦虑的。我排查这类问题的顺序是先学习率再数据。学习率太高损失会在初始阶段震荡甚至升高学习率太低损失会以一个极慢的速度下降看起来像“不降”。如果学习率没问题那就怀疑数据样本是否全部是同一类内容导致模型学到的是局部模式数据流是否正确有没有把标签错配到输入上我通常会做一个快速测试用一小段单样本比如 16 条让模型反复拟合几十步如果训练损失能降到接近 0说明模型和数据链路没问题问题出在更大的训练配置上如果连单样本都拟合不了那就是代码 Bug。梯度爆炸通常表现为损失突然变成 NaN 或者剧烈抖动常见原因是学习率太大、Batch 内长序列的梯度范数过大。解决手段就是梯度裁剪。我一直开着max_grad_norm1.0并且每步记录梯度范数一旦发现超过 5立刻停机检查。梯度范数是一个非常重要的监控指标建议每个人都养成盯它的习惯。显存 OOM 大多数不是“真的显存不够”而是“没有合理地控制显存峰值”。先检查 Batch Size 和序列长度减少后能否训练再开启梯度累积、激活重计算和混合精度。如果还是 OOM就要看代码里有没有显存泄漏——比如在循环里重复创建了张量而没有释放。我的经验是 90% 的 OOM 可以通过 Batch Size 减半加上梯度累积解决剩下 10% 才是真正需要换卡的场景。6.2 过拟合与数据污染验证集的正确打开方式小模型在大语料上不容易过拟合但小模型在小语料上非常容易过拟合。我在第二轮迭代时因为想快速验证训练管线使用了缩减版数据集结果训练损失降到 1.8验证损失却停在 3.2 附近差距越来越大这就是典型的过拟合信号。过拟合的应对首先是收集更多数据其次是增强数据多样性再次才是正则化手段。我也尝试过在训练中叠加 Dropout但对语言模型而言数据量的价值远大于正则化带来的收益。所以我在项目迭代里始终坚持一旦发现过拟合优先扩展语料来源。数据污染是另一个容易被忽视的坑。如果验证集中混入了训练时见过的文本验证损失会虚低模型真实能力被高估。所以数据集划分要格外严谨。我的做法是按发布时间或文档 ID 做哈希划分而不是简单随机抽样这能避免同一篇文档的多个片段同时出现在训练集和验证集里。6.3 训练问题速查表整理一个速查表方便大家直接对照排查现象可能原因排查方向解决手段损失完全不降学习率过高或过低先看学习率曲线尝试峰值 1e-4 到 3e-4 区间损失降但很快停滞序列长度过短或词表不合理观察生成文本的 Token 切分调整序列长度或词表大小损失变为 NaN梯度爆炸或数值不稳定查看梯度范数启用梯度裁剪、降低学习率训练损失降、验证损失升过拟合对比训练/验证损失曲线增加数据、提升数据多样性显存 OOMBatch 过大或激活占用过高监控显存峰值梯度累积、激活重计算、混合精度生成内容反复重复去重不彻底或数据质量低抽样检查训练数据加强数据去重与质量过滤7. 从模型到可用系统推理服务与迭代闭环7.1 模型导出、量化与推理服务让模型真正可用训练完成之后模型还只是一个权重文件真正可用还需要跨过推理服务这道门槛。模型导出时我统一转成了 Safetensors 格式然后用 Hugging Face 的推理框架加载。以我的经验自建推理服务的起点可以直接用 vLLM它内置了 PagedAttention 等显存优化技术吞吐量比朴素实现高出不少。关键的区别在于 KV Cache 的管理模型生成时要把前面 Token 的 Key 和 Value 缓存下来复用朴素实现会为每次生成分配大量显存而 vLLM 的分页式管理能显著减少浪费。如果对延迟有更高要求就需要考虑量化。我测试过 AWQ 和 GPTQ 两种主流量化方案在小模型上 AWQ 对生成的精度损失控制更好。这里要特别提醒一点推理模型的量化要格外谨慎。推理链通常很长会生成几百上千个 Token每一步的量化误差都可能累积导致最后结果偏差。我在实测中发现一个在数学基准测试上表现尚可的推理模型经过 4-bit 量化后准确率下降明显高于同等规模的普通对话模型。如果业务对推理质量要求高建议至少保留 8-bit 量化档位。7.2 数据、模型、评估三件套可持续迭代的工程闭环模型部署上线只是开始真正让系统持续变强的是迭代闭环。我最终跑通的闭环长这样线上服务收集用户反馈与 Badcase。人工分析 Badcase提炼高频失败模式。针对失败模式构造新数据混合到已有训练集中。增量训练或继续微调模型。跑回归测试对比新模型与旧模型在评估集上的表现只有全部通过才允许上线。这套闭环说起来简单执行起来容易偷懒。我自己的体会是一定要把评估集当作“代码测试”一样管理每次模型更新都必须跑一遍完整的回归测试不允许“感觉变好了就上线”这种没有任何依据的操作。没有回归测试的迭代就是在给线上服务埋雷。部署阶段还有一个小经验上线前要对模型做一次压力测试。用不同长度的输入请求连续调用推理服务观察响应时间、显存占用和错误率。很多模型在单例测试时表现良好一旦并发请求上来KV Cache 的内存问题就会暴露。提前压测能避免不少线上事故。写在最后的几句实在话从零构建一个 AI 系统最大的感受就是“每一步都不简单但每一步都可控”。插件、框架、现成接口能帮你节省很多时间但它们不能代替你对基本原理的理解。我的建议是如果你对语言模型内部机制还不够清楚不要急着上多卡、上大模型、上分布式训练。先把一个小模型从数据到训练到部署完整走一遍把所有环节里“不知道为什么但它跑通了”的地方搞明白再去追求规模。最后分享一个小技巧把训练过程中的每个决策都记录下来尤其是超参数、数据来源、清洗规则这些看似琐碎的信息。你的下一次实验、下一次升级都依赖这些记录。AI 工程真正比拼的不是谁模型更大而是谁的迭代闭环更完整、更稳定。
阅读完成 · 觉得有帮助?