ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI工程从零搭建:数据、训练、部署与监控全流程实践

AI工程从零搭建:数据、训练、部署与监控全流程实践

说实话,第一次听到"ai-engineering-from-scratch"这个标题时,我脑子里冒出来的画面是深夜图书馆里有人对着白板推导反向传播公式,或者像某些硬核教程那样从零手写一个TensorFlow。后来自己真正在这个领域折腾了几年,才意识到这个标题最准确的理解不是"从零手搓深度学习框架",而是"从零到一搭建一条完整的AI工程流水线"。

如果你早就会用PyTorch训练模型,却在项目交付时频繁翻车——训练好的模型部署不上、测试集指标好看但线上效果拉胯、换了个人就复现不出结果,那这篇文章就是写给你看的。我不会跟你聊"AI赋能""智能升级"这种空话,只会聊我在实际项目中从数据收集、实验管理、模型部署到线上监控踩过的坑和最终沉淀下来的方法。这套东西适合刚好带一个AI项目、但还没形成工程化体系的小团队,也适合准备从"算法工程师"往"AI工程师"方向进阶的个人开发者。

1. 先说清楚:AI工程到底在解决什么问题

1.1 跑通和工程化是两码事

很多刚接触AI的同学有个错觉:让模型在Jupyter Notebook里跑通一个epoch,项目就算完成了80%。这个想法在真实业务里特别危险。我在本地跑过的项目至少有十几个,其中真正能顺利走向生产的不到三分之一。剩下的要么卡在数据质量上,要么卡在模型怎么部署给业务方调用,要么卡在模型上线两个月后性能悄悄退化,而整个团队毫不知情。

AI工程要解决的核心问题,本质上只有三个:可复现、可服务、可迭代。

可复现的意思是,无论你在哪里跑同一个训练脚本,用同一份数据和配置,得到的模型性能应该是一致的。这听起来简单,实际做起来非常折磨人。Python版本、CUDA版本、依赖库的锁定、随机种子的固定,每一个环节都能成为复现路上的绊脚石。我踩过一次印象很深的坑:某次实验记录显示AUC是0.872,同事在同一台服务器上重新跑,结果变成了0.865,排查了半天才发现是NumPy在某个版本后改变了随机数生成的算法,而requirements.txt里写的是numpy>=1.18,两个环境解析出来的版本不同,结果自然对不上。

可服务的意思是模型必须拿到真实业务环境中接受考验。离线指标再漂亮,上线后面对真实的输入分布、延迟要求和并发压力,才算是真正"能用"。可迭代则是说,模型不是上线就完事,你会发现数据在变、业务在变,模型必须快速迭代适配。如果整个流程里每一步都是手工操作,迭代一次要耗费两周,那这个AI项目从一开始就不是工程,而是科研实验室的Demo。

1.2 为什么强调"from-scratch"这个视角

市面上有大量"十分钟快速搭建AI应用"的教程,也有各种号称全自动的AutoML平台。这些工具当然好,但如果只是用它们把模型跑出来,对工程体系的认知会一直停留在黑盒阶段。from-scratch这个视角的本质,是逼迫你亲手把每一个环节过一遍,理解数据从哪来、训练跑在哪、模型如何被调用、失败如何被发现。

举个很直观的例子。我见过有人用云平台的"一键训练"功能,点点鼠标就拿到一个图像分类模型,精度还不错。但模型要上线时,对方完全不知道训练数据是怎么做预处理的,也不知道模型服务的请求格式是什么,平台文档写得再清楚,他也对应不上自己的业务场景。反而是那些从数据清洗开始,一步一步手工搭流程的工程师,哪怕用的工具简陋一些,对系统的掌控力也高得多。

所以这篇文章的路线是:不追求最花哨的工具链,而是用最稳妥、最常见的工程手段,从零搭起一条完整的AI流水线。每一步我都会告诉你为什么这么做,以及有哪些替代方案。

