GitHub 上有个叫 ai-engineering-from-scratch 的项目我盯了很久。它是那种“不讲道理直接开干”的路线图从张量求导手推到 Transformer 的注意力机制再一路做到能跑的推理模型原型。如果你也想把大语言模型从黑盒变成白盒搞清楚数据管道、训练循环、推理部署每一环到底在干什么这个项目背后的思路能帮你省掉至少三个月走弯路的时间。我断断续续啃了三周把里面最核心的章节拆开重组结合自己实际做过的小型语言模型和推理模型实验写下这篇偏实操的记录。适合已经开始用现成框架但对训练细节发怵的人也适合正准备从零构建自己的第一个 NLP 项目的同学。1. 为什么说AI工程必须“从零开始”1.1 不是复读教程而是拆掉黑盒现在进入 AI 工程领域的门槛低到离谱HuggingFace 两行代码就能加载一个模型OpenAI 的 API 一个请求就能完成对话。但这种便利会带来一个隐蔽的问题当模型效果好时你觉得一切都很简单当模型效果差时你根本不知道从哪里开始排查。我自己带过几个刚入行的同事最常见的状态是模型 loss 不降了第一反应是加大 batch size第二反应是换一个更大的预训练模型。问他们 AdamW 里的 W 代表什么梯度裁剪是在裁什么很多人说不上来。这不怪他们框架封装得实在是太好了好到你不需要理解反向传播也能训练出一个能跑的模型。但工程上一定会遇到框架覆盖不了的问题推理延迟太高、显存不够、数据分布和预训练阶段不匹配、某个层在特定输入下产生 NaN。这时候如果你对背后的张量运算、梯度流动、注意力机制没有直觉就只能靠瞎试。从零开始看似绕远其实是最近的捷径。我手动实现过一个几十行的自动微分引擎之后再看 PyTorch 的 autograd 报错完全不慌了。因为我知道它背后就是在构建一张计算图从顶层 loss 开始反向传播把梯度分配到每个叶子节点上。手写过一次你对训练循环的理解就不一样了optimizer.step() 真正做的事就是根据每个参数的梯度微调参数值。1.2 从零构建能给日常工作带来什么很多人问我以后又不打算自己训练模型为什么要学这些我通常会反问他你调 API 的时候知道为什么同一个模型换一种提示词效果差这么多吗你微调开源模型的时候知道哪些层该冻结、哪些层该放开吗从零构建至少能带来四样东西调参不再靠玄学。理解了学习率、batch size、warmup、梯度裁剪各自作用于训练过程的哪个环节你就能根据 loss 曲线的具体表现做针对性调整而不是一次试十组超参。排查问题更快。你知道问题到底出在数据管道、模型实现、训练策略还是部署环境。这个分诊能力比任何调参技巧都值钱。模型选型更合理。你知道为什么某些任务用一个小模型加微调就够了某些任务则必须扩大数据规模你也知道从零训练一个专用小模型有时候反而比微调一个大模型更划算。能真正读懂论文。理解了 attention 机制和 RLHF 背后的最小实现之后再去看最新的推理模型论文你会发现那些公式不再是一堆希腊字母而是一个个你能在代码里复现的操作。这不是说每个项目都要重造轮子。而是给工具箱里加一套“透视镜”。我后来做业务微调时从零训练阶段获得的直觉帮我快速定位过两次非常隐蔽的问题一次是 tokenizer 词汇表和训练语料不一致导致生成全是乱码一次是数据 pipeline 里混入了空文本导致 loss 周期性震荡。这种问题如果没有底层知识光靠调参是不可能解决的。2. 从零构建AI工程的整体地图2.1 从数据到部署一条完整工程链路很多人以为“从零构建 AI”就是写一个模型类跑几个 epoch完事。但真正的 AI 工程是一条完整的链路模型训练只是其中一个环节。一个最小可用的 AI 工程项目至少包含六段数据收集与清洗决定模型能学到什么的上限。垃圾进垃圾出这句话在从零训练时体现得淋漓尽致。数据准备包括采样、tokenization、batch 构造、训练集验证集划分。这一环决定了数据进入模型时的形态。模型设计选择架构、层数、头数、维度。小项目可以直接用 GPT 风格 decoder-only 架构。训练包括损失函数、优化器、学习率调度、梯度裁剪、评估策略。这里是最容易翻车的部分。评估与推理用验证集计算 loss/perplexity人工检查生成样本写推理脚本。部署与监控将模型导出、封装成接口、监控线上指标。哪怕只是本地玩具项目你也应该走一遍这个流程。我在学习 ai-engineering-from-scratch 对应的实践路径时用的数据集是莎士比亚全集的一个子集大概 1MB 文本。这个规模对现代 GPU 来说小得可笑但它具备真实数据工程的所有要素有重复句式、有罕见单词、有长短句分布不均的情况。正是这个小小的数据集让我把所有环节都串了起来。数据环节的优先级应该是最高的。我见过太多人花一周时间调试模型最后发现是数据集本身有重复样本导致训练集和验证集泄漏。从零开始做项目时我建议先花 40% 的时间把数据 pipeline 弄清楚这比研究模型结构更重要。2.2 三种学习路径的取舍从零构建 AI 工程并不意味着每个环节都从零开始写。你可以走三条不同的路径它们适合不同的场景路径适合场景学习成本必须掌握的核心调用现成 API快速验证、非核心业务低提示词设计、上下文管理、成本控制微调开源模型定制风格、数据敏感场景中数据工程、评估体系、微调框架从零训练一个小模型深度理解、特殊场景高模型结构、训练算法、推理部署这三条路径并不是互斥的而是一条递进关系。我自己是先做 API 调用然后做微调最后才开始从零训练小模型。有了最后一步的体验再回头看微调和 API 调用视角完全不同你会明白为什么模型指令遵循能力需要特定数据格式去激发也会明白为什么 RAG 和微调在不同场景下各有优劣。如果你被“build a large language model from scratch”这个概念吸引我建议先明确一点你不需要从零训练一个 70B 参数的大模型。训练一个 10M 参数的小型 GPT足够让你经历语言模型训练的全部核心环节从错误中学习的密度反而更大。个人开发者或者学生最务实的路径是控制模型规模、控制数据规模把注意力放在理解每个环节上。2.3 环境与工具链如何选型从零开始动手前先把环境做成可复现的。我见过太多人卡在第一步CUDA 版本和 PyTorch 不匹配模型跑起来全是 illegal memory access还以为是代码写错了。我推荐的一套最小工具链是Python 3.10虚拟环境用 conda 或 venv。PyTorch 2.x注意 CUDA 版本要和驱动匹配。HuggingFace Tokenizers用于生产级的 BPE 分词。实验日志用 WB 或 MLflow没有账号的话用本地 CSV 记录也行。部署测试用 ONNX Runtime 或纯 PyTorch 导出。技术选型的原则只有一条服务于实验速度而不是追求最新最全。尤其是第一次从零构建时不要在一开始就引入分布式训练框架、自动调参平台、完整 MLOps 流水线。先用最简单的脚本在单卡甚至 CPU 上跑通一个极小模型。跑通之后你再逐步引入工程化工具。我自己的习惯是先把 requirements.txt 固定版本号然后再写第一行模型代码。这样即使过两周再打开项目也不会因为依赖升级搞得崩溃。这一点对任何项目都适用。3. 核心细节从张量到语言模型3.1 手写一个微型自动微分引擎任何从零构建项目的第一站都应该是自动微分。深度学习模型不管结构多复杂优化过程本质上都是前向传播计算 loss反向传播计算梯度梯度下降更新参数。现代框架用符号微分但你手写一遍才能真正理解链式法则是怎么被落实成代码的。一个微型自动微分引擎的核心只有几类操作加法、乘法、幂、激活函数。下面是我学习时写的简化版 Value 类支持加法和乘法class Value: def __init__(self, data, _children(), _op): self.data data self.grad 0.0 self._backward lambda: None self._prev set(_children) self._op _op def __add__(self, other): other other if isinstance(other, Value) else Value(other) out Value(self.data other.data, (self, other), ) def _backward(): self.grad out.grad other.grad out.grad out._backward _backward return out def __mul__(self, other): other other if isinstance(other, Value) else Value(other) out Value(self.data * other.data, (self, other), *) def _backward(): self.grad other.data * out.grad other.grad self.data * out.grad out._backward _backward return out def backward(self): self.grad 1.0 topo [] visited set() def build_topo(v): if v not in visited: visited.add(v) for child in v._prev: build_topo(child) topo.append(v) build_topo(self) for v in reversed(topo): v._backward()这段代码的思路非常朴素每个 Value 节点保存自己的数据、梯度、参与运算的子节点以及一个计算局部梯度的回调函数。backward 的时候先做拓扑排序确保梯度从输出向输入流动再依次调用每个节点的回调。手写一遍之后你就明白为什么我们会执着于“数值梯度对比”你可以用有限差分法算出每个参数的近似梯度然后和反向传播得到的梯度对比。如果误差小于 1e-6说明自动微分实现正确。这是从零开始项目里最值得做的测试因为它能帮你排除掉一大类隐藏 bug。3.2 实现Transformer核心组件自动微分是骨架Transformer 是肉。想要从零构建一个大语言模型核心就是把 GPT 风格的 decoder-only transformer 每一块都写明白。组件作用常见坑Token Embedding 位置编码把 token 索引映射成连续向量忘记缩放 embedding、位置编码维度错误Multi-Head Self-Attention让每个 token 根据上下文更新表示mask 处理错误、忘记除以 sqrt(d_k)LayerNorm 残差稳定深层网络训练归一化维度和实现方式搞混Feed-Forward MLP逐位置的非线性变换维度不匹配、激活函数选择不当Causal Mask保证自回归生成不出错mask 矩阵布尔值方向搞反导致未来信息泄漏注意力公式是理解整个模型的关键一环Attention(Q,K,V)softmax(QK^T / sqrt(d_k))V。Q 和 K 的维度是 d_k除以 sqrt(d_k) 是因为当维度变大时点积数值会变大softmax 会进入饱和区梯度会变得极小。这个缩放看似微小但它直接决定了训练稳定性。实现 attention 的时候我最推荐的练习方式不是直接用 nn.MultiheadAttention而是自己写一个单层 attention然后用官方实现的结果做对比。当你手工实现的结果和官方完全一致时你对“多头”的理解才会从图片变成代码多头的本质不是多个独立的注意力模块而是把 Q、K、V 分成多组并行计算最后拼接起来再经过一个输出投影。3.3 从零构建一个推理模型的要点最近“build a reasoning model from scratch”这个话题很火。所谓推理模型简单说就是能先输出一段内部思考过程再给出最终答案的模型典型表现是数学题和代码题的正确率显著提升。从零构建一个类 reasoning 模型最常见的技术路线分两段第一段是预热用包含思维链chain-of-thought的数据做 SFT让模型学会“先想后答”。思维链数据的构造不一定要买昂贵的数据集可以自己写规则生成比如要求模型先复述题目条件再推导中间步骤最后给答案。第二段是强化用强化学习或偏好优化对答案做规则奖励。回答结果正确就加分错误就减分模型在搜索空间里慢慢学会生成正确解题路径。开源社区里常见的做法是用 GRPO它的优势是不需要训练一个单独的奖励模型用规则打分就能更新策略。如果你真的要做到“from scratch”就需要先预训练一个小模型再在这个小模型上做推理能力的后训练。这个链路比较重但我建议仍然用小规模验证比如用 100M 参数左右的模型在数学题数据集上跑一遍。你会发现模型能不能学会推理很大程度上取决于数据里中间步骤的完整度而不是模型的绝对规模。我在做推理模型实验时最大的体会是评估是最难的一环。模型的思考过程很长但最后答案可能只有一句话如果评估只看最终答案很容易误判。正确做法是把中间过程和最终答案分开打分还要做人工抽检。3.4 训练策略与超参数选择写对模型结构只是第一步。从零训练时超参数选择决定了模型会不会收敛以及收敛到多好的局部最优。我给小模型做实验时一组比较稳妥的默认配置是超参数推荐值说明优化器AdamW解耦权重衰减泛化更好学习率3e-4 到 6e-4太小收敛慢太大会震荡等效 batch size约 0.5M tokens比固定样本数更好理解warmup steps1000~2000避免早期剧烈梯度weight decay0.1配合 AdamW 使用学习率调度cosine decay后期稳步微调梯度裁剪1.0防止梯度爆炸为什么需要 warmup因为训练初期模型参数是随机的梯度噪声很大此时用大学习率很容易冲进不好的区域。warmup 让学习率从极小值逐步升到设定值相当于让模型先稳定踩几步再加速。为什么 AdamW 里要区分 weight decay 和 L2 正则这是 Adam 系列的经典问题Adam 会把 L2 正则的梯度按历史梯度缩放反而让正则效果打折扣。AdamW 直接把 weight decay 从梯度中解耦出来在参数更新时单独衰减权重。这个细节看起来不起眼但在小模型实验里有时候会让验证损失差不少。我不建议一上来就每天试一组超参。先跑一个小 batch打印梯度范数和 loss 初始值。loss 初始值应该接近 ln(vocab_size)。如果不是说明模型初始化或数据管道有问题这时候调超参没有意义。4. 实操用一个小项目串联整个工程链路4.1 数据集准备与分词纸上谈兵够了直接上手。我用的是经典的 tiny-shakespeare 数据集全量大概 1MB 文本。这个数据规模的好处是几秒钟就能加载CPU 也能训练非常适合做端到端实验。第一步是分词。学习阶段我推荐用字符级 tokenizer因为词表很小差不多只有 65 个字符编码逻辑一眼就能看明白# 字符级 tokenizer便于学习和调试 chars sorted(list(set(text))) stoi {ch: i for i, ch in enumerate(chars)} itos {i: ch for i, ch in enumerate(chars)} encode lambda s: [stoi[c] for c in s] decode lambda l: .join([itos[i] for i in l])为什么不直接用 BPE因为 BPE 涉及学习合并规则、处理未知 token这些细节会干扰你对核心训练流程的注意力。字符级 tokenizer 在玩具项目中完全够用等你把链路跑通再换成 SentencePiece 或 HuggingFace Tokenizers 不迟。数据集准备好之后还需要构造训练集和验证集。我习惯按 90% / 10% 顺序切分不随机打乱因为文本类任务需要保持上下文连续性。这个阶段最重要的是可视化几个 batch确认 token 索引映射正确输入输出对齐没有偏移一格。输入输出错位是最常见但最难察觉的数据 bug。4.2 模型定义与训练循环模型我直接用 GPT 风格的 decoder-only transformer配置如下class GPTConfig: vocab_size 65 n_layer 6 n_head 6 n_embd 384 block_size 256 dropout 0.0这个配置参数量大概在 10M 量级一张普通 GPU 上训练几十分钟能出效果。block_size 代表上下文窗口长度这里设成 256意思是模型每次只能看到最近 256 个字符。训练循环是每一本从零构建教程都会写到的核心部分for step in range(max_iters): x, y get_batch() logits, loss model(x, y) optimizer.zero_grad() loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step()每一行都值得理解x 是输入 token 序列y 是标签序列torch 里习惯把标签和输出 logits 一起喂给 loss 函数。optimizer.zero_grad() 必须先清空上一步的梯度否则梯度会累加。loss.backward() 完成反向传播把梯度写到每个参数上。clip_grad_norm_ 把梯度范数限制在 1.0 以内防止个别 batch 的极端梯度把参数推飞。optimizer.step() 根据梯度和优化器状态更新参数。训练过程中我建议打印初始 loss。如果 vocab_size65那么随机模型的 loss 理论上接近 ln(65)4.17。如果初始 loss 偏离这个值很多先检查数据管道和模型输出不要急着训练。正常训练几十步后 loss 应该快速降到 3 以下最后能降到 1.5 左右。生成文本时使用自回归采样。每生成一个 token把它拼到输入序列后面再让模型继续预测下一个 token。这里注意 temperature 参数温度越高采样越随机温度越低越倾向于概率最大的 token。调试时可以用 temperature0.8看生成文本是否语法通顺。4.3 评估、推理与导出模型训练完很多人就直接完事了。但 AI 工程要求你必须有评估和部署闭环。评估阶段至少要看三个指标验证集 loss、困惑度 perplexity、人工生成样本质量。perplexity 等于 exp(loss)可以直观理解为模型对下一个 token 预测的“惊讶程度”。如果困惑度是 4可以粗略理解为模型每次预测有 4 个等概率候选不确定性较低。评估之后是导出。PyTorch 模型最简单的导出方式是torch.save(model.state_dict(), mini_gpt.pt)但如果要模拟真实部署可以继续使用 TorchScript 或 ONNX 导出一个推理接口。这一步的意义不是真的上生产而是逼你思考模型输入输出的边界tokenizer 应该和模型一起打包输入序列应该怎么对齐输出应该怎么解码。很多从零开始项目最后失败在“模型训练好了但没法被业务调用”就是因为把部署想得太简单。我在导出时踩过一个坑用 ONNX 导出时动态轴声明不完整导致推理时只能接受固定长度输入。后来学乖了导出前先写一个测试脚本输入不同长度的序列确保推理一致。5. 常见问题与排查技巧实录5.1 训练Loss不下降怎么办这是从零构建时最容易劝退人的问题尤其是你盯着 loss 曲线看了半天它纹丝不动。我把常见情况整理成一张速查表现象可能原因快速检查loss 一直不降学习率太大或太小打印梯度范数用 lr1e-4 重跑loss 变成 NaN学习率过高、数据里有 NaN降低学习率开启梯度裁剪验证集 loss 升高过拟合或数据泄漏检查重复样本加 dropout生成全是重复模型容量不足、采样温度太低增大模型提高 temperature我的习惯是准备一个微型冒烟测试从训练集里抽几十个 batch让模型强行记忆这批数据。如果模型连小批数据都无法过拟合说明模型实现有问题如果很快过拟合说明模型和数据管道是通的。这个测试只要几分钟能过滤掉一半以上的低级错误。5.2 显存/内存不足怎么处理本地训练小模型最常见的问题就是 OOM。显存占用主要有三个来源激活值、参数、优化器状态。激活值通常在训练时占据大头尤其是长序列和注意力矩阵。缓解手段按优先级排列减小 batch size 或 block_size这是最简单的办法。开启梯度累积多次前向反向后再更新一次参数等效于增大 batch size。使用混合精度训练用 AMP 将部分计算降到 fp16/bf16显存减半速度还能提升。开启梯度检查点不保存所有中间激活用时重新计算用计算换显存。最后才考虑换更大显存的卡或加 CPU offload。需要注意梯度累积时每步的 loss 应该除以累积步数否则等效 batch size 变大但学习率没变可能出现不稳定。混合精度训练时参数更新最好保留一份 fp32 主副本避免精度漂移。5.3 推理慢与部署踩坑从零训练的模型即使模型很小推理时也可能很慢这通常是因为没有做 KV cache。Transformer 自回归生成时每生成一个新 token 都要重新计算前面所有位置的 Key 和 Value如果每次从头算复杂度随序列长度平方增长。KV cache 的做法是缓存在内存里每步只计算新位置的 Key/Value复杂度降为线性。做任何生产级推理系统KV cache 都是标配。其他优化手段还包括量化、批处理、以及使用更高效的推理框架。但我不建议在你的第一个从零构建项目里就引入这些。先把功能跑对再考虑性能。部署时最常见的坑是 tokenizer 不一致。训练时用字符级 tokenizer部署时换成了 BPE或者预处理时多了一个空格、少了一个空格模型输出就会完全乱掉。我的习惯是把 tokenizer 对象和模型参数一起保存部署时绝不单独替换。6. 我在实际操作中的几条心得最后分享几条我刷完 ai-engineering-from-scratch 这条路线之后的真实体会。第一不要一上来就追求复现大模型。我现在做任何新模型结构都会先在几百条样本上做“能记住”的冒烟测试。模型如果连小样本都拟合不了换更大的数据量只会放大问题而不是解决问题。第二尽量把每一步都可视化。loss 曲线、梯度范数、注意力权重都打印出来看一眼。你看见的问题才是能解决的问题。很多 bug 藏在数字里不打印出来永远发现不了。第三从零开始不代表拒绝现成工具。你可以用 PyTorch、用 HuggingFace 的数据集库、用预训练的 tokenizer这些工具帮你节省的是重复造轮子的时间而不是理解原理的过程。关键是知道自己改了什么、为什么改。第四写实验记录比写代码还重要。哪怕只是用 CSV 记下每轮的模型配置、学习率、数据版本、最终 loss一个月后回来看都会感激当时的自己。AI 工程里可复现性才是真正的护城河。如果你也准备用这条路径作为自己的起点我只有一条建议别等所有原理都学透了才动手。在你的笔记本电脑上先跑通一个 10M 参数的微型模型再回去看那些论文你会发现注意力机制、梯度下降、思维链训练全都变成了老朋友。