我大概花了一年时间,才把“ai-engineering-from-scratch”这条路完整走通。这个英文标题直译过来就是“从零开始做AI工程”,但真正上手之后你会发现,它讲的根本不是“从零手写一个神经网络”,而是怎样把一个看起来能用的模型,变成一套长期稳定、能迭代、能上线、出问题能查根因的系统。这篇文章就围绕我实际搭建AI工程体系的过程展开,把技术路线、选型逻辑、线上实操和踩坑记录一次性整理出来。适合正在从算法岗往工程化方向过渡的同学,也适合已经拿模型做了两三期POC、但每次上线都手忙脚乱的团队参考。
“从零开始”这四个字其实最容易误导人。很多人以为要从头搭一套分布式训练平台,才叫AI工程。但以我这一年落地的经验来看,对大多数中小团队来说,从零开始意味着两个更实际的目标:第一,把现有模型用工程手段完整地包起来,让它能稳定对外提供服务;第二,把数据、实验、部署、监控这些环节串成一条标准流水线,不用每次上新模型都重新发明一遍流程。理解了这层含义,你才不会在一开始就被各种重型工具带偏方向。
1. AI工程的全局图景:先弄清楚它到底在解决什么
1.1 AI工程师和算法工程师,分工差异比想象中大
我见过太多团队在这两个岗位的边界上吵架。算法工程师的核心目标是模型精度:在离线评测集上把AUC、F1、召回率这些指标做上去,工作是实验性的、探索性的。AI工程师则完全不同,他关心的是模型上线之后还能不能稳定跑,接口响应快不快,数据分布变了模型会不会悄悄掉点,以及新模型发布后能不能在半小时内回滚。
这就像一家餐厅,算法工程师是研发新菜的厨师,注重菜品的口味和创意;AI工程师则是负责整个后厨运转的运营者,需要考虑供应链、出餐速度、食材新鲜度,以及今天客人多了会不会导致出菜顺序混乱。一家餐厅如果只靠厨艺高超的厨师,却没有一套稳定的后厨流程,客人一多就会崩。AI项目也是如此——模型精度再高,线上接口一压就超时、数据一漂移就掉点,业务方照样不买账。
从我个人的实践来看,AI工程最核心的价值,就是把“模型效果”这个波动很大的东西,装进一套确定性很强的系统里。模型推理是其中最具不确定性的环节,所以工程手段要做的,就是给它加上约束:标准化的输入输出格式、可复现的环境、可观测的运行状态、可回滚的版本策略。这样即便模型本身出了问题,系统也不会整体失控。
1.2 从零到一需要掌握的技术栈全景
“ai-engineering-from-scratch”如果要对应一份技能清单,大致可以分为四个层面。数据层面关注的是数据从哪来、如何校验、版本如何管理;模型层面关注的是实验如何追踪、参数如何复用、模型文件如何注册;服务层面关注的是推理接口怎么封装、并发怎么处理、容器怎么构建;运维层面关注的是日志怎么采集、指标怎么监控、模型漂移怎么发现。
把这四个层面落到具体工具上,我目前用得比较顺手的一套组合是:Python为主语言,Pandas和SQL处理数据,DVC管理数据和模型版本,MLflow做实验追踪和模型注册,FastAPI提供推理服务,Docker负责环境一致性,Prometheus加Grafana做基础监控,再配合一套简单的漂移检测脚本处理数据分布变化。这套组合并不复杂,但胜在每一样都能快速上手、出问题能很快定位,完全不需要一开始就上Kubernetes或者完整的MLOps平台。
很多人问我要不要直接学全套Kubeflow或Airflow,我的建议是暂时不要。学习成本高、运维成本更高,对于第一个从零到一的工程化项目来说,单机Docker加脚本编排足够解决问题。先把流程跑通,再根据瓶颈逐步引入重型工具,这才是务实的路径。
2. 从数据处理到模型部署:技术选型背后的真实考量
2.1 数据处理:别小看第一公里,它决定后面的一切
AI工程里最容易被轻视的就是数据处理环节。很多人以为训练模型才是核心,结果数据问题动不动就让你排查一整晚。我在这部分踩过的坑比模型本身的坑多得多,比如训练环境和生产环境读取同一张表,因为字段类型不同导致模型推理结果完全不同;再比如特征列的顺序在生产环境悄悄变了,模型却没有任何报错,只是结果变得莫名其妙。
数据处理层面我推荐至少在前期就坚持三件事。第一,特征列的顺序和类型必须在进入模型前做严格校验,用代码检查列名集合和dtype,而不是默认“看起来差不多就行”;第二,数据集和特征集都要做版本管理,DVC会在每次数据变更后生成一份哈希记录,任何时候都能回到训练那一刻的数据状态;第三,在离线训练阶段就要模拟生产环境的数据清洗逻辑,千万别训练一套代码、上线另一套代码。
举个例子,我之前在做复购预测项目时,特征工程里有一项是“用户最近30天下单金额”。这个特征在离线训练时用的是“截至标签当天的数据”,但生产环境一不小心就写成了“截至当前时刻的实时数据”。两者时间口径差一天,线上预测结果就完全失真。这种问题只有靠严格的代码复用和清晰的流程定义才能避免,靠人盯人检查是盯不住的。
2.2 模型实验追踪:用MLflow而不是自己写JSON
刚开始做工程化时,我也会用JSON文件记录实验参数和指标。实验几次还好,超过二十次之后就疯了:目录里全是一堆“exp_0317_v2_final”文件夹,根本分不清哪个实验用了什么参数、对应的模型文件是哪一个。后来我咬牙把MLflow引进来,才真正体会到实验追踪工具的价值。
MLflow最核心的功能有三个:一是自动记录每次运行的参数、指标和代码版本,二是把训练产物(模型文件、预处理逻辑)统一注册到一个模型仓库里,三是提供一套模型服务API,可以和FastAPI无缝衔接。引入之后,我再也不需要在文件夹里翻来翻去找“上一次最好的那个模型”,输入一条命令就能查到所有实验的对比曲线,然后挑出最优的版本直接进入上线流程。
比较关键的使用习惯是:每次训练都要把训练集和验证集的具体版本号(DVC的哈希值)也记录到MLflow里。否则就会出现数据已经更新过几个版本,你却忘了当时的模型是在哪个版本的数据上训练的,导致复现不出来的尴尬情况。好的实验追踪不是只记参数,而是把环境、数据、代码、产物全部绑定在一起。
2.3 服务化部署:为什么选FastAPI做推理入口
模型部署最朴素的需求,是把训练好的模型暴露成一个HTTP接口,让业务系统调用。这一步可选框架不少,Flask、FastAPI、Django都能做,但我最终选择了FastAPI,主要原因是它天生支持异步,对高并发的推理请求有更好的吞吐表现;自带OpenAPI文档,可以快速测试接口;还有完善的请求校验机制,会用Pydantic自动验证输入格式。
在服务化部署时,有几个细节特别值得注意。模型最好在服务启动时加载一次,放到全局对象里复用,而不是在每次请求时重新load——前者响应时间在毫秒级,后者可能要好几秒。推理函数里要避免对传入的numpy数组做原地修改,否则多个并发请求会互相污染数据。还有一点,接口返回结构尽可能保持稳定,至少要包含模型版本号、推理结果、耗时这几个字段,这样后期排查线上问题会方便得多。
我通常的做法是:服务启动时从MLflow模型仓库拉取指定版本的模型,加载到内存;对外暴露一个/predict接口,输入JSON格式的特征数据;内部完成数据校验、特征对齐、推理、结果包装四步;最后顺手把每次请求的日志写入结构化文件,方便后续做监控和分析。这套模式既简单又稳定,完全适配绝大多数中小规模的AI应用场景。
3. 一天上线一个可用模型服务:核心实操全记录
3.1 先选准场景:以“复购预测”为例
纸上谈兵再多,不如直接跑一个完整项目。为了演示“ai-engineering-from-scratch”,我以电商复购预测为例:根据用户过去一段时间的行为数据,预测未来七天内用户会不会再次下单。这是一个典型的二分类问题,数据量不大、业务含义清晰,非常适合用来展示完整工程链路。
定义清楚业务问题之后,第一步是确定标签。我取“预测窗口”为7天,用户如果在窗口内有任意一笔有效支付订单,标签记为1,否则为0。这里有一个关键细节:训练数据必须采用“时间点切分”,而不是随机切分。否则用未来数据训练、过去数据预测,会产生数据泄露,离线指标看起来很好看,线上效果却一塌糊涂。具体操作上,我用某一天作为切分点,之前的数据构建特征,之后七天的数据打标签。
评估指标也要想清楚。复购预测中正负样本往往不平衡,直接看准确率没有意义。我选择AUC作为主指标,兼顾召回率和精确率;同时从业务角度关注提升度,对比模型筛选出的高潜用户和随机用户的实际复购率差异。这些指标要提前定义好,并且写进评估脚本,而不是上线之后再想起来做对比。
3.2 训练加记录:一套可以原样复用的代码骨架
下面这段训练逻辑,是我在实际项目中提炼出的骨架,换数据集基本都能直接套用。核心思路是:先加载数据,再做特征工程,然后切分样本,训练LightGBM模型,并把这次实验的所有信息记录到MLflow。
import pandas as pd import lightgbm as lgb from sklearn.model_selection import train_test_split import mlflow # 1. 读取数据 df = pd.read_csv("user_features.csv") # 2. 特征工程:新增时间窗口特征 df["days_since_last_order"] = (pd.Timestamp("2024-06-01") - pd.to_datetime(df["last_order_time"])).dt.days # 3. 切分样本:按时间点切分,避免数据泄露 train_df = df[df["feature_date"] < "2024-06-01"] test_df = df[df["feature_date"] >= "2024-06-01"] X_train = train_df.drop(columns=["label"]) y_train = train_df["label"] X_test = test_df.drop(columns=["label"]) y_test = test_df["label"] # 4. 启动实验追踪 with mlflow.start_run(): params = { "objective": "binary", "learning_rate": 0.05, "max_depth": 5, "n_estimators": 500 } # 记录参数 mlflow.log_params(params) # 训练模型 model = lgb.LGBMClassifier(**params) model.fit(X_train, y_train, eval_set=[(X_test, y_test)], eval_metric="auc", callbacks=[lgb.early_stopping(50)]) # 记录指标 y_pred = model.predict_proba(X_test)[:, 1] auc = lgb.metric._eval_auc(y_test.values, y_pred) mlflow.log_metric("test_auc", auc) # 记录数据版本,方便回滚定位 mlflow.log_param("data_version", "20240601_v2") # 注册模型 mlflow.lightgbm.log_model(model, artifact_path="model")这段代码看起来简单,但每个环节都有讲究。feature_date字段承担了时间点校验的功能,训练数据和测试数据严格按时间隔离;MLflow记录data_version这一步,是为了日后排查“某次实验效果波动是因为数据更新还是代码变更”这类问题;LightGBM的早停机制则保证模型在验证集AUC不再提升时及时停止,避免过拟合。
实际运行时,你可以用mlflow ui启动一个本地看板,在浏览器里直观看到所有实验的AUC对比和参数差异。当你跑过几十次实验之后,回头看这个看板,就会理解为什么我说实验追踪是AI工程的刚需,而不是可选的加分项。
3.3 容器化部署:把模型装进一个不会“水土不服”的盒子里
训练好模型只是第一步,更关键的是让它能在任何机器上一键启动。容器化是解决环境一致性问题的最直接手段。下面是一个典型的Dockerfile,把FastAPI服务、模型文件、依赖库全部打包在一起。
FROM python:3.10-slim WORKDIR /app # 先安装依赖,充分利用缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制模型文件和服务代码 COPY model/ ./model/ COPY app/ ./app/ EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]requirements.txt里有一个极其重要的原则:所有依赖库都要锁定精确版本号,至少精确到小版本。我曾经因为scikit-learn从1.2.2升到1.3.0,导致一个决策树模型的特征顺序行为发生变化,线上预测结果和本地验证完全对不上。锁版本看起来是个小动作,却能省掉后续无数个掉头发的夜晚。
服务端的代码我用FastAPI实现,核心逻辑是把MLflow保存的模型加载进内存,然后定义预测接口。这里有个性能注意点:模型文件如果比较大,而服务又开了多个worker进程,每个进程都会加载一份模型副本,内存会成倍消耗。所以当我需要在生产环境做并发扩容时,会更倾向于用单worker加异步批处理的方式,而不是简单粗暴地多开进程。
3.4 给部署后的模型装上“眼睛”:日志和监控
模型上线只是开始,能不能持续稳定地服务,取决于你有没有建立一套可观测体系。我的做法分为三个层次:第一层是基础设施监控,用Prometheus抓取CPU、内存、接口延迟等基础指标;第二层是模型服务日志,每次请求都记录模型版本、输入特征摘要、预测结果、耗时;第三层是数据分布监控,周期性对比线上输入特征和训练时特征的分布差异。
日志记录这一层最简单,但最容易被人忽略。很多团队上线后才发现线上数据分布已经和训练数据差得很远,却完全不知道是从何时开始漂移的,就是因为没有日志。我养成了一个习惯:每个预测请求都记录一个request_id,关联上模型版本号和输入特征的关键统计量。这样一旦业务方反馈“某类用户预测不准”,可以立刻按请求ID抽取出当时的输入数据,定位是模型问题还是数据问题。
数据分布监控则可以用一些统计指标来判断。比如连续特征可以计算均值、方差的变化,分类型特征可以看类别占比的偏移。更专业的做法是计算PSI(群体稳定性指标),当PSI超过一定阈值时触发告警。这不是一个需要很复杂工具才能解决的问题,一段简单的Python脚本定期跑一下,就能帮你发现绝大多数数据漂移风险。
4. 我踩过的那些坑:问题排查与避坑实录
4.1 环境不一致导致的“一模一样的代码,不一样的结果”
这是我遇到最多的问题类型,也是AI工程新人最容易踩的雷。本地训练环境和线上部署环境用的依赖版本不同,哪怕只差一个Pandas小版本,特征处理的默认行为就可能悄悄变化。更隐蔽的是,joblib保存的模型文件里记录着sklearn的类路径,一旦线上环境的类路径对不上,模型甚至可能加载失败。
我的解决方案是双管齐下:第一,所有依赖精确锁版本,并且写在同一份requirements.txt里;第二,部署统一走Docker镜像,让训练环境和推理环境尽量使用同一个基础镜像。如果你的团队已经有CI/CD流程,就把镜像构建放在流水线里,确保每次部署用的镜像和验证过的镜像一致。这个方法听上去朴素,却是解决环境类问题的最可靠路径。
4.2 数据漂移悄悄发生,模型掉点却毫不知情
有一个项目完成了上线,初始效果不错,可一个月后业务方反馈推荐准确率明显下降。我查了很久代码和基础设施,发现都不是原因,最后对比历史输入数据和训练数据才确认:用户活跃度的分布发生了明显偏移,大量高活跃用户变成了低频用户,模型在这样的新分布上表现自然大打折扣。
这个教训让我彻底意识到,监控数据分布和监控系统指标同等重要。我在每一条预测请求日志里增加了特征摘要,每周跑一次分布对比脚本,把连续特征的均值、标准差、分位数和分类特征的占比变化输出成报表。只要发现异常,就立即启动数据分析流程判断是临时波动还是持续性漂移。宁可让数据问题尽早暴露,也不要等业务投诉之后再去补救。
4.3 并发上来之后,接口延迟和内存双双失控
还有一次让我印象非常深刻的上线事故:模型刚上线时请求量不大,一切正常;后来接入核心业务后QPS暴涨,接口延迟从100毫秒飙升到10秒,内存也持续飙升,最后服务直接OOM挂掉。排查下来有两个原因:一是模型加载在了全局对象里,但FastAPI每个worker都会复制一份模型,内存消耗成倍增加;二是推理前对输入数据做了一些低效的类型转换,导致CPU计算阻塞。
解决思路包括:降低worker数量、提高单worker的异步处理能力;对输入特征做统一的缓存和预转换;如果模型较大,用ONNX Runtime做推理加速,把单次推理耗时控制到毫秒级。另外,在接口外层加一层简单的超时控制,避免上游服务因为下游拖慢而被整体拖垮。这类性能问题通常不会在测试阶段暴露,所以上线前一定要做简单的压测,哪怕只模拟两倍峰值流量也能发现大量隐患。
4.4 常见问题速查表:三年踩坑浓缩成一张表
| 现象 | 常见原因 | 排查方向 | 解决建议 |
|---|---|---|---|
| 本地推理正确,容器内结果异常 | 依赖版本不一致 | 检查requirements与镜像版本 | 锁定精确版本,统一训练/部署镜像 |
| 接口首次请求很慢,后续变快 | 模型首次加载耗时 | 查看日志中的首次延迟 | 启动时预热,提前加载模型到内存 |
| 并发升高后大量超时 | 同步阻塞/模型推理太慢 | 压测定位耗时环节 | 异步化、批处理、ONNX推理加速 |
| 线上效果逐步下降,系统无异常 | 数据漂移 | 对比输入和训练分布 | 建立PSI监控和周期分布报表 |
| 新模型上线后想回滚但找不到版本 | 模型未注册管理 | 查看模型仓库历史版本 | 所有模型统一注册到MLflow |
| 多个请求互相干扰导致结果错乱 | 原地修改了输入数组 | 检查推理函数内部逻辑 | 输入数据先copy再处理 |
| 内存持续上涨最终OOM | 多worker加载多份模型 | 查看进程数和内存占用 | 减少worker或改共享内存方案 |
5. 从零到平台化:这套体系还能怎么继续生长
5.1 从离线模型到在线学习:让系统感知变化
当你的API服务稳定运行一段时间后,自然会遇到新的问题:模型无法适应快速变化的业务环境。比如电商大促期间用户行为模式明显改变,基于旧数据的模型会系统性失灵。这时就可以考虑引入在线学习和定期重训机制。
但我的建议依然是从低成本方案起步。先用定时任务每周或每天重新训练一次模型,配合监控系统确认模型效果是否下滑。如果重训成本太高或效果不明显,再考虑增量更新。这里有个关键经验:重训用的数据分布也要纳入监控,不然你今天拿到的“最新数据”可能本身就是有偏的,重训出来的模型只会让问题更严重。平台化的每一步都要有监控和可回滚兜底,才能保证整体稳定。
5.2 完善发布策略:从全量发布到灰度发布
如果只有一个模型服务,直接全量更新问题不大。但当你维护多个模型、多个版本并行时,就必须引入灰度发布和回滚机制。我的做法是在服务配置里支持模型版本号动态切换:先让5%的流量打到新版本模型上,观察核心业务指标是否正常;再逐步扩大流量比例;如果发现问题,一键切回旧版本。
灰度发布的前提是,每次发布都保留上一个版本的可直接调用接口。这里不得不再提一次模型注册的重要性,把每个版本的模型连同其数据版本、指标、发布时间记录在案,才可能在关键时刻快速决策:应该回滚到哪个版本,回滚后风险有多大。这套流程看着简单,却是从“能用”走向“平台化”的分水岭。
最后说一点个人体会。做AI工程这件事,最大的收获不是学会了几种工具,而是养成了一种系统思维:看问题不再只盯着模型精度,而是看整体系统是否可控、可追溯、可回滚、可观测。“ai-engineering-from-scratch”这条路真正走通之后,你会发现模型只是整个体系里很小的一部分,数据、环境、服务、监控加起来才是那个让AI真正创造价值的完整引擎。建议所有想入门的同学,少纠结框架,多动手把一个项目完整地上线跑一遍,踩过的坑会让你成长得比看一百篇教程都快。