为什么要做“从零开始造AI工程”
我先说个场景。简历上写着“熟悉TensorFlow/PyTorch”,GitHub上挂着几个notebook,面试的时候能聊两句Transformer,但一到真实业务里,老板让把一个模型做成线上接口服务,要能扛住流量、要能监控、要能回滚、要能版本迭代,很多人当场就卡住了。我见过太多这样的人,也包括几年前的我。
所以当我看到ai-engineering-from-scratch这个项目标题时,第一反应就是:这玩意儿踩中了绝大多数AI从业者的命门——从“会跑模型”到“会做系统”之间那条巨大的鸿沟。它不是一个教你调参的教程,不是又一个Transformer原理精讲,它是在告诉你:如果让你一个人、一台机器、从裸机开始,把一个AI应用完整地造出来、跑起来、维护下去,你到底需要掌握哪些能力,按什么顺序学,每一阶段要做到什么程度才算过关。这类项目解决的核心痛点就是“学了一堆碎片化知识,但无法组装成一套能用的系统”。适合的人群也很明确:已经能跑通简单模型、但没正经做过工程化项目的学生,刚入职做算法落地不久的新人,以及想从纯算法岗转向AI工程岗的开发者。
这篇内容,我就把它当作一次完整的拆解和一个“过来人”的经验记录,把这套能力地图给你摊开,顺带附上实战代码、选型分析和踩坑记录。
1. 先拆解:ai-engineering 到底在学什么
1.1 AI 工程师不是调包侠:能力栈全景
很多人对AI工程的误解是:只要会写模型结构、会训练,剩下的交给后端工程师就行。真做一次端到端的项目就会发现,工程化的“脏活累活”多到超出想象。一个完整的AI工程能力栈,至少覆盖这么几层:
数据处理层是地基。真实环境里的数据远没有Kaggle比赛那么干净,缺失值怎么处理、异常值怎么识别、类别特征怎么编码、时间序列怎么对齐、多源数据怎么join,每一项都直接影响模型上限。这层的能力要求是“数据敏感度”——拿到一批数据,你能在半小时内找到明显的问题,而不是把脏数据直接喂进模型。更进阶的还包括数据版本管理、数据管道编排、特征存储。
训练与实验层是核心。这层不仅包括模型架构设计和调参,还包括实验追踪、结果对比、超参数搜索、分布式训练策略。“可复现性”在这里是被反复强调的——今天跑出来的结果,是不是改了两行代码之后还能复现?依赖版本有没有锁死?随机种子有没有固定?很多新手吃过大亏:上周实验结果好,这周怎么跑都复现不了,最后发现是某个库悄悄升级了。
服务化与推理层是落地关键。模型训练好只是开始,真正的工程挑战在于把模型封装成接口、合理管理算力资源、降低推理延迟、提升吞吐量。这个层面要求你懂模型量化、Batch推理、缓存策略、请求排队,还要能判断该用同步接口还是异步任务。再加上模型的AB测试、灰度发布和监控告警,就又是一整套方法论。
基础设施与运维层是长跑保障。模型上线后不是万事大吉,数据分布会漂移,模型效果会衰减,线上反馈需要回流再训练。这一层要求你理解容器化部署、CI/CD流程、日志收集、指标监控、告警规则,以及最核心的——整套流程能不能自动化。如果一个环节还需要人工手动操作,那它就是不稳定的来源。
说白了,AI工程是“算法+系统+数据”的交叉学科,每一项都需要刻意练习。
1.2 为什么选择“从零开始”的方式
我在带新人的时候发现一个规律:直接丢给他一个现成项目,让他在上面加需求,上手很快,但一旦出了奇怪的问题,他根本不知道从哪里排查。因为他对整个系统的“因果链”没有建立起来——他不知道哪里出了问题应该往哪一层去查。
而“从零开始”的方式,看起来慢,实则快。你得自己装环境、自己建数据管线、自己写训练脚本、自己设计接口、自己部署服务。每一个环节都亲手敲过一遍之后,你对系统的理解是从底层长出来的,不是从上层覆盖下来的。就好比学做饭:跟着菜谱炒出一个菜容易;不打火、不备菜,光是理解“为什么要热锅凉油”、“为什么先下葱姜蒜”,就得自己动手烧过几回厨房才能明白。
这个项目的路线,本质上就是在强制你“由下往上”构建知识体系。不给你现成的模板工程去抄,而是让你搞清楚每个模块存在的理由、它解决的问题、不做的后果。这叫“慢就是快”。
2. 从零到上线:一条完整的 AI 工程路线图
2.1 基础层:数学、Python 与数据操作
别急着上深度学习框架。我见过太多人PyTorch写得飞起,结果一个简单的梯度推导写不出来,一个pandas的groupby操作要想半天。工程化落地需要的基础,其实比很多人想象中窄但更深:
数学基础不需要学到数学系的程度,但线性代数中的矩阵乘法、特征分解、奇异值分解,概率论中的常见分布、条件概率、最大似然估计,微积分中的偏导数和链式法则,这三块是硬底线。它们不一定每天直接出现在代码里,但当你读论文、调loss、改网络结构时,它们是理解一切的基础语言。做AI工程还要额外补一点数值计算的内容,比如浮点精度的坑、梯度消失爆炸的成因,这些都属于“数学照进现实”的部分。
Python工程能力才是真正的分水岭。写脚本和写工程是两码事。合格的AI工程师至少要熟练:Python的面向对象设计、类型注解、装饰器和上下文管理器;虚拟环境管理(venv、conda、poetry);包管理配置;以及基本的单元测试和日志规范。不要小看这些“不性感”的内容,它们是团队协作和项目可维护性的命根子。
数据操作部分,SQL + pandas是你吃饭的家伙。很多面试官喜欢考一个真实的取数场景:给你三张表,要统计某个维度的转化率,并要求考虑去重、空值、时间窗口等问题。这背后的本质是考察你是否能精准地理解业务指标和数据形态。数据操作过关之后,再去学更系统的数据管道工具(Airflow、Prefect)也不会太吃力。
2.2 训练层:框架、实验管理与模型开发
框架选型上,现在是PyTorch的天下,这没什么悬念。TensorFlow在部分生产场景还有存量,但新项目绝大多数是PyTorch。关键不是记住API,而是理解一个训练脚本的结构化套路。我习惯把训练脚本分为几块:数据加载器(Dataset/DataLoader)、模型定义、损失函数和优化器配置、训练循环、验证循环、检查点保存和日志记录。每一块各司其职,改起来互不干扰。
实验管理方面,至少要会用一款工具追踪每次实验的配置、指标和产物。MLflow是比较轻的选择,支持记录params、metrics、artifacts,一行代码就可以接入。W&B体验更好但要联网,团队内部用起来要考虑成本。重点不在工具本身,而在于建立一种习惯:每次实验必留记录,每条记录必带可复现信息——代码版本、数据版本、配置、随机种子、环境依赖,缺一不可。
调参这块,最容易掉进去的是“手动乱试”。遇到效果不好就调一下学习率、换一换网络层数,漫无目的地瞎试,既浪费时间又得不到可迁移的经验。比较推荐的做法是:先跑一个小规模基线,确定loss能下降、指标有意义,再使用Optuna这类工具做结构化超参搜索。别一上来就上贝叶斯优化,先用随机搜索和网格搜索找感觉,再逐步精细。
2.3 服务层:模型打包、推理优化与 API 化
训练完成 ≠ 工作完成。模型要变成产品,必须过一道“工程转换”的工序。
模型导出是第一步。PyTorch训练好的.pt权重文件,通常不能直接被线上服务加载。常规操作是先把模型转成TorchScript,或者进一步导出为ONNX格式。ONNX的好处是中间表示层,之后可以接不同的推理引擎(ONNX Runtime、TensorRT),还能顺便做图优化和算子融合。生产环境里我一般直接上ONNX,跨框架的兼容性好,优化也省心。
推理优化涉及的手段很多:半精度推理(FP16)能把显存占用和延迟同时降低接近一半;Batch推理就是把多个请求拼在一起过一遍模型,通常能成倍提升吞吐;自带缓存的重复请求处理也能有效降低压力;最终手段才是模型量化——把权重从FP32压到INT8,精度往往还能控制在可接受范围内,但速度提升非常可观。量化这块要多留一个心眼:不是所有模型都能直接量化不掉点,必须拿自己的数据做充分验证。
API化最常用的组合是 FastAPI + Uvicorn,不用我说你也能在网上找到大量示例,后续我会给出一个完整示例。FastAPI的优势在于自动生成OpenAPI文档、类型校验完善、并发性能不错,而且生态成熟,和Pydantic配合做请求/响应的校验几乎是天然的。把模型加载放到全局变量,推理函数封装成异步接口,一个能用的服务也就一百多行代码。
2.4 运维层:监控、版本管理与持续集成
很多从零开始的教程讲到这里就停了,那其实还不是完整工程。真正线上跑着的东西,必须有监控和迭代闭环。
监控方面,除了常规的CPU、内存、GPU、QPS、延迟这些基础设施指标,更值得关注的是模型效果指标——比如线上预测分布的均值漂移、特征缺失率变化、用户反馈的负面率上升。这些信号意味着数据分布变了,模型可能需要更新了。用Prometheus + Grafana做基础设施监控,用自定义埋点上报业务指标,基本能覆盖绝大多数需求。
版本管理比很多人以为的更复杂。模型文件本身要版本化,训练用的数据要版本化,特征计算逻辑要版本化,甚至连prompt模板(如果是LLM应用)都要版本化。DVC可以做数据和模型版本管理,和Git配合良好;MLflow的Model Registry也能管模型版本和stage流转。核心思路就一条:任何影响线上预测结果的东西,都必须能回滚到历史上的某个精确状态。
CI/CD对于AI工程来说也别具一格。除了常规的代码测试和构建,还要加入“数据验证”和“模型评估”两道关卡——数据schema变了触发告警,模型在验证集上指标低于阈值就跑不过Pipeline。把质量控制前置,比靠人盯靠谱得多。
3. 实操实录:把一套模型端到端跑起来
3.1 项目结构与数据管线
理论说完,看一个最小可行的端到端结构。假设我们要做一个“影评情感分类”服务,数据是IMDB评论,这个规模适中,能在一台普通开发机上跑通,是很好的练习素材。
我推荐的项目结构是这样:
ml-project/ ├── configs/ # 配置文件(yaml) ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── src/ │ ├── data/ # 数据加载与预处理 │ ├── models/ # 模型定义 │ ├── train.py # 训练入口 │ └── predict.py # 推理封装 ├── services/ │ └── api.py # FastAPI 服务 ├── tests/ # 单元测试 ├── scripts/ # 运维脚本 ├── requirements.txt └── README.md不复杂,但分层清晰。数据进raw之后不要动,所有清洗逻辑写到src/data/里,输出到processed/。这样别人接手时能清楚地看到“原始数据长什么样、哪里改过、改成了什么样”。
数据处理这一块最容易忽视的是文本标准化的一致性。训练时你做了小写化、去停用词、词形还原,那推理时也必须走完全一样的流程。我见过线上服务忘了调小写函数,导致模型输入分布和训练完全不匹配,精度直接崩掉。解决办法很简单:把预处理逻辑封装成一个类或者一个函数,训练和推理共用同一个模块,不允许两套逻辑各自实现。
另外数据划分也用得上几行严谨代码——训练/验证/测试的比例控制在8:1:1,并且要按标签分层抽样,保证类别比例在三个集合里一致。用sklearn的train_test_split加stratify参数就能搞定,这是很多人会忽略的细节。
3.2 训练脚本里容易忽视的三件事
训练脚本的核心逻辑大多数人都能写出来,无非就是forward、loss、backward、step。我见过太多跑得起来的脚本,但能进生产的脚本,还得补三样东西:
第一,固定随机种子。保证在同样的数据、同样的代码版本下,多次训练结果一致。做法是在入口统一设置:
def set_seed(seed: int = 42): import random import numpy as np import torch 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 = False有人觉得这样会拖慢训练,确实会,但换来的是实验可复现性。跑实验阶段可以关掉,正式训练或者出报告时一定要打开。
第二,Early Stopping + 模型检查点。很多人训练用固定epoch数,但其实更稳的是在验证集上监控loss,连续N轮没有改善就提前停止,并且只保留验证指标最好的那次权重。写成代码就是:
best_val_loss = float("inf") patience = 0 for epoch in range(max_epochs): train_loss = train_one_epoch(model, train_loader) val_loss = evaluate(model, val_loader) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pt") patience = 0 else: patience += 1 if patience >= early_stop_epochs: break不要只把最后一个epoch的权重存下来,那不是最好的模型。
第三,训练日志要能被人看懂。用标准库的logging或者第三方库都行,关键是每轮记录loss、学习率、当前epoch、耗时,还要能方便地导出成结构化数据(比如json lines),这样后续画loss曲线时直接读取,而不用去抠终端输出。
这里有一个经验数字:如果训练脚本启动后,前几个batch的loss没有明显下降趋势,大概率是学习率设置有问题,或者数据/标签对不上。别傻等,先停下来排查。
3.3 模型导出与推理服务化
训练完成后,把PyTorch模型导出为ONNX。以我们简单的LSTM分类模型为例:
import torch from models import build_model model = build_model(vocab_size=50000, embed_dim=128, hidden_dim=256, num_classes=2) model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) model.eval() dummy_input = torch.randint(0, 50000, (1, 128)) # (batch, seq_len) torch.onnx.export( model, dummy_input, "model.onnx", export_params=True, opset_version=13, input_names=["input_ids"], output_names=["logits"], dynamic_axes={"input_ids": {0: "batch_size", 1: "sequence_length"}} )dynamic_axes这里务必设置成动态轴,否则模型只能接受固定形状的输入,线上遇到不同长度的句子就傻了。
ONNX模型配合FastAPI做服务,整体架构干净利落。注意一个关键细节:模型是全局加载一次,不要每次请求都重新加载。
from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort import numpy as np from src.data.preprocess import preprocess_text app = FastAPI() sess = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"]) class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): probability: float label: str @app.post("/predict", response_model=PredictResponse) def predict(req: PredictRequest): input_ids = preprocess_text(req.text) # 返回 np.array shape=(seq_len,) logits = sess.run(None, {"input_ids": input_ids.astype(np.int64)})[0] prob = float(1 / (1 + np.exp(-logits[0]))) label = "positive" if prob > 0.5 else "negative" return PredictResponse(probability=prob, label=label)还要注意,ONNX Runtime有个providers参数。机器上有NVIDIA GPU的话可以试试CUDAExecutionProvider,没有就老老实实CPUExecutionProvider。选择provider的顺序会影响生效优先级,一般把CUDA放前面。
启动服务用一行命令:
uvicorn services.api:app --host 0.0.0.0 --port 8000 --workers 2这里workers的个数不是越多越好。如果你的服务是CPU密集型推理,worker数一般不超过CPU核心数;如果是IO密集型,可以适当多开。而且每个worker都会复制一份模型到内存里,内存不够时多开反而拖垮整体性能。
3.4 上线前必须做的压测与稳定性检查
模型一旦准备上线,我先做三件事,都不复杂,但能避免大量线上事故。
第一件,写个简单的压测脚本,模拟连续发N个请求,看延迟和成功率。不用上复杂工具,直接用locust或者自己用asyncio写个并发脚本就够了。我通常会设置一个并发梯度:10并发、50并发、100并发逐步往上加,同时观察P50和P99延迟的变化。如果P99比P50高出五六倍,大概率存在某个资源瓶颈,常见的就是Python GIL限制让模型推理排队了,或者数据预处理和推理争抢CPU。
第二件,检查最大输入长度的边界情况。文本类接口,用户不会按照你训练时的长度来。如果输入的padding后超过模型支持的最长长度,可能直接报错或者截断后信息丢失。一定要在代码里显式处理超长输入逻辑,比如截断、告警、或者丢弃。
第三件,冷启动和内存占用检查。模型服务启动后,观察一下常驻内存和首次请求的耗时。你会发现第一个请求特别慢——因为懒加载的库、模型初始化都堆积在第一次调用上。如果线上有这个情况,建议在服务启动时做一次“预热请求”,让所有资源在接收真实流量之前就位。
压测下来如果发现延迟超标,个人建议先不要急着加机器,先确认是不是模型太大、预处理里存在低效代码、或者没用上batch推理。我在实际项目里遇到过很多次,代码层面一个小小的优化,效果比多加一台机器都明显。
4. 从零搭建中的典型问题与排查手册
这部分全是实际踩过的坑,每一个都让我印象深刻。
4.1 训练环境和生产环境不一致
经典中的经典。开发机上用的是Python 3.10 + PyTorch 2.1,线上容器里是Python 3.8 + PyTorch 1.13,结果模型加载直接报错,或者推理结果微妙地不一致。解决这个问题只有一条路:把环境作为项目的一部分管起来。用requirements.txt锁死顶层依赖不算够,生产环境建议直接把整个依赖树锁死:
pip freeze > requirements_lock.txt更好的方式是使用容器镜像,把CUDA、Python版本、系统库、Python依赖全部固化在镜像里。每次训练和部署都用同一个镜像,从根上消除环境漂移问题。这一点我在很多从零教程里都没看到,但它其实是工程化最核心的习惯。
4.2 数据泄漏与评估失真
数据泄漏在NLP任务里比较隐蔽。一个常见的坑:做文本分类时,你用整个数据集做了词表(vocabulary)构建,然后才切分训练/验证/测试集。这样测试集里出现的词已经在词表中“见过”了,模型实际上从词表中获取了本不该接触的信息。在机器学习中这叫“预处理泄漏”,会让验证指标虚高,上线后崩盘。
正确做法是先切分数据集,然后用“训练集”部分单独拟合词表或者统计特征,验证集和测试集只做变换,不做拟合。这一步踩过之后,我写任何数据处理代码都会先问自己:这个统计量是“全局统计”还是“训练集统计”?如果是前者,线上怎么办?
4.3 显存溢出与批量策略
训练时显存溢出(OOM)几乎是每个人都会遇到的。常规解法是减小batch size,但这会拖慢训练。我一般先做三件事:
- 检查输入tensor的dtype,确保是FP32或FP16而不是默认双精度(Double),双精度的显存占用是两倍;
- 检查是不是有变量在计算图中被全程持有,导致无法释放显存;
- 用
torch.cuda.empty_cache()在合适时机清理缓存,但这不是根治方案。
如果以上都做了还是OOM,可以考虑梯度累积——用多个小batch的梯度累加后再更新参数,效果等效于大batch,但显存占用平稳许多。
4.4 模型不可复现与乱糟糟的Prompt
模型不可复现的最大元凶就是不固定随机种子和依赖版本升级。如果做LLM应用,还发现同样的Prompt和参数,输出结果每次都变化,那是因为温度系数(temperature)没设为0,模型天然有随机性。这种随机性在开发调试时会让你疯掉,“这行代码明明没改,怎么效果不一样了”。
排查思路是逐层隔离:先锁模型推理的随机性(设置do_sample=False或temperature=0),再锁数据加载的随机性(固定shuffle的seed),最后锁训练初始化的随机性。从外层往里一层层排查,总能定位到问题所在。
4.5 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 训练loss不降 | 学习率太大/太小,数据标签错位 | 输出前几个batch的prediction与label对比 |
| 验证集指标远低于训练集 | 过拟合/数据泄漏 | 检查预处理是否用全局统计,增加正则 |
| 线上延迟偏高 | 模型过大、预处理低效、无batch | 压测定位瓶颈,考虑量化或加缓存 |
| 模型加载失败 | 序列化格式不匹配、依赖版本不一致 | 对比训练与加载环境torch/torchvision版本 |
| 首个请求特别慢 | 懒加载 | 启动时做预热请求 |
| 复现失败 | 随机种子未固定、依赖变更 | 锁定随机种子,依赖全量冻结 |
5. 工具选型:我的取舍与理由
5.1 框架选择:为什么是 PyTorch 生态
PyTorch在研究和生产两个维度上同时占优,这不是偶然。它的动态图机制让调试变得非常直观,你可以在执行到任意一行时打印中间结果,这对于开发效率太关键了。配套生态也最丰富,Hugging Face Transformers天然优先支持PyTorch,torch.compile又能把训练加速一截。
TensorFlow的优势曾经在于生产部署闭环完整(TF Serving、TF Lite),但这两年PyTorch这边也有了TorchServe和ONNX Runtime,差距在逐渐抹平。从招聘市场看,纯PyTorch的岗位需求明显更多,我身边的新项目也几乎都是PyTorch一家独大。对于从零开始的你,选PyTorch不会错。
5.2 服务化方案:FastAPI+ONNX Runtime 对比 Triton
模型上线时,大部分场景用不到重型推理引擎。FastAPI + ONNX Runtime的组合是我最常用的默认方案,原因很简单:代码直观、调试方便、依赖轻、部署灵活。它适合QPS要求中等(几十到几百)、模型不算复杂、团队规模不大的情况。
但如果流量大、模型多、需要动态Batch、需要同时服务多个模型版本,NVIDIA Triton Inference Server会是更专业的选择。它内置动态Batch和并发推理优化,性能远好于自己手写。代价是运维复杂度明显上升,配置和理解成本都不低。我的建议是:先把FastAPI方案跑通、压测到接近瓶颈,再评估是否有必要上Triton。不要一上来就用重武器,那是给需要扛大流量的人准备的。
5.3 实验与数据版本管理:MLflow、DVC 的实际体验
MLflow 我用了三年多,整体体验是“轻量但够用”。实验追踪这块,每次训练自动记录参数和指标,UI里对比结果非常直观。Model Registry管理线上模型版本做得也不错,配合阿里云OSS或S3存模型文件,基本能满足小型团队的需求。
DVC 的数据版本管理理念我很认同:不复制数据本身,而是用Git记录数据文件的哈希和版本关系。数据文件存在共享存储里,需要时通过DVC pull拉取。这样可以随时切换回“某次实验用的那一版数据”。虽然初期配置有点繁琐,但一旦团队需要协作、复现历史结果,这套体系的价值立刻显现。
工具没有绝对的好坏,关键是匹配团队规模、业务需求和你自己的维护能力。选型的基本逻辑永远是:先用最简单的方案跑通流程,再根据痛点渐进式引入更复杂的工具。
最后分享一点个人体会
从零开始造AI工程,最磨人的不是某个具体技术点难到学不会,而是“你不知道自己不知道什么”——你以为提交了模型就是完成,结果被线上召回、数据漂移、可复现性这些问题轮番教做人。这个项目之所以有价值,就是因为它逼着你把整条链路走一遍,让你在坏事发生之前先知道坏事可能长什么样。
如果你正在走这条路,我的建议是:别贪多,先做透一个最小闭环。数据、训练、导出、上线、监控,哪怕是一个玩具模型,也要把它生产化。然后,再在这个闭环上一点点添加复杂度。做得深比看得广重要得多,因为工程能力的本质不是知识量,而是应对不确定性的经验积累。