2. 数据是工程的起点:获取、清洗与版本管理

2.1 先用数据冒烟测试,再谈建模

数据工程师圈子里流传一句话:"数据决定模型的天花板,模型只是逼近这个天花板。"我做了这么多项目,越来越认同这句话。很多团队花大量时间调参、换模型结构,效果始终提不上去,回头一查,数据的标签错了一半,压根没有优化的空间。

我在每个项目启动时,会先花30%左右的时间做一件事:数据冒烟测试。也就是抽取一小部分数据样本,人工检查它的完整性、一致性和分布情况。这里的要点是只看原始数据本身,不带任何建模预设。比如做一个用户评论情感分类项目,我会先随机抽200条评论,看一眼标签分布是否均衡,评论里是否混杂着大量非目标语言,是否存在同一句话被标注为不同情感的情况。这些检查做完了,才会考虑用什么模型。

我总结了一套最小检查清单,你可以直接拿去用:

检查项方法常见问题
数据完整性统计缺失率、空值、异常字符大量字段为空但没被清洗
标签质量抽样人工复核,计算不一致率标注标准不统一,同类样本标签差异大
分布均衡性统计各类别样本占比严重不均衡但没做处理,导致模型偏科
时间有效性按时间维度切分样本量近期样本占比过低,模型学的是历史规律
特征分布看数值型特征的min/max/mean,类别特征的频次表数据泄漏的早期信号,比如特征里混入了目标值

这套检查看起来朴素,但拦下了我至少五个"看起来能做但其实根本没法用"的失败项目。有个做销量预测的项目,数据量很大,建模的人花了三周时间精调LSTM,效果就是上不去。我接手后看了一眼特征分布,发现训练集里包含了未来日期的促销活动信息——模型在训练时"看到了未来",离线测试自然漂亮,上线后这些未来信息根本不存在,预测直接崩掉。这类问题如果不先做数据体检,靠调参是永远发现不了的。

2.2 数据清洗与特征工程的工程化套路

数据清洗这件事,很多人以为就是"去掉缺失值",实际操作时远比这复杂。我通常把清洗分三步走:格式统一、缺失处理、异常值识别。

格式统一最烦的是字符串类数据,比如用户的手机号有的带区号、有的不带,有的是数字格式、有的是科学计数法格式。这类问题没有统一答案,只能基于对业务的理解做规则处理。我的建议是每一个清洗操作都要写成函数,不要用一段几十行没人看得懂的脚本拼凑,否则数据集每次更新,清洗逻辑就得重跑一遍,且完全无法追溯。

缺失处理要分类型来看。数值型特征我通常会先区分是"真缺失"还是"有业务含义的缺失"。比如用户的收入字段为空,可能表示用户没填写,也可能表示该用户无收入来源,这两种情况如果都简单地填0或填均值,会把完全不同的信息混为一谈。我常用的做法是保留一个"IsMissing"标志特征,让模型自己去学习缺失状态的影响。类别型特征则通常加一个"UNKNOWN"类别,比强行删掉样本要稳妥。

异常值识别这里我给大家一个技巧:不要只看Boxplot或Z-score,要结合业务逻辑来判断。一个用户在一个月内下单1000次,可能在统计上是异常值,但对于B2B采购企业的账号来说完全正常。不加判断地把这类样本当成噪声删除,等于把业务中最有价值的头部客户信息扔掉了。

所有清洗逻辑都建议写成可复用的Python脚本,输入是原始数据,输出是清洗后的数据,附带一份清洗报告,记录每一条规则的执行情况。这份报告会被训练团队、部署团队和后续维护的人反复翻阅。

2.3 数据版本管理:模型的Git

我见过太多次"模型回滚后发现训练数据对不上了"的悲剧。模型A是用8月30日导出的数据训练的,模型B是用9月15日导出的数据训练的,如果这两个导出时刻的数据构建脚本不一样,那么模型A和模型B之间的性能差异,你根本分不清楚是数据变了还是模型结构变了。

