如果你看到一个叫ai-engineering-from-scratch的仓库大概能猜到作者想表达什么——把AI工程从零开始完整地搭一遍。我最近就在做这件事边做边把经验整理成了这个项目。这篇博文算是项目的中期总结也是我踩了不少坑之后的实战笔记。内容既适合打算转行AI工程、之前只跑过notebook的朋友也适合已经在做算法、但想搞清楚“模型上线之后到底发生了什么”的人。我会把AI工程拆成数据、训练、部署、运维四段来讲每一段都会给出我能直接复用的思路和代码片段以及那些文档里不会写、只有亲手做过才知道的细节。1. 先搞清楚“AI工程”和“From Scratch”到底是什么1.1 AI工程不是炼丹是一条完整的流水线很多人对AI工程的理解是“训练一个模型”这是最大的误区。训练模型只是中间的一个环节甚至不是最耗时的环节。一个真正能跑起来的AI项目至少包含数据采集与清洗、特征构建、实验管理、模型训练、评估验证、服务化部署、线上监控、模型更新这些环节。任何一个环节断了模型都落不了地。我见过太多这样的情况算法同学在notebook里把准确率调到了97%兴冲冲地要把模型交给工程团队结果发现训练脚本依赖一堆全局变量、数据路径写死了、模型文件不知道对应哪份数据集部署时需要重写整个预处理逻辑。这就像你做了一道菜但菜谱只有你自己看得懂别人连锅都找不到。AI工程要做的就是把“实验室里能跑的代码”变成“别人也能维护、线上也能稳定跑的系统”。1.2 From Scratch不是重造轮子是不当黑盒调包侠提到from scratch有一类人会走进另一个极端什么都要自己手写连梯度反向传播都要从头实现一遍。我尊重这种学习方式但工程上这么做既不经济也不必要。PyTorch、FastAPI、PostgreSQL这些成熟的工具本身就是经过大规模验证的“轮子”你不需要重造它们但你需要知道它们是怎么工作的、在什么情况下会出问题、怎么把它们的性能发挥出来。在我看来ai-engineering-from-scratch的真正含义是不把任何一个环节当黑盒。你用PyTorch训练没问题但你要清楚DataLoader是怎么把数据喂给模型的你用FastAPI做服务没问题但你要知道模型是怎么加载进内存的、为什么每次请求不能重新load一次模型你用Docker部署没问题但你要明白镜像层的缓存机制会给迭代带来什么影响。那么哪些东西值得自己动手写我的标准是核心业务逻辑和评估逻辑必须自己掌控。数据清洗规则、特征构造逻辑、评估指标的计算这些决定了模型的上限和可信度不能拿现成函数一套就完事实验记录、模型版本管理、前后处理代码这是工程化的骨架必须自己搭建。而训练框架、推理引擎、部署基础设施能用成熟的就直接用省下来的时间拿去调模型比什么都值。2. 从零搭项目技术栈、目录与数据管理2.1 技术栈选型先问自己一个问题再选工具选技术栈之前先问自己一个问题这个项目是个人练手项目、团队内部工具还是面向外部用户的高并发服务三种场景的答案完全不同。个人项目可以把所有东西塞进一台服务器用Python全栈搞定团队内部工具要考虑可维护性和交接成本高并发服务则要认真考虑服务化拆分和性能优化。我给ai-engineering-from-scratch定的最小技术栈是Python 3.11作为主语言PyTorch做模型训练FastAPI做推理服务PostgreSQL存业务数据Redis做缓存如果需要的话Docker做部署。这个组合的好处是每一层都是当前生态里社区最活跃、踩坑资料最多的选择遇到问题搜一下基本都有答案。不要一上来就上Kubernetes、Spark、Kafka这些重型武器新手阶段它们带来的复杂度远大于收益。再说说为什么选FastAPI而不是Flask或Django。FastAPI原生支持异步、自带接口文档、基于Pydantic做请求校验写出的代码结构天然清晰。做模型推理服务时绝大多数操作其实是I/O瓶颈——接收请求、调用模型、返回结果异步支持能显著提升并发能力。Flask虽然简单但很多“高级功能”需要自己拼插件Django又太重了适合做业务系统不适合做轻量推理网关。2.2 目录结构用一套能“长出来”的组织方式项目一开始就要把目录结构定好否则写到第300行代码就开始乱了。我现在的推荐结构长这样ai-engineering-from-scratch/ ├── configs/ # 训练、推理的配置文件 │ ├── train_config.yaml │ └── serving_config.yaml ├── data/ │ ├── raw/ # 原始数据只读不写 │ ├── processed/ # 清洗后的数据 │ └── versions/ # 数据版本快照 ├── src/ │ ├── data/ # 数据加载与预处理 │ ├── features/ # 特征构建 │ ├── models/ # 模型定义与训练逻辑 │ ├── serving/ # 推理服务 │ └── utils/ # 日志、通用工具 ├── tests/ # 单元测试与冒烟测试 ├── scripts/ # 一键训练、一键部署脚本 ├── notebooks/ # 探索性分析禁止放生产代码 └── docker/ # Dockerfile 与编排文件这个结构的核心思路是数据、代码、配置三者分离。数据放在数据目录配置放进configs代码按职责分包。每次实验只需要改配置文件不需要改代码数据变更用版本号管理模型训练时记录用哪个版本的数据才能做到实验可复现。有一点要强调notebook只用来做探索性分析和可视化永远不要把生产逻辑写进notebook。我见过有人把预处理逻辑写在notebook里训练脚本直接读取notebook导出的文件一旦单元格执行顺序变了结果就完全不可复现。正确的做法是把所有核心逻辑沉淀到src/下的Python模块里notebook只是调用它们。2.3 数据管理模型效果变差的头号元凶是“数据变了”数据是AI工程的隐形地基。很多时候模型效果变差不是模型代码出了问题而是数据悄悄变了上游系统改了字段含义、样本分布发生漂移、标注标准换了。如果数据没有版本管理你将面临一个经典的难题——“这个模型是用哪份数据训出来的”我的做法很简单每一份进入训练流程的数据集都打上版本标签。小项目不需要上DVC这种重量级工具最简单的方案是原始数据文件以train_v1.csv、train_v2.csv这种方式命名不覆盖、不删除训练配置里写明数据版本号同时记录一下数据文件的md5校验值。这样出现任何诡异问题第一反应就是去对比两个版本的数据差异八成能快速定位。数据泄漏是另一个必须从源头防住的坑。举一个我真实遇到过的例子做一个用户流失预测模型离线AUC高达0.98上线后却完全不能用。排查之后发现训练集和测试集的拆分没有按用户ID去重同一个用户的不同时间记录被分到了两边模型等于见过“将来”的信息。另一个经典泄漏是特征里包含了目标信息本身比如给“是否取消订阅”建模特征里竟然有“当月已申请退订”这种本末倒置的字段。建立数据管线时必须把“按实体拆分”“特征时间窗口限定在预测时刻之前”这些规则当成硬约束写进代码里。3. 训练阶段先跑通闭环再谈优化3.1 第一个里程碑不是“高精度”而是“完整跑通”我在这个项目上最深刻的一个经验是先别急着调精度先把整条链路跑通。哪怕拿10%的数据、只训练1个epoch、用一个最简单的线性模型也要让数据从原始文件走到接口返回预测结果。这个最小闭环一旦打通你就有了一条“基线轨道”后续所有优化都是在轨道上迭代而不是每次都在不同地方翻车。跑通闭环之后立刻建立基线模型。基线模型可以很蠢甚至可以直接用逻辑回归或者简单统计规则。它的价值不在于效果多好而在于给你一个参照系后续换了复杂模型提升到底有多少很多项目做到最后发现复杂模型只比baseline高1个百分点却带来了成倍的部署和运维成本这时候你需要一个清醒的决策依据。3.2 实验管理好记性不如烂笔头机器也一样训练过程中最忌讳的事情是同一个模型跑了好几个版本最后分不清哪个效果最好、用了什么参数、基于哪份数据。没有实验记录的AI项目回头的每一步都是“重现不了的玄学”。我带这个项目的习惯是每次实验至少记录以下几项实验编号、模型结构、超参数配置、数据版本、代码分支或commit号、评估指标、模型文件保存路径、备注。实验编号模型学习率数据版本准确率F1备注exp-001LogisticRegression0.01v10.910.87基线exp-002XGBoost0.01v10.950.91特征标准化后exp-003XGBoost0.02v20.940.90数据版本升级指标反而降如果你愿意折腾可以用MLflow写一次代码就自动记录这些信息。但即使只用Excel记录也比什么都不记强得多。实验记录的核心是“可复现”别人拿着你的记录能跑出同一份结果这个项目才真正算工程化。3.3 超参数与随机种子为什么你的结果总是“忽高忽低”调参是训练阶段最容易浪费时间的环节。我的建议是分阶段粗调和细调第一步用大跨度网格覆盖关键参数比如学习率直接试0.1、0.01、0.001找到大致方向第二步在表现最好的区域附近做小范围搜索。不要一上来就上贝叶斯优化等高级算法先理解每个参数对结果的影响方向高级工具用起来才有意义。另一个必踩的坑是随机种子。深度学习训练中数据加载的随机打乱、模型参数的随机初始化、dropout的随机行为都会影响最终结果。如果你不固定随机种子同样的代码跑两次结果完全不同你根本分不清是代码改动带来的变化还是随机波动。早期的项目每到调参就头疼后来我强制要求所有训练脚本固定seed——包括Python的random.seed、NumPy的np.random.seed、PyTorch的torch.manual_seed才算把实验间的“噪声”降下来。固定随机种子并不会让结果完全一致但至少能让主要趋势稳定表现出来。这时候你再调参看到的效果差异才是真实可信的。3.4 评估指标准确率高不代表你的模型真的有用模型评估是训练阶段最容易自我欺骗的地方。我做过一个文本分类项目整体准确率97%但把混淆矩阵摊开一看两个低频类别的召回率只有30%——用户最关心的恰好是这两个类别。只看单一指标你会以为模型很强大实际上它在关键场景里基本是废的。所以要建立一套多维评估体系切分类别看精确率和召回率画混淆矩阵找典型错误样本人工复盘。另外我强烈建议维护一个固定的“黄金评测集”——大约几百条人工标注、覆盖所有业务场景和疑难边界的样本。每次迭代都拿同一份评测集跑一遍模型效果升降一目了然。黄金评测集还能有效防止过拟合因为它是独立于训练集和测试集之外的“第三方裁判”。4. 部署与上线让模型变成可用的服务4.1 部署方案选型不是所有模型都需要GPU推理很多人一提到部署就要上GPU集群这是巨大的成本浪费。部署方案应该由延迟要求和模型复杂度共同决定。部署方案适用场景优点缺点FastAPI在线服务实时预测、低延迟部署简单、调试方便需要维护服务进程批处理任务离线批量预测实现简单、可重试延迟高不适合实时ONNX/TFLite嵌入式移动端、边缘设备离线可用、资源占用小算子支持有限、模型结构受限我的经验是先把模型压到一台CPU机器上测一下推理延迟。如果模型不大比如几百MB以内的树模型或蒸馏后的小模型CPU推理延迟两三毫秒完全没必要上GPU。只有大模型或高并发场景才需要考虑GPU和推理加速方案。4.2 写一个能上生产的推理服务就这么几件事我用FastAPI写推理服务核心逻辑只有几十行。但有几件事是必须做对的。第一模型只加载一次。模型文件在服务启动时加载进内存之后每次请求直接复用千万不能在请求函数里反复加载模型。第一次加载可能要几秒钟之后每次推理只需几毫秒这就是缓存带来的效果。from fastapi import FastAPI from pydantic import BaseModel import joblib import numpy as np app FastAPI() model joblib.load(models/baseline.joblib) class PredictRequest(BaseModel): features: list[float] app.get(/health) def health(): return {status: ok} app.post(/predict) def predict(req: PredictRequest): x np.array(req.features).reshape(1, -1) pred model.predict(x)[0] return {prediction: int(pred)}第二接口要做好输入校验。Pydantic的BaseModel自动完成字段类型校验请求里传了空数组或错误类型直接返回400不会让脏数据进到模型层。这能省去大量防御性代码。第三线上模型前后处理逻辑必须和训练时完全一致。很多部署事故出在“训练和推理时对数据的预处理不一致”训练时特征做了log变换线上忘了做训练时填充缺失值的策略是均值填充线上用的是0填充。我踩过一次这种坑后把预处理逻辑统一封装成一个函数训练脚本和推理服务都调用同一个函数从根源上杜绝不一致。4.3 Docker化与CI/CD让“在我电脑上能跑”这句话失效“在我电脑上能跑”是工程协作里最让人崩溃的一句话。Docker把环境、依赖、代码一起打包基本消除了这个问题。我项目的Dockerfile很简单FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, src.serving.main:app, --host, 0.0.0.0, --port, 8000]有几个坑值得提醒。一是基础镜像别用带完整桌面环境的版本python:3.11-slim这种精简镜像能把体积缩到原来的十分之一。二是容器里尽量用非root用户运行避免安全风险。三是依赖锁定要精确到版本甚至用hash校验否则过几个月再来构建依赖更新可能导致行为变化。CI/CD对AI项目来说不只要跑代码层面的单元测试还要跑训练冒烟测试和推理一致性测试。我现在的流程是每次提交代码先跑一遍预处理模块的单元测试然后用极小数据量跑一个epoch验证训练脚本不崩最后加载已训练模型对固定样例做推理断言输出和上次一致允许极小误差。这样绝大部分问题在CI阶段就能暴露而不是上到生产环境才炸。5. 上线之后监控、回滚与持续维护5.1 模型监控模型上线只是开始不是结束模型部署上线很多人的心态是“大功告成”。实际上模型上线之后才是真正考验工程的阶段。模型的性能会随着线上数据的分布变化而衰退用户行为变了、市场环境变了、上游数据变了都可能让一个原本表现良好的模型慢慢失效。你需要一套监控体系来发现这些“生病”的迹象。我最推荐的监控指标是PSI群体稳定性指数它衡量线上特征分布和训练时特征分布的差异。简单理解就是训练时模型见到的和线上实际遇到的分布差了多少。PSI小于0.1表示稳定0.1到0.25之间需要警惕超过0.25就说明分布发生了显著变化。实现上可以对每个特征分箱分别统计两个分布的占比再逐箱计算差异累加。如果你不想自己写很多机器学习平台都内置了PSI计算工具但自己实现一遍能加深理解。除了特征分布还要监控模型的预测分布本身预测结果的均值有没有突变预测为某个类别的比例是不是从30%突然跳到了60%这些都能直观地反映线上环境的变化。5.2 日志、告警与业务反馈闭环线上服务的日志要记录哪些内容我的标准是请求ID、模型版本号、输入特征摘要、预测结果、推理耗时、时间戳。这些日志是事后排查问题的“案发现场”。有一次线上模型效果异常正是靠日志里记录的模型版本号发现流量被路由到了旧版本的服务一秒定位。告警不能什么都不设也不能天天乱响。我刚开始做得比较粗暴设了一堆阈值结果天天被告警打断最后形成“狼来了”效应。后来学乖了只设少量真正需要人介入的告警比如服务错误率飙升、延迟超过SLO、PSI超过警戒线。每条告警都带上当前指标值、历史趋势、可能的原因和检查指引收到告警的人才知道下一步该干什么。业务反馈闭环是AI工程最容易缺失的一环。模型预测完了结果到底对不对对业务有没有帮助这需要产品侧把用户反馈、业务结果数据回传过来。没有反馈闭环模型就永远是“盲人摸象”——你只能看到预测值却不知道预测得对不对。5.3 模型更新与回滚做好最坏的打算模型更新比想象中更危险。新模型离线评估指标更好上线后却可能因为某个边缘case翻车。这时候如果直接全量切流量影响范围就会很大。成熟的团队会做金丝雀发布或者影子部署新模型先接收一小部分流量比如5%观察几分钟到几小时预测分布是否正常、服务质量是否达标再逐步放量。我建议从第一天起就在接口返回值里带上模型版本号并且让模型文件名携带版本信息比如model_v3.joblib。遇到问题想回滚时直接切回旧版本的模型文件就行不需要重新部署服务。这种设计在一开始只需要多写一行代码带来的却是灾难时刻只剩按钮可按的从容。6. 常见问题与排查经验踩过的坑都在这里我把新手做AI工程时最常遇到的问题整理成一个速查表这算是这个项目到目前为止最有价值的一份沉淀。现象可能原因排查方法训练loss不降学习率过大/数据标签错乱/代码bug先在小数据上训练看是否能过拟合到100%离线指标很好线上效果差数据分布漂移/特征泄漏/前后处理不一致计算PSI对比训练和线上数据分布推理延迟很高模型每次请求重复加载/batch没做确认模型是否只加载一次开profile分析耗时内存持续增长模型多次加载/缓存无上限检查加载逻辑确认请求级对象是否被持续持有同样的代码结果复现不了随机种子未固定/依赖版本漂移固定所有seed锁定依赖版本看起来预测很准但业务没增长指标与业务目标没对齐回到业务目标重新设计评估指标排查问题的套路我从多次翻车中总结成一句话先复现再定位最后修复。线上出了问题不要急着改模型先用同样的输入在本地跑一遍确认是线上推理的问题还是模型本身的问题。如果不是模型的问题再去查服务、查数据链路。这个过程听起来简单但能帮你避开至少一半的无效改动。再分享一个实战经验AI项目出问题先查数据再查代码。数据变了、数据错了、数据格式和预期不符占了AI事故的一大半。有一次我的模型在凌晨突然预测结果全变查了一个小时代码才发现上游数据源那天开始返回了一位小数精度更高的数值虽然看起来只是“略微精确”但特征值变化直接改变了预测分布。从那以后我的服务里多了一个给模型输入特征做异常检测的逻辑特征区间、缺失率、均值一旦超过阈值就告警。这个逻辑用几行代码就能实现带来的安全感却很实在。最后说一点我对“从零开始”这个短语的最新体会真正难的从来不是写出第一行代码而是坚持把整条链路走完。数据、训练、部署、监控每一环都有无数个“看起来可以省掉的步骤”但恰恰是那些步骤决定了项目能不能持续跑下去。这个仓库里存的不只是代码更是这一路上所有的错误和修正。如果你也准备从零开始做一个AI项目不要贪多先拿一个最简单的场景把上面这条链路完整走通一遍你会比那些只会调参的人对AI工程的理解深得多。