
1. “from scratch”不是从空文件夹开始而是从系统原则开始收到“ai-engineering-from-scratch”这个标题时我第一个反应是又要聊“从零写代码”了。但过去两年带团队从裸机到完整AI系统的那段经历让我意识到工程意义上的from scratch和普通人理解的那个“从零”根本不是一回事。大多数教程展示的是给你一个空目录pip install然后写模型、跑训练、出指标。真实项目里这套流程跑十万次也出不了可上线的东西。因为AI工程的核心不是“模型代码”而是一整套围绕数据、算力、评估、部署、迭代的系统能力。你从零开始做的不只是模型是整个闭环。这篇文章我想用我自己实际走通的路径来拆解一个完全没有AI工程基础的人要怎么一步步搭出一套能训练、能评估、能上线的AI系统——具体到GPU选型、数据清洗、训练脚本、评估设计、部署监控每一个环节的取舍和踩坑。适合谁看第一种是想从传统后端转AI工程方向的同学第二种是已经在用开源模型但想深入训练层的开发者第三种是被“一句提示词搞定一切”误导了、想搞清楚AI工程到底是什么的初学者。我会尽量少讲玄学多给能直接落地的判断依据。1.1 工程上的from scratch第一步不是写模型而是定基线很多人上来就选模型、调框架这是顺序错了。真正的起点应该是定“最小可用闭环”你要解决的问题是什么输入输出长什么样用最粗糙的方式能不能跑通一版。哪怕这版效果很烂但它定义了后续所有优化的基准线和衡量标准。我习惯用“三个问题”做启动这个系统最终给谁用回答这个问题决定你在评估阶段是重准确率还是重延迟。数据从哪来、长什么样回答这个问题决定整个管线地基是否稳固。如果效果很差用什么指标证明“很差”这比“我要做到多好”更实际。这三问没想清楚先别开GPU账单。我在第一年做过一个反例GPU到位后所有人兴奋地开始训练三周后模型出来了但发现没有一套完整的评估流程来证明它的价值结果只能靠演示PPT汇报研发方向也跟着飘。后来我彻底改用“先定指标再定架构最后才开训练”的顺序项目节奏才稳定下来。1.2 最小闭环的三个层级把AI工程拆成三个层级你就能清楚自己缺哪块数据层采集、清洗、格式化、去重、配比、版本管理。训练层架构选型、分布式配置、超参搜索、checkpoint管理、日志监控。上线层推理服务、性能压测、监控告警、灰度更新、漂移检测。大多数刚入门的人只会盯着训练层但实际上90%的问题出在数据层和上线层。我从零搭系统时把所有精力按比例分配数据层占40%上线层占30%训练层占30%。这不是拍脑袋而是复盘多次项目后得出的经验——模型训练决定的是效果上限数据决定的是这个上限有多扎实部署决定的是效果能不能变成价值。2. 硬件与软件环境从裸机到能训练要跨过的不是一条命令的坑当你决定从零干活时第一脚踩进的坑叫环境。环境问题最恶心的地方在于它不是逻辑错误但会让所有后续工作直接归零。而且这部分的经验很难从文档里学因为大家机器不一样、驱动不一样、框架版本不一样。2.1 GPU选型先算显存再谈品牌训练7B模型到底需要什么样的卡我基于自己做过的一个案例来算以bf16精度为例7B参数模型权重占14GBAdam优化器状态约占56GB每参数约8字节fp32主权重、动量、方差梯度在bf16下约14GB。因此单机全参数训练光是模型状态就需要约84GB显存。这就是为什么很多人一上手7B全参数微调就爆显存——单卡背不动。公开的推理侧需求就小得多了7B模型bf16推理约14GBint8量化约7GBint4量化约4GB。所以你的用途直接决定卡的选择场景显存需求推荐方案单卡微调1B以下小模型8-16GB单张RTX 4090即可7B模型推理部署16-24GB单张4090或A107B模型全参数/大规模微调80GB2×A100或4×3090训练/微调13B以上模型显存越大越好8×A100/H100集群我的经验是如果预算有限优先考虑单张显存大的卡而不是数量多的卡。因为跨卡通信的带宽、分布式同步的调试成本远比单卡多等几分钟要贵。能用一张卡解决的问题绝不为了面子开两台机器。2.2 环境锁定CUDA、PyTorch、依赖三件套的相互锁定环境里最容易炸的雷是版本排列组合。我实测中比较稳定的一组是CUDA 11.8 配 PyTorch 2.1.x适配大多数NVIDIA驱动463的机器CUDA 12.1 配 PyTorch 2.3.x在比较新的卡L40S、H100上表现更好Python 3.10 属于安全区3.11虽然快一点但很多旧依赖没适配。这里我强烈建议用Docker而不是在宿主机上裸装。一个原因复现性。我们之前有个人在本地跑通一切换一台新机器后环境重建花了整整两天最终定位到是numpy版本和torch版本不兼容导致的一个诡异警告。用Docker把CUDA、cuDNN、Python、PyTorch全打进镜像团队每个人的运行环境才真正一致后面再也没出现过“在我机器上能跑”这种烂梗。Docker镜像的基底我建议这样组合FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 RUN apt-get update apt-get install -y python3.10 python3-pip git RUN pip install torch2.1.2 --index-url https://download.pytorch.org/whl/cu118 RUN pip install transformers datasets accelerate deepspeed注意一个细节devel镜像比runtime镜像大很多但包含了完整的编译工具链后续如果你要装flash-attention这类需要现场编译的库没有编译工具链会当场卡死。如果你确定不需要编译任何自定义算子选runtime版本更轻量。2.3 一次真实的环境崩溃经历版本锁定的教训说一下我自己踩过的最大一次环境坑有一次项目进度紧张团队一个同学在本地宿主机上升级了显卡驱动结果和容器里原本固定的CUDA版本不兼容所有用到GPU的容器直接起不来。更糟的是因为这个容器没有做网络存储上的单独备份checkpoint全在容器内的/tmp目录重启后全部丢了浪费了我们半个月的训练成果。从那以后我定了三条铁律所有训练产物checkpoint、日志、数据集版本索引必须放外部卷容器内一律不落盘。GPU驱动、容器镜像、训练脚本三者绑定同一个“环境清单”发布任何升级都要全量回归。每次训练启动前跑一个几秒钟的probe脚本确认GPU可用、显存足够、依赖版本符合预期而不是直接开始正式任务。import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) print(torch.cuda.mem_get_info(0))这个小脚本每次都帮我提前发现至少三类问题驱动失效、显存被别人占了、CUDA版本和PyTorch不匹配。3. 数据管线的从零构建真正的AI工程重心在这里我见过太多人把时间花在调参上却对训练数据的使用方式是糊弄的。但事实是数据决定模型效果的上限而模型只是去逼近这个上限。调参、架构改进都是在让模型更接近数据所承载的知识。所以从零搭建AI系统数据管线必须当作重中之重。3.1 数据从哪来公开数据集 自有数据清洗对于一个from scratch项目你大概率没有现成的干净数据。我的习惯分成三步走第一步先用公开数据集跑通流程。比如要做中文对话模型先用开源的中文通用语料把基座能力练出来至少让模型不说胡话。第二步将行业或自有数据做“知识注入”针对你的具体场景做微调。第三步用真实用户反馈或人工标注数据做不断迭代。公开数据集的获取要注意授权边界尽量选择明确标注了可以用于研究的语料。我常用的有通用爬虫类语料、代码类语料、学术类语料。规模方面一个1B参数的小模型练到初步可用的状态大约需要50-100B token。这个数字听起来很大但经过清洗去重后实际可用数据量会少很多所以要提前做好数据配额规划。3.2 清洗链路从原始文本到训练样本的六个步骤我搭的清洗管线大致分六步编码修正与格式标准化去掉不可见字符、统一换行符、修正乱码。语种识别与过滤只保留目标语种用开源语种识别模型跑一遍。精确去重与模糊去重精确去重用hash模糊去重用MinHash这一步对训练质量影响极大。实验数据表明不做去重时模型会频繁复述网络上本身就很流行的长文本片段而不是真正理解内容。垃圾信息过滤按规则过滤广告、弹窗、纯符号、渲染模板等噪声。质量评分过滤训练一个小型质量分类器按质量高低排序后截断尾部。内容安全过滤涉及违法、暴力、仇恨等内容的样本直接删除。每一步都要做“清洗前后对比抽样”否则你不知道自己到底过滤掉了什么有没有误删有价值的数据。我遇到过一种情况规则过滤把代码中所有含和符号的文本当成HTML标签删了结果训练语料里的代码能力大幅下降。后来加了白名单和转义处理才解决。这类经验文档一般不会写但踩过一次就记住了。3.3 构造训练样本格式的实操细节训练样本的构造不是简单地把原文塞给模型而要考虑你训练的tokenizer如何分词、上下文窗口如何利用。以指令微调为例我常用的格式模板是|system|你是一个有帮助的AI助手。/s |user|请解释什么是梯度下降。/s |assistant|梯度下降是一种通过迭代更新参数来最小化损失函数的优化算法。/s这个模板里的特殊分隔符必须和tokenizer里的special token保持一致。很多新手在这里翻车训练时用了特殊token但加载模型推理时忘了添加导致生成效果和训练时不一致。上下文窗口分配也是坑。我建议按比例控制系统提示占不超过5%用户指令占20%左右回答占70%以上。如果数据集里大量样本都是“极长问题极短回答”模型会学到一个坏习惯——回答越来越敷衍。我当时做了一个自动化统计每个样本的“问题长度/回答长度”比例分布超过1.5的比例超过一定阈值就要警觉了。4. 模型架构与训练管线跑通第一个稳定基线数据扎实了才轮到模型和训练。这一块我的建议是“能不造轮子就不造但必须看懂轮子怎么转”。从零构建AI工程不等于从零逐行实现Transformer。你要做的是在成熟框架之上快速搭建可控的训练闭环。4.1 选型决策微调开源模型还是从头预训练对绝大多数项目我的建议是先微调开源模型而不是从头预训练。从头预训练一个LLM需要的高质量token量、算力和工程复杂度个人或小团队很难承受。但一些小参数模型1B左右从头预训练用几张A100还是可行的也能让你对预训练有真正的体感。我自己实际做过的方案是先用1B规模做内部实验验证数据管线、训练脚本和评估流程跑通再决定是否需要上更大的模型。这个策略帮我用很低的成本积累了预训练经验。等真的上了7B模型时团队对训练崩溃、loss爆炸这些常见问题已经有了应对预案不至于手足无措。4.2 核心训练循环不只是“model.fit()”用HuggingFace生态时我习惯直接用Trainer类先跑通但不会从这里毕业。因为很多复杂的调试需求比如混合精度缩放、梯度累积、自适应batch size需要你真正理解训练循环里每一步在干什么。一份最基础的自定义训练循环骨架from transformers import AutoModelForCausalLM, AutoTokenizer, get_cosine_schedule_with_warmup import torch model AutoModelForCausalLM.from_pretrained(模型路径) tokenizer AutoTokenizer.from_pretrained(模型路径) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, betas(0.9, 0.95)) scheduler get_cosine_schedule_with_warmup(optimizer, num_warmup_steps500, num_training_steps10000) # 梯度累积示例当显存不够时每2步更新一次 accumulation_steps 2 for step, batch in enumerate(dataloader): outputs model(**batch) loss outputs.loss / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() scheduler.step() optimizer.zero_grad()注意这里一个容易忽略的细节loss outputs.loss / accumulation_steps。如果不除梯度累积N步后等效batch size变成N倍但学习率没变模型会直接发散。别小看这一行我见过不止一个同事在这里困惑为什么加了梯度累积后损失变成NaN。4.3 损失不降时的系统化排查路径跑训练最常遇到的就是loss不降或者反复横跳。我的排查顺序是先看数据加载对不对用单个batch打印一次手工对比样本内容和tokenzier解码结果。上次出现训练loss持续偏高最后发现是数据格式和tokenizer的chat模板不匹配特殊token全变成了未知token。再看batch size和学习率是否匹配batch size从8改成32时学习率也应该相应放大。一个经验值是线性缩放规则lr_new lr_old * sqrt(batch_new / batch_old)实测比等比例缩放更稳定。然后检查梯度打印梯度的范数如果某个位置突然变成NaN大概率是输入数据里出现了极端长文本或异常值。最后怀疑优化器状态Adam的epsilon默认值是1e-8在混合精度训练时容易导致数值不稳定改成1e-6或1e-5能解决一部分NaN问题。另外训练初期loss快速下降很多人会很兴奋但这是常见假象模型在做“语言模型最基础的词频预测”还没进入真正的语义理解。要认真判断训练是否健康应该多加几个辅助指标困惑度、每类样本的loss、生成样本的困惑度主观评测。5. 评估体系基准分数之外如何建立工程信心模型训练完毕后评估是整个工程最容易被糊弄过去、但影响最深远的一环。如果你用的评估标准不合理后面一切优化方向都可能是错的。5.1 公开基准的陷阱做题能力强不代表交付可用很多人喜欢用公开榜单的分数来证明模型好坏但实际部署时很快会发现分数和体感完全对不上。原因主要有三个第一公开基准可能存在数据泄漏模型训练语料里可能已经包含了测试题或相似表述。我就亲眼见过一个模型在榜上分数很高但在我们自建的行业评测集上连基本业务场景都答不明白。第二公开基准和你的真实场景分布不一致。通用问答做得好不代表能正确处理你产品的输入分布。第三基准本身是静态的模型迭代后可能出现“刷题”效应——模板相似时表现好换个问法就崩。所以我的原则是公开基准只作为参考必须搭配自建评估集。5.2 构建自建评估集的方法我搭自建评估集时会分三类回归测试集从历史badcase里沉淀至少100条。每轮迭代后都跑一遍防止修了一个问题引出三个新问题。领域能力测试集针对真实业务场景设计比如你做一个法律问答助手就需要准备纠纷、条款解释、文书写作等各类问题各几十条。鲁棒性测试集测试模型的抗干扰能力包括错别字、口语化表达、长问题、多轮上下文等。评估脚本我会用统一接口封装让所有模型版本都能自动过同一套测试。这个工程投入非常值得因为当模型从7B换成13B时如果你的评估方式没变你是分不清效果提升到底来自模型规模还是来自新数据清洗带来的噪声。5.3 指标到可用性的最后一公里指标只告诉你模型“平均水平”如何但真实系统的体验往往被“长尾badcase”和“极端输入”支配。所以光看平均指标还不够还要统计阴性指标回答为空的比例。重复生成的比例常见于训练不足的小模型。出现乱码、特殊符号的比例。回答不完整生成被打断的比例。非法输入的处理比例比如用户问完全不相关的问题时是礼貌拒绝还是胡编。这些指标不需要很复杂统计出来你就知道模型距离真正上线还差多远。我个人遇到过最夸张的情况是一个模型准确率比上一版高了5个百分点但“空回答率”从0.5%飙到5%。如果只看准确率这版差点被直接发布。加长尾统计后才发现问题出在某个prompt模板变体导致系统指令被误识别成了用户输入。6. 部署与运维模型跑起来只是开始真正的工作是让它稳定跑着训练结束、评估通过很多人觉得大功告成。实际上部署阶段才是AI工程和普通算法Demo分道扬镳的地方。一个模型在推理环境里如何扛住并发、如何监测效果下降、如何安全更新版本这些话题在学术论文里不会出现但在生产环境里天天都在发生。6.1 推理服务的资源估算与吞吐优化我从零搭推理服务时最早踩的坑是以为“模型能加载就能服务”。但实际上推理服务需要面对的并发压力会让显存翻倍甚至更多。以7B模型bf16推理为例单请求峰值需要的显存约14GB权重 几GB KV cache。并发不大时单张24GB的卡可以承载但当并发上来KV cache的占用会迅速增长需要限制最大并发数否则会OOM崩溃。推荐的实践是使用支持continuous batching的推理框架比如vLLM它能把不同请求的生成阶段拼在一起大幅提升吞吐。我实测中同样的单卡从朴素HuggingFace pipeline到vLLM吞吐能提升好几倍这对小团队来说是最划算的优化。部署还需要关注一个容易被忽略的参数max_tokens。如果不对生成长度做上限和合理提示一个用户发个“你好”模型可能滔滔不绝输出几千字直接把服务带宽和显存全部耗尽。我在生产配置里默认max_new_tokens512只有少数特定场景才放宽。6.2 监控指标不要只盯延迟和错误率模型上线后监控不能只依赖传统的“服务存活延迟错误率”。AI系统最大的风险是“服务正常但效果已经崩了”。我会额外监控三类指标输入与输出的长度分布如果用户的prompt平均长度突然变长可能说明用户使用模式在变化如果模型输出长度骤减可能是推理参数被误改或行为异常。用户显式反馈比如点赞、点踩、举报的比例。这是最直接的信号哪怕只能覆盖一小部分流量也足够发现趋势问题。输出隐式质量信号对输出文本做困惑度计算、嵌入向量距离检测看它和正常输出分布是否发生漂移。这是一种无需人工也能持续运转的粗糙质量监测。漂移检测的做法不难定期对线上输入输出做embedding然后和历史分布计算距离距离超过阈值就告警。但要注意在某些合法场景下比如产品促销导致话术变化输入分布自然会发生偏移这时要人工区分是“业务变化”还是“模型异常”避免狼来了。6.3 模型更新的灰度策略模型更新比代码更新危险得多代码的逻辑是可预期的模型的行为是概率性的尤其在大语言模型场景下同一个prompt在换了一个模型版本后它的回答风格可能天翻地覆。我目前采用的策略分三步影子部署、小流量灰度、全量发布。影子部署是指新老模型同时接收线上请求但只有老模型的输出返回给用户新模型的输出只记录下来做离线比较。这一步成本低、风险为零却能帮你积累“同一批真实请求下新旧版行为差异”的证据。等差异可控后切5%的流量做灰度用户反馈和监控指标稳定24小时后再逐步扩大。一旦发现异常立刻切回老版本。这个方法并不复杂但它的价值在于让“模型迭代”从一个研发岗位的事变成了一个工程闭环的事。没有这层机制你永远不敢放心升级模型有了它升级模型变成了一件有流程保护的操作。7. 踩坑实录三个最容易被忽视的工程细节最后分享三个在真正从零搭建AI系统时让我付出过代价的细节。它们看起来都很小但每一个都影响过整个项目周期。7.1 随机种子复现性工程的起点很多训练脚本里写了seed 42但并没有真正实现复现。原因在PyTorch的随机源不只是torch.manual_seed还有numpy的随机源、Python的random、CUDA的随机源、cuDNN的算法选择。一个完整的固定种子代码应该是import random, numpy as np, torch def set_seed(seed: int 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False我见过一个项目把随机种子到Dataloader的shuffleTrue也做了“不固定”设置导致每轮迭代的数据顺序不同说是“让训练更随机”。结果就是同一个脚本在同样的环境里跑两次loss曲线差异巨大根本无法判断是改了一行代码带来的效果还是随机波动。后来把所有能固定到的地方全固定住实验效率直接翻倍。以后所有实验记录里种子和超参一样都要被记录在案。7.2 混合精度训练中的loss缩放与梯度溢出混合精度训练AMP能显著提速但第一次用时会频繁遇到loss突然变成NaN或inf。原因是fp16下数值范围有限loss非常大时梯度在反向传播过程中容易溢出。解决办法往往不是调模型而是调整loss缩放策略torch.cuda.amp.GradScaler的init_scale调低或者开启growth_interval动态调整。另外有人说fp16不行就换bf16bf16确实比fp16稳得多因为它保留了和fp32一样的指数范围但代价是精度略低。实测时我用bf16训练7B模型没有出现任何数值不稳定问题fp16则偶尔要调整。如果你不幸遇到loss爆掉最好先保存一份“爆炸前的checkpoint”来分析而不是直接重训。那一刻的梯度统计往往能帮你定位是哪一层出了问题比如某个embedding层梯度异常大。只重训练而不分析下一次会在同一个位置再炸一次。7.3 DataLoader的并行瓶颈GPU在等数据很多小团队用单机单卡训练以为瓶颈在GPU算力但其实经常卡在DataLoader的数据准备上。GPU计算快数据加载慢导致GPU利用率只有30%甚至更低。关注GPU利用率应该成为训练日常低于80%就要找原因。常见原因和解法num_workers0数据在GPU主线程里加载GPU全程等待。解法把num_workers调到CPU核心数的2到4倍。做了大量实时数据增强训练时CPU算不过来。解法将部分增强逻辑放到数据预处理阶段完成离线算好直接喂给训练。磁盘IO本身慢文件在机械硬盘上反复读。解法把整个数据集加载到内存里前提是数据量不大或者换NVMe SSD。使用了复杂的自定义collate_fn每个batch都要做动态padding和排序。复杂逻辑最好提前缓存。这些问题的排查方式很直观在训练循环里打印dataloader一个batch的耗时如果明显高于GPU算一个batch的耗时瓶颈就在数据管线。按上面四条逐个排查基本能解决95%的GPU空转问题。我自己实际训练中会定期记录“GPU利用率、数据加载耗时、每step耗时”三个数字一旦某天GPU利用率突然掉下来先怀疑同事是不是在同一个机器上跑了别的任务再看是不是数据管线出了问题。AI工程里保持对数字的敏感度比掌握再多理论都实用。最后说一个我个人的习惯每次新项目启动或者带新人的时候我都会逼着大家把“从零开始”这件事拆成“从数据、从环境、从评估、从部署”四个方向去盘点而不是只说一句“我要训练一个模型”。AI工程真正的门槛从来不在于你想要什么模型而在于你有没有一套系统能把一个想法稳定地变成线上真实可用的能力。这个系统从零开始建的时候前面的每一步都决定后面能不能少还债。