数据版本管理在传统软件开发里有一套成熟方案(Git),但Git是为文本设计的,对二进制的大文件数据集并不友好。我现在的做法是,用DVC(Data Version Control)这类工具把数据文件存到云存储或NAS,用一个描述性的YAML文件记录当前版本对应的文件哈希、数据来源和生产时间,然后把YAML文件提交到Git仓库。这套方案的好处是:训练环境拉取代码时,只需按DVC配置拉取对应的数据版本,数据本身不用进Git,代码仓库保持轻量,数据变更也全部被记录。

不要觉得这是过度设计。只要你的项目生命周期超过两个月,数据就一定会在期间发生变更。有了版本管理,你才能回答"为什么这个月的模型性能比上个月差"这类灵魂拷问。答案可能是数据分布变了、业务规则变了、甚至是数据采集的代码被改坏了,而版本管理让你有据可查,而不是全凭猜。

3. 模型开发与训练:从"跑通Jupyter"到"工程化训练"

3.1 先跑通模型,再用工程手段管起来

模型开发的第一步,永远是先让模型跑通,哪怕是个超小的baseline。不要一上来就追求最佳超参、最优结构,那样只会让你陷入无穷无尽的调参循环,还容易怀疑人生。我先跑一个默认配置的模型,跑通整个流程,拿到一个基础的性能数据。这个baseline的作用是建立参照系,后续所有的优化都能跟它对比,判断改进是真的有效,还是随机波动。

我习惯的baseline训练配置是这样的:用一个中等规模的数据子集,设置固定的随机种子,记录训练集和验证集的loss曲线,保存最基础的评估指标。这一步最大的价值不是模型效果,而是暴露工程链路中的各种问题——比如数据读取方式太慢、特征构造有bug、验证集划分方式不妥等等。链路通了,后面的一切才有意义。

3.2 实验追踪与超参管理:让训练有的不断回溯

在做模型迭代时,我强烈建议从第一天就引入实验追踪工具。无论你用MLflow还是Weights & Biases,核心原则是一样的:每一次训练都记录下完整的"实验现场"——模型结构、超参数、训练代码的Git提交号、数据版本、评估指标和模型文件路径。

为什么这件事极其重要?因为在真实项目里,你不可能只跑一两次实验。一个稍微正式一点的项目,实验次数至少是几十次,多则上百次。如果没有追踪体系,你会在第三周彻底分不清哪个实验用的是什么参数、为什么那个版本的效果好但代码已经改得面目全非。我见过一个很夸张的例子:团队leader让工程师复现一版两周前AUC最高的模型用于上线,工程师翻遍了聊天记录和脚本文件名,只找到了一堆model_final_v2_really_final.py,没有一个能直接跑通。

MLflow的使用方式我一直控制在最小可用状态:先定义一个统一的训练脚本,接受外部参数传入,然后用MLflow的log_params和log_metrics记录超参与指标,用log_artifact保存模型文件。这样所有实验都会自动沉淀,并且共享一个统一的入口,方便比较。每次训练前给自己立个规矩:记录这条实验想验证的假设,训练完看一眼结论是否验证了假设,然后留下文字备注。好的实验追踪是工程团队的时间机器,让你能在任何一个时间点退出"迷之调参"状态。

3.3 训练过程的常见误区与排查

训练阶段大家问得最多的就是"loss不下降怎么办"。我的第一反应往往不是调参,而是检查数据管道。下面是我常用的一组排查顺序,从低开销到高开销:

  1. 确认损失函数是否匹配任务类型(分类用交叉熵,回归用MSE,注意别用反)。
  2. 检查输入数据的范围:如果特征数值量纲差异过大,模型很难收敛,特别是使用SGD这类对尺度敏感的优化器时。
  3. 验证数据与标签对齐:这一点极其隐蔽,数据管道里如果shuffle逻辑错了,输入和标签对不上,训练不会报错,但loss会一直震荡或者诡异下降。
  4. 检查学习率:过大的学习率会导致loss发散,过小则收敛极慢。从0.001开始逐年调整是比较稳妥的起点。

