
很多人在看到“ai-engineering-from-scratch”这个标题时第一反应都是这不就是又一个AI入门教程吗其实真不是。这个标题真正想表达的不是怎么调库、怎么训练一个模型而是怎么从零开始把AI做成一个能落地、能维护、能迭代的工程系统。我在过去的项目里见过太多类似的场景模型在Notebook里跑得好好的准确率也不低但一旦要部署上线问题像连环雷一样炸开——环境依赖冲突、数据管线断裂、推理延迟超标、模型版本回滚困难。这些问题的根源不在算法而在工程化。这篇文章要聊的就是一条从零开始建立AI工程能力的完整路径包括基础设施搭建、数据管线设计、训练流程规范化、模型服务化部署以及整套过程中的排查技巧。适合那些已经有Python基础、跑过几个开源模型但还没真正把一个AI项目做成可靠系统的开发者。1. AI工程到底在做什么为什么这么强调“从零开始”1.1 先搞清AI工程和“炼丹”的区别AI工程AI Engineering这个概念这两年提得特别多但你把它拆开看本质上是两件事的合流算法工程化和系统工程化。算法工程化解决的是“模型怎么训练、怎么评估、怎么调优”的问题这部分和机器学习工程师的工作重叠度很高。系统工程化解决的则是“模型怎么被稳定地调用、怎么和业务系统集成、怎么长期运行不崩”的问题这更像传统后端工程师的领地。很多人只做了前半段就觉得自己在做AI工程然后到了真实项目里被后半段反复毒打。我见过有人把训练好的模型直接打包成一个脚本放在服务器上跑结果每次调用都要重新加载一次模型权重一次推理要等十几秒根本没法用。这种问题不是模型不行是工程链路没有设计。所以“ai-engineering-from-scratch”强调的“从零开始”不是从线性代数开始学而是指把你已有的AI知识重新放进工程化的框架里从环境管理、项目结构、数据管线、实验追踪、模型部署这些地基逐层搭起来。1.2 工程化要解决的三个核心矛盾我自己的经验是AI工程从零开始搭建核心要解决三个矛盾。第一个是开发环境和生产环境不一致。你在本地用Python 3.10、CUDA 12.1、PyTorch 2.1训练好好的到了服务器上发现系统自带的Python是3.8驱动版本也对不上模型直接跑不起来。工程化的第一课就是把这个不确定性干掉。第二个是实验不可复现。做AI项目尤其是做模型调优的时候每天都在跑实验。如果连哪天改了哪个参数、用了哪份数据、跑出来的指标是多少都没记录那调优就是瞎折腾。很多人自定义文件名保存模型比如model_v2_final_really_final.pth这种习惯放到工程里就是灾难。第三个是模型和业务的衔接断层。模型训练出来只是一个静态产物要接入业务系统就需要标准化的接口、合理的错误处理、性能监控和版本管理。这些工作在学术界没人管但在工业界是逃不掉的。从零开始做AI工程本质就是把上面三个矛盾一个一个解掉每一步都有明确的目标和验证方式。2. 从零搭建AI工程的基建层2.1 Python环境与依赖管理别再全局乱装包很多初学者习惯直接在服务器上pip install装到全局环境里一开始项目少没什么感觉项目多了以后各种依赖冲突会让人崩溃。我接手过的项目里有因为numpy版本不一致导致数据形状对不上的有因为OpenSSL版本问题导致requests请求报错的排查起来非常费时间。我现在的建议是从项目第一天起就用虚拟环境管理依赖并且严格锁定版本。工具有几个选择venv、conda、poetry、uv各有优势。venv是Python自带的方案轻量但管不住Python解释器版本conda擅长管理Python版本和CUDA相关依赖做AI项目用得很顺手poetry/uv更偏工程化支持pyproject.toml统一管理依赖、构建和发布。我的习惯是本地开发用conda建环境配合pip freeze导出requirements.txt到了部署阶段再用Docker把整个环境打包成镜像。Docker不光是解决环境一致性的最强手段还能在出问题的时候秒级回滚到上一个版本。这里有一个非常重要的实操细节不要偷懒用pip install torch直接装CPU版。PyTorch的官方安装命令是分平台的你需要根据本机CUDA版本选择对应的安装方式。我遇到过不止一次有人因为装成了CPU版训练速度慢了几十倍还以为是代码写得有问题。举个例子Conda环境创建一个完整AI项目的标准动作是这样conda create -n ai-eng python3.10 -y conda activate ai-eng conda install cudatoolkit11.8 -c nvidia pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118装完之后别急着跑模型先验证CUDA状态import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))看到设备名称和显存信息输出才算环境真的没问题。没做这个验证就往下走属于给自己埋坑。2.2 实验追踪与代码结构设计环境解决了接下来是项目结构。我见过太多AI项目的目录长这样一个主目录下堆着十几个没注释的.ipynb还有一堆名字叫“最终版”“最终版2”的脚本。这种项目的维护成本高到离谱。这里分享一个我自己实践过比较顺手的AI项目结构ai-engineering-from-scratch/ ├── configs/ # 配置文件模型参数、训练超参都放这里 ├── data/ # 原始数据和处理后数据 │ ├── raw/ │ └── processed/ ├── src/ │ ├── data/ # 数据加载和预处理代码 │ ├── models/ # 模型定义 │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估入口 │ └── serve.py # 推理服务入口 ├── experiments/ # 每个实验单独一个文件夹记录参数和指标 ├── models_saved/ # 训练产出的模型权重 ├── notebooks/ # 用于探索性分析的notebook ├── requirements.txt └── README.md把notebook和正式代码分开是我特别坚持的。notebook适合做数据探索和模型原型验证但是不适合承载训练逻辑。模型训练一旦需要反复运行、修改和对比脚本加配置文件的方式远比notebook可靠。实验追踪方面我喜欢把每一个实验的关键信息自动记录到一个统一的表格里。最轻量的方案是自己写一个记录函数把时间、数据版本、模型参数、关键指标追加到CSV里。项目再大一点可以用MLflow或者Weights Biases。这些工具能自动记录超参数、loss曲线和模型产物省心不少。但不要犯另一个错误工具装的越多、流程越复杂人就越不愿意记录。哪怕只用CSV坚持记也比工具齐全而不记录强得多。3. 核心闭环实操从数据到模型服务3.1 数据管线样本质量比模型参数更决定上限很多人的关注点全部放在模型结构上把数据管线当成跑龙套的这是一个认知误区。实际做项目你会发现模型决定了效果的上限数据决定了能不能接近这个上限。一个合格的数据管线应该解决几个问题数据来源清晰、预处理可复现、训练验证不交叉泄漏。数据来源清晰意味着你知道每一份数据是从哪来的、什么时间采集的、清洗过哪些规则。一个记忆深刻的坑项目上线后模型效果在线上明显不如离线测试后来一查发现离线训练数据里混入了大量数据的重复样本模型相当于“背答案”了。预处理可复现意味着同样的代码跑同样的输入一定产出同样的输出。这里我强烈建议任何洗数据的脚本都别在notebook里改一遍执行一遍而是写成带固定随机种子和参数配置的.py脚本。训练验证不交叉泄漏更关键。特别是做时间序列类任务时如果用未来的数据来预测过去指标会虚高得很离谱。即使不是时间序列也要在数据划分阶段留出独立测试集并且划分方式保持固定。数据泄漏是AI项目里最隐蔽、最致命的问题之一。我建议的实操方式第一步把原始数据落盘保存一份不轻易改动第二步写独立的预处理脚本产出processed数据第三步在训练脚本里固定random seed设定数据划分策略的版本号。这样训练脚本每次读到的数据都是稳定一致的排查问题时也好定位。3.2 训练脚本设计可复现比“跑得通”重要得多训练脚本是整个AI工程的核心环节。很多人写的训练脚本能跑但跑第二次结果就不一样——换了一台机器结果又不一样。这会让后续所有优化工作失去参照系。要让训练可复现必须管住三个随机源框架的随机种子、数据加载的随机顺序、以及模型初始化的随机性。代码上可以这样处理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固定随机种子只是第一步。工程上我会把实验参数统一放进一个配置文件比如configs/experiment_001.yaml然后用命令行参数指定加载哪份配置data: raw_path: data/raw/user_behavior.json processed_path: data/processed/user_behavior_20240601.parquet train_ratio: 0.8 random_seed: 42 model: name: resnet18 pretrained: true dropout: 0.3 training: batch_size: 64 learning_rate: 0.001 epochs: 50 optimizer: adamw weight_decay: 0.01 scheduler: cosine训练脚本里再通过加载配置来启动。这样不管谁拿到这份代码只要配置一致跑出来的结果就是一致的。这一步做扎实后面调参才有意义。训练过程中还要注意模型的保存策略。我强烈建议每个epoch结束都保存一个checkpoint而不是只在最后保存一个。同时把“验证集上最优”的模型单独复制一份命名为best_model.pth。这样如果训练过程出问题——比如第40个epoch开始严重过拟合——你可以回退到验证分数最高的那个版本不会因为一次糟糕的后半程训练前功尽弃。3.3 模型服务化本地跑通不等于能上线训练出模型权重只是完成了前一半工作。模型最终要被人或者系统使用服务化交付是绕不开的环节。最忌讳的做法把训练脚本改一改直接写个循环接收请求。这里面问题很多——并发处理能力差、异常处理缺失、没有接口约束调试起来还特别麻烦。我个人比较推荐的做法用FastAPI搭一个标准的模型推理服务把模型加载、推理、结果返回封装成清晰的接口。一个最小可用的推理服务长这样from fastapi import FastAPI, HTTPException from pydantic import BaseModel import torch from src.models.model import Net app FastAPI() class PredictRequest(BaseModel): features: list[float] model None device torch.device(cuda if torch.cuda.is_available() else cpu) app.on_event(startup) def load_model(): global model model Net() state_dict torch.load(models_saved/best_model.pth, map_locationdevice) model.load_state_dict(state_dict) model.to(device) model.eval() app.post(/predict) def predict(req: PredictRequest): if model is None: raise HTTPException(status_code503, detailModel not loaded) try: tensor torch.tensor(req.features, devicedevice) with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim-1) return {probabilities: prob.tolist()} except Exception as e: raise HTTPException(status_code500, detailstr(e))这里面有几个细节值得注意。第一模型加载放在startup事件里而不是每次请求进来时加载。模型权重动辄几百MB甚至上GB每次请求都加载一次性能会崩到没法用。第二用pydantic定义了请求体的结构这相当于接口的契约传参不规范时框架会直接返回400错误比自己在代码里手写各种类型判断干净得多。第三predict函数里用了torch.no_grad()推理阶段不需要计算梯度关掉它可以省掉大量内存和计算开销。第四对输入异常做了捕获不会因为某个脏数据就把整个服务进程搞挂。服务启动后可以用一段请求来验证curl -X POST http://localhost:8000/predict \ -H Content-Type: application/json \ -d {features: [0.1, 0.5, 0.2, 1.2, -0.4, 0.9]}看到类似{probabilities: [[0.01, 0.89, 0.10]]}这样的返回体就说明服务链路已经通了。4. 常见问题与排查技巧实录4.1 环境类问题速查做AI工程环境类问题出现的频率最高。我整理了一个速查表都是我自己或带的人踩过的问题现象可能原因解决思路torch.cuda.is_available()返回FalseCUDA驱动和PyTorch版本不匹配执行nvidia-smi查看驱动支持的CUDA版本选择和PyTorch匹配的cudatoolkit版本训练时显存不足CUDA OOMbatch_size过大或模型过大先调小batch_size确认可运行后逐步增大检查是否有变量泄漏到GPU显存同一个脚本两次运行结果不一致缺少随机种子固定或DataLoader的shuffle顺序变化设置固定随机种子并开启DataLoader的worker固定机制本地正常服务器上读取中文路径失败编解码环境和本地不一致在代码中统一用pathlib处理路径并在容器内设置UTF-8编码环境问题排查有一条铁律先确认最简单的问题再往复杂方向排查。很多人一上来就怀疑代码逻辑结果最后发现是环境变量没配好。这种时间浪费完全可以避免。4.2 训练与推理环节的典型坑训练环节最常见的坑是过拟合和欠拟合的误判。很多人看到训练集loss下降了、验证集loss不降反升第一反应就是调模型结构其实先要做的是检查数据泄漏和数据划分方式。有一次我们的验证集loss始终压不下来排查很久后发现是预处理阶段把全局数值归一化的参数用在了验证集上导致分布偏移。改成只在训练集上拟合归一化参数后问题立刻消失。推理环节最常见的问题则是性能瓶颈和精度偏差。性能瓶颈多发在不该用GPU的地方用了GPU或者在CPU上运行了未优化的模型。如果面向CPU部署建议先用torch.compile或ONNX导出做推理优化再考虑并发方案。精度偏差则要注意训练阶段如果做了数据增强比如随机裁剪、随机翻转推理阶段务必要关闭这些操作否则预测结果会莫名抖动。这一点新手极易踩中。4.3 模型版本管理模型本身也是需要做版本管理的。我用过最简单好用的方案训练完后在models_saved目录下保存模型的同时把对应的配置文件、数据版本号、训练日志一并拷贝到一个子目录里命名格式统一比如experiment_012_lr1e-3_resnet18。这样不管过多久拿到这个目录就能完整复现当时的实验。工具上如果小团队做轻量管理用DVC管理数据和模型文件、配合Git做代码版本管理就足够。不要为了追新而上太重的工具能用得久、用得顺比大而全更实际。5. 从零开始的路线图和个人体会5.1 一条可落地的时间线如果你现在完全是从零开始我建议按这个节奏推进第一周把环境基建搭好。建conda环境、安装PyTorch并验证CUDA可用、搭建Dockerfile、初始化工程目录结构。这一周不写任何模型代码只把工程骨架立起来。第二周选择一个你熟悉的公开数据集完成数据处理管线的编写。固定数据划分方式、预处理逻辑、以及保存processed数据。第三周到第四周跑通第一个训练闭环。从最简单的模型结构开始完成train.py和evaluate.py并确保实验可复现。这个阶段不必追求效果追求流程顺畅。第五周把模型服务化。用FastAPI封装推理接口编写一个调用测试脚本验证线上和离线表现的一致性。这个时间线是我实际带人走过多次的节奏。特别要强调的是不要跳过第一周的环境基建那部分虽然枯燥但值回所有时间。5.2 在工程化过程中哪些能力比调参更重要这个事儿我特别有感触。刚开始做AI项目的时候我觉得“会调模型”“懂最新网络结构”最重要做了几年回头看真正影响项目成败的往往是工程能力。数据工程能力排在第一位。不管是数据清洗、数据标注流程设计还是训练集和验证集的划分这些决定模型学到的是什么。我们项目里的一个深刻教训为了提升模型在困难样本上的效果直接把这些困难样本重复了10遍加入训练集结果模型在验证集上过拟合严重上线后完全不行。后来才明白采样策略、数据增强也应该当成代码逻辑来管理。第二位是对模型评估的理解。很多人只看准确率却忽略了精确率和召回率在不同业务场景里的权重差异。我做一个内容审核项目时宁可牺牲一点精确率也要把召回率拉高因为漏掉违规内容比误伤正常内容代价高得多。评估指标的设计必须在训练之前就和业务方对清楚。第三位才是模型结构选型和超参调优。现在有很多AutoML工具和成熟的预训练模型可以复用这个环节的门槛一直在降低。5.3 长期项目维护的一些心得项目上线只是开始后续的长期运维才是考验。模型会随着数据分布的变化而效果衰减监控不能只盯着服务器的CPU和内存更要盯模型输出的分布漂移。我现在做线上AI服务一定会记录每一次请求的输入特征摘要和输出结果摘要用来定期和训练集的分布做对比。一旦发现输入分布的偏移达到某个阈值就自动触发告警提醒需要补充新数据重新训练。这套东西不需要特别复杂的系统用简单的统计就行但它能帮你在问题发生之前就发现苗头。另外不同业务的模型最好不要塞在一个服务里。模型个数越多、版本越杂出问题时的爆炸半径就越大。我见过一个团队把十几个模型放在一个服务进程里结果某次更新了一个模型的权重服务内存直接爆掉所有业务同时受影响。合理的做法是核心模型单独部署独立版本、独立扩缩容。从零开始做AI工程其实不是一个学习路线问题而是一个思维转变问题。它要求你把对模型的关注扩展到数据、部署、监控、迭代的整条链路。经历几次线上事故、踩过几个坑之后你会越来越认同这个判断真正拉开AI项目差距的不是模型的层数而是工程化的深度。