经常有人问我AI engineering from scratch 到底是什么是照着网上的帖子把《Build a Large Language Model from Scratch》这类书里的代码一行行敲出来还是把 PyTorch 官方教程全部跑完就算完事我的答案是这两种都只碰到半张皮。真正称得上从零开始的 AI 工程是你能把一条 AI 项目的完整链路——需求拆解、数据处理、模型构建、训练调试、评估、部署、线上回归——从无到有地搭起来并且对每一步为什么这么做都能讲清楚。这篇内容就是我把这条链路完整走了一遍之后留下的记录和踩坑复盘。它适合两类读者一是刚入行想认真搞 AI 工程的开发者二是已经在用现成模型做业务、但总觉得地基不牢、想回来补底层认知的人。1. 先聊清楚什么是从零开始的 AI 工程1.1 模型训练只是 AI 工程的一半很多刚接触这个领域的人第一次看到训练脚本能跑起来就以为大功告成。我早期项目也踩过这种坑拿公开数据集微调一个文本分类器测试集准确率刷到 95%觉得项目马上能上线了。结果真正到了业务侧才发现标签本来就是错的线上输入的文本格式和训练数据对不上线上分布和离线分布差了一截那 95% 根本不值一提。所以我现在跟人聊 AI 工程一定会说一句训练出一个模型只是中点前面有数据后面有部署两头的工程量加起来至少占整个项目的七成。所谓 from scratch我理解的核心不是不抄别人代码而是你能把两头的事情也真正接起来而不是只会跑一个 notebook 然后交差。模型突然不收敛了你知道去哪查线上预测崩了你能够在三分钟内定位到是数据管线的锅还是推理服务的锅这才叫工程。1.2 从零开始不等于重新发明轮子这里有一个很常见的误解觉得 from scratch 就得所有东西从矩阵乘法开始写。我在实际工作中基本没见过这么干的也没必要这么干。PyTorch、Hugging Face、各种开源 tokenizer 库本质上是你的菜刀和砧板而所谓会做菜是你会熬高汤、知道油温怎么判断、酱汁裂开了知道怎么救。对应到 AI 工程上就是你可以用现成的组件但优化器突然给你跑出 NaN 时你找得到原因eval 的指标和业务目标对不上时你能察觉到。坦白说能调通一个开源仓库和能从零搭起一条新项目的流水线是两种能力。前者像是照着说明书装宜家家具后者是给你一堆板材你得自己琢磨出哪块该干嘛。大部分网上的教程只训练了前一种能力这也就是为什么一定要亲手做一遍模型从零写、数据从零收拾、部署从零接的完整项目。1.3 和查论文、刷课程的区别论文和课程给你展示的是一条清洁路径数据是精心挑选的代码是能跑通的训练曲线是漂亮的。但真实的 AI 工程里你面对的是脏数据、版本冲突的依赖、莫名其妙的显存报错、以及昨天明明好好的今天为什么不收敛的玄学问题。我把这两者比作读米其林菜谱和真正在后厨掌勺的区别菜谱告诉你放 3 克盐后厨的锅、盐的粒度、火力都会让这个3 克变味。还有一个容易被忽略的点论文不会告诉你它试了多少种 seed、排了多少次 bug 才画出那根平滑的 loss 曲线。网上那些build a reasoning model from scratch的热门项目看起来就像是可以跟着一步步复现的教程实际上它们背后用了大量工程技巧——数据过滤、RL 训练稳定性、大规模并行调试。这些恰恰是课程不会展开讲只有自己从零走一遍才能摸到的部分。2. 冷启动环境三件基础设施的搭建与选择题2.1 算力怎么选本地 GPU 还是云实例做任何 AI 工程之前先把算力问题想清楚不然你会把大量时间浪费在环境折腾上。我自己的经验是小规模探索用本地卡周期性的正式训练再包若干小时云实例。下面是一张我经常给新人看的对比表你把需求往里面套就能定位选项适合场景优点要注意的坑本地 GPU调试、小模型、数据处理边际成本低、数据不出本机显存上限明显、长时间满载伤卡云单卡实例中等规模实验、短周期训练灵活、用完即停数据存储和网络带宽另收钱别忘关云多卡 / 多机大规模预训练、长训练任务能跑真正的大模型运维复杂、成本陡增、分布式 bug 排查难我对从零开始做练习的建议很朴素先找一台哪怕只有 8G 显存的机器把流程全部跑通再考虑按小时租高档卡。因为初期大部分时间都在改代码、查数据真正全力算起来的时间没多少。租一台 80G 显存的机器在那儿干等编译是浪费钱最有效的方式。另外不管哪种方式都要学会watch -n1 nvidia-smi时刻确认显存和利用率别等 OOM 了才发现。2.2 训练框架选择PyTorch 还是 JAX框架的选择看起来是件小事实际上会影响你后面很长一段时间的调试方式。PyTorch 的生态最全断点调试顺滑出问题网上随便一搜就能找到解决方案对从零开始的项目几乎是唯一推荐。JAX 我接触过一阵函数式风格和 XLA 编译确实有意思在 TPU 场景也有优势但它对初学者的心智负担明显更大很多工具链也没有 PyTorch 那么顺手。我现在的固定组合是 PyTorch Hugging Face datasets accelerate。accelerate 这个库很值得提它让你用几行配置就在单卡和多卡之间切换既不破坏你对训练循环的控制又不用自己硬写分布式样板代码。微调阶段会用 transformers 的 Trainer但自己从零写模型练习时我建议手动写训练循环因为只有这样你才知道 Trainer 到底替你做了什么后面遇到奇怪问题才有排查方向。2.3 数据、代码、实验记录的组织方式基础设施里最容易被人忽略的是工程规范。我见过不少人项目做了一半实验跑了一堆最后说不清楚哪个模型用了哪份数据、哪个超参组合效果最好。这件事一张简单的目录结构和一份实验记录表就能解决。experiments/ ├── 001_baseline_ctf/ │ ├── config.json │ ├── data_note.md │ ├── train.log │ ├── val_metrics.json │ └── checkpoints/ ├── 002_tokenizer_bpe/ │ ├── config.json │ └── ... └── README.md每个实验目录独立config.json里写死全部超参data_note.md记录数据版本和清洗规则val_metrics.json存评估结果。大文件比如 checkpoint用 gitignore 排除代码才进版本库。如果你还有余力可以上一套 MLflow 或者 WB但我退一步说哪怕你只用 Excel 做实验记录也比什么都不记强一百倍。关键是每次实验只动一个变量否则你永远不知道提升来自哪个改动。3. 手搓一个极简语言模型数据和人力的真正门槛3.1 数据从哪里来清洗与采样策略很多教程直接用已经处理好的数据集让你产生一种数据根本不花时间的错觉。实际项目里数据才是耗时大户。做练习的时候我推荐从这几类数据下手TinyShakespeare 这种几十 MB 的文本集或者维基百科的清洗版本。目的不是数据量大而是能让你快速开展反复实验。我自己的习惯是先拿 1MB 左右的数据把整个链路跑通再扩展到全量数据这样调试一次训练循环只需要几分钟。清洗这一步最值得花心思。基本套路是按行去重、按长度过滤太短说明可能是噪声、剔除乱码和异常字符、最后用抽样看一眼头尾分布。有一条经验我经常挂在嘴边宁可要一万条干净数据也不要一百万条混了错误答案的数据。脏数据对训练的影响是静默的它不一定让 loss 爆炸但会让模型学会你没打算让它学的东西。3.2 Tokenizer 的隐藏陷阱Tokenizer 是语言模型工程里最容易出 bug 的地方却又最不受重视。很多人直接用预训练 tokenizer觉得省事就行但当你需要自己训练一个 tokenizer 时坑马上就来了。from tokenizers import Tokenizer, models, pre_tokenizers, trainers tokenizer Tokenizer(models.BPE()) tokenizer.pre_tokenizer pre_tokenizers.ByteLevel(add_prefix_spaceTrue) trainer trainers.BpeTrainer(vocab_size8192, special_tokens[|endoftext|]) # 然后喂入一组文本文件训练即可这里有几个我踩过的关键点。第一special token 必须在训练 tokenizer 之前先声明好否则词表里没有它们的位置后面加 token 的时候还要重训整个词表。第二训练和推理阶段的前处理必须完全一致比如空格处理、大小写规范差一点点都会让模型看起来笨了一点而且很难定位。第三对于小规模练习char-level字符级tokenizer 完全够用还能省掉一堆分词细节的烦恼让你把注意力集中在模型本身。3.3 一个可运行的迷你 Transformer 骨架把模型从零写一遍是理解大模型最有效的方式。下面这个 MiniGPT 是我经常给新人推荐的最小实现用 PyTorch 的高层 API 保留可读性又能体现 decoder-only 架构的核心逻辑import torch import torch.nn as nn class MiniGPT(nn.Module): def __init__(self, vocab_size, d_model128, nhead4, num_layers4, block_window256): super().__init__() self.token_emb nn.Embedding(vocab_size, d_model) self.pos_emb nn.Embedding(block_window, d_model) decoder_layer nn.TransformerDecoderLayer( d_modeld_model, nheadnhead, dim_feedforward512, activationgelu, norm_firstTrue, batch_firstTrue ) self.decoder nn.TransformerDecoder(decoder_layer, num_layersnum_layers) self.lm_head nn.Linear(d_model, vocab_size) self.block_window block_window def forward(self, idx): b, t idx.shape token_x self.token_emb(idx) # (b, t, d_model) positions torch.arange(t, deviceidx.device).unsqueeze(0) pos_x self.pos_emb(positions) # (1, t, d_model) x token_x pos_x mask torch.triu(torch.full((t, t), float(-inf)), diagonal1).to(idx.device) logits self.decoder(x, x, tgt_maskmask) return self.lm_head(logits)这个模型很迷你但它具备 decode-only 模型的所有关键部件输入 embedding、位置 embedding、带因果 mask 的 transformer 层、输出 logits 的 LM head。如果这段代码你不能从头写出来说明你对大模型的理解还停留在 API 调用层。写出来只是第一步更重要的是拿它去训练、去观察 loss、去让它在几十分钟内产生出人意料的结果。我不建议大家一上来就上那种几层就要改很多代码的nanoGPT教程因为那里面的实现细节太多初学者容易照抄完还是不理解为啥要那样写。我这个版本简单到可以让你把每个 tensor 的 shape 都手算清楚之后再去读那些更完整的开源实现理解成本会低很多。3.4 训练循环里最容易翻车的细节模型骨架写完之后训练循环是下一个分水岭。这里有几个细节我几乎每次教别人都会强调一遍optimizer.zero_grad() logits model(seq[:, :-1]) # 预测下一个 token loss F.cross_entropy( logits.reshape(-1, vocab_size), seq[:, 1:].reshape(-1) ) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()第一序列要错位对齐输入是序列的前 n-1 个 token标签是后 n-1 个 token这样模型学的是根据已知预测下一个而不是把当前位置复述一遍。这个错位写错了loss 会低得诡异但生成完全没法看。第二使用自动混合精度AMP时梯度缩放器要套在 loss 和 optimizer 外面这套组合顺序错了模型就会时好时坏。第三如果你的 batch 大小受显存限制用梯度累积loss loss / accum_steps # 累积前先除保证总梯度量级一致这些细节在 Trainer 这类封装好的工具里不需要你管但一旦你跳过它们直接用封装库遇到 loss 曲线怪异时你根本没有排查破口。至少亲手写一次训练循环值回票价。4. 从能跑到能用评估、调参和坏掉的 Loss 曲线4.1 只看 Loss 会骗人搭建你的评估集训练一旦跑起来最自然的反应是盯着 loss 曲线看。但 loss 下降只能说明模型在做最大似然估计不能说明它生成的文本像样、语义连贯。我见过不少 loss 刷到很低的模型生成结果全是重复短语和无效 token。所以从第一次训练开始就要同步建立评估集。最简单的做法是人工准备 20~30 个短 prompt存在一个固定文件里每次训练一段时间后抽出来用不同温度生成一遍肉眼对比输出质量。再配几个量化指标做参考。比如下面这样记录版本验证集困惑度prompt 示例生成结果温度 0.8人工判断ckpt-100068.2Once upon a time随机字符零星单词差语言未成形ckpt-300042.7Once upon a time短句语法较完整但重复可观察ckpt-500031.5Once upon a time有叙事结构但话题漂移接近可用你会发现困惑度从 68 降到 31 的过程中生成的语义质量并不是线性提升的。只靠数值你会高估中间那版但人工看很容易得出还是不行的判断。这也是所谓大语言模型 from scratch练习里最宝贵的经验学会建立自己的评估直觉。4.2 常见 Loss 怪相与分析这里把我踩过的高频问题按现象列出来遇到可以直接对照排查现象可能原因排查动作一开始就 NaN学习率过大、AMP 溢出、数据里有 inf先去掉 AMP 试一遍再降学习率降得很慢、卡在高位tokenizer 词表过大、模型规模不足、数据太脏检查 token 覆盖率切小词表清洗异常样本中间突然尖峰然后恢复碰到了损坏 batch、或 checkpoint 恢复时优化器状态没带上打印尖峰前后 batch 内容检查 resume训练 loss 降、验证 loss 回升过拟合增加 dropout、减小模型容量、加数据增强我自己的一个强烈建议是遇到问题先回退到最小可复现状态也就是用最小的数据、最少的参数、尽量简单的方式先让训练稳定再逐步加回复杂度。比如 NaN 问题几乎有三分之一是 AMP 的锅三分之一是学习率三分之一是数据。如果你一上来就是完整大模型配置根本无法定位。4.3 如何用实验记录表做决策调参这件事看起来是碰运气实际上是可以系统化的。我的记录表一般长这样每个字段都有实际意义实验编号 | seed | 学习率 | d_model | 层数 | 数据规模 | 验证loss | 生成质量 | 备注seed 这一列很多人不重视但小数据集上 seed 带来的方差极大可能完全改变你的判断。同一个配置换三个 seed如果验证 loss 波动超过 0.5那说明任何小超参改动都没有统计意义。这时候不要急着继续调参而是先扩大数据或者增大 batch。另一个原则是一次只变一个变量这听起来太朴素但我在实际项目里依然频繁看到有人同时改了两三个超参结果提升或恶化都解释不清楚只能留在项目里成为隐患。5. 部署一个自己训练的模型工程坑比训练坑更密5.1 显存、批大小与推理吞吐的平衡训练跑通之后部署是另一道坎。很多人以为训练都搞定了推理还不是小菜一碟结果栽在显存估算和吞吐设计上。一个粗颗粒度的经验公式模型参数占用的显存大约是参数量的 2 倍fp16再加上推理时动态增长的 KV cache。KV cache 的大小大致是每 token 每层需要 2 个矩阵的缓存乘上 head 数和 head 维度再乘上 2 字节精度。也就是说模型越大、上下文越长显存消耗得越快。推理时还要想清楚你的业务要的是延迟还是吞吐。在线对话场景要延迟低通常 batch size 小批量处理场景要吞吐高batch size 要尽可能撑上去。这里有一个取舍点把 batch 从 1 提到 8每个样本的延迟会变高但每秒钟处理的 token 总数会明显提升。所以一边骂为什么这么慢之前先问一句自己这个场景真的需要单次响应几十毫秒吗5.2 量化与裁剪让模型先跑得动模型写好了也训练好了但一部署发现显存不够、速度太慢量化是第一步常规操作。PyTorch 的动态量化非常容易上手对 CPU 部署尤其适用from torch.quantization import quantize_dynamic model_int8 quantize_dynamic(model, {torch.nn.Linear}, dtypetorch.qint8)这一步通常能把模型体积压到原来的四分之一速度也有提升代价是精度可能轻微下降。关键是做量化之前先记录原始模型的评估指标量化后做同样评估看差距能不能接受。GPU 场景则一般优先用 fp16 / bf16而不是 int8因为很多 GPU 上的 int8 kernel 反而更慢。除了量化裁剪和蒸馏也是路子但对从零开始的项目来说先把量化玩明白就够用了。5.3 服务化从脚本到 API 的最后一公里部署的最后一公里是把模型包成一个服务。最简陋的版本就是 FastAPI 加一个接口但这里有好几个工程细节决定你线上是否会翻车from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class GenerateRequest(BaseModel): text: str max_new_tokens: int 64 model None app.on_event(startup) def load_once(): global model model load_my_model_checkpoint() model.eval() app.post(/generate) def generate(req: GenerateRequest): if model is None: raise HTTPException(status_code500, detailmodel not loaded) with torch.inference_mode(): out my_generate(model, req.text, req.max_new_tokens) return {text: out}关键点三个第一模型要在进程启动时加载一次不要每次请求都重新反序列化否则响应时间会慢到没法用。第二推理外面套torch.inference_mode()别开梯度省显存还省时间。第三记录请求和响应日志哪怕只是简单写到本地文件后面做线上评估和排查都会救你一命。很多从零做项目的同学在这一步之前都非常得意直到上了线上才意识到训练环境有 PyTorch 顺手版本生产环境却往往没有 GPU、没有 NVIDIA 驱动、没有你本地那一堆包——这些都要在服务化之前考虑清楚。6. 从零开始的路线图我的个人经验和精简清单6.1 我给新人的三个月路线图如果你现在正准备开始自己的AI engineering from scratch之旅我建议直接按下面这个计划走每个阶段都让你交付一个可运行的东西时间内容完成标志第 1-2 周Python PyTorch 基础DataLoader 和 Tensor 操作能自己写一个完整的数据加载管道第 3-4 周数据清洗 训练自己的 tokenizer手里有一份干净 10MB 文本集第 5-6 周写 MiniGPT 并训练到收敛loss 曲线平滑能生成可读文本第 7-8 周搭建评估集 排查 2 个训练 bug能解释自己遇到的每个 loss 异常第 9-10 周模型量化 FastAPI 服务化接口能响应并记录日志第 11-12 周自选课题如复现一个小论文结果写一篇完整的实验复盘这个路线表的逻辑是先让每个环节的最小版本转起来再逐环节深化。没有必须先掌握所有理论再动手的说法AI 工程是做中学的典型领域。等你完成这个计划再看网上那些from scratch的大项目你会发现你已经能看懂它们的大部分操作甚至能判断哪些步骤是为了赶实验而做的不完善工程。6.2 在 scratch 实践中我学到的三条心得第一先跑通再优化。这是我反复讲但依然想强调的一条。很多人一开始就想把模型做到最好、把管道做到最完善结果一个月过去连端到端都没跑起来。正确顺序是哪怕用 1MB 数据、64 维 embedding也要先看到一条完整的训练曲线让损失确实下降。这个确实下降的确定性价值远比一次调大模型带来的随机突破重要。第二调试要用二分法找问题。模型表现差先判断是数据问题、tokenizer 问题、模型代码问题还是训练超参问题。用最小规模的模型在同一个小数据集上做对照实验把嫌疑范围一步步缩小。我见过太多人面对一个 7B 模型跑出来的坏结果无处下手就是因为没有小规模复现的习惯。第三日志和实验记录是隐形生产力。没记录你今天调参得到的好结果明天就变成了一次无法复现的偶然事件有记录你能从历史实验里清晰地看出哪个改动有效、哪个方向的尝试是死路。6.3 关于从头构建大模型这个方向的一点看法这两年大家热衷讨论build a large language model from scratch和build a reasoning model from scratch原因很直接每个人都想知道这些庞然大物内部到底发生了什么。我的看法是这些项目当学习材料非常好但别把它们当成现成的业务代码也别因为没复现出论文效果就怀疑自己。很多所谓 from scratch 的项目已经在数据过滤、RL 结构、并行策略上堆了大量你已经看不见的工程技巧。真要看懂并吸收这类项目前提就是你自己亲手经历过完整的小模型闭环。我常跟朋友说如果连 1000 万参数的模型你都没办法让它稳定收敛、并且讲清楚它生成质量为什么原地踏步那给你 70 亿参数只会更快翻车。规模不会解决理解问题只会放大理解问题。所以如果你想追 reasoning model 的潮流先去把语言模型从零练熟再尝试那些加了思维链和强化学习的高级玩法这才是稳的路径。最后分享一个我个人反复体会到的小经验与其花大量时间搜罗各种资料不如今天就用自己的文本文件跑起一个最小的语言模型。看着它从乱码变成有意义的句子那种对原理的掌控感是任何教程都给不了的。把它跑顺了你才真正有资格去拆那些更大、更复杂的from scratch项目。
阅读完成 · 觉得有帮助?