我见过太多人想学AI工程,第一步就跑去啃论文、背模型结构,结果折腾三个月,连一个能自动训练、自动评估、可复现的脚本都没跑通。这其实不是什么智商问题,而是方向从一开始就走偏了。“ai-engineering-from-scratch”这个标题底下,藏着一条更务实的路子——先建立工程思维,再谈模型和算法。今天这篇文章,我就把自己从零开始做AI工程时踩过的坑、总结出的方法、以及真正能跑通的最小流程,完整拆给你看。
如果你是个刚入行的开发者、想转AI的工程师、或者已经能跑通notebook但总被同事说“代码不工程化”的人,那这篇文章特别适合你。我不讲高深的模型理论,只讲一个从零到一、能上线、能维护、能快速迭代的AI工程链路,包含工具选型、目录结构、数据管道、模型封装、部署监控这些平时文档里很少讲透的东西。
1. AI工程不是“跑模型”,而是“造流水线”——整体设计与思路拆解
1.1 为什么我从“工程”而不是“模型”开始
很多人会把“AI工程师”误解成“调模型的人”,天天在notebook里换参数、跑评估、再看准确率。但真实业务里,模型的训练只是整个链路里最不起眼的一环。数据从哪来、怎么清洗和校验、特征怎么对齐、模型怎么被线上服务调用、预测结果怎么监控,这些才是让AI真正产生价值的部分。
我个人的体会是:把AI当工程来做,核心是降低不确定性。模型训练本身有随机性,数据有波动,线上输入和训练分布有偏移,如果每个环节都靠手工、靠人肉记着“上次是怎么跑的”,项目一复杂就会崩。所以第一步不是选个最牛的网络结构,而是先设计一条稳定、可重复、出问题能回溯的流水线。
用开餐馆来类比:模型是那几道招牌菜,但AI工程是整个后厨的供应链、出餐流程和食安检查。菜谱再好,出餐乱套,顾客照样投诉。工程能力就是保证“每一桌的麻婆豆腐都是一个味道”的那套机制。
1.2 最小可行AI工程长什么样
一个从零开始的AI工程项目,至少要包含下面几个环节,缺一个后面都会补交“学费”:
- 需求定义:清楚这个模型要解决什么问题,评估指标是离线指标还是线上指标。
- 数据接入与校验:原始数据怎么进来,格式、缺失值、分布情况都要有记录。
- 特征管道:原始字段如何变成模型能吃的数值型特征,训练和推理必须走同一套代码。
- 模型训练与评估:训练脚本支持指定的随机种子,评估结果能跟历史版本对比。
- 模型封装与部署:把模型变成HTTP接口或批处理任务,而不是一个孤独的
.ipynb文件。 - 监控与回滚:线上预测有没有异常,数据分布有没有漂移,模型版本能不能一键切回旧的。
这个闭环听起来简单,但大部分新手项目死在第四步之前。能跑通notebook,不代表能跑通工程;能跑通工程,才真正把AI用起来了。我后面会拿一个具体项目,按这个链路手把手走一遍。
2. 工具链选型的底层逻辑:成熟、可调试、不折腾
2.1 Python环境和依赖管理:别一开始就陷入地狱
从零开始做AI工程,第一个拦路虎永远是环境。我见过太多人直接把包装在系统Python里,过俩月某个库升级,另一库崩了,就只能重装系统,这是纯折磨。我个人建议从第一天就用虚拟环境,并且固定Python版本。
最省心的组合是conda+pip。conda负责创建隔离的Python环境,比如python=3.11,避免系统Python被污染;pip负责装Python包。一个项目的依赖必须写在requirements.txt或pyproject.toml里,而且要锁版本,不能用pandas这种裸包名,得写pandas==2.2.2。
有一个实操原则:能用conda装numpy、pytorch这类带二进制依赖的包就尽量用conda,它会自动处理底层库冲突;纯Python包再走pip。混着用时注意别让两个包管理器打架,如果出了“依赖不一致”的报错,最简单粗暴的解法是删掉环境重建,而不是手动一个个去降级。
2.2 框架怎么选:PyTorch还是TensorFlow,还是先别学框架
很多新手纠结到底学PyTorch还是TensorFlow,我给的答案是:如果你不是部署在安卓/IOS端优先,选PyTorch。它的调试体验更接近普通Python,print中间张量很方便,生态里几乎所有新模型都会先出PyTorch版本,社区排错经验也多。
但如果你只是做表格数据、风控评分、推荐排序这种经典机器学习任务,连深度学习框架都不必上。scikit-learn+LightGBM+XGBoost往往效果又好又省资源。我从零开始的第一个AI工程就是LightGBM做的,训练快,可解释性工具现成,部署也不愁。
框架选型的本质不是“哪个更厉害”,而是“哪个能让你的迭代速度最快”。选工具要优先考虑成熟的、资料多的、你摔倒了能有人拉你一把的。
2.3 数据与实验管理:让每一次尝试都留下脚印
不管理实验,等于白跑。我早期写训练代码,经常是跑完一轮结果不错,但忘了用哪组参数、哪份数据、哪个随机种子,后面想复现只能靠猜,非常痛苦。
后来我固定了一套简单有效的实验管理方式:每次训练都记录三样东西——config.yaml(参数配置)、metrics.json(评估指标)、模型权重文件,统一放在带时间戳的目录下,目录名像是experiments/20250115_1030_rf_v1。如果项目再大一点,接上MLflow或Weights & Biases都很值得。但要记住,工具是帮你的,不是绑架你的;先从手写目录和JSON开始,跑顺了再上专业平台,千万不要一上来搞一堆K8s、Docker、MLflow全家桶,还没学会跑就被工具累死了。
3. 从零到一:搭建第一个AI工程的完整实操
3.1 环境准备的四步操作
我这里用一个实际项目举例:用Titanic数据集训练一个二分类模型,再把它封装成HTTP服务。整个过程你可以照着敲一遍,这就是一条完整的AI工程微缩流水线。
第一步:安装环境管理工具。如果你没有Miniconda,去官网装一个。装完打开终端,敲下面代码创建独立环境,这一步能避免后面99%的依赖冲突:
conda create -n aieng python=3.11 -y conda activate aieng第二步:安装核心依赖。Titanic项目用不到深度学习框架,直接用机器学习库就够了:
pip install pandas scikit-learn lightgbm joblib pyyaml pip install fastapi uvicorn第三步:建目录。一个干净的工程结构,比什么魔法都管用。我常用的目录是这样:
ai-engineering-from-scratch/ ├── data/ # 原始数据,只读不改 ├── src/ # 源码 │ ├── data.py # 数据加载与清洗 │ ├── features.py # 特征工程 │ ├── train.py # 训练与评估 │ └── serve.py # 模型服务化 ├── models/ # 输出模型文件 ├── experiments/ # 实验记录 └── requirements.txt每次看到把数据和代码混在一起、全部写在root目录下的项目,我就头皮发麻。目录不只是美观,它直接决定了这个项目能不能被别人接手、能不能被复用。
第四步:把装好的依赖固定下来:
pip freeze > requirements.txt等到哪天环境坏了,直接conda create -n aieng_new python=3.11 -y && pip install -r requirements.txt就能一键恢复。我后期换电脑、换服务器,全靠这个习惯省下半天到一天的时间。
3.2 数据管道与训练脚本:别追求花哨,先保证可复现
数据清洗和特征工程,是整个项目里最需要小心的地方。我在src/data.py里写了一个简单的加载函数,保留字段筛选、缺失值处理、类型转换三件事:
# src/data.py import pandas as pd def load_data(path: str) -> pd.DataFrame: df = pd.read_csv(path) df["Age"] = df["Age"].fillna(df["Age"].median()) df["Embarked"] = df["Embarked"].fillna("S") df["Sex"] = df["Sex"].map({"male": 0, "female": 1}) df["Pclass"] = df["Pclass"].astype(int) return df然后是特征工程和训练脚本。我特意把随机种子固定成42,保证每次训练结果几乎一致。这一步在工程上极其重要——如果不能复现,你永远没法确定一个效果提升到底是参数变了还是运气变了。
# src/train.py import joblib import lightgbm as lgb from sklearn.metrics import accuracy_score, roc_auc_score from sklearn.model_selection import train_test_split from data import load_data SEED = 42 # 这里用 yaml 记录配置,便于追踪 config = { "model": "lgbm", "n_estimators": 500, "max_depth": 4, "learning_rate": 0.05, "seed": SEED, } df = load_data("data/titanic.csv") X = df[["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked"]] X = pd.get_dummies(X, columns=["Embarked"], drop_first=True) y = df["Survived"] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=SEED, stratify=y ) model = lgb.LGBMClassifier(**config) model.fit(X_train, y_train) val_pred = model.predict_proba(X_val)[:, 1] auc = roc_auc_score(y_val, val_pred) print(f"Validation AUC: {auc:.4f}") joblib.dump(model, "models/lgbm_v1.joblib")这里有个很多新手容易忽略的细节:训练和预测时的特征矩阵列顺序、列名必须完全一致。如果你在训练时用了one-hot编码,预测时也必须走同一个features.py处理逻辑,不能手动拼个DataFrame就塞给模型,否则轻则报错、重则预测结果全都乱了。
3.3 把模型封装成服务:从notebook到API
模型训好只是第一步。真正让业务方用起来,通常要提供一个HTTP接口。用FastAPI写一个最小的预测服务,代码很短:
# src/serve.py import joblib import pandas as pd from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() model = joblib.load("models/lgbm_v1.joblib") class Passenger(BaseModel): pclass: int sex: int age: float sibsp: int parch: int fare: float embarked_S: int embarked_Q: int @app.post("/predict") def predict(p: Passenger): feature_names = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "Embarked_S", "Embarked_Q"] sample = pd.DataFrame([[ p.pclass, p.sex, p.age, p.sibsp, p.parch, p.fare, p.embarked_S, p.embarked_Q ]], columns=feature_names) prob = model.predict_proba(sample)[0][1] return {"survival_probability": round(float(prob), 4)}启动服务的命令是uvicorn src.serve:app --host 0.0.0.0 --port 8000,随后用curl或requests发POST请求就能拿到预测结果。我在实际项目里会在接口里加一层输入校验、日志记录和版本号返回,这样线上出问题能通过日志快速定位是哪个版本的模型干的“好事”。
3.4 监控和回滚:上线不是终点
模型上了线,不代表事情结束。我见过太多项目,模型上线时效果不错,但三个月后业务方说预测越来越不准。原因往往是线上的真实数据和训练数据分布已经不一样了,术语叫“数据漂移”。
所以工程上要做两件事。第一,保留历史模型文件,别删。我把模型命名为lgbm_v1.joblib、lgbm_v2.joblib,每个版本对应实验记录里的config.yaml。一旦线上效果变差,能立刻切回上一个版本,这是最朴素的回滚方案。第二,记录线上预测值的基本分布,比如均值、方差、分类比例。如果某天分布突然和训练集差异巨大,就是出现漂移的信号。
这里也可以引入更专业的监控工具比如Prometheus + Grafana、Evidently,但那是后话。一个从零开始的AI工程,先把记录日志、版本控制和手动回滚做扎实,远比堆一堆看起来很炫的监控系统更实在。
4. 常见问题与排查技巧实录
4.1 环境与依赖的四个高频坑
我基于实操经验,把新手最容易遇到的环境问题整理成一张速查表,你在报错现场直接对号入座就行:
| 现象 | 最常见原因 | 解决建议 |
|---|---|---|
ModuleNotFoundError找得到名字却装不上 | 装到了错误的Python环境 | which python确认当前环境;激活conda环境再装 |
CUDA error: no kernel image available | PyTorch/CUDA版本不匹配 | 根据CUDA版本选择对应PyTorch安装命令 |
昨天能跑的代码今天报numpy相关错误 | 依赖被悄悄升级了 | 用pip freeze锁版本;不要轻易全局pip install -U |
conda环境下pip install装错位置 | 用了系统pip而不是conda环境的pip | 先conda activate 环境名,再用python -m pip install |
有一个我必跟新手强调的习惯:任何安装命令,优先使用python -m pip而不是裸pip。因为很多系统里pip指向的是老版本Python,或者被其他工具劫持了,python -m pip能保证你装到当前激活的Python解释器对应路径,能少踩很多坑。
4.2 数据泄漏:最隐蔽的“效果幻觉”
数据泄漏是AI工程里最阴险的坑之一。它的典型症状是离线指标非常漂亮,AUC高达0.99,上线后却一塌糊涂。原因通常是你把不该用的信息喂给了模型,或者数据处理时没有把训练集和验证集分开处理。
举个例子:你先把全部数据的缺失值用全局中位数填充,再切训练/验证集,这时候验证集已经“偷看”到了全局分布信息,评估结果就会虚高。正确做法是先切分,再在训练集上算中位数,然后分别应用到训练集和验证集。对于时间序列数据,绝对不能随机打乱切分,要按时间切,否则未来信息会泄漏进训练集。
我自己的经验是:写完训练脚本后,反复想一个问题——线上预测时模型能拿到哪些字段,训练时就必须只用这些字段。任何“当时觉得为了方便”的字段,都可能是泄漏源。
4.3 模型不收敛或过拟合的排查顺序
模型效果不对劲,不要急着换模型结构。我通常按这个顺序排查:先看数据有没有问题(缺失、异常值、标签错误),再看特征有没有放进去、缩放有没有处理好,最后才调模型参数。顺序反了,只会越调越乱。
对于典型的结构化数据模型,过拟合的重要信号是训练集指标和验证集指标差距过大。这时候优先降低模型复杂度,比如限制max_depth、增大min_child_samples、提高正则化系数,而不是盲目加数据。加数据是个美好的愿望,但实践中脏数据越多,模型越容易学偏。先把当前特征和质量吃透,效果往往比堆数据更明显。
4.4 部署时的模型序列化版本坑
很多人训练完用joblib或pickle保存模型,结果部署到服务器上加载时报错,或者参数对不上。这个坑多半是本地环境和服务器环境的包版本不一致。模型文件本身不跨Python版本、不跨scikit-learn版本保证兼容,所以部署机必须和训练机保持同版本的依赖。
我踩过一次很狠的:本地用 scikit-learn 1.4 训练,服务器上是 1.2,加载模型直接崩。后来我的做法是,每次训练完在experiments目录下同时固定一份requirements.txt,部署时严格用这份文件重建环境。虽然笨,但稳。
还有一个经验是不要直接拿sklearn的Pipeline对象在服务里手动拼字段,最好在模型包里同时存一份特征名列表,部署时校验线上输入和训练时特征是否完全一致。工程里“防御性编程”想得越多,线上睡得越安稳。
最后分享一个小技巧
如果你也在从零开始做AI工程,我强烈建议你在第一周就强制自己完成一次“手工闭环”——不依赖任何平台,从下载数据开始,用脚本完成清洗、训练、评估,然后用FastAPI起一个接口,让同事或朋友真实调用一次。这会让你对整条链路产生整体感知,比看十篇教程都有效。
整个过程里我最想强调的一个心态是:别追求一步到位,先用最简单的方式跑通,再去迭代工程化。很多人学了三天Docker、K8s、MLflow,项目还没开始就倒在了工具学习上。工具永远是服务于流程的,先把流程走通,工具自然知道该补在哪。从我带过的新人来看,能在两周内独立跑通数据到API闭环的,后面基本都成长得很快。如果你现在还在纠结该怎么开始,不用想太多,先把conda环境建起来,跑完第一个脚本再说。