看到“ai-engineering-from-scratch”这组词,我第一反应不是某个框架或者某个模型,而是这两年我自己做AI项目落地时踩过的那些坑。很多人聊AI,习惯性把注意力放在模型结构、损失函数上,但真正把一个AI系统从零做到能稳定服务于业务,工程化才是那个最容易被低估、也最决定成败的环节。我前后完整做过几轮AI项目,从需求拆解、数据接入、特征计算,到模型训练、上线部署,再到线上监控和持续迭代,今天想把这一整套“从零开始做AI工程”的路线和心得体会,按我自己的思路完整梳理一遍。这篇文章适合正要开始搭建AI项目的工程师,也适合那些发现自己“算法调得不错,但始终没做成工程”的人参考。
1. 项目概述与整体设计思路
1.1 “从零开始”到底意味着什么
很多人看到“from scratch”会以为是在否定现有工具链,要自己写神经网络、自己实现分布式训练框架。我的理解完全不是这样。我更愿意把“从零开始”理解为:不直接依赖某个一体化AI平台,而是自己掌握从数据到模型再到服务这条链路上的每个关键环节。开源框架、云资源都可以用,但每一层的设计决策必须由自己做出:数据仓库怎么建,特征怎么算,模型怎么训练,服务怎么部署,指标怎么监控。这条链路完整跑通之后,再换一个业务场景,这套方法论可以快速复制,这才是“从零开始”的真正价值。
这样做的好处是可控性极强。业务数据可以精细到字段级别去管理,特征逻辑可以精确到每一行计算,模型上线之后遇到问题,排查起来也能直接定位到具体环节。坏处也很明显:工作量大,技能栈要求宽。如果说调模型是普通选手的活,做AI工程需要的是能理解系统、网络、存储、数据建模的全面型选手。我见过不少团队一开始用很成熟的AI平台快速搭出了能力,但一旦线上效果出现波动,排查问题的难度极高,因为底层细节完全不可见。用平台省下的时间,最后都变成了交学费。
1.2 AI工程链路里的三个核心子系统
如果把我做过的项目抽象一下,AI工程其实是在构建三个相互咬合的子系统:数据处理系统、模型训练系统、服务与反馈系统。数据处理系统负责把原始日志、业务库数据变成可以用于训练的特征,相当于模型的“粮仓”;模型训练系统负责实验探索、超参调优、模型产物管理和评估;服务与反馈系统负责把模型预测结果送到业务线上,并持续采集反馈数据形成闭环。三者之间不只是一条直线,很多反向依赖往往被忽略——训练数据分布发生变化,会直接影响线上推荐效果;线上预测结果的反馈,又会成为下一轮训练的数据来源。
我在这类项目里习惯先画一张端到端的数据流图,但我不用复杂的架构图工具,就是一张白纸、一支笔,从数据源是什么、存储在哪里、谁消费什么数据,到训练周期多久、模型产物怎么发布,再到线上特征从哪儿来、预测结果落在什么表里。先把这条链路看清楚,后面写代码才不会乱。很多工程师一上来就钻进Pandas和模型代码里,结果做到一半发现有些字段线上根本没采集,只能返工。所以我坚持先梳理依赖关系,再动具体代码,这个顺序能省掉大量的返工时间。
2. 基础设施与技术选型
2.1 写AI工程代码,光会Python可不够
写AI工程的代码,只掌握numpy、pandas、torch远远不够。工程代码和算法脚本之间最大的区别在于:脚本只负责“今天跑通”,工程代码要保证“明天也能跑通,别人也能接手”。我推荐在项目里至少要求以下几件事:统一使用类型注解,函数参数和返回值都要写清楚;引入单元测试,用pytest保护数据和特征计算逻辑;使用结构化日志代替满天飞的print;模块之间通过接口连接,不直接互相调用内部变量。这些习惯听起来很枯燥,但排查线上问题的时候,价值会成倍放大。
我见过最典型的情况是:特征计算代码里埋了一个隐蔽的时区错误,训练的时候数据对齐没问题,到了线上预测阶段,因为业务库里的时间戳比较新,逻辑直接错位,整个模型效果大幅下降。这种问题如果不给特征计算写单测,几乎不可能在代码评审里发现。所以我在项目里定了一条铁律:凡是能跑单测的逻辑,必须先把单测跑绿再谈训练模型。工程化不是给算法套枷锁,而是让算法能稳定地发光。
2.2 环境与依赖管理的血泪教训
Python的依赖管理有多混乱,做过AI工程的人一定深有体会。项目里同时用numpy、pandas、pytorch、scikit-learn的时候,版本之间互相冲突是家常便饭。尤其pytorch版本一换,CUDA版本也要跟着换,整个环境可能直接起不来。我现在的方案是:用uv做虚拟环境和依赖管理,配合pyproject.toml把项目依赖锁定到具体版本,环境可复现,团队里任何人都能一键安装出完全一致的环境。和conda那种把所有东西都塞进去的方案相比,uv更轻量,解析依赖的速度也快很多。
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| conda | 包管理能力强,适合科学计算 | 环境臃肿,依赖解析慢 | 快速试错,本地探索 |
| venv + pip | 轻量,Python原生支持 | 依赖版本容易失控 | 简单脚本 |
| poetry | 依赖和构建统一 | 部分场景依赖解析慢 | 标准Python工程 |
| uv | 极快,锁文件可复现 | 年轻,生态还不够大 | 我现在的首选 |
我特意把“重复构建环境”这件事放在项目早期做。第一次做项目时因为依赖没锁定,上线前重新构建环境,发现新环境里的pandas版本和训练时不一致,读数据的默认行为改了,线上预测结果直接出偏差。那次之后,我所有AI项目都必须有锁定的依赖文件,上线构建脚本里还会先做环境校验,确认关键依赖版本与训练环境完全一致才允许部署。
2.3 数据管道不能靠手动跑脚本
Notebook适合做探索性分析,但真实项目里的数据管道必须脚本化编排。我的习惯是:所有数据接入、清洗、特征计算都写成可重跑的Python模块,再配合调度框架定时执行。简单项目用cron或者Prefect就够,复杂项目用Airflow。绝不能让AI工程师手动跑数据任务,因为那样的流程无法审计、无法重放,出了问题也没法追溯。
一个特征计算任务可以长这样:先从数仓取最近7天的行为日志和用户画像,做特征拼接,写回特征表。这个任务必须支持回填,也就是参数里能指定业务日期范围。这样如果某天数据质量异常,修复后可以只重跑那几天的数据,不用全量刷新。任务之间的依赖和超时告警也要设置清楚:数据接入任务失败就不要往下触发特征计算,特征计算超时立刻报错,这样第二天早上看到的是一个干净完整的结果。这些环节看起来和模型没有直接关系,但它们恰恰是AI工程能否稳定运转的关键。
3. 数据与特征工程:AI工程的地基
3.1 数据质量校验是第一道安全网
做AI工程之前,要先解决数据“能不能被信任”的问题。原始数据进入系统后的第一道关卡就是质量校验,我写过一个通用的数据质量检查模块,它会自动检查这些维度:表的字段数量是否和预期一致、空值率是否超过阈值、业务日期字段是否有缺失日期、数值字段的最大最小值是否在合理区间、样本量是否出现明显低于历史均值的情况。每项检查对应不同的告警等级,严重问题直接阻断下游任务,一般问题只记录日志继续跑。
举个例子,我在做内容推荐场景时,线上行为日志经常因为客户端埋点故障出现连续小时空数据。如果没开质量校验,模型训练时这些空时段会被当成“用户无行为”,特征分布被污染,模型评估结果会异常乐观或异常悲观。开了校验之后,一旦发现连续几个小时样本量为0或者骤降,调度自动暂停,拉起数据修复流程。这一步是后续所有模型结果的安全网,也是线上稳定性最重要的一道防线。
3.2 特征计算的结构化管理与防泄漏
特征工程是AI项目里最容易被低估的部分。很多教程用两列Pandas算出一个特征就算完事,真实工程里特征动辄几十上百个,来源分散,时间口径复杂,没有结构化管理很快会失控。我的做法是:库给每个特征分配一个明确的业务ID,命名规范里必须包含实体、聚合方式、时间窗口,比如user_behavior_cnt_7d表示用户近7天行为数量。这个规范能帮团队在大量特征里快速定位问题,也能避免多人协作时重复造轮子。
另一个极其关键的工程问题是防数据泄漏。训练样本里的特征值,必须严格来自样本业务时间点之前的数据,绝不能用“未来”信息。最容易踩雷的场景是:做训练集时先按行为日志把特征算完,再和标签关联,如果特征计算窗口跨过了标签时间点,模型就相当于偷看了答案。我在特征库设计里专门加了一层时间对齐机制:所有特征都以sample_time这个业务时间为基准,只允许使用sample_time之前的数据窗口。这个时间对齐逻辑我写成了公共模块,任何新特征上线都必须经过它,不给拍脑袋留机会。
3.3 数据集切分不能偷懒
常规机器学习教程里教的随机切分训练集和测试集,到了真实业务场景里并不适用。业务数据普遍带时间结构,随机切分会把同一时间段内的相似样本同时分进训练集和测试集,评估结果虚高。等模型上线面对全新数据时,泛化能力立刻打回原形。我的做法是:按业务时间切分,比如前60天训练,中间7天验证,最后7天测试,并且保证测试集的时间严格晚于验证集。这样评估出来的结果才更接近线上的真实表现。
另一个细节是样本唯一ID的问题。每条训练样本必须有一个全局唯一的sample_id,在特征计算、模型预测、效果评估各个阶段都保留这个ID。这样日后排查坏样本时,才有一整条链路可以回溯。如果没有唯一ID,遇到线上表现异常,连“这个预测是谁产生的、用了哪些特征”都说不清楚,只能靠猜。数据工程里“可追溯”不是可选项,是刚需。这个sample_id字段看着不起眼,但它是整个数据链路里最重要的索引,有了它,一切排查才有抓手。
4. 模型开发与训练的工程化
4.1 从Notebook走向模块化工程
模型开发的起点通常是Notebook,这没有问题,探索阶段需要灵活。但如果项目长期停留在Notebook里,一定无法稳定上线。我的做法是:一旦方向验证通过,立刻把代码结构化到规范的项目目录里。目录一般长这样:config放实验配置和模型超参,data负责数据加载、清洗、特征计算,models放模型定义、训练逻辑、评估脚本,serving负责模型服务化与线上预测逻辑,tests放单元测试和集成测试。
这个结构不是标准答案,但对团队协作有明显的帮助。新成员进来后可以快速找到对应代码位置,训练脚本和评估脚本分离之后,调参时不用改动数据加载代码,模型迭代也不会破坏服务稳定性。我判断一套工程化程度的最低标准是:删除一个人的本地目录,整个链路还能不能从零重新跑出来。如果能,说明依赖清晰、模块解耦,项目基本合格;如果不能,说明代码和某台电脑的某个目录产生了隐性耦合,迟早会出事。
4.2 实验管理和配置管理是复现的基石
调过模型的人都有体会:试过的参数、换过的特征、跑出来的指标,靠人脑记忆根本不现实。哪怕只调两三个超参数,连续一周下来就会忘记上一次最优结果对应的具体配置。我习惯用MLflow管理实验,每个实验记录下特征版本、数据版本、代码commit号、超参全量、评价指标和模型产物路径。只要每次训练前把这些参数固化,之后任何时候回看实验结果,都能精确复现当时的环境。
与之配套的是配置文件机制。我不在代码里写死超参,而是把一组可调参数放在yaml文件里,训练脚本启动时读取。这样改参数只需要改配置,不需要碰代码。典型的配置长这样:
data: train_start: "2025-01-01" train_end: "2025-02-28" val_end: "2025-03-07" feature_table: "feature_user_behavior" model: name: "deepfm" embedding_dim: 32 hidden_units: [256, 128, 64] dropout: 0.1 train: batch_size: 2048 learning_rate: 0.001 epochs: 20 early_stop_patience: 3配置文件和训练结果绑定保存,实验管理立刻上一个台阶。我每次跑完实验都会把config文件复制到实验目录里,而不是只靠MLflow的界面记录,双重保险,杜绝了“这次到底用了什么参数”的争论。
4.3 训练流程的效率与稳定
训练环节的生产效率同样值得重视。小团队GPU资源有限,堆卡不如把资源用细。我在训练流程里一定会做这几件事:混合精度训练,把FP32改成FP16,显存占用下降明显;梯度累积,用小batch模拟大batch的效果;早停和模型检查点,只保留表现最好的权重,防止跑到后期过拟合。这套组合下来,同样的任务训练时间能缩短30%到50%,显存占用也友好很多。
训练过程要有可视化监控。loss曲线、学习率曲线、验证集核心指标曲线,这些最好在训练过程中实时可见,不要等训练结束才发现模型发散。我自己的做法是在训练脚本里加入结构化日志,每个epoch结束输出关键指标到日志系统,同时上报到监控面板。这样半夜跑的实验,早上起来第一眼就能判断状态。如果训练中途出现NaN输出,发现得越早,浪费的算力越少。训练稳定性和代码质量一样,都应该被纳入工程管理的范围。
5. 模型评估与上线部署
5.1 离线评估选对指标,别只看AUC
离线评估指标的选择,直接决定你对模型效果的判断是否准确。不要只看一个AUC就拍板,不同业务的核心目标差异很大。做内容推荐,最终关心的是点击率、留存率;做风控场景,更关注召回率和误报率。我习惯把业务目标拆成离线代理指标:比如推荐场景用AUC评估排序质量,同时看不同用户群体的分层AUC,避免头部用户拉高整体指标,长尾用户的体验被完全牺牲。分层评估在真实业务里特别实用,因为它能暴露“平均数陷阱”。
做阈值选择时也必须结合业务成本来计算。以推荐场景为例,如果模型输出的是点击概率,阈值定低了会有大量无效曝光,定高了又会错过很多潜在点击。我会在验证集上扫描不同阈值下的精确率和召回率,再根据业务是“宁可错推还是宁可漏推”来确定最终阈值。阈值不是拍脑袋定的,而是脚本跑出来的,同时把结果记录在实验配置里,方便后续追溯和调整。这样每次更新模型,都能清楚地回答“阈值为什么这么定”。
5.2 模型服务化的两种形态与一致性校验
AI工程里的模型服务,不等于简单启动一个HTTP接口。如果业务是每天离线批量生成结果,比如早上的推荐列表,用离线批量预测把结果写入线上表就够,简单稳定、适合调试。如果业务需要实时预测,比如用户请求来了马上返回结果,那就需要在线API。在线API我习惯用FastAPI封装模型推理逻辑,配合模型版本管理,保证接口升级时能做到平滑切换。
在线预测最容易出问题的一个点,是训练时的特征和上线时的特征不一致。模型训练用的是批量离线特征,上线后逻辑必须能从请求里实时算出相同的特征。这个一致性是整个体系的卡脖子问题。我的做法是:把特征计算逻辑单独抽出来,训练前和上线前调用同一份特征代码,并用一组标准输入做回归测试。只要这份特征代码没有分支差异,线上评估结果才可能与离线对得上。另一个实用技巧是服务启动时做一次输入输出兼容性验证,用固定样本跑通推理,防止旧模型或二进制格式不兼容导致线上直接报错。
5.3 线上监控与反馈闭环
模型上了线,事情才刚刚开始。AI工程做得好不好,很大程度体现在监控体系上。我在每个模型服务里都会记录四类指标:请求量、预测结果分布、推理延迟、特征缺失率。预测结果分布的意义在于,如果某种状态突然倾斜,比如预测值整体偏高或整体偏低,大概率是特征数据变了或模型已经失效。特征缺失率则是线上特征填充失败的预警,这个值一旦升高,要么是上游数据断流,要么是线上特征代码出错,必须立刻处理。
更进一步的闭环是把线上反馈接回来:预测结果连同业务结果,比如用户点没点、用户满不满足,写入反馈日志,定期回流到训练集。这样模型每周都能增量学习最新的数据分布,不会随着时间推移慢慢失效。我在做这套系统的时候特别注意反馈延迟的问题:有些行为要几天之后才能产生明确标签,所以回流训练数据的窗口要留足够长,否则会把尚在观察期的样本错标成负样本,污染训练集。这个细节直接决定增量学习的质量。
6. 常见问题与避坑实录
6.1 离线效果不错,线上却变差的原因和排查
这是AI项目里被问得最多的问题。根据我的经验,大多数情况不是算法问题,而是数据不一致。第一种是特征泄漏,训练时用了未来信息,离线评估虚高;第二种是训练数据和线上数据的分布偏移,比如训练集里用户群体是半年前的,线上用户结构已经变了;第三种是特征代码在训练和推理两条路径上出现分支,线上算出来的特征和训练时对不上。排查顺序上,先对齐“采样时间窗口和特征代码版本”,再谈调模型的事。
具体排查动作上,我先对比线上和离线两个数据集的字段分布,计算PSI,哪个特征分布偏移超过0.2就先盯哪个。再把线上预测日志里真实预测值和模型打分做分布对比,确认不是服务端bug。很多时候最后找到问题就只是一行时区代码写错了,或者某个字段上游没更新。如果没有监控数据,排查过程会变成大海捞针。我把常见的现象、原因和排查动作整理成了一个速查表,团队内部一直用:
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 离线AUC高,线上点击率低 | 特征泄漏或采样偏差 | 检查特征时间窗口,校验训练/线上分布 |
| 预测值整体偏高 | 线上特征缺失被默认值填充 | 看特征缺失率监控,检查特征代码 |
| 线上效果每周稳定下滑 | 数据分布漂移,反馈延迟低估 | 计算PSI,检查反馈标签回流窗口 |
| 服务偶发超时 | 模型推理过长,上游特征接口慢 | 看延迟分位数,加缓存和超时控制 |
6.2 数据异常和训练失败的恢复流程
数据任务失败并不可怕,没有规范的恢复流程才可怕。我在项目里对数据管道设置了重试机制:依赖的外部系统超时,允许自动重试有限次数;数据质量校验失败,暂停后续任务,等人工确认;训练任务设置内存上限、超时时间和最大重试次数,防止偶发OOM导致整个流水线中断。平时把这些处理预案写好,真正出事情的时候才不用半夜起来拍脑袋。
强烈建议在训练和预测链路的每个关键函数加“空数据保护”。比如预测服务发现特征向量里全是空值,不要直接抛异常让线上请求崩溃,而是走兜底策略返回安全值并触发告警。生产环境里稳定性大于一切,偶尔损失一点预测精度换取服务可用,是完全值得的。我早年吃过一次亏:上游表延迟,推理服务拿到的特征全是空值,模型输出变成NaN,线上接口直接开始报错,还没来得及定位问题,业务方电话已经打过来了。从那之后所有必填特征都有默认值和对应的告警指标。
6.3 小团队如何平衡迭代速度和工程质量
最后聊一些个人经验。很多小团队做AI项目,最大的矛盾是“快速验证”和“工程规范”之间的拉扯。我见过太多项目为了尽快上线,跳过单测、跳过配置管理、直接把Notebook代码改成服务,结果上线后每一个改动都像重新做一次手术。我的体感是,工程化规范要“小而精”,不需要一步到位上全套平台,但最基础的几样必须从第一天开始做:代码进Git仓库、依赖锁版本、特征和数据有版本记录、训练结果有实验日志、预测服务有监控告警。这些看起来琐碎,但它们才是AI工程长期稳定运转的底座。
这些年做下来,我最大的体会是:不要把AI工程当成一个纯粹的算法项目来做,它本质上是一个系统工程。算法只是其中一个环节,数据质量、特征一致性、服务稳定性、可观测性,每一项都可能成为瓶颈。多在这些“非算法”的事情上花时间,模型上线后的每一次迭代,你都会感受到这些基础工作带来的回报。如果你现在正打算从零开始搭一套AI工程,我建议你从第一天就把监控和可观测性放进去,不要等出事了再补,这条经验对我来说是花了不少代价才换来的。