
1. 从零搭建AI工程能力为什么我劝你别再“调包”了“ai-engineering-from-scratch”这个标题第一次看到的时候我就觉得挺有意思。它不像那些“三天速成大模型”或者“手把手教你微调GPT”之类的标题那么浮夸反而透着一股子踏实劲儿——从零开始把AI工程这件事真正搞明白。我做了十多年一线开发最近五六年一直在跟机器学习、深度学习相关的项目打交道。说实话我见过太多人上来就pip install transformers然后复制一段示例代码跑通了就觉得自己会AI工程了。可真到了线上环境模型推理延迟飙到几百毫秒、显存动不动就OOM、数据管道三天两头挂掉的时候整个人就懵了。这就是典型的“只会调包不懂工程”。所以当我看到“ai-engineering-from-scratch”这个项目标题时我第一反应是终于有人愿意把AI工程当作一门正经的工程学科来对待了而不是把它简化成几个API调用。这个项目适合谁呢我认为有三类人最应该关注第一类是有一定编程基础但没系统接触过AI工程实践的开发者第二类是做过后端或数据工程想转型AI方向的工程师第三类是在小团队里什么都得自己扛的全栈选手。如果你属于这三类中的任何一类那接下来的内容应该能帮你省下不少自己摸索的时间。这篇文章我会从项目整体设计思路、核心技术细节、实操落地过程、常见问题排查这几个维度把“从零构建AI工程能力”这件事拆开揉碎了讲。我不会只告诉你“怎么做”更会告诉你“为什么这么做”以及“我当时踩了哪些坑”。2. 整体设计思路AI工程到底在工程什么2.1 先搞清楚AI工程和模型训练的区别很多人把AI工程等同于训练模型这是一个特别大的误区。训练模型只是整个AI系统里的一环而且往往不是最耗时的那一环。真正的AI工程要解决的问题是如何让一个模型在生产环境里稳定、高效、可维护地运行并且能持续迭代。我习惯把AI工程拆成四个层次来看。最底层是基础设施层包括计算资源管理、存储、网络这些往上是数据层涉及数据采集、清洗、特征工程、数据版本管理再往上是模型层包括模型训练、评估、调优、压缩最上面是服务层负责模型部署、推理优化、监控告警、A/B测试。这四个层次缺一不可而“from scratch”的意义就在于你得对每一层都有基本的掌控力而不是只会其中某一层。为什么我强调“掌控力”而不是“精通”因为在实际工作中你不可能每个环节都做到专家级别但你必须知道每个环节在干什么、边界在哪里、出了问题该往哪个方向排查。举个例子线上推理延迟突然从50ms涨到200ms如果你只懂模型层你可能会去检查模型结构是不是有问题但如果你对服务层和基础设施层也有概念你就会先去看是不是GPU利用率上去了、是不是batch size被动态调整了、是不是有内存泄漏导致频繁GC。这种全局视角才是AI工程师和调参侠的本质区别。2.2 为什么选择“从零构建”而不是“站在巨人肩膀上”现在开源工具链已经非常成熟了PyTorch、TensorFlow、ONNX、Triton、Ray、MLflow随便拎一个出来都能解决一大类问题。那为什么还要“from scratch”这不是重复造轮子吗我的理解是这样的用工具和懂原理是两码事而从零构建是打通这两者之间壁垒的最短路径。你当然可以用HuggingFace的Trainer三行代码启动训练但当你需要自定义一个损失函数、需要修改数据采样策略、需要实现一种新的分布式训练并行方式时如果你不理解底层是怎么运作的你就只能去翻源码、提issue、等社区回复。而如果你曾经从零实现过一个简化版的训练循环你就知道梯度累积是怎么回事、混合精度训练在什么位置插入缩放、分布式数据并行怎么同步梯度。这些知识不是靠读文档能读出来的必须自己动手写一遍。另外从零构建还有一个好处你会对“抽象泄漏”特别敏感。所有高级框架都是建立在抽象之上的而抽象总有泄漏的时候。当你自己实现过底层逻辑你就能在框架出问题时快速定位到是哪一层抽象出了问题而不是像盲人摸象一样到处乱试。2.3 项目整体架构的取舍逻辑基于“从零构建”这个核心思路我在设计自己的AI工程学习路径时遵循了三个原则。第一个原则是最小依赖。能不用第三方库就不用必须用的时候优先选轻量级的。比如数据处理我一开始只用NumPy和Pandas后来发现Pandas在超大规模数据上性能不行才引入Polars。模型训练先用纯NumPy实现了一个简单的全连接网络和反向传播然后再用PyTorch重写一遍做对比。这样做的好处是每一层依赖的引入都是经过深思熟虑的而不是一开始就堆一堆库上去。第二个原则是可观测优先。从第一天起就要把日志、指标、追踪这些东西加进去。很多人做项目喜欢先跑通再说结果后面出了问题完全不知道从哪里查起。我的做法是哪怕是一个最简单的推理脚本也要记录输入输出的形状、推理耗时、内存占用这些基本信息。这些日志在开发阶段看起来没什么用但到了排查线上问题的时候就是救命稻草。第三个原则是渐进式复杂化。不要一上来就搞分布式训练、模型并行、异构计算这些高级玩意儿。先用单机单卡把整个流程跑通然后逐步增加数据量、模型规模、并发请求观察系统在哪个环节先扛不住再针对性地引入解决方案。这样做的好处是你清楚地知道每个复杂度是为了解决什么具体问题而引入的而不是为了炫技。3. 核心细节解析数据管道、模型训练与服务化3.1 数据管道AI工程里最容易被低估的环节如果让我给AI工程的各个环节按重要性排个序数据管道绝对排第一。我见过太多项目模型结构设计得很漂亮训练技巧也很花哨但数据管道一塌糊涂最后效果就是上不去。数据管道要解决的问题包括数据从哪里来、怎么清洗、怎么切分、怎么喂给模型、怎么保证训练和推理时的一致性。先说数据加载。很多人直接用DataLoader就完事了但你要知道DataLoader的num_workers设置是有讲究的。设太小GPU等数据利用率上不去设太大CPU内存爆掉或者进程间通信开销超过数据加载本身的开销。我的经验值是num_workers从CPU核心数的四分之一开始试然后观察GPU利用率。如果GPU利用率低于80%就适当增加如果CPU内存使用率超过70%就减少。再说数据预处理。这里有一个经典的坑训练时的预处理和推理时的预处理不一致。比如训练时对图像做了归一化推理时忘了做或者用了不同的均值方差结果就是模型效果断崖式下跌。我的做法是把预处理逻辑封装成一个独立的模块训练和推理都调用同一个模块从源头上杜绝不一致的可能。还有一个容易被忽略的点是数据版本管理。你改了清洗逻辑、换了数据源、调整了采样策略这些变更如果不记录后面模型效果变化了你根本不知道是模型的问题还是数据的问题。我一般用DVC或者简单的文件哈希来管理数据版本每次训练都记录对应的数据版本号。3.2 模型训练从单卡到多卡的平滑过渡模型训练这块我想重点讲两个东西混合精度训练和梯度累积。这两个技术在实际项目中用得非常多但很多人只是知道概念不知道具体怎么实现、什么时候该用。混合精度训练的核心思想是用FP16做前向和反向计算用FP32保存模型权重。这样做的好处是显存占用减少将近一半计算速度也能提升。但FP16的数值范围比FP32小很多容易出现梯度下溢或者上溢。解决方案是损失缩放在反向传播之前把损失乘以一个缩放因子这样梯度就不会下溢在更新权重之前再把梯度除以这个缩放因子。PyTorch的amp模块已经自动处理了这些但你要理解它在干什么不然出了问题不知道怎么调。梯度累积解决的是显存不够但想要大batch size的问题。具体做法是多次前向反向计算梯度但不更新权重等累积到一定步数再统一更新。这里有一个细节BatchNorm层在梯度累积时会有问题因为每次前向的batch统计量是基于小batch算的和真正的大batch不一致。解决方案是用SyncBatchNorm或者干脆用GroupNorm、LayerNorm替代。从单卡到多卡我建议的路径是先单卡跑通然后用DataParallel做最简单的多卡再过渡到DistributedDataParallel。DataParallel虽然简单但效率低因为它是单进程多线程GIL锁会限制性能。DistributedDataParallel是多进程每个进程独立控制一张卡效率高很多但配置也复杂一些需要设置MASTER_ADDR、MASTER_PORT、RANK这些环境变量。3.3 服务化让模型真正产生价值模型训练出来只是第一步把它部署成服务让业务方调用才是产生价值的地方。服务化这块我踩过的坑最多这里挑几个重点讲。第一个是推理框架的选择。PyTorch原生推理、TorchScript、ONNX Runtime、TensorRT这几个方案各有优劣。PyTorch原生最灵活但性能最差TorchScript是中间态兼顾灵活性和性能ONNX Runtime跨平台好但算子支持有限TensorRT性能最好但绑定NVIDIA硬件且转换过程容易出问题。我的建议是如果对延迟不敏感直接用PyTorch原生如果追求性能且模型结构固定用TensorRT如果需要跨平台部署用ONNX Runtime。第二个是动态batch和动态shape。线上请求的batch size是不固定的有时候来一个请求有时候来一批。如果每次都用固定batch size要么浪费计算资源要么增加延迟。解决方案是用Triton Inference Server或者自己实现一个请求队列攒够一定数量或者等待一定时间就触发一次推理。这里要权衡的是延迟和吞吐没有标准答案得根据业务需求来定。第三个是模型版本管理和灰度发布。新模型上线不能一下子全量替换得先小流量验证。我一般用模型注册中心来管理版本每个版本有唯一的ID和元数据。灰度发布时通过请求头或者用户ID来做流量切分观察新模型的各项指标没问题后再逐步扩大流量比例。4. 实操过程从零搭建一个完整的AI工程流水线4.1 环境准备与依赖管理动手之前先把环境搞干净。我强烈建议用conda或者venv创建独立的虚拟环境不要直接在系统Python里装包。依赖管理用requirements.txt或者pyproject.toml把版本号锁死。我吃过亏有一次本地开发环境跑得好好的部署到服务器上因为numpy版本不一样结果报了一个莫名其妙的错误查了半天才发现是版本问题。基础依赖清单大概是这样NumPy做数值计算Pandas或Polars做数据处理PyTorch做模型训练FastAPI做服务框架Uvicorn做ASGI服务器Prometheus客户端做指标暴露loguru做日志。这些都是经过大量项目验证的稳定选择不需要追求最新版本用稍微旧一点但稳定的版本反而更省心。conda create -n ai-eng python3.10 conda activate ai-eng pip install numpy pandas torch fastapi uvicorn prometheus-client loguru4.2 数据管道的具体实现数据管道我分成了三个模块读取、转换、加载。读取模块负责从不同数据源本地文件、数据库、对象存储把数据读进来统一转换成Pandas DataFrame或者PyArrow Table。转换模块负责清洗、特征工程、切分。加载模块负责把处理好的数据喂给模型。这里重点讲一下数据切分。很多人直接用train_test_split随机切分这在数据有时间属性的场景下是错的。比如用户行为预测你用未来的数据训练、用过去的数据测试效果肯定虚高。正确做法是按时间切分用过去的数据训练用之后的数据验证和测试。如果是时序数据还要做滚动窗口切分。import pandas as pd from sklearn.model_selection import TimeSeriesSplit df pd.read_parquet(data/features.parquet) df df.sort_values(timestamp) tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(df): train_df df.iloc[train_idx] val_df df.iloc[val_idx] # 训练和验证4.3 模型训练循环的完整实现我用PyTorch写一个完整的训练循环包含混合精度、梯度累积、学习率调度、早停这些常用技巧。这个模板我在多个项目里复用稳定性很好。import torch import torch.nn as nn from torch.cuda.amp import autocast, GradScaler from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, dataloader, optimizer, scaler, scheduler, accumulation_steps4, devicecuda): model.train() total_loss 0 optimizer.zero_grad() for step, (inputs, targets) in enumerate(dataloader): inputs, targets inputs.to(device), targets.to(device) with autocast(): outputs model(inputs) loss nn.functional.cross_entropy(outputs, targets) loss loss / accumulation_steps scaler.scale(loss).backward() if (step 1) % accumulation_steps 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() scheduler.step() total_loss loss.item() * accumulation_steps return total_loss / len(dataloader)这段代码里有几个关键点。autocast上下文管理器自动把适合的算子转成FP16GradScaler负责损失缩放和梯度裁剪。梯度累积通过除以accumulation_steps来实现这样累积后的梯度和真正的大batch是等价的。学习率调度器在每个优化步之后更新而不是每个前向步。4.4 服务化部署与监控服务化我用FastAPI加Uvicorn简单直接。模型加载在应用启动时完成避免每次请求都加载模型。推理接口接收JSON格式的输入返回JSON格式的输出。from fastapi import FastAPI from pydantic import BaseModel import torch import numpy as np app FastAPI() model None class PredictRequest(BaseModel): features: list class PredictResponse(BaseModel): prediction: list latency_ms: float app.on_event(startup) def load_model(): global model model torch.jit.load(model.pt) model.eval() app.post(/predict, response_modelPredictResponse) def predict(request: PredictRequest): import time start time.time() with torch.no_grad(): inputs torch.tensor([request.features], dtypetorch.float32) outputs model(inputs) prediction outputs.softmax(dim-1).tolist()[0] latency (time.time() - start) * 1000 return PredictResponse(predictionprediction, latency_mslatency)监控这块我用Prometheus客户端暴露几个关键指标请求总数、请求延迟分布、模型推理耗时、错误率。这些指标用Grafana展示设置告警规则。比如推理延迟P99超过200ms就告警错误率超过1%就告警。5. 常见问题与排查技巧实录5.1 训练不收敛的排查思路训练不收敛是新手最常遇到的问题没有之一。我的排查顺序是这样的先看数据再看模型最后看超参数。看数据主要检查三件事标签有没有问题比如标签全是0或者全是1、输入特征有没有异常值比如NaN或者Inf、数据分布有没有严重偏移。我遇到过一次数据里混入了一批未清洗的脏数据特征值范围从0到1变成了0到10000导致模型梯度爆炸。解决办法是在数据管道里加一层异常值检测和截断。看模型主要检查初始化和结构。初始化不能全零否则所有神经元的梯度都一样网络学不到东西。结构方面检查输入输出维度是否匹配、激活函数是否合适、有没有忘记加BatchNorm或者Dropout。看超参数主要检查学习率。学习率太大损失震荡不下降学习率太小损失下降极慢。我的经验是从1e-3开始试如果损失震荡就降到1e-4如果下降太慢就升到3e-3。另外warmup对Transformer类模型很重要前几百步用很小的学习率之后再线性增加到目标学习率。5.2 显存溢出的常见原因与解决方案显存溢出OOM是另一个高频问题。原因无非这几个batch size太大、模型太大、中间激活值太多、有内存泄漏。batch size太大最好解决直接调小就行。但调小之后如果效果下降可以用梯度累积来补偿。模型太大可以考虑模型并行或者用更小的模型。中间激活值太多可以用梯度检查点gradient checkpointing来用时间换空间。内存泄漏比较隐蔽通常是某个地方持有了不该持有的引用导致计算图无法释放。PyTorch里常见的是在训练循环里把loss或者outputs存到了列表里忘记detach。# 错误做法loss持有计算图导致显存无法释放 losses [] for batch in dataloader: loss model(batch) losses.append(loss) # 这里loss还带着计算图 # 正确做法detach或者转成Python标量 losses.append(loss.detach().item())5.3 推理服务性能优化的几个实用技巧推理服务性能优化我总结了一个优先级顺序先做模型量化再做算子融合最后做请求批处理。模型量化是把FP32权重转成INT8模型大小减少四分之三推理速度提升两到四倍精度损失通常在1%以内。PyTorch的quantize_dynamic可以一行代码搞定动态量化适合LSTM和Linear层多的模型。算子融合是把多个连续的小算子合并成一个大的算子减少kernel launch的开销。TensorRT和ONNX Runtime都支持自动算子融合但需要你把模型导出成对应的格式。请求批处理是把多个请求攒在一起推理充分利用GPU的并行能力。Triton Inference Server内置了动态批处理功能配置一下就行。如果自己实现可以用一个队列加一个后台线程攒够batch size或者超时了就触发推理。5.4 常见问题速查表问题现象可能原因排查方法解决方案训练损失不下降学习率太小、数据有问题、模型初始化不当检查数据分布、打印梯度范数调整学习率、检查数据管道、换初始化方法训练损失震荡学习率太大、batch size太小观察损失曲线降低学习率、增大batch size、加梯度裁剪验证损失上升过拟合对比训练和验证损失加正则化、Dropout、早停、数据增强显存溢出batch size太大、内存泄漏用torch.cuda.memory_summary()查看减小batch size、梯度累积、梯度检查点推理延迟高模型太大、没有量化、没有批处理用profiler分析各层耗时量化、算子融合、动态批处理服务不稳定内存泄漏、并发问题查看服务日志和监控指标加内存限制、用异步框架、做压力测试6. 一些掏心窝子的经验分享做AI工程这些年我最大的体会是工程能力比算法能力更稀缺也更值钱。算法可以学论文可以读但工程能力是靠一个个项目喂出来的是靠一次次线上事故磨出来的。你调包调得再熟练遇到没见过的问题还是得抓瞎。但如果你有从零构建的经验你就有了拆解问题、定位问题、解决问题的能力这种能力是通用的换个框架、换个业务场景照样能用。另外我想说的是不要追求一步到位。我见过很多人一上来就想搞一个完美的系统结果光设计就花了一个月代码一行没写。正确的做法是先跑通一个最小闭环哪怕这个闭环很粗糙然后在这个基础上逐步迭代。先让数据能流进来再让模型能训练再让服务能跑起来最后再优化性能、加监控、做高可用。每一步都有产出每一步都能验证这样你才有正反馈才能坚持下去。最后分享一个我个人的小习惯每次遇到问题并解决之后我都会写一个简短的复盘记录记下问题现象、排查过程、根本原因、解决方案。这些记录积累下来就是我自己的知识库。下次遇到类似问题直接翻记录就行不用从头再查一遍。这个习惯看起来不起眼但长期坚持下来效率提升非常明显。