
最近被问得最多的一类问题几乎都是同一个句式我想做 AI 工程但不想只会调包想真正从零跑通一个项目该从哪里下手网上关于ai-engineering-from-scratch的资料要么太浅——教你装个库然后调接口三分钟出个 demo但你心里清楚自己什么都没学会要么太深——上来就是几千亿参数大模型的论文和源码第一周就会劝退。这篇文章想走一条中间路线以从零亲手构建一个微型语言模型为贯穿主线把 AI 工程从环境搭建、模型实现、训练调参到部署服务的完整链路走一遍并把我踩过的坑一并写出来。它适合非科班转行、刚入门的 AI 工程师也适合想弄懂 Transformer 内部原理的学生——不需要你有深厚的数学背景但需要你真的愿意动手跑代码。1. 先别急着敲代码AI 工程的从零需要先盘三样东西很多人的第一反应是打开 PyTorch 开始写模型但我想先说句可能不太中听的话如果你不清楚自己要解决什么问题代码敲得越快后面返工越狠。AI 工程不是写模型这一个动作而是一条贯穿数据处理、模型实现、训练调优、部署验证的流水线。从零开始的意思是你要把这条流水线亲手打通一次而不是把每个齿轮都从铁矿石开始炼。1.1 数学到底要学到什么程度先说数学这是劝退率最高的环节。我的结论是你不需要数学系本科的背景但三样东西必须有直觉——矩阵乘法、导数和链式法则、Softmax 与交叉熵。矩阵乘法对应的是数据在层之间怎么流动一个形状为[batch, seq_len, embed_dim]的张量经过若干个线性变换最终变成[batch, seq_len, vocab_size]的分数矩阵这中间每一步你都得能说出谁和谁相乘、结果是什么形状。导数的直觉用来理解梯度下降——把训练过程看成下山loss 是海拔梯度是脚下最陡的方向学习率是你迈步的幅度学习率太大会跨过山谷太小则半天走不动。链式法则在反向传播里无处不在但实际工程中你很少手推梯度PyTorch 的 autograd 已经代劳了。你需要做的是知道梯度是从 loss 一端传回来的因此 loss 写错了整个模型的权重都会朝着错误方向调整。Softmax 和交叉熵则需要搞明白模型输出的 logits 为什么要转成概率以及交叉熵为什么是分类任务最常用的损失。我见过不少人上来就把交叉熵和 MSE 混用结果 loss 曲线诡异得让人怀疑人生。1.2 编程基础线Python 到底要掌握多少编程基础方面我的建议比大多数人想象的低。你不需要精通 C也不需要会写装饰器工厂但以下能力是硬门槛能熟练使用 Python 的类与继承、会读写文件、掌握列表推导式、理解map/filter这类函数式工具、会用torch.Tensor做基本的形状变换。一个很实际的自查方式是你能不能不用搜索引擎在半小时内写一个读取 CSV、过滤空值、按列做归一化的脚本如果能你的 Python 基础就够用了。如果不能先花两周把这块补上否则后面每个环节都会卡壳。还有一个容易被忽略的点读代码的能力。AI 工程里你读别人源码的时间往往比写自己代码的时间还长。从零开始不等于从零发明你要学会在 GitHub 上找一个结构清晰的中小型开源项目能说出它的目录里每个文件大概负责什么这才是真实工作里的核心技能。1.3 为什么主线选做一个会排序的小模型我之所以把从零构建一个能学会排序的迷你 Transformer作为贯穿全文的主线有三个原因。第一排序任务天然需要模型捕捉序列中元素之间的相对关系这正是注意力机制的主场它能让你直观感受到为什么 Transformer 能行。第二这个任务的数据可以无限生成不需要去网上爬数据集也不会遇到版权和数据清洗的麻烦。第三它的规模足够小——几万条样本、一个 6 层以内的小模型、一块 8GB 显存的显卡就能在半小时内跑完非常适合反复调参实验。这个选择也和你可能在热搜里看到的两条路线有关一条是build a large language model from scratch很多人指的是 Sebastian Raschka 那本同名书另一条是build a reasoning model from scratch属于当前很火的方向。我的看法是这两条路线都不是给第一天学习的人准备的。先亲手造一个能跑通全流程的微型模型你才有资格去读那些更宏大的 from scratch 教程否则你连里面讨论的扩展法则和推理时计算发生在哪个环节都定位不到。下面这张表是我建议的最小前置清单你可以对照自查。前置项最低要求自查方式Python 编程类、继承、列表推导式、文件读写半小时内写一个数据清洗脚本数学基础矩阵乘法、链式法则、Softmax手动推导一个 3 层 MLP 的前向形状Linux 终端conda 创建环境、pip 安装、查看 GPU在终端执行nvidia-smi并读懂输出PyTorch 张量操作会创建张量、做 reshape、走一个简单的nn.Module前向把一段 numpy 数组手动转成 tensor 并完成矩阵乘2. 环境搭建是新手第一道坎本地开发机、conda 与版本地狱我第一次带朋友入门时十个人里有六个倒在环境搭建这一步。这听着很蠢但实际情况就是如此——python 版本、CUDA 版本、PyTorch 版本、cuDNN 版本任何一环不对齐import torch都可能直接报错。环境问题最折磨人的地方在于报错信息往往不是你版本不对而是找不到某个动态链接库这类让人摸不着头脑的话。2.1 开发机选型从一台带 GPU 的笔记本开始先解决一个最实际的问题我需要什么硬件很多人的误区是一上来就租 A100觉得显存越大越好。但我们的迷你排序模型参数量在几十万到几百万之间8GB 显存的环境已经完全够用。我的建议是如果你手头有一台显存不低于 8GB 的消费级显卡比如 RTX 3060、4060 或同级别直接在本地跑省去一切网络传输的麻烦。如果没有也不用急着买卡用按小时计费的云 GPU 平台、Kaggle Notebook 或者 Google Colab 都能完成这个项目。重点不是显卡多贵而是你能不能快速迭代——本地环境能让你改一行代码立刻看到效果这对学习阶段的体验提升是巨大的。2.2 conda 环境与 CUDA 匹配的实操细节环境搭建的黄金法则是不要把 PyTorch 装进 conda 的base环境永远为项目单独创建一个环境。给你看一套我反复用的初始化命令conda create -n ai-from-scratch python3.10 -y conda activate ai-from-scratch pip install torch --index-url https://download.pytorch.org/whl/cu118 pip install numpy pandas matplotlib tqdm这里有个关键点很多人第一次栽跟头就是因为它先执行nvidia-smi看驱动支持的 CUDA 版本然后选择对应的 PyTorch 安装版本。比如驱动显示CUDA Version: 12.1你就应该用cu121的安装源如果驱动版本较旧就选cu118。注意不需要手动安装 CUDA 工具包——PyTorch 会自带运行时所需的 CUDA 库你动系统级 CUDA 反而容易把环境搞崩。装完以后务必执行下面这段检查代码import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果torch.cuda.is_available()返回的是False先不要慌按顺序检查驱动是否装了、PyTorch 版本是否匹配驱动支持的 CUDA 版本、conda 环境是否激活对了。九成问题出在这三处。我见过最离谱的一个案例是朋友在 conda 环境里pip install torch之后发现 import 的是系统 Python 里残留的 CPU 版本——所以创建环境后第一件事就是which python确认路径。2.3 依赖锁版本防止三天后环境突然崩掉学习阶段你一定会频繁安装各种库pip install装多了之后最恐怖的事情是某次升级把 PyTorch 的依赖悄悄替换了。所以我从一开始就养成了一个习惯每次跑通一个项目立刻把当前环境的依赖导出一份pip freeze requirements.txt这个文件一并提交到 Git 仓库里。下次换机器或者三个月后回来维护这个项目一条pip install -r requirements.txt就能复现环境。做 AI 工程环境可复现性和代码可复现性同等重要——你的模型在别人的机器上跑不出同样的结果一半的锅是环境不一致剩下的一半才是随机种子的问题后者我在第 4 章再细说。3. 从零手写迷你版 Transformer把注意力机制拆到骨头里环境就绪后我建议花一周时间把注意力机制彻底搞明白。别急着看那种带你逐行复现 GPT 的源码先理解注意力是什么——它就是一句话模型在预测当前这个位置时应该把输入序列里的哪些位置看得更重。排序任务是个绝佳的切入点模型要输出的数字顺序取决于输入序列里每一个数字的大小关系注意力权重恰好能表示这种谁和谁有关的对应。3.1 为什么第一课必须是 Transformer你可以先回想一下过去几年的 AI 热潮几乎所有主流模型都是 Transformer 的变体。从最早的翻译模型到如今的大语言模型Attention Is All You Need这篇论文提出的架构经过各种放大与改进但核心机制没变。所以如果只选一个组件从零实现注意力机制是性价比最高的选择——它既是理解现代 AI 的关键也是后续读论文时出现频率最高的概念。我不是说 RNN 不重要它仍然是理解序列建模的好教材但如果你目标明确是走进现代 AI 工程Transformer 是最短路径。3.2 注意力机制的 PyTorch 实现与维度讲解先看一个最简版本的缩放点积注意力把它彻底看懂比被一堆封装好的nn.MultiheadAttention黑盒强得多import torch import torch.nn as nn import torch.nn.functional as F class SelfAttention(nn.Module): def __init__(self, embed_dim, head_dim): super().__init__() self.head_dim head_dim self.q nn.Linear(embed_dim, head_dim, biasFalse) self.k nn.Linear(embed_dim, head_dim, biasFalse) self.v nn.Linear(embed_dim, head_dim, biasFalse) def forward(self, x): # x: [batch, seq_len, embed_dim] Q self.q(x) # [batch, seq_len, head_dim] K self.k(x) # [batch, seq_len, head_dim] V self.v(x) # [batch, seq_len, head_dim] scores Q K.transpose(-2, -1) * (self.head_dim ** -0.5) # scores: [batch, seq_len, seq_len] weights F.softmax(scores, dim-1) return weights V这段代码的核心逻辑是把每个位置的输入映射成三个向量——查询 Q、键 K、值 V。Q K^T算出的是每个位置和其他所有位置的匹配分数除以sqrt(head_dim)是为了防止分数过大导致 Softmax 进入梯度饱和区这一步叫做缩放也是缩放点积注意力名字的由来。Softmax 之后权重矩阵的每一行都是当前位置去看其他位置时的关注度分布。最后用权重去加权求和 V得到当前位置的输出。我在带人入门时最喜欢问一个问题为什么这里要缩放你想想如果两个向量维度很大点积的数值会随着维度增大而变大一旦点积数值过大Softmax 输出的概率分布就会非常尖锐——几乎把全部权重集中到最大值上其他位置几乎分不到注意力梯度也会变得非常小。这就是head_dim ** -0.5存在的意义。这个细节在面试里也经常被问到理解了它你是真的懂了注意力而不是背住了公式。3.3 架构拼图多头、位置编码与残差连接的职责单头注意力只是积木的第一块。真正让 Transformer 强大的是几个组件协同工作。多头注意力把embed_dim拆成多个子空间让不同的头去关注不同的关系——在排序任务里一个头可能关注相邻数字的大小另一个头可能关注某个数字在全局的位置这种并行关系建模能力是它比单头强的原因。位置编码解决的是注意力本身无序的问题——如果把序列打乱注意力计算的结果是一样的但语言是有顺序的所以必须把位置信息注入到输入里。常见的做法是正弦位置编码或可学习位置编码原理都是在 embedding 上叠加一个位置向量。残差连接和层归一化则是训练稳定性的保障。残差让梯度有一条高速公路从深层传回浅层防止深层网络退化层归一化则让每一层的输入分布保持在合理范围显著减少训练对学习率的敏感度。你可以这样记注意力负责看哪里残差负责让网络更深也没事归一化负责让训练过程别发疯。这三样东西组合起来才是一个完整的 Transformer 块。3.4 设计一个能验证模型真的学会了的微型任务我不建议直接去复刻语言建模那需要至少一个像样的语料库。更好的选择是设计一个排序任务给模型一串随机打乱的数字让它输出排序后的序列。这个任务有几个好处数据可以无限生成验证指标非常明确——输出序列完全正确就算对并且它天然需要模型捕捉元素之间的相对关系。数据准备的逻辑很简单import torch def make_batch(batch_size32, seq_len10, max_num50): inputs [] targets [] for _ in range(batch_size): nums torch.randint(1, max_num, (seq_len,)) inputs.append(nums) targets.append(torch.sort(nums).values) return torch.stack(inputs), torch.stack(targets)你可以把每一个数字当作一个 tokenembedding 层把数字映射成向量Transformer 读入整个乱序序列在输出端每个位置预测排序后的第 i 个数字。模型能不能学会排序靠的不是记住数据而是真正理解输入里的哪个数字是这里最小的、哪个是第二小的。当你看到验证准确率一点点爬上去的时候你对模型学会了一个抽象规则这个说法会有完全不同的体感。4. 训练小模型时的真实翻车现场显存告急、loss 不降与复现不了训练环节是劝退率第二高的地方而且比环境问题更隐蔽——环境报错至少是明面上的训练出问题经常是loss 曲线看起来有点奇怪却又说不清哪里错了。这一章我把训练迷你模型时最常见的三类问题拆开讲每一类都是我真金白银踩出来的。4.1 显存不够的三种解法与取舍哪怕模型很小你也可能会撞上CUDA out of memory尤其是在调大 batch 或者序列长度的时候。别急着换卡先考虑三个手段按性价比排序方案做法代价适用场景减小 batch size一次少喂几条样本梯度更抖需要降低学习率快速定位 bug 时首选梯度累积每 N 个 batch 累积一次梯度再更新训练时间稍长需要多写几行循环代码想保持大 batch 效果但显存有限混合精度训练torch.autocast配合GradScaler某些硬件上可能损失精度有显存余量且训练速度慢时梯度累积的原理是用时间换空间本来想一次看 64 条样本现在分 4 次每次看 16 条把梯度累加 4 次再更新权重。这样既模拟了大 batch 的平滑梯度又把瞬时显存占用压下来。代码上要注意optimizer.zero_grad()要挪到累积循环的外面参数更新也只在累积完成后执行一次。4.2 loss 不降时的排查顺序模型训练不出来大多数人第一反应是换更大的模型或者加更多数据但我劝你先冷静按照固定顺序排查。第一步先让模型过拟合一个 batch。取 4 到 8 条样本反复训练几十步如果 loss 能降到非常低说明模型的表达能力没问题问题出在数据 pipeline 或者训练逻辑上如果连一个 batch 都过拟合不了那就要检查代码 bug 了。这个技巧能帮你把模型结构问题和数据问题快速隔离。第二步检查标签和数据对齐。排序任务里最容易犯的错是输入和标签错位了一个位置或者没有做 pad 处理就送进CrossEntropyLoss。第三步检查学习率。学习率过大loss 会剧烈震荡甚至变 NaN学习率过小loss 可能一直在一个平台期缓慢下降。我惯用的方法是设一个lr从 1e-5 到 1e-1 的对数扫描跑同样的步数看哪个量级 loss 降得最快。4.3 复现难题随机种子被低估的杀伤力训练完一个模型后你想把结果复现一遍却得到不同的 loss 曲线这种事几乎每个 AI 工程师都经历过。从零学习的阶段我建议你从一开始就养成固定随机种子的习惯import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)但我要提醒你一个残酷的事实固定随机种子也不能保证跨设备完全复现。因为 GPU 上的某些算子为了追求速度采用了非确定性算法浮点数运算的舍入顺序在不同硬件上可能有微小差异。好在迷你模型对这一点不敏感你只需要保证在自己机器上能复现。更实际的做法是每次训练把超参数、数据版本、代码 commit 哈希一起记录下来这样哪怕隔了一个月再回头看你也能知道当时跑的结果对应的是什么配置。4.4 常见训练报错速查表最后给你一张我遇到的最高频报错速查表收藏了能省不少时间报错常见原因快速解法shape mismatch标签维度与 logits 维度过不去检查模型输出的[batch, seq_len, vocab]与标签的[batch, seq_len]CUDA out of memorybatch 或序列太长按第 4.1 节三种方案处理NaN in loss学习率太大或梯度爆炸降低学习率加梯度裁剪clip_grad_norm_RuntimeError: expected scalar type Floatembedding 或 label 类型不对统一torch.long与torch.floatDataLoader worker died数据集里的动态生成逻辑有问题先去掉多进程num_workers0排查5. 从 checkpoint 到能用的服务把模型推到生产边界很多入门教程到这里就结束了——训练完画两张 loss 曲线图然后发一条朋友圈。但 AI 工程和 AI 实验的根本区别在于前者必须让模型被人用起来。哪怕只是一个本地跑的脚本你也得走一遍导出模型、加载模型、提供接口的全过程。这一章我们用 FastAPI 搭一个最短推理服务把迷你排序模型变成随时可调用的接口。5.1 训练产物到底是什么state_dict 与 checkpoint训练结束后你的模型权重保存在哪儿PyTorch 里最常见的两种保存方式model.state_dict()只保存权重参数是一个字典torch.save(model, path)则会序列化整个模型结构。我强烈建议只用第一种因为它更稳健加载时的代码更清晰torch.save(model.state_dict(), sort_model.pt)加载的时候你需要先定义一个和训练时结构完全一致的模型实例再调用load_state_dictmodel MiniTransformer(vocab_size50, d_model64, nhead4, num_layers2) model.load_state_dict(torch.load(sort_model.pt, map_locationcpu)) model.eval()这里有个新手必踩的坑map_locationcpu一定要写否则你在没有 GPU 的机器上加载会报错。另外我特别想强调model.eval()这条——它把模型切换为推理模式关闭 Dropout 和 BatchNorm 的训练行为。很多人忘了这句线上推理结果莫名其妙地不稳定就是这个原因。5.2 用 FastAPI 包一个最短推理服务选 FastAPI 而不是 Flask 或者 TorchServe理由很简单FastAPI 自带请求参数校验、自动生成接口文档代码直观对新手极其友好。下面是一个足够用的最小示例from fastapi import FastAPI from pydantic import BaseModel import torch app FastAPI() model load_sort_model() # 加载模型并切换到 eval 模式 class SortRequest(BaseModel): nums: list[int] app.post(/predict) def predict(req: SortRequest): tensor torch.tensor([req.nums], dtypetorch.long) with torch.no_grad(): logits model(tensor) # [1, seq_len, vocab] preds logits.argmax(dim-1)[0].tolist() return {sorted: preds}注意with torch.no_grad()——推理时我们不需要计算梯度关闭梯度记录既省内存又加速计算。如果你模型支持批处理可以进一步把接口改成接收一个数组数组一次处理多条请求吞吐量会高很多。5.3 端到端验证里最容易出错的点服务跑起来之后真正的坑才刚开始。我见过最多的问题是训练时好好的部署后结果全乱。原因通常只有一个推理时的输入预处理和训练时不一致。比如训练时数字范围是 1 到 49你把 50 传入模型vocab 只有 50 个位置embedding 查表直接越界。又比如训练时序列长度固定是 10你把长度 5 的序列塞进去模型因为位置编码的截断逻辑不同输出完全乱掉。所以部署前必须做一次端到端验证curl -X POST http://127.0.0.1:8000/predict \ -H Content-Type: application/json \ -d {nums: [5, 1, 9, 3, 7]}这一步走通之后你的从零构建模型并部署整个闭环就算真正完成了。接下来你想继续深入方向就很清晰了换上真实文本数据集、加大模型规模、加缓存和并发甚至做成流式输出都是在现有骨架上做增量。6. 三个月实操路线与两条热门方向的终局对标如果你读到这儿说明你已经具备把一个小模型从无到有跑通的能力。但 AI 工程的知识面很宽第 5 章之后你可能会迷茫接下来学什么。最后这一章我给你一份可执行的三个月路线图并把开篇提到的两条热门搜索方向放在一起对标方便你确认自己的位置。6.1 三个月的分阶段安排阶段核心目标可交付产物第 1 个月补基础跑通第一个 MLP在分类任务上实现训练/验证/预测全流程第 2 个月迷你 Transformer 全流程排序模型训练完成loss 曲线和准确率可复现第 3 个月部署读源码FastAPI 接口上线能对开源项目结构做拆解第 1 个月的核心任务不是学一堆理论而是让你习惯改代码→看结果→再改代码的循环。不要花大量时间做笔记把时间花在跑通一个小型分类任务上比如手写数字识别。第 2 个月回到本文第 3、4 章的路线把迷你 Transformer 玩熟。第 3 个月我强烈建议你选一个开源的中小型 NLP 项目比如一个简化版的语言模型实现从头到尾读一遍源码画出它的数据流和模块边界——读源码是进阶的必经之路它会教你真实的工程组织方式。6.2 哪些时间不该花我也想把那些看起来特别有价值、但对初学者极度不友好的方向拉出来排雷。不要一上来就手写优化器PyTorch 的 AdamW 已经经过千锤百炼你写一版大概率不如它稳定不要一开始就钻研 CUDA kernel 或 FlashAttention那是模型足够大之后的优化课题不要在没跑通基础模型前去搞 RLHF 或者 PPO那是建立在成熟基座之上的进阶玩法。这些内容都有一个共同特征它们需要一个能正常工作的基线作为前提。先建立基线再谈优化。6.3 两条热门搜索方向对标你可以把build a large language model from scratch理解成把本文的迷你模型放大到真正能写作文的程度那是一条完整的学习路径Sebastian Raschka 的同名书就是沿着这条路径写的从数据编码、注意力机制到预训练和微调非常适合在完成本文第 3 章之后作为进阶教材。而build a reasoning model from scratch则是更新的热点——重点在于让模型学会先思考再回答涉及推理轨迹的构造、训练策略的设计属于研究前沿需要更强的工程基础和论文阅读能力。我的判断是先扎实完成迷你模型的构建与部署再决定向哪条路线深入两条路都需要能亲手跑通一个系统的地基。最后说一点个人体会。我见过太多人把从零开始理解成我要把能从底层造的东西全造一遍结果三个月过去还卡在数学公式和环境配置里。真正的 from scratch不是拒绝一切现成工具而是亲手建一遍自己的工作流从数据到模型、从训练到部署每一步都知道自己在做什么、为什么这么做、出了问题去哪里排查。等你走完一遍再回头看那些动辄几十亿参数的开源项目你会发现它们不再是一团迷雾而是一个你可以逐步拆解、理解、甚至改造的系统。这条路没有捷径但如果你愿意动手机会一直都在。