另一个高频问题是"显存溢出"(CUDA Out of Memory)。这个问题通常不是因为模型太大,而是因为batch size和输入尺寸的组合超限。我常用的方法论是二分法——先把batch size降到1,确认能跑通,再逐步扩大,直到找到临界值。生产训练里我不追求把显存压满,留出20%的余量能避免很多偶然性的OOM。

3.4 什么时候才需要分布式训练

有些同学一训练就想着上分布式,恨不得搞个多卡并行。实际上大部分团队的训练任务,单张显卡就足够。我判断是否需要分布式的标准很朴素:单卡训练时间是否可以接受。如果一次全量训练需要3天,每周都要迭代一版模型,单卡显然无法满足节奏,这时再上分布式才有明确的收益。如果一次训练只需几小时,为分布式引入的调试成本反而会拖慢项目进度。

如果确实需要分布式,首选PyTorch DDP(DistributedDataParallel),它在多卡训练时同步梯度的效率比较高,使用方式也比DataParallel优雅得多。启动命令可以像这样:

torchrun --nproc_per_node=4 train.py

脚本内部的模型定义和训练循环几乎不需要大改,只需初始化torch.distributed并包装模型。但注意,分布式训练里数据加载的shuffle逻辑要格外小心,多进程如果各用各的随机种子,可能导致不同卡看到的数据组重复,影响训练效果。我自己踩过这个坑,最后统一用了一个全局共享的种子生成种子序列,问题才解决。

4. 部署与推理:把模型变成真正好用的服务

4.1 部署方案怎么选:别一眼就上微服务

模型训练完了,接下来是部署环节,这也是"AI工程师"和"算法工程师"拉开差距的地方。很多人拿到训练好的模型,第一反应是写一个Flask服务,然后扔到生产服务器上。这个思路本身没错,但有几个细节必须提前想清楚。

第一个问题是框架选择。单纯把PyTorch模型包装成HTTP接口直接对外服务,通常不是最优解。PyTorch的推理接口对并发支持不够好,高并发场景下容易CPU/GPU资源分配不均,导致请求排队。更常规的路线是用专门的推理服务框架来托管模型,比如TorchServe、TensorFlow Serving或者ONNX Runtime配一个服务网关。我建议的做法是:先把模型导出成ONNX格式,用ONNX Runtime做推理,再包一层轻量HTTP服务。这样做的好处是模型与训练框架解耦,线上服务不需要安装完整的PyTorch环境,资源占用和延迟都更可控。

第二个问题是模型加载的生命周期。模型文件应该只在服务启动时加载一次,不要在每个请求里重新加载,否则并发一高就必然卡顿。我见过不少新手项目把torch.load写在请求函数内部,结果每个请求都要等几秒钟——这个坑太经典了。正确做法是服务初始化时把模型放进内存,请求进来只做前向计算。

4.2 推理优化的关键指标:延迟、吞吐、显存

部署前必须定义清楚两个指标:P95延迟和吞吐量。P95延迟说的是95%的请求能在多少毫秒内返回,这个指标比平均延迟更有参考价值,因为少数慢请求往往会拖垮用户体验。吞吐量说的则是在满足延迟要求的前提下,系统每秒最多能处理多少请求。

这两个指标直接决定了你的部署方案。如果业务方要求P95延迟在50毫秒以内,但模型本身推理就要80毫秒,那就需要考虑优化手段了。我常做的优化手段有以下几种:

  • 模型剪枝与量化:把FP32的模型转成FP16甚至INT8,推理速度常常能提升2~5倍,代价是精度可能有少量损失。对精度敏感的,可以先做FP16,一般损失很小。
  • Batch推理:把多个请求拼成一个batch,一次前向计算处理多个样本,这是GPU推理提升吞吐最有效的手段。缺点是会抬升单请求的等待时间,适合异步场景。
  • 缓存:对重复性较高的请求,在Redis或内存里缓存推理结果,能直接省掉模型计算。

