1. 从零搭建AI工程能力,为什么大多数人卡在“会调包”这一步
“ai-engineering-from-scratch”这个标题,我第一次看到的时候,脑子里蹦出来的不是某个具体项目,而是一类人:他们能熟练地pip install各种框架,能照着教程跑通一个图像分类或者文本生成的 demo,但一旦让他们从零搭一个能用的 AI 工程流水线,就立刻卡壳。这不是个别现象,而是当前 AI 学习路径里一个非常普遍的结构性断层。
我自己带过不少刚入行的同学,也见过很多工作两三年、想从传统后端或数据分析转 AI 工程的人。他们最大的问题不是不懂模型原理,而是不懂“工程”这两个字在 AI 场景下到底意味着什么。学术界教你的是如何设计一个网络、如何调参让指标涨两个点;但工业界要的是:数据怎么进来、特征怎么存、模型怎么训、训完怎么部署、部署完怎么监控、监控到异常怎么回滚。这一整条链路,才是 AI 工程的核心。
所以这篇内容,我想把“从零构建 AI 工程能力”这件事拆开揉碎讲清楚。它适合三类人:第一类是完全没接触过 AI 工程、但有一定编程基础想入行的;第二类是会跑 demo 但没做过完整项目的;第三类是有后端或数据工程经验、想补齐 AI 侧工程能力的。我会尽量用从业者的视角,把每个环节“为什么这么做”“不这么做会怎样”“实际踩过什么坑”都讲透,而不是只给一堆工具名和命令。
需要先明确一个认知:AI 工程不等于机器学习。机器学习关注的是模型本身,AI 工程关注的是让模型在真实业务里稳定、可维护、可扩展地跑起来。这两者的关系,有点像“造发动机”和“造整车”——发动机再强,没有传动、底盘、电控,也上不了路。很多人学 AI 就是一直在学怎么造更好的发动机,却从来没想过整车怎么组装。
2. 环境与工具链的从零搭建:别一上来就装一堆用不上的东西
2.1 为什么我不建议新手直接上全套 MLOps 平台
很多教程一上来就让你装 MLflow、Kubeflow、Feast、Airflow 这一整套,仿佛不搭个“平台”就不叫 AI 工程。我实测下来的结论是:如果你连一个完整的训练-推理闭环都没跑通过,装这些只会让你在配置上耗掉两周,然后放弃。
正确的做法是分层搭建。第一层是语言和基础库:Python 3.10 以上、NumPy、Pandas、scikit-learn。第二层是深度学习框架,选一个就行,PyTorch 目前生态最友好。第三层是实验管理,先用最轻量的方式——比如手动记录到 CSV 或者用 TensorBoard。第四层才是部署和服务化。第五层才是自动化和监控。
这个顺序不能乱。我见过太多人跳过前三层直接搞第四层,结果模型本身都没调明白,就开始折腾 Docker 和 K8s,最后两头都没落地。
2.2 虚拟环境与依赖锁定:一个被严重低估的工程习惯
从零做 AI 工程,第一个必须养成的习惯是:每个项目独立虚拟环境,并且锁定依赖版本。这不是洁癖,是血泪教训。AI 领域的库版本兼容性极其脆弱,PyTorch 2.0 和 2.1 在某些算子上的行为可能就不一样,transformers 库一个小版本升级可能就改了默认参数。
我推荐用conda或者venv建环境,然后用pip freeze > requirements.txt或者conda env export锁定。但这里有个坑:pip freeze会把所有间接依赖都写进去,有时候反而导致跨平台装不上。更稳的做法是用pip-tools,维护一个requirements.in只写直接依赖,然后编译出锁定的requirements.txt。
# 推荐做法 python -m venv .venv source .venv/bin/activate pip install pip-tools # requirements.in 里只写直接依赖 pip-compile requirements.in -o requirements.txt pip-sync requirements.txt这样做的价值在于:半年后你或者同事要复现这个项目,能保证装出来的环境和你当时一模一样。AI 项目最怕的就是“在我机器上能跑”。
2.3 目录结构:工程能力的第一个外显标志
一个人是不是真的做过 AI 工程,看他项目目录结构就能猜个八九不离十。新手通常把所有代码堆在一个main.py或者几个 notebook 里;有工程经验的人会有清晰的分层。
我常用的结构是这样的:
project/ ├── configs/ # 配置文件,yaml 或 json ├── data/ │ ├── raw/ # 原始数据,只读 │ ├── interim/ # 中间处理结果 │ └── processed/ # 最终训练用数据 ├── src/ │ ├── data/ # 数据加载与预处理 │ ├── features/ # 特征工程 │ ├── models/ # 模型定义 │ ├── train.py # 训练入口 │ ├── predict.py # 推理入口 │ └── utils/ # 通用工具 ├── notebooks/ # 探索性分析,不参与生产 ├── tests/ # 单元测试 ├── requirements.in └── README.md这个结构的关键在于:data分三层,src按职责分模块,notebooks明确隔离。很多人把 notebook 里的代码直接复制到生产,这是大忌。notebook 适合探索,但它的执行顺序是隐式的、状态是混乱的,不能作为工程代码。
注意:
data/raw目录一定要设为只读,任何清洗和转换都输出到interim或processed。这样你永远可以回溯到原始数据,不会因为一次错误的覆盖而丢失数据源。
3. 数据管道的工程化:AI 项目 80% 的坑都在这里
3.1 数据版本管理:为什么 git 管不了数据
做过传统软件工程的人转 AI,第一个不适应就是:代码可以用 git 管,数据怎么办?一个数据集动辄几个 G,放 git 里直接把仓库撑爆。但数据又必须可追溯——你三个月前训的那个模型,用的是哪一版数据?
我试过几种方案。最简单的是用 DVC(Data Version Control),它把大文件存在远程存储,git 里只存指针。另一种是用时间戳或哈希命名数据快照,配合一个元数据表记录。小团队我推荐后者,简单直接:
data/processed/ ├── train_20240115_a3f2c1.parquet ├── train_20240120_b7e4d9.parquet └── metadata.csv # 记录每个文件的来源、处理脚本版本、行数、字段metadata.csv里至少要有:文件名、生成时间、上游数据版本、处理脚本的 git commit、样本数、关键字段的统计摘要。这样出问题时能快速定位是哪一步引入的。
3.2 数据校验:别等模型训完才发现数据有问题
这是我最想强调的一点。很多人拿到数据直接喂给模型,训了几个小时发现 loss 不降,回头一查发现某列有大量空值或者异常值。数据校验必须前置。
我常用的校验维度包括:字段是否存在、类型是否正确、取值范围是否合理、缺失率是否超标、类别分布是否偏移。可以用pandera或great_expectations这类库,也可以自己写简单的断言。
import pandera as pa from pandera import Column, DataFrameSchema, Check schema = DataFrameSchema({ "user_id": Column(int, Check.greater_than(0)), "age": Column(int, Check.in_range(0, 120), nullable=True), "label": Column(str, Check.isin(["A", "B", "C"])), }) # 校验,不通过直接抛异常 validated_df = schema.validate(raw_df)关键理念是:数据校验失败应该让流程中断,而不是打个 warning 继续跑。因为脏数据进入训练,产出的模型就是不可信的,后面所有工作都白费。
3.3 特征存储与训练/推理一致性:最隐蔽的坑
AI 工程里有一个非常隐蔽但杀伤力极大的问题:训练时特征计算方式和推理时不一致。比如训练时你用全量数据算了一个归一化的均值方差,推理时却用单条数据自己算,结果分布完全对不上,模型效果暴跌。
解决这个问题的核心思路是:把特征计算逻辑抽成独立的、可复用的模块,训练和推理都调用同一份代码。更进一步,用特征存储(Feature Store)来统一管理。小团队不一定上 Feast 这种重型工具,但至少要有一个features.py,里面每个特征函数都是纯函数,输入原始数据输出特征值。
# features.py def compute_age_bucket(age: int) -> str: if age < 18: return "minor" elif age < 60: return "adult" else: return "senior" # 训练和推理都 import 这个函数还有一个细节:归一化用的均值、方差、分位数这些统计量,必须作为模型 artifact 一起保存,推理时加载,而不是重新计算。我习惯把它们存成一个preprocessor.pkl,和模型文件放一起。
4. 模型训练与实验管理:让每一次实验都可复现
4.1 配置驱动:把超参数从代码里赶出去
新手写训练脚本,超参数通常直接硬编码在代码里,改一次跑一次。这样做的后果是:你跑了二十组实验,最后记不清哪组对应哪个结果。正确做法是配置驱动,所有超参数写进 yaml,训练脚本只读配置。
# configs/exp_001.yaml model: name: resnet18 num_classes: 10 train: batch_size: 64 lr: 0.001 epochs: 30 optimizer: adam data: train_path: data/processed/train_20240115_a3f2c1.parquet val_split: 0.2训练入口接收配置路径,把配置内容、git commit、开始时间、环境信息一起记录到实验日志。这样任何一次实验都能精确复现。
4.2 实验追踪:轻量方案往往比重型平台更实用
实验追踪工具我用过不少,MLflow、Weights & Biases、TensorBoard。我的建议是:个人或小团队,先用 TensorBoard 加一个 CSV 日志就够了。MLflow 适合需要集中管理多人的场景,但它本身也需要维护一个服务,有额外成本。
不管用什么工具,要记录的核心信息是固定的:超参数、每轮的训练/验证指标、最终模型路径、数据版本、代码版本。我习惯在训练脚本里加一段:
import json, subprocess, datetime run_meta = { "config": config, "git_commit": subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip(), "start_time": datetime.datetime.now().isoformat(), "data_version": config["data"]["train_path"], } with open(f"runs/{run_id}/meta.json", "w") as f: json.dump(run_meta, f, indent=2)这个meta.json就是实验的身份证,任何时候都能查。
4.3 随机种子与可复现性:别小看这一行代码
AI 实验的不可复现性,很大一部分来自随机性。Python 的 random、NumPy 的 random、PyTorch 的 random、CUDA 的随机,全都要固定。而且要注意,即使全部固定,某些 CUDA 算子仍然是非确定性的,需要额外设置。
import random, numpy as np, torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = Falsecudnn.deterministic = True会牺牲一点性能,但换来可复现性,做实验阶段非常值得。上线推理时可以关掉。
提示:可复现性不是绝对的,跨不同硬件、不同驱动版本仍可能有微小差异。工程上追求的是“同一环境下可复现”,而不是“任何环境都完全一致”。
5. 从模型到服务:部署环节最容易翻车的地方
5.1 模型序列化:pickle 不是唯一选择,也不总是好选择
训练完的模型怎么存?很多人直接用torch.save(model, path)存整个模型对象。这样做的问题是:它依赖模型类的定义,如果代码重构了类名或结构,加载就失败。更稳的做法是只存state_dict,加载时先实例化模型结构再 load。
# 保存 torch.save(model.state_dict(), "model.pt") # 加载 model = MyModel(config) model.load_state_dict(torch.load("model.pt")) model.eval()对于跨框架或者需要长期归档的场景,ONNX 是更好的选择,它把模型结构和权重一起序列化,不依赖原始训练代码。但 ONNX 对某些自定义算子支持有限,转换时可能报错,需要权衡。
5.2 推理服务:FastAPI 是性价比最高的起点
部署推理服务,我强烈推荐从 FastAPI 开始。它轻量、异步支持好、自带文档,几十行代码就能起一个可用的服务。
from fastapi import FastAPI from pydantic import BaseModel import torch app = FastAPI() model = load_model() class Request(BaseModel): features: list[float] @app.post("/predict") def predict(req: Request): x = torch.tensor([req.features]) with torch.no_grad(): out = model(x) return {"prediction": out.argmax().item()}但这里有几个工程细节必须处理:模型加载只做一次(放在启动时,不要每次请求都加载)、输入校验(pydantic 帮你做了)、异常处理(模型推理失败要返回明确错误而不是 500 堆栈)、批处理支持(高并发下单条推理效率极低)。
5.3 容器化:Docker 不是可选项,是必选项
“在我机器上能跑”这个问题的终极解法就是容器化。AI 项目的 Docker 镜像有个特殊难点:CUDA 和深度学习框架的版本匹配。我建议直接用官方的基础镜像,比如pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime,不要自己从 ubuntu 开始装。
Dockerfile 的写法也有讲究:依赖安装和代码复制分开,利用镜像层缓存。依赖不变时,改代码不需要重装依赖。
FROM pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY configs/ ./configs/ CMD ["uvicorn", "src.serve:app", "--host", "0.0.0.0", "--port", "8000"]镜像大小也是个实际问题。训练镜像可能几个 G,推理镜像要尽量精简,用runtime而不是devel基础镜像,能省不少空间。
6. 监控、回滚与持续迭代:上线只是开始
6.1 数据漂移监控:模型不会突然变差,是数据先变了
模型上线后效果下降,绝大多数情况不是模型坏了,而是输入数据的分布变了。这叫数据漂移。监控数据漂移,核心是监控输入特征的统计分布,和训练时的基准分布做对比。
常用的指标是 PSI(Population Stability Index)或者 KL 散度。PSI 计算简单,工程上好落地:
def psi(expected, actual, buckets=10): breakpoints = np.percentile(expected, np.linspace(0, 100, buckets + 1)) expected_counts = np.histogram(expected, breakpoints)[0] / len(expected) actual_counts = np.histogram(actual, breakpoints)[0] / len(actual) expected_counts = np.clip(expected_counts, 1e-6, None) actual_counts = np.clip(actual_counts, 1e-6, None) return np.sum((actual_counts - expected_counts) * np.log(actual_counts / expected_counts))PSI 小于 0.1 认为分布稳定,0.1 到 0.25 需要关注,大于 0.25 就要告警了。这个阈值不是绝对的,要根据业务敏感度调整。
6.2 模型性能监控:没有标签时怎么办
有监督模型上线后,真实标签往往延迟才能拿到,甚至拿不到。这时候怎么监控模型性能?两个思路:一是监控代理指标,比如推荐系统的点击率、风控系统的通过率;二是监控预测分布的稳定性,如果模型输出的分布突然偏移,往往意味着输入有问题。
我实际项目里的做法是:预测分布监控 + 业务代理指标 + 定期人工抽样评估,三者结合。单靠任何一个都不够可靠。
6.3 回滚机制:上线前就要想好怎么退
这是很多团队忽略的一点:新模型上线,效果不好怎么快速回滚?如果每次回滚都要重新部署、重新加载,那故障时间会很长。
我的建议是:模型版本化管理,服务启动时加载多个版本,通过配置或接口切换。这样回滚只是改一个配置项,秒级生效。
models/ ├── v1.0.0/ │ ├── model.pt │ └── preprocessor.pkl ├── v1.1.0/ │ ├── model.pt │ └── preprocessor.pkl └── current -> v1.1.0 # 软链接,切换版本就是改这个链接配合一个健康检查接口,新版本上线后先小流量灰度,观察指标正常再全量。这套流程听起来重,但真出问题时能救命。
7. 我在从零构建 AI 工程能力过程中踩过的几个真实坑
第一个坑是过早优化。我刚开始做 AI 工程时,总想着一步到位搭一个“完美”的流水线,结果花了两周搭架子,模型本身还没跑通。后来我学乖了:先用最土的办法把闭环跑通,哪怕数据是手动拷的、模型是脚本硬编码的,先让整个链路能跑。跑通之后再逐步替换每个环节,用工程化的方式重构。这个顺序非常重要,因为只有闭环跑通了,你才知道每个环节真正的痛点在哪。
第二个坑是忽视数据质量。我曾经做过一个项目,模型在验证集上指标很好,上线后效果一塌糊涂。排查了三天,最后发现是训练数据里有一批样本的标签是错的,而验证集恰好没覆盖到那批数据。从那以后,我养成了两个习惯:一是数据校验必须前置且严格,二是训练集和验证集的划分要按时间或业务维度,不能简单随机分。
第三个坑是低估了推理性能的重要性。训练时 batch size 开大,GPU 跑满,感觉很爽。但上线后是单条请求,延迟要求 100ms 以内,这时候才发现模型太大、预处理太慢。所以从项目一开始就要考虑推理场景的约束,模型选型不能只看精度,还要看延迟和吞吐。
第四个坑是文档和交接。AI 项目的人员流动很常见,如果代码没有文档、实验没有记录、数据没有说明,接手的人要从头猜。我现在坚持每个项目至少有三份文档:README 讲怎么跑、DATA.md 讲数据来源和处理、EXPERIMENTS.md 记录关键实验和结论。这三份文档花不了多少时间,但能省下后面无数沟通成本。
8. 给想从零入门的同学一条可执行的路径
如果你现在完全没做过 AI 工程,我建议按这个顺序走,每一步都要动手做出来,不要只看。
第一步,用 scikit-learn 做一个完整的分类项目,从数据加载、特征处理、训练、评估到保存模型,全部写在一个脚本里。目标是理解 AI 项目的基本流程。
第二步,把第一步的脚本重构成模块化的代码,拆成 data、features、models、train 几个模块,用配置文件管理参数。目标是理解工程化组织。
第三步,用 FastAPI 把模型包成服务,写一个简单的客户端调用。目标是理解训练和推理的差异。
第四步,用 Docker 把服务容器化,确保在另一台机器上能一键跑起来。目标是理解环境一致性。
第五步,加数据校验、实验记录、监控指标。目标是理解生产级 AI 系统需要什么。
这五步走完,你对 AI 工程的理解会超过大部分只会调包的人。每一步都会遇到问题,而解决问题的过程,就是能力真正增长的过程。工具会变,框架会更新,但这条链路的工程思维是不变的。