几年前我给自己定了一个目标:把ai-engineering-from-scratch变成现实。那会儿我对AI工程的理解非常粗暴——把PyTorch官方示例跑通,再套一个Flask接口,就算“会做AI”了。结果第一个真实项目就在生产环境翻车,训练集准确率看着还行,上线后面对真实请求直接崩掉。后来我走了很多弯路,才慢慢理解AI工程不是“模型代码”,而是模型周围那一圈容易被忽略的管线:环境、数据、实验、部署、监控,缺一环都不行。
如果你正准备开始自己的AI项目,或者已经跑过几个模型但卡在“demo能跑、生产不可用”这个阶段,这篇文章就是我一路从零踩过来之后的结构化复盘。我会把每一步为什么这么选、具体命令怎么做、以及那些没人提前告诉你的坑,尽量原原本本写出来。目标只有一个:让你少走我走过的弯路。
1. 先打破幻觉:AI工程不是“写模型”,而是一整套系统工程
1.1 为什么我一开始的“AI项目”全部失败
我做过一个文本分类工具,当时的感觉非常良好:GitHub上克隆BERT示例,改改数据路径,训练几个epoch,准确率92%,然后我用Flask包了一个接口,感觉自己已经是“AI工程师”了。结果这个工具一放到真实业务数据上,准确率直接掉到70%左右。最奇怪的是我本地测试还好好的,上了服务器就变样。
后来排查出三个致命原因,每一个都和模型结构无关:
- 训练数据分布和真实数据分布完全不一致。官方demo用的是新闻语料,我的业务数据是客服工单,词汇、句式、长度都不一样,模型在demo上看到的“经验”在真实场景里基本不适用。
- 代码、预处理、模型版本全都没有记录。我今天改了分词逻辑,明天又换了模型权重,到了复盘的时候根本不知道哪个版本对应哪次实验结果。
- 推理性能完全没有设计。单个请求在CPU上要3秒多,根本没法在生产环境用。
这三个问题,没有一个是“换个更好的模型”能解决的。它们全是工程问题。
1.2 AI工程与AI研究的本质区别:稳定性 > 酷炫
我以前以为AI工程就是AI研究的一个子集,后来发现这两个领域的目标差异很大。
AI研究追求在公开数据集上的指标上限,算法创新是核心;AI工程追求的是在真实、受限、多变的环境里,让模型稳定、可控、可住地输出结果。工程里的“住”包括可复现、可监控、可回滚。
我做过一个不算恰当的类比:研究像是实验室里造一辆能跑500km/h的赛车,工程则是造一辆在城市里每天通勤、耐造、不出毛病的家用车。赛车很酷,但大多数时候我们需要的是后者。
所以我给自己的第一条原则是:把对“新模型”的痴迷先压一压,把注意力放到模型之外的管线上。管线不牢,模型再炫也没用。
2. 从零搭建AI工程的第一步:环境与项目骨架
2.1 我最终选定的技术栈:Python、PyTorch、MLflow、Docker
技术选型的原则是“先用最主流、最成熟的东西”,不要在起步阶段追求独特。我的最终选择是:
| 组件 | 作用 | 为什么选它 |
|---|---|---|
| Python 3.10+ | 主语言 | 生态最全,AI相关库基本都围绕它 |
| PyTorch | 深度学习框架 | 动态图调试友好,社区活跃,自定义模型心智负担低 |
| MLflow | 实验跟踪与模型管理 | 能记录参数、指标、产物,复现实验的标配 |
| Docker | 环境打包 | 解决“我本地能跑,你环境跑不了”的核心矛盾 |
| DVC | 数据版本控制 | 数据集和代码一样需要版本化追踪 |
为什么不选TensorFlow?不是它不好,而是对于工程调试来说,PyTorch的“运行时才建图”方式让我更容易定位问题。你如果更熟悉TensorFlow也没问题,关键在于把整个工程管线搭起来,而不是纠结框架本身。
2.2 用Cookiecutter搭建可复制的项目模板
很多新手拿到数据就开始写train.py,写到一半文件路径乱飞,连自己都找不到昨天生成的结果。我的建议是先搭一个可复制的项目骨架,推荐用Cookiecutter。
安装:
pip install cookiecutter然后可以用现成的AI项目模板,比如:
cookiecutter https://github.com/drivendata/cookiecutter-data-science它会生成一个包含关键目录的结构,我最终沉淀下来的是这样:
. ├── configs/ # 项目配置,yaml或json ├── data/ │ ├── raw/ # 原始数据,只读不修改 │ ├── processed/ # 清洗后数据 │ └── external/ # 外部引入数据 ├── notebooks/ # 探索性分析,不参与线上流程 ├── src/ │ ├── data/ # 数据处理代码 │ ├── features/ # 特征工程 │ ├── models/ # 模型定义和训练 │ └── api/ # 推理服务 ├── models/ # 产出的模型文件 ├── tests/ # 测试代码 └── Makefile # 一键执行常用流程这个结构最重要的原则是:数据、代码、产物分离。你不会在改代码的时候误伤数据,也不会在训练完不知道该把模型文件放到哪里。
2.3 环境依赖管理的坑:conda、pip、poetry该怎么选
环境管理是第一个容易翻车的地方。我早期直接用pip freeze > requirements.txt打包环境,后来发现这个文件里混了很多不该锁的间接依赖,换一台机器装完经常起冲突。
后来我采用的方案是:
- 用conda管理基础环境(Python版本、CUDA、系统级库);
- 用poetry管理项目级别的依赖,并提交
poetry.lock文件。
创建环境:
conda create -n ai-engineering python=3.10 conda activate ai-engineering pip install poetry poetry init poetry add torch transformers fastapi uvicorn mlflow dvc为什么非要用poetry?因为poetry.lock会把所有依赖的精确版本和传递依赖都锁住。只要有这个文件,任何人在任何机器上执行poetry install,得到的环境都和我一样。conda也能锁,但它的环境解析和跨平台支持有时候会让人抓狂。
注意:不要只写一个requirements.txt然后手动维护。我第一次项目就是手动维护,结果某次升级torch之后,模型结果变了但没人知道,查了三天才发现是依赖版本变了。
3. 数据工程:模型好不好,早在数据处理时决定了
3.1 拿到原始数据后我做了什么(清洗、去重、格式归一)
很多AI项目的时间分配里,数据整理占掉的精力比模型训练多得多。我自己的经验是,一个项目里50%以上的时间都会花在数据上,不是在“玩数据”,而是在“让数据变成模型能吃的样子”。
我拿到原始数据后的固定流程:
- 缺失值处理:先看每一列缺失比例,缺失过多的字段要么丢弃,要么用“unknown”之类的占位符,绝不能用随机数强行填充。
- 去重:这一步要定义清楚“重复”是什么。文本数据里,一行字完全相同是比较容易判断的重复;但语义上相近但表达不同的样本,比如“订单没到”和“我的包裹还没送过来”,不能简单去重,需要业务规则或人工判断。
- 格式归一:日期统一成
YYYY-MM-DD,文本统一成小写(除非大小写有语义),标点符号做统一处理。 - 标签分布检查:打印每个类别的样本数量,如果有类别特别少,要提前决定是否做采样、加权或者收集更多数据。
- 异常样本剔除:比如单条文本长度超过正常范围100倍,多半是复制粘贴错误,保留反而会干扰模型。
我做文本分类时,原始数据里有大量长度超过2000字的“垃圾样本”,实际上是页面爬取时把导航栏和广告也抓下来了。清洗之后,模型的表现提升非常明显——这个变化远大于我换一个预训练模型带来的提升。
3.2 标注质量怎么把控:双人标注+抽检
如果你的项目需要自建标注集,质量控制是关键。我最早是自己标,标了一个下午,觉得挺简单。后来发现人的注意力会下降,到了后半段,同一个样本的标签可能跟前面都矛盾。
推荐的方案是双人标注加抽检:
- 每个样本至少两个人独立标注;
- 计算两人标注的一致性,如果一致性低于某个阈值(比如75%),说明标注指南本身有问题,要先修改指南而不是继续标;
- 对已标注数据定期抽检,抽检比例建议不低于5%。
一致性指标可以简单算:
agreement = sum(1 for a, b in zip(labels_1, labels_2) if a == b) / len(labels_1)只看准确率不够,还要看每个类别的召回和精确度,尤其是少数类。类别不平衡是文本分类里最常见的问题,如果不处理,模型会“聪明地”把所有样本都预测成多数类,整体准确率看着挺高,但少数类全错。
3.3 数据版本管理:为什么我不用CSV直接覆盖
代码有Git版本管理,数据也必须要有。我早期经常干的事情是:把data.csv改一下,重新训练,过两天觉得效果还不如之前,想回退到旧数据,然后发现旧数据已经被覆盖了,彻底找不到。
DVC(Data Version Control)解决的就是这个问题。它不把数据本身放到Git里,而是用指针文件记录数据的版本。
常用命令:
dvc init dvc add data/processed git add data/processed.dvc git commit -m "更新清洗后的数据"以后想切换到任意一个历史数据版本,只需要:
git checkout <commit> dvc checkout数据和代码的版本就一起出来了。这个操作看起来多花了几分钟,但在项目周期长的时候价值极大。我至少有两次靠DVC救回了被覆盖掉的旧数据集,不然整个实验对比都要重做。
4. 模型训练:从“能跑通”到“能用的”关键转折
4.1 选定基线模型后,先做一次完整训练而不是急着调优
新手最容易犯的错是一上来就上最强模型,比如直接上几十层的大模型,指望效果一步到位。我的经验是反过来的:先做一个最简单的基线模型,走通整个流程,再逐步升级。
基线模型可以是逻辑回归、小型CNN或者一个小号的预训练模型。它的作用不是拿出去打比赛,而是帮你确认:
- 数据预处理和加载管线是否正确;
- 训练流程能否完整跑通;
- 评估方式是否符合业务目标。
我第二个项目就学乖了,先用TF-IDF + 逻辑回归跑了一遍,虽然准确率不如BERT,但从数据处理到评估接口全部跑通之后,我再把模型换成BERT,很快就能看到提升。如果一开始就直接上BERT,出了问题都不知道是数据的问题还是模型的问题。
4.2 训练中的指标监控:loss、验证集、过拟合判断
训练时不能只盯着终端里刷新的loss,要同时看训练集和验证集的曲线趋势。最简单有效的判断规则:
| 现象 | 结论 |
|---|---|
| 训练集loss下降,验证集loss也下降 | 还算正常,继续观察 |
| 训练集loss下降,验证集loss不降或上升 | 过拟合信号,需要加正则/减小模型 |
| 训练集loss和验证集loss都高 | 欠拟合或模型容量不足、特征不够 |
评估分类模型时,只看accuracy会骗人。我处理过一个类别不平衡项目,正负样本比1:9,模型把所有样本都预测成负类,accuracy是90%,看起来很高,但正样本一个都没抓到。这种场景必须看precision、recall、F1,甚至画PR曲线。
建议在训练时把评价指标定义成一个独立模块,至少包含:
- loss
- accuracy
- macro F1 / weighted F1
- 少数类的precision/recall
这些指标不光要打印,还要通过日志记录下来。
4.3 实验记录:MLflow怎么帮我记住每次超参数
训练一次模型,涉及的数据版本、代码版本、超参数、随机种子、评估结果,信息量很大。如果不记录,三天之后你就会忘掉“当前这个模型到底是怎么来的”。
我用的是MLflow Tracking。基本用法:
import mlflow with mlflow.start_run(): mlflow.log_param("model_name", "bert-base") mlflow.log_param("learning_rate", 2e-5) mlflow.log_param("batch_size", 16) mlflow.log_metric("eval_f1", 0.87) mlflow.log_artifact("model.bin")跑完之后,在浏览器打开MLflow UI,能看到每一次实验的参数和指标,一眼就能对比哪个实验更好。有了它,我再也不会出现“这个结果好像是用某个版本跑出来的,但我忘了是哪个版本”这种状态。
对于团队协作,MLflow还能做Model Registry,可以给模型打上Staging、Production的标记——这个后续部署特别有用。
5. 部署与上线:真正让我崩溃的是这里
5.1 本地能跑和线上能跑是两个世界
模型训练完之后,本地跑通只是第一步,上线才是真正的考验。我吃过的亏包括:
- 本地有GPU,服务器是纯CPU,模型推理慢得没法用;
- tokenizer版本不一致,线上环境的tokenizer和训练时不是同一个版本,导致文本被切成不一样的分词,预测结果直接变化;
- 本地内存够大,服务器容器内存只有2GB,模型加载完直接OOM。
所以“本地能跑”和“线上能跑”完全不是一回事。部署前一定要明确线上环境的硬件限制:CPU还是GPU,内存多少,允许的推理延迟是多少。这些参数直接决定你要不要做模型量化、剪枝或者换更小的模型。
5.2 用Docker打包模型服务的完整流程
Docker解决的是环境一致性问题。我的标准做法是把推理服务做成一个Docker镜像,确保任何环境行为一致。
一个最小的FastAPI推理服务:
# app.py from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class PredictRequest(BaseModel): text: str @app.post("/predict") def predict(req: PredictRequest): # 这里加载模型,做预处理和推理 result = {"label": "positive", "score": 0.98} return result对应的Dockerfile:
FROM pytorch/pytorch:2.0.1-cuda11.7-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY models/ ./models/ EXPOSE 8000 CMD ["uvicorn", "src.api.app:app", "--host", "0.0.0.0", "--port", "8000"]构建镜像:
docker build -t ai-engineering-service . docker run -p 8000:8000 ai-engineering-service注意:模型文件比较大的时候,不建议直接塞进镜像,因为镜像会变得特别大,每次推送都很慢。更好的做法是用共享存储(比如S3或NFS),在容器启动时把模型加载进来。
5.3 推理服务的性能优化:批处理、缓存、动态batch
上线之后,你很快会发现“单个请求推理耗时”不是唯一指标。还有两个常见指标:吞吐量(每秒能处理多少个请求)和延迟(单个请求从发出到返回需要多久)。这两个指标往往要权衡。
几个我在实践中最有效的优化手段:
- 缓存:对完全相同的请求直接返回之前结果。文本分类里,重复问题特别多,加一个简单的缓存能减少一半以上的计算量。
- 动态batch:不要让每个请求单独跑一次推理,而是把并发请求攒起来,凑成一批再推理,能显著提高GPU或CPU利用率。实现起来可以用队列或者FastAPI的BackgroundTasks。
- 模型序列化优化:如果只用推理,可以导出为TorchScript或ONNX格式,比直接跑原始PyTorch模型更快,内存占用也更低。
我做过一个经验数据:同样的BERT模型,单条请求CPU推理耗时3.2秒;用动态batch(一次处理8条)之后,平均每条降到0.8秒左右。这就是工程优化的力量。
5.4 上线后的监控:数据漂移和模型退化
模型上线不是终点,而是开始。我负责过的模型上线后,最怕的就是“静默退化”——线上指标慢慢变差,但没人知道。
最基础的监控包括两件事:
- 输入数据监控:记录线上输入的特征分布(比如文本长度、关键词频率、缺失值比例),定期和训练集分布比较。如果分布变化明显,说明发生了数据漂移。
- 预测分布监控:统计模型输出的预测类别比例,如果某天某个类别的预测比例突然飙升,大概率是线上数据变化了,需要告警。
简单的漂移检测不一定非要复杂统计模型,可以先看均值、方差、分位数的变化。比如训练集里文本平均长度是120个字符,线上如果连续几天变成200个字符,说明业务场景或数据来源变了。
我建议上线前至少把“输入特征统计摘要”和“预测结果统计摘要”落库,并配置一个简单的阈值告警。哪怕只有日志和定时脚本,也比完全没有强。
6. 工程化复盘:从个人Demo到团队协作
6.1 代码、数据和模型的版本管理如何统一
一个人做项目可能还能靠记忆力,但多人协作必须把代码、数据、模型三者关联起来。我的做法是:
- Git管理代码,每次实验切换对应commit;
- DVC管理数据,用
data/processed.dvc记录数据版本; - MLflow管理模型,每次训练都会记录Git commit、DVC数据版本、超参数、评估指标。
这样一套打通后,任何人拿到一次实验的run_id,都能复现出完全一样的模型。这种能力在交接工作时特别重要——别人不用来问你“当时的预处理是什么样的”,直接查记录就行。
6.2 自动化测试在AI项目中的特殊做法
很多人以为AI项目没法写测试,其实不是。我通常把AI项目测试分成三层:
- 数据测试:检查数据列是否存在、标签是否在合法集合内、缺失值比例是否超过阈值。数据测试在每次数据更新时都会跑。
- 特征转换测试:同样的输入,特征转换函数在不同版本上要保证输出一致。比如tokenizer版本升级后,要测试同一句话的分词结果是否和升级前一样。
- 模型输出格式测试:模型接口返回的JSON结构、score范围、label集合要固定。上游调用方依赖这些契约,不能今天返回
label明天变成prediction。
我踩过一个经典坑:某次重构把模型输出里的分数含义从“正类概率”改成了“得分减去0.5之后的偏移量”,下游团队没收到通知,结果线上业务逻辑直接错乱。写个输出格式测试就能避免这种问题。
6.3 我对AI工程新人最想说的几点经验
我从零走完这条路之后,有几条很实在的心得:
- 先跑通端到端最小闭环,再优化指标。一开始就用最简单的模型把“数据→训练→部署→请求”走通,这个闭环就是你的及格线。之后每一步提升,都是在闭环上做加固。
- 多用Makefile或脚本把流程串起来。我后期会在项目根目录写
Makefile,把make data、make train、make serve都固化下来。手动敲命令越少,越不容易出错。 - 日志一定要多打。模型推理时记录输入的摘要和输出的摘要,出了问题才能回溯。没有日志的AI服务等于在黑夜中闭眼开车。
- 不要盲目追逐最新模型。真实业务先解决的是可靠性和成本,新模型如果能带来明显收益再换,否则老模型维护得好同样有价值。
这些经验多数不是从书上学来的,而是踩坑之后总结的。我希望你能在看完这篇文章后,把“从零开始AI工程”变成一条清晰可见的路径:环境、数据、训练、部署、监控,每一步都不神秘,但每一步都需要认真对待。