优化顺序建议:先量化,再做batch,最后考虑剪枝。量化实现成本最低,效果立竿见影。剪枝需要重新训练或微调,周期较长,尽量放在后面。

4.3 灰度发布与回滚:别让模型更新变成事故

模型上线不是"替换旧模型"这一个动作,而是一个需要谨慎操作的过程。我强烈建议任何模型上线都走灰度发布流程:先让新模型接收5%的流量,观察线上指标一段时间,确认没有异常再逐步扩大到全量。这个操作的依据很简单——离线评估的样本和线上真实流量始终存在偏差,只有让新模型接受真实的流量考验,才能看出问题。

回滚方案也必须提前准备好。最稳妥的做法是同时保留旧模型的部署版本,灰度期间如果新模型表现异常,一键切回旧版本服务。这里有一个小经验:如果新模型在灰度期间出现效果下降,第一时间不要急着回滚,先看监控数据,确认下降是偶发波动还是系统性问题,避免误伤一次本来有效的升级。但如果5分钟内P95延迟直接翻倍或错误率飙升,不用犹豫,立刻回滚,先恢复服务再说。

5. 评估、监控与持续迭代:模型上线只是开跑

5.1 离线评估的局限性:指标好不代表业务好

在模型上线前,我们通常用准确率、AUC这类指标在测试集上评估模型。这些指标有它的意义,但它衡量的是"模型在静态数据上的预测能力",而不是"模型在真实业务中带来的价值"。举个我经历过的例子:一个推荐系统模型的AUC提升0.02,团队很高兴地上了线,结果业务侧反馈人均点击率并没有变化,甚至略有下降。后来分析发现,AUC的提升主要来自二分类划分地更准确了,但对用户真正关心的推荐多样性几乎没有帮助。这说明,离线指标与业务目标之间天然存在一个"翻译"断层,而这个断层只有用线上业务指标来填补。

所以我在评估模型时,一定会定义一套业务指标,跟算法指标分开看。比如电商推荐场景,业务指标是"点击率、转化率、人均下单金额";智能客服场景,业务指标是"问题解决率、用户满意度、平均对话轮数"。模型上线前后对比的是这些业务指标,而不是只看AUC。

5.2 线上监控:数据漂移、特征漂移与模型漂移

模型部署上线后的第一件事,就是建立监控。如果不在上线之初就搭好监控体系,模型性能退化、数据分布偏移、特征缺失率飙升,这些问题你都要等到用户投诉后才能发现,那时候影响面已经很大了。

线上监控的核心有三个。

数据漂移是最常见也最容易被忽视的。你的训练数据来自2024年上半年,但2024年下半年业务拓展了新地区,用户构成发生了变化,新老用户的分布跟训练集分布不一样了,模型的准确性自然会下降。检测数据漂移的常用手段是PSI(Population Stability Index),拿当前线上数据的特征分布,跟训练集的分布做对比,超过阈值就告警。

特征漂移指的是单个特征的分布发生变化,比如某个特征在训练时是0~1之间的值,上线后变成了10~100,这种变化会造成模型输出不稳定。我在实际项目里会为每个关键特征设置上下限告警,一旦超出合理范围,立刻收到通知。

模型漂移则是说模型自身的预测分布变了,常见于在线学习场景。监控模型预测值分布的均值、方差,如果出现突然跳变,多半意味着输入分布或者模型内部状态出了问题。

监控告警尽量做到"先内部后外部"。内部告警发生,你还可以慢慢排查;用户投诉发生时,项目就已经处于被动状态了。

5.3 持续迭代的节奏与触发机制

模型不是"训练一次,用到天荒地老"。数据在变,业务在变,模型需要持续迭代。但迭代也不是"每周重训一次"这么简单,关键是要判断"什么时候该重训"。

