1. 从零搭建AI工程体系,为什么我劝你别急着调包
"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地,但绝大多数都是教你import torch然后跑个预训练模型,或者调个API做个聊天机器人。真正讲"从零搭建AI工程体系"的内容,少得可怜。
我自己在这个坑里摸爬滚打了三年多。最开始也是典型的调包侠,觉得会用HuggingFace的pipeline就算入门了。直到有一次线上服务出了故障——推理延迟突然从200ms飙到3秒,日志里全是OOM,我盯着监控面板完全不知道从哪下手。那次事故让我意识到一个问题:你会用工具,不代表你理解工具背后的工程体系。
所谓"AI工程",和"AI算法"是两码事。算法关注的是模型结构、损失函数、训练策略;工程关注的是这套东西怎么稳定、高效、可维护地跑在生产环境里。一个模型在Jupyter Notebook里跑出99%的准确率,和它在线上扛住每秒上千次请求,中间隔着的就是整个AI工程体系。
这个项目标题"ai-engineering-from-scratch"要解决的核心问题就是:当你抛开所有现成的框架和平台,从最底层开始构建一套AI系统时,你需要掌握哪些东西?这不是让你重复造轮子,而是让你在造轮子的过程中真正理解轮子是怎么转的。
适合谁来参考这份内容?三类人:第一类是有一定Python基础、想往AI工程方向转的开发者;第二类是在做AI项目但总觉得"知其然不知其所以然"的工程师;第三类是技术负责人,需要评估AI系统的技术选型和架构设计。如果你只是想快速跑个demo,那这份内容可能不适合你——因为它讲的是"从零",意味着很多地方需要你亲自动手。
我接下来的分享会围绕一个完整的AI工程链路展开:从数据管道的搭建,到模型训练的基础设施,再到推理服务的部署和监控。每一块我都会告诉你为什么这么做、怎么做、以及我踩过哪些坑。
2. 整体架构设计:从数据到服务的全链路拆解
2.1 为什么选择"分层解耦"而不是"端到端一把梭"
很多人做AI项目喜欢一把梭:一个脚本从读数据开始,到训练模型,再到保存权重,全写在一个文件里。原型阶段这么干没问题,但一旦要迭代,你就会发现牵一发而动全身。改个数据预处理逻辑,训练代码要跟着改;换个模型结构,推理服务也得动。
我在实际项目中采用的是分层解耦的架构。整个系统拆成四层:数据层、训练层、服务层、监控层。层与层之间通过明确定义的接口通信,比如数据层输出的是标准化的特征文件,训练层只负责消费这些文件,不关心数据是怎么来的。
这么设计的好处是什么?举个例子,有一次我们需要把数据源从MySQL换成数据仓库,因为数据层做了抽象,训练层和服务层的代码一行没改,只换了数据层的适配器就完成了迁移。如果当初是一把梭的写法,这个迁移至少得花两周。
具体分层如下:
| 层级 | 职责 | 核心组件 | 输出物 |
|---|---|---|---|
| 数据层 | 数据采集、清洗、特征工程 | 数据管道、特征存储 | 标准化特征文件 |
| 训练层 | 模型定义、训练、评估 | 训练框架、实验管理 | 模型权重、评估报告 |
| 服务层 | 模型加载、推理、API | 推理引擎、Web服务 | 预测接口 |
| 监控层 | 性能监控、数据漂移检测 | 指标采集、告警 | 监控面板、告警通知 |
2.2 技术选型的核心考量:别被"最新最强"带偏
选型这件事,我的原则是:成熟度优先于先进性,可维护性优先于性能。很多团队一上来就用最新的框架,结果遇到问题连文档都找不到,社区也没人踩过坑。
以训练框架为例,PyTorch和TensorFlow我都用过。PyTorch的动态图机制在调试时确实方便,print一下就能看到中间结果;TensorFlow的静态图在部署时性能更好,但调试起来让人抓狂。我最终选PyTorch,理由很简单:调试时间占了开发时间的70%以上,动态图省下来的调试时间远比那点性能差异值钱。
推理引擎的选择更关键。我对比过几种方案:
- 原生PyTorch推理:最灵活,但性能一般,适合流量不大的场景
- ONNX Runtime:跨平台好,性能比原生PyTorch提升30%-50%,但算子支持有限
- TensorRT:NVIDIA平台性能最强,能提升2-5倍,但绑定硬件,迁移成本高
我的建议是:先用ONNX Runtime跑通,等流量真的上来了再考虑TensorRT。过早优化是万恶之源,我见过太多团队在日请求量还不到一万的时候就花大力气搞TensorRT,结果模型一更新就得重新转换,维护成本极高。
2.3 目录结构设计:让代码自己说话
一个清晰的目录结构能省掉大量沟通成本。我现在的项目模板是这样的:
ai-project/ ├── configs/ # 配置文件 │ ├── data.yaml │ ├── model.yaml │ └── serve.yaml ├── data/ # 数据相关 │ ├── raw/ # 原始数据 │ ├── processed/ # 处理后数据 │ └── features/ # 特征文件 ├── src/ # 源代码 │ ├── data/ # 数据管道 │ ├── models/ # 模型定义 │ ├── train/ # 训练逻辑 │ ├── serve/ # 推理服务 │ └── utils/ # 工具函数 ├── experiments/ # 实验记录 ├── tests/ # 测试代码 └── scripts/ # 运维脚本这个结构的关键在于配置和代码分离。所有可变的参数都放在configs/里,代码只负责逻辑。这样换数据集、调超参数都不需要改代码,改配置文件就行。我吃过亏——曾经把学习率硬编码在训练脚本里,结果做超参数搜索的时候得改代码重新跑,效率极低。
3. 数据管道搭建:AI工程的隐形地基
3.1 数据清洗:80%的时间花在这里不是玩笑
业内有个说法:AI项目80%的时间花在数据上,20%花在模型上。我一开始不信,觉得模型才是核心。做了几个项目之后发现,这个比例还是保守了。
数据清洗要解决的核心问题是:原始数据里的噪声、缺失、不一致,会直接导致模型学到错误的东西。我做过一个文本分类项目,原始数据里有大量HTML标签和特殊字符,没清洗直接训练,模型准确率只有60%多。花了两天做清洗,同样的模型结构,准确率直接到85%。
清洗的常见操作包括:
- 去重:完全重复的样本直接删掉,近似重复的用SimHash或MinHash检测
- 缺失值处理:数值型用均值/中位数填充,类别型用众数或单独标记为"未知"
- 异常值检测:用IQR或Z-score方法识别,超过阈值3倍以上的考虑剔除
- 格式统一:日期格式、编码格式、大小写统一
注意:清洗规则一定要记录在案,并且可复现。我见过有人手动在Excel里删了几行数据,结果后面怎么都复现不出当时的实验结果。
3.2 特征工程:让模型少走弯路
特征工程的核心思想是:把领域知识注入到数据里,降低模型的学习难度。好的特征能让简单的模型达到复杂模型的效果。
以我做过的一个用户流失预测项目为例。原始数据只有用户的登录记录、消费记录这些原始字段。如果直接把原始字段喂给模型,效果一般。但我做了几个特征之后,效果明显提升:
- 最近一次登录距今天数:比原始的时间戳更有信息量
- 近7天登录频率:反映用户活跃度的变化趋势
- 消费金额的环比变化:捕捉消费习惯的突变
这些特征的计算逻辑都不复杂,但需要你对业务有理解。特征工程没有标准答案,它取决于你的业务场景和数据特点。
特征存储也是个关键问题。训练时计算的特征,推理时也要用同样的逻辑计算,否则会出现训练-推理偏差。我的做法是把特征计算逻辑封装成独立的函数,训练和推理都调用同一份代码。
3.3 数据版本管理:别让"数据变了"成为背锅侠
数据版本管理是很多人忽略的环节。模型效果突然下降,你怀疑是数据问题,但如果没有数据版本记录,你根本不知道当前用的数据和上周用的有什么区别。
我的做法是用DVC(Data Version Control)管理数据版本。每次数据更新都打一个tag,训练时记录用的是哪个版本的数据。这样出问题的时候可以快速回溯。
# 初始化DVC dvc init # 添加数据文件到DVC管理 dvc add data/processed/train.csv # 提交变更 git add data/processed/train.csv.dvc data/processed/.gitignore git commit -m "update training data v1.2" # 打tag git tag -a "data-v1.2" -m "training data version 1.2"DVC的原理其实很简单:它把大文件存在别的地方,Git里只存一个指针文件。这样既保证了版本可追溯,又不会让Git仓库变得巨大。
4. 模型训练基础设施:不只是跑个fit那么简单
4.1 实验管理:让每次训练都有迹可循
做AI研究最痛苦的事情之一,就是跑了上百次实验之后,忘了哪次用的什么参数、结果如何。我早期用Excel记录,后来发现根本不够用——参数太多,结果太多,手动记录容易出错还费时间。
后来我用了MLflow做实验管理。每次训练自动记录参数、指标、模型文件,还能在Web界面里对比不同实验的结果。
import mlflow import mlflow.pytorch # 开始一次实验 with mlflow.start_run(run_name="bert-base-lr2e5"): # 记录超参数 mlflow.log_param("learning_rate", 2e-5) mlflow.log_param("batch_size", 32) mlflow.log_param("epochs", 10) # 训练模型... # 记录指标 mlflow.log_metric("accuracy", 0.92) mlflow.log_metric("f1_score", 0.91) # 保存模型 mlflow.pytorch.log_model(model, "model")MLflow最大的价值在于可对比性。你可以在UI里同时选中多个实验,直观地看到不同参数对结果的影响。我靠这个功能发现了一个反直觉的结论:在这个项目里,学习率从2e-5调到3e-5,效果反而下降了,而1e-5和2e-5差别不大。如果没有系统的实验记录,这种细微的差异根本发现不了。
4.2 分布式训练:什么时候需要,怎么搞
单卡训练慢的时候,自然会想到分布式。但分布式训练不是银弹,它有自己的适用场景和坑。
先说什么时候需要分布式:
- 模型太大,单卡显存放不下(比如大语言模型)
- 数据量太大,单卡训练一个epoch要几天
- 需要快速做超参数搜索
如果只是模型稍微大一点,单卡训练几个小时能跑完,我建议先别上分布式。分布式训练的调试成本很高,通信问题、同步问题、梯度问题,每一个都能让你调一整天。
真要用分布式,PyTorch的DDP(DistributedDataParallel)是目前最成熟的方案。核心代码就几行:
import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP # 初始化进程组 dist.init_process_group(backend="nccl") # 模型包装 model = model.to(local_rank) model = DDP(model, device_ids=[local_rank]) # 数据采样器要加DistributedSampler sampler = DistributedSampler(dataset) dataloader = DataLoader(dataset, sampler=sampler, batch_size=batch_size)注意:用了DistributedSampler之后,每个epoch开始前要调用
sampler.set_epoch(epoch),否则每个epoch的数据划分都一样,会影响训练效果。这个坑我踩过,找了半天才发现是采样器的问题。
4.3 训练监控:别等跑完了才发现问题
训练过程中的监控很重要。我见过有人跑了一天的训练,结果发现loss从第一个epoch就没降过,白白浪费了一天。
需要监控的核心指标:
- Loss曲线:训练loss和验证loss都要看,如果验证loss开始上升而训练loss还在下降,说明过拟合了
- 学习率:如果用学习率调度器,要确认学习率按预期变化
- 梯度范数:梯度爆炸或消失的早期信号
- GPU利用率:如果GPU利用率一直很低,说明数据加载是瓶颈
我用TensorBoard做可视化,配合自定义的回调函数,每隔一定步数就记录一次指标。这样训练过程中随时能看到曲线,有问题及时中断调整。
5. 推理服务部署:从Notebook到生产环境
5.1 模型导出:ONNX是个好东西
训练完的PyTorch模型不能直接用于生产推理,需要先导出成适合部署的格式。ONNX是我最常用的中间格式,它有几个好处:跨框架、跨平台、性能好。
导出ONNX的核心步骤:
import torch.onnx # 设置模型为评估模式 model.eval() # 构造示例输入 dummy_input = torch.randn(1, 3, 224, 224) # 导出 torch.onnx.export( model, dummy_input, "model.onnx", input_names=["input"], output_names=["output"], dynamic_axes={ "input": {0: "batch_size"}, "output": {0: "batch_size"} }, opset_version=13 )dynamic_axes这个参数很关键。如果不设置,导出的模型只能接受固定batch size的输入。设置之后,batch size可以动态变化,部署时更灵活。
导出之后一定要验证:用同样的输入,分别跑PyTorch模型和ONNX模型,对比输出是否一致。我遇到过一次导出后精度下降的问题,原因是某个算子在不同opset版本下行为不一致,换了个opset版本就好了。
5.2 服务框架选型:FastAPI还是Triton
推理服务的框架选择取决于你的需求复杂度。
FastAPI适合简单场景:模型不多、流量不大、不需要复杂的调度。它的优势是开发快、调试方便、Python生态无缝衔接。
from fastapi import FastAPI import onnxruntime as ort import numpy as np app = FastAPI() session = ort.InferenceSession("model.onnx") @app.post("/predict") async def predict(data: dict): input_array = np.array(data["input"], dtype=np.float32) outputs = session.run(None, {"input": input_array}) return {"prediction": outputs[0].tolist()}Triton Inference Server适合复杂场景:多模型、多框架、需要动态批处理。它的动态批处理功能特别实用——多个请求自动合并成一个batch,显著提升GPU利用率。
我的建议是:先用FastAPI把服务跑起来,等遇到性能瓶颈再考虑Triton。不要一开始就上重型武器,维护成本太高。
5.3 性能优化:从200ms到50ms的实战记录
线上服务最怕的就是延迟高。我优化过一个文本分类服务,从最初的200ms降到了50ms,过程如下:
第一步:定位瓶颈。用py-spy做性能分析,发现60%的时间花在数据预处理上,而不是模型推理。
第二步:优化预处理。原来的预处理是纯Python循环,改成NumPy向量化操作后,预处理时间从120ms降到了15ms。
第三步:启用ONNX Runtime的优化。设置graph_optimization_level为ORT_ENABLE_ALL,推理时间从80ms降到了35ms。
第四步:批处理。把多个请求攒成一个batch一起推理,平均延迟进一步降到50ms以下。
| 优化阶段 | 预处理耗时 | 推理耗时 | 总延迟 |
|---|---|---|---|
| 优化前 | 120ms | 80ms | 200ms |
| 向量化后 | 15ms | 80ms | 95ms |
| ONNX优化后 | 15ms | 35ms | 50ms |
| 批处理后 | 15ms | 20ms | 35ms |
心得:性能优化一定要先测量再优化。我见过有人凭感觉优化,花了一周改代码,结果发现瓶颈根本不在那里。
6. 监控与运维:上线只是开始
6.1 模型性能监控:准确率不是唯一指标
模型上线之后,你需要持续监控它的表现。但监控不只是看准确率——线上环境往往没有实时标签,你拿不到准确率。
我用的替代指标包括:
- 预测分布:如果预测结果的分布突然偏移,说明输入数据可能变了
- 置信度分布:模型对预测的置信度如果整体下降,说明遇到了不熟悉的数据
- 输入特征统计:均值、方差、缺失率等,和训练时对比
这些指标不需要标签就能计算,能提前发现很多问题。我有一次发现某个特征的均值突然偏移了3个标准差,排查后发现是上游数据源改了字段格式,导致解析错误。如果没有监控,这个问题可能要等到用户投诉才会发现。
6.2 数据漂移检测:当世界变了,模型也得变
数据漂移是指线上数据的分布和训练数据不一致。这是模型效果下降最常见的原因。
检测方法主要有两种:
- 统计检验:用KS检验或PSI(Population Stability Index)比较训练集和线上数据的分布
- 模型检测:训练一个分类器来区分训练数据和线上数据,如果分类器准确率高,说明两者分布差异大
PSI的计算公式是:
PSI = sum((实际占比 - 预期占比) * ln(实际占比 / 预期占比))PSI小于0.1说明分布稳定,0.1到0.25之间需要关注,大于0.25说明分布显著变化,需要考虑重新训练模型。
我一般每周跑一次漂移检测,如果PSI超过阈值就触发告警,评估是否需要更新模型。
6.3 常见故障排查速查表
线上出问题是常态,关键是要快速定位和解决。我整理了一份常见问题速查表:
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 推理延迟飙升 | GPU显存不足 | 查看nvidia-smi | 减小batch size或清理显存 |
| 预测结果异常 | 输入数据格式变化 | 对比输入特征统计 | 修复数据管道 |
| 服务频繁重启 | 内存泄漏 | 监控内存使用曲线 | 检查代码中的循环引用 |
| 准确率下降 | 数据漂移 | 计算PSI指标 | 重新训练模型 |
| 请求超时 | 并发过高 | 查看QPS和响应时间 | 增加实例或启用批处理 |
这份表是我在实际运维中总结的,覆盖了80%以上的常见问题。遇到新问题就补充进去,慢慢就形成了一套排查体系。
7. 我踩过的那些坑和给你的建议
7.1 训练-推理偏差:最隐蔽的bug
训练时用一套预处理逻辑,推理时用另一套,导致模型在线上表现和离线评估差距很大。这个问题极其隐蔽,因为两套逻辑单独看都没错,只是不一致。
我的解决方案是把预处理逻辑封装成独立的模块,训练和推理都调用同一份代码。具体做法是把预处理函数放在src/data/preprocess.py里,训练脚本和推理服务都import这个模块。这样只要改一处,两边都生效。
7.2 版本管理混乱:模型、数据、代码三者要对齐
模型效果好的时候,你要能回答:这个模型是用哪个版本的代码、哪个版本的数据、哪组超参数训练出来的?如果回答不了,那这个模型就无法复现,也无法迭代。
我的做法是三位一体管理:
- 代码用Git管理,每次训练记录commit hash
- 数据用DVC管理,每次训练记录数据版本tag
- 超参数用MLflow管理,每次训练自动记录
这样任何一个模型都能追溯到它的完整来源。
7.3 过度工程化:别为了架构而架构
我见过一些团队,项目还没跑通就搞了一套复杂的微服务架构,结果开发效率极低,改一行代码要部署三个服务。
我的建议是:从简单开始,遇到问题再演进。一个FastAPI服务能解决的问题,不要拆成五个微服务。一个PostgreSQL能存的数据,不要上数据湖。架构是演进来的,不是设计出来的。
7.4 忽视测试:AI项目也需要单元测试
很多人觉得AI项目没法做单元测试,因为结果是概率性的。但实际上,很多组件是可以测试的:
- 数据预处理函数:给定输入,输出应该是确定的
- 特征计算逻辑:可以用手工计算的结果做验证
- API接口:可以用mock模型测试请求响应格式
我现在的项目要求核心模块的测试覆盖率不低于70%。这个要求看起来高,但实际做下来发现,大部分bug都能在测试阶段发现,省下了大量线上排查的时间。
7.5 文档缺失:三个月后的你看不懂三个月前的代码
AI项目迭代快,代码变动频繁。如果没有文档,三个月后你回头看自己的代码,可能完全不记得当时为什么这么写。
我的文档习惯是:
- 每个模块顶部写清楚这个模块的职责和输入输出
- 关键函数写清楚参数含义和返回值格式
- 重要的设计决策记录在
docs/decisions/目录下,说明背景、方案、理由
这些文档不需要写得很正式,几句话说明白就行。关键是在写代码的时候顺手写,不要等到项目结束了再补——那时候你早就忘了。
8. 从零到一的完整实操路线
如果你现在要开始一个AI工程项目,我建议按这个顺序推进:
第一周:搭骨架。创建项目目录结构,配置好Git和DVC,写好配置文件模板。不要急着写模型代码,先把基础设施搭好。
第二周:通数据。实现数据加载和预处理管道,确保能稳定地产出训练数据。这一步做完,你应该能回答:数据从哪来、怎么处理、存到哪去。
第三周:跑模型。实现一个最简单的模型,跑通训练-评估-保存的完整流程。不要追求效果,先追求流程通畅。
第四周:做服务。把训练好的模型部署成API服务,能接收请求返回预测。这一步做完,你就有了一个端到端的AI系统。
第五周开始:迭代优化。在跑通的系统上逐步优化:提升模型效果、优化推理性能、完善监控告警。
这个路线看起来慢,但每一步都踩实了,后面会越来越快。我见过太多人跳过前两步直接搞模型,结果数据出了问题、流程跑不通,反而浪费更多时间。
最后分享一个我个人的习惯:每做完一个项目,花半天时间写一份复盘文档。记录这个项目里做了什么决策、遇到了什么问题、怎么解决的、如果重来会怎么做。这份文档是你最宝贵的经验积累,比任何教程都有价值。