说实话,我第一次听到「AI工程师」这个称呼的时候,自己先在心里打了个问号——这不就是调模型的人吗?但真正扎进去做了几年之后才发现,ai-engineering 跟我们对 AI 的浪漫想象完全是两码事。它不是写几行代码、跑一个训练脚本那么简单,而是一整套从数据、训练、评估、部署到长期维护的流水线工程。这篇文章想聊的,就是我从零开始摸索 ai-engineering 的完整路线、技术栈选型,以及那些在文档里查不到、只能靠踩坑换来的经验。
这套东西适合谁?两类人。第一类是刚入门的开发者,知道 Python、懂点基础算法,但面对「AI 工程化」这个词一头雾水;第二类是已经在跑模型、但总觉得项目停留在 notebook 阶段的同学,想把自己的工作流变得能复现、能上线、能维护。无论你属于哪一类,看完这篇文章应该能理出一条清晰的实践路径。
1. 先搞清楚 AI 工程和「调模型」到底差在哪
1.1 为什么那么多课程学完,你依然做不出能用的东西
市面上很多课程的问题在于:教的是「模型」,不是「工程」。它们的核心内容是各种网络结构、损失函数、优化器,最后用 MNIST、CIFAR 之类整理好的数据集跑一遍训练,准确率到了 99%,教程就结束了。但现实中没有任何项目会给你一个干干净净的数据集。
真实的项目长什么样?数据分散在十几个 Excel、数据库表和日志文件里,格式混乱、时间字段对不上、同一用户的 ID 在不同表里还不一样。等你把数据理顺,模型选型、调参的时间可能只占整个项目的两成都不到。ai-engineering 的本质,是用工程手段解决 AI 落地中的稳定性、可复现性和效率问题,不是让模型在测试集上好看。
打个比方,课程里教的是怎么烤一个漂亮的蛋糕,而工程化要求的是:你有一套标准配方,换任何一台烤箱都能烤出同样品质的蛋糕;你有质量检测流程,烤糊的批次能被及时发现;你还有配送和售后系统,蛋糕交到客户手上还是完好的。模型训练只是其中最闪亮但最不费力的一环。
1.2 AI 工程师真正每天在解决的四类问题
我自己的经验里,AI 工程化的工作可以归纳成四个词:数据、速度、稳定、协作。
数据问题是永远的主角。数据缺失怎么补、异常值怎么处理、分布漂移怎么检测,这些听起来不性感,但每一个都能让项目翻车。速度问题指的是从试验到上线的迭代效率,你做一个实验要多久?改一个特征要重新跑全量数据吗?稳定问题更直接,模型今天效果不错,明天线上效果是否还一样?用户行为一变,模型会不会失灵?协作问题则是多人开发时,模型训练的重现性、代码和数据的版本管理。
这四个问题,任何一个没有处理好,模型再好也白搭。而它们恰恰不是深度学习理论课程会教的东西。
2. 从零开始的技术栈怎么选
2.1 环境管理:别在第一步就把自己的机器搞坏
很多新手的第一课其实是「环境地狱」。我见过太多人 pip install 装了一堆包,最后 pandas、numpy、torch 各自依赖的版本互相冲突,干脆重装系统。做 ai-engineering 的第一课,不是学什么算法,而是学会用虚拟环境把项目隔离起来。
我早期的方案很简单:项目一开始就建一个独立的虚拟环境,所有依赖写进 requirements.txt 或者 environment.yml。每次装新包之前先想想是不是真的要装,别像逛超市一样想到什么拿什么。
# 创建一个项目专属环境,指定 Python 版本 conda create -n ai-eng python=3.10 conda activate ai-eng # 安装核心依赖 pip install numpy pandas scikit-learn torch torchvision matplotlib如果后来需要复现一个旧项目,conda 的 environment.yml 能把整个环境(包括 Python 小版本)原样恢复,这是 venv 很难做到的。我强烈建议把 conda 作为环境管理首选,虚拟环境这件事上多花十分钟,后面能省两天。
2.2 优先掌握 PyTorch,必要的话再补一个框架
框架选型是我被问得最多的问题之一。我的回答是:除非你有特殊理由,否则上来就学 PyTorch。整个生态对研究者太友好了,动态图调试直观,大量预训练模型和论文代码都以它为准。TensorFlow 有自己的优势,尤其是在生产部署和移动端,但对从零开始的人来说,PyTorch 的学习曲线更平缓、社区资源更集中,遇到问题搜一下基本都有现成答案。
真正要避免的是两个框架同时学。我见过不少人在 TensorFlow 和 PyTorch 之间反复横跳,今天看这个教程用 TF,明天那个用 PyTorch,结果两个都学得不深。先把 PyTorch 吃透,再回头理解另一个只是 API 迁移问题。
框架只是工具,重要的是你理解其中的核心抽象:张量、自动求导、优化器、数据集与 DataLoader。这些概念在任何框架里都是相通的。
2.3 工程工具链:Git、Docker、CI/CD 一个都不能少
AI 工程师首先得是合格的软件工程师,这话一点不夸张。Git 不用说了,任何项目都需要。Docker 很多人觉得是后端的事,但等你需要复现别人环境的时候,一个 Dockerfile 比十页文档都有用。CI/CD 在 AI 项目里通常体现在自动化测试和训练流程里——代码合并前先跑一遍静态检查,数据变化后自动触发评估脚本。
我自己的标准工具链是:Git + conda + Docker + MLflow + DVC,再加一个 CI 平台(GitHub Actions 或 GitLab CI 都行)。这套组合解决了我日常 90% 的工程化需求。别急着一次全上,按项目需要逐步加,但 Git 和 conda 是第一天就要用的。
3. 数据工程:AI 项目里最被低估的 80% 工作量
3.1 数据获取与清洗的实操细节
数据清洗听起来枯燥,但它的产出质量直接决定模型的上限。垃圾进,垃圾出这句话我在每次分享时都要重复。具体到操作层面,我总结了一套自己的标准流程。
第一步是探索性分析。拿到任何数据集,先别急着建模,用 pandas 跑一下 describe、info、isnull().sum(),看数值分布是否合理,看缺失值比例,看时间字段是否连续。第二步是处理缺失值。数值型列我一般用中位数填充而不是均值,因为中位数对异常值不敏感;类别列用众数填充,或者单独标一个 unknown 类别。第三步是处理异常值。不要一刀切地删,先把超出 3 倍标准差的样本列出来人工看看,很多「异常」其实是有业务含义的。
import pandas as pd import numpy as np df = pd.read_csv("raw_data.csv") print(df.info()) print(df.describe()) # 缺失值处理:数值列用中位数,类别列用众数 for col in df.select_dtypes(include=[np.number]).columns: df[col] = df[col].fillna(df[col].median()) for col in df.select_dtypes(include=["object"]).columns: df[col] = df[col].fillna(df[col].mode()[0])一个容易踩的坑是train/test 数据要一起清洗,或者用同一套统计量。如果你先对训练集填充了中位数,测试集也必须用训练集算出来的中位数填充,不能单独算,否则就是数据泄漏的一种特殊形式。把这个写成 Pipeline,每次预测都走同一套流程。
3.2 特征工程:把原始数据变成模型能理解的形态
特征工程是很多教程一句话带过、但实际效果最明显的地方。我见过单纯做一组设计良好的特征,效果直接超过换一个更复杂的模型。具体到实操,有三类特征值得重点关注。
时间特征是最容易被忽视的。一个交易时间字符串,你可以拆出星期几、是否周末、一天中的哪个时段,这些信息对用户行为预测极其有效。顺序特征也一样,用户是第几次访问、距离上次访问间隔多少天,这些隐含的行为节奏是静态特征捕捉不到的。还有聚合特征,比如用户过去七天的平均消费金额、最大消费金额、消费次数,这些能刻画用户的短期活跃度。
做特征工程有一条痛苦的经验:每一个新特征,都要同步更新清洗和验证的代码。特征不是拍脑袋加进去就完事,你得能回答新特征的分布是什么、缺失率多少、对模型效果是否有正向贡献。我之前习惯用一个 feature_list.py 文件集中定义所有特征,再配合一份特征的说明文档,这样半年后回头看还能记得每个特征当时为什么这么设计。
3.3 用 DVC 把数据版本管起来
Git 管代码,DVC 管数据。为什么要管数据版本?因为模型的复现不只是复现代码,还要复现训练时的数据。你改了一个清洗逻辑,整个数据集就可能变了,模型结果自然不同。如果不记录数据版本,调了三个月参之后回头想对比基线,你根本说不清当时用的是哪份数据。
DVC 的基本用法不复杂。初始化之后,用dvc add跟踪数据文件,它会生成一个 .dvc 文件和对应缓存,把 .dvc 提交到 Git 就相当于给数据打了一个版本标签。配合云存储(S3、OSS、或者本地共享盘),团队里每个人都能拉取同一份数据。
dvc init dvc add data/raw/train.csv git add data/raw/train.csv.dvc git commit -m "add train data v1"DVC 的使用心得只有一个字:早。项目第一天就建立数据版本管理,比项目半年后再补要轻松得多。数据是 AI 工程的原油,原油的品质和来源必须可控。
4. 训练迭代的工程化:让模型「可复现」比「跑得快」更重要
4.1 实验追踪:MLflow 的配置与使用
训练实验的次数多了之后,你会发现自己陷入了「参数地狱」:学习率 0.001 的模型跑过、0.0001 的也跑过,哪个效果更好?当时用了什么 Batch Size?数据是什么版本?如果你靠脑子记,三天之后一定乱套。
MLflow 是解决这个问题的主力工具,我用的是它的 Tracking 组件。每个实验记录下超参数、指标、代码版本、数据版本,以及最重要的——模型产物本身。用法很简单:
import mlflow import mlflow.pytorch with mlflow.start_run(): mlflow.log_param("learning_rate", lr) mlflow.log_param("batch_size", batch_size) mlflow.log_metric("val_accuracy", val_acc) mlflow.pytorch.log_model(model, "model")之后就形成了一个天然比较平台,跑过的实验全在里面躺着,谁好谁坏一目了然。配置 MLflow 本身只需要一个 tracking 地址,本地跑就直接默认路径,团队协作时部署一个 MLflow Server 就行,半小时的事。
这里有一个容易踩的坑:一定要为每个项目设置独立的 experiment_name。MLflow 默认什么实验都堆在同一个列表里,上百次实验混在一起想找指定项目的记录,体验极其痛苦。我现在的规范是实验名的格式固定为项目名/数据集版本/主干功能,保证所有可追溯信息在名字里就能看到。
4.2 超参数调优的正确姿势
很多人调参还是靠手动试,试个几十组全凭心情。工程化的做法是写一个自动化搜索脚本,配合网格搜索或贝叶斯优化,把整个搜索过程也记录下来。
我的做法是先用随机搜索跑一批宽广范围,锁定有希望的区间,再在这个区间里跑贝叶斯优化精调。Optuna 是我用得最多的库,它的 API 非常顺滑:
import optuna def objective(trial): lr = trial.suggest_float("lr", 1e-5, 1e-2, log=True) batch_size = trial.suggest_categorical("batch_size", [16, 32, 64]) dropout = trial.suggest_float("dropout", 0.1, 0.5) # 训练模型并返回验证集损失 return val_loss study = optuna.create_study(direction="minimize") study.optimize(objective, n_trials=50)调参真正重要的是保证其他条件不变。每改一个超参数,其他一切(数据版本、数据划分方式、模型结构、随机种子)都必须锁死,否则你根本不知道效果提升是哪一个改动带来的。这也是实验追踪工具存在的原因。
4.3 训练代码的结构化写法
Notebook 适合做探索,但不适合做工程。到了工程化阶段,训练代码应该是一个结构清晰的项目目录,任何一个新同事 clone 下来都能跑。
下面是我个人比较常用的目录模板:
project/ ├── configs/ # 配置文件(yaml),记录超参数 ├── data/ # 原始数据与处理后数据 ├── src/ │ ├── dataloader.py # 数据加载与预处理 │ ├── model.py # 模型结构定义 │ ├── train.py # 训练脚本 │ ├── evaluate.py # 评估脚本 │ └── predict.py # 推理脚本 ├── experiments/ # MLflow 实验记录 ├── notebooks/ # 探索性分析,仅用于试验 └── requirements.txt训练脚本本身要支持命令行参数输入,然后跟 configs 里的 yaml 文件配合。这样每次实验的完整配置都可以被记录下来,训练、评估、推理三个入口保持分离,互不干扰。我还有个习惯:模型定义文件里不写任何业务逻辑,只接受明确的输入输出维度,这样换任务时模型代码可以直接复用。
5. 部署与监控:模型做完只是开始,上线才是考验
5.1 用 FastAPI 快速包装推理服务
模型训练完,不能就这么躺在 .pt 文件里。你得把它变成别人能调用的服务。FastAPI 是我用过最顺手的模型服务框架,性能好、自带接口文档,更重要的是它对异步和类型检查的支持很友好。
一个最简单的推理服务,核心代码大约几十行:
from fastapi import FastAPI from pydantic import BaseModel import torch app = FastAPI() model = torch.load("model.pt", map_location="cpu") model.eval() class InputData(BaseModel): feature_a: float feature_b: float timestamp: str @app.post("/predict") async def predict(data: InputData): # 注意:必须走与训练时相同的特征处理流程 features = preprocess(data) with torch.no_grad(): pred = model(features) return {"prediction": pred.item()}需要注意的细节:加载模型放在了模块导入阶段而不是每次请求时加载,因为模型加载是耗时操作,放请求里会严重影响响应时间。推理时务必包在torch.no_grad()里,既省显存又提速。
5.2 Docker 化部署的关键细节
把服务容器化,是让模型能在任何环境稳定运行的前提。Docker 解决了最容易出问题的依赖一致性。我推荐使用官方 PyTorch 镜像作为基础镜像,而不是从一个空白 ubuntu 镜像自己装 CUDA、PyTorch,那样既慢又容易踩版本兼容的坑。
FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY ./src /app/src COPY ./models /app/models EXPOSE 8000 CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]构建镜像时,记得通过 .dockerignore 把数据文件、notebook、实验记录排除在外,镜像体积能小一大截。模型文件单独挂载或走对象存储复制,不要打进镜像。这样模型更新时不需要重新构建镜像,只要替换模型产物就能完成升级,部署效率高很多。
5.3 模型上线后监控什么
模型部署后,监控跟模型本身同样重要,因为线上数据分布会漂移。用户行为变了、市场环境变了,几个月前训练的效果可能就不再成立。监控工作至少有三个方面。
预测监控要盯输出的分布变化,比如预测值的均值、分位数是否出现明显偏移。特征监控更提前,对每个输入特征做实时统计,如果某个特征分布突然偏离训练集的范围,就是数据漂移的信号。效果监控在最上层,如果业务指标(比如点击率、转化率)持续下滑,说明模型实时效果在退化。
我建议把核心监控指标接入现有的告警系统,一旦指标超过阈值就触发告警,同时把预测样本存下来留作复盘数据。线上效果永远以业务指标为准,不是以离线评测集为准,这个观念必须一开始就建立起来。
6. 一条完整的从零实践路径(附踩坑记录)
6.1 第一个项目怎么选
很多人想直接复现大模型,但大模型项目重、坑多、资源消耗大,不适合用来建立工程化体系。第一个项目,我推荐选一个小但完整的任务,比如预测用户次日是否活跃(二分类),或者预测商品销量(回归)。这类任务数据集小、业务含义清晰、评估指标直观,可以在几周内走完整个 ai-engineering 流程:从数据获取、清洗、特征工程、训练、实验追踪,到部署、监控。
6.2 端到端迭代过程实录
我当时做的第一个完整项目是一个小型销量预测系统。数据来自内部数据库的一张订单表和一张商品表,大概也就几千行,但这个项目让我把整个 pipeline 跑通了。
第一轮迭代花了一周时间做数据清洗和特征工程,写特征代码的时间远超训练代码。最开始训练时,验证集损失怎么都不下降,排查了半天发现是学习率设置过大,梯度在震荡。改成学习率调度(先用 warmup 再用余弦退火)之后,训练曲线才稳定下来。
遇上特征泄漏也很有意思。第一次做验证效果极好,但我发现测试集上的预测比真实值系统性偏高。排查后发现是训练时把一个「当周发生的事件」类特征混进去了,而这个特征在预测时根本拿不到未来的值。这次教训之后,我的特征定义里就明确注明了每个特征在预测时刻是否可用。
6.3 常见问题与排查技巧速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 训练 loss 不下降 | 学习率过大/过小 | 先试试 0.01、0.001、0.0001 三档 |
| 验证效果差,训练效果好 | 过拟合或泄漏 | 检查特征是否用到未来信息,加大正则 |
| 训练和推理结果不一致 | 数据预处理流程不一致 | 统一封装预处理函数,离线在线共用一套逻辑 |
| 线上预测延迟高 | 模型太大或未用批处理 | 考虑模型量化、减少单次输入量 |
| 发布后指标骤降 | 数据分布漂移 | 对比线上输入分布与训练集分布 |
还有一个最常见也最容易忽略的问题:随机种子。如果你训练代码里没固定随机种子,同样的配置每次跑出来的结果都不一样,你就很难判断一个改动到底有没有效果。我现在的训练脚本里固定了 Python、NumPy 和 PyTorch 三个随机种子,保证可复现性。
最后一个经验是:做 ai-engineering,要习惯跟「不确定」打交道。模型不会每次都有提升,数据也不会刚好都是干净的,线上问题更不会按照文档来。所以我把每一条踩过的坑都记下来,形成自己的一份问题排查手册,遇到相似现象时直接翻,效率比重新查资料高得多。
这个内容后续往深走,还可以加一套自动重训机制,把监控告警和训练流水线串起来,做到模型效果下降后自动触发重训。但那是进阶话题了,先把从数据到部署这条主链路跑通,你已经可以称得上是一个合格的 AI 工程师了。