我个人的经验是设计自动化触发机制,而不是定时重训。核心信号来自监控指标:当数据漂移指数超过阈值,或者某个核心业务指标连续多天低于历史基线,就自动触发一次重新训练流程。重训用的数据要滑动窗口化,用最近三个月的数据训练,再跟上一版模型进行A/B对比,效果好再灰度上线。

这套机制的收益非常明显。它让模型的迭代不再是"靠人的自觉",而是"靠系统的规则"。大家都忙的时候,至少系统不会忘记模型需要更新。有工程师离职了,迭代流程也还在,不会因为某个人不在了而全面停滞。

6. 常见问题与排查技巧实录

6.1 环境不一致:为什么你的结果别人复现不了

前面提过一个案例:相同的训练脚本,不同环境的依赖版本不同,结果就大相径庭。这个问题在多人协作或者换服务器的场景里极其高频。解决思路是"环境即代码"。能用Docker的时候就尽量用Docker,把Python版本、CUDA版本、依赖库版本全部固化进镜像。哪怕只是本地跑实验,也至少把requirements.txt精确锁定到具体的版本号,不要用>=这类模糊语法。

如果团队里已经有多个环境不一致的情况,最快的排查方法是对比两台机器上关键依赖的版本列表信息。其次是直接在Docker里重建训练环境,把GCC版本、cuDNN版本这些都纳入容器镜像,拒绝"在我机器上能跑"这类说法。

6.2 数据泄漏:最隐蔽也最致命的坑

数据泄漏是个老话题,但每一次遇到它,我依然会头皮发麻。最经典的泄漏场景是:训练集和测试集的划分过于随意,导致同一个样本(或同一个用户的不同记录)同时出现在两边,模型在测试时"作弊",测试指标虚高。更隐蔽的是时序数据中特征里包含未来信息,比如用"当天总销售额"预测"当天是否大促",但总销售额本身就是大促的结果。

我的排查方法很朴素:先做一次高预警的特征审查,把特征按来源分类,凡是"生成时间晚于预测目标时间"的特征全部打上问号。然后做对抗验证:训练一个二分类器,让模型判断某条样本属于训练集还是测试集。如果分类器的AUC显著高于0.5,说明训练集和测试集的分布差异太大,极可能存在泄漏或不合理的划分。

6.3 评估指标与业务目标不匹配:天天优化AUC,业务纹丝不动

这是很多AI项目推进到中期会遇到的瓶颈。算法团队把AUC从0.78优化到0.85,业务方却感觉不到任何变化。原因通常是评估指标选错了。AUC衡量的是模型把正样本排在负样本前面的能力,它和"业务方关心的转化金额、用户满意度"并不是一回事。

我的建议是,从项目启动的第一周就拉着业务方把"业务指标"定义清楚,并且把业务指标嵌套进模型评估流程。比如推荐场景,不只是看模型预测的点击概率,还要上线后看真实点击率、停留时长、GMV变化。算法指标用于模型迭代过程中的快速反馈,业务指标用于判断模型是否真的给业务带来价值。

最后说说我的一点体会

从零开始搭一套AI工程体系,最累的不是某个单点技术难,而是要同时盯住数据、训练、部署、监控各个环节,还要敢于承认"模型离线效果好"和"业务上能用"之间的鸿沟。我做过十几个项目之后才真正理解,AI工程不是为了把某个指标刷到最高,而是为了让模型在真实环境中稳定、可靠、可追溯地产生价值。

如果你现在正好要起步,我最后还有一个很具体的建议:别急着引入一大堆花哨的MLOps工具,先把一条最简链路手工跑通——数据文件放在固定位置、训练脚本统一入口、模型版本编号留在服务配置里、监控表格每天跑一次。这套基础链路稳定了,再逐步加工具和自动化。工程这件事,永远是先有秩序,再谈效率。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进