1. 从零搭建AI工程能力:一个项目标题背后的完整学习路径拆解
第一次看到"ai-engineering-from-scratch"这个标题的时候,我脑子里蹦出来的第一个念头是:终于有人把这件事说清楚了。市面上讲AI的教程铺天盖地,但绝大多数要么停留在调包层面——import一个库,fit一下,predict一下,完事;要么直接跳到论文复现,满屏公式推导,看得人头皮发麻。真正卡在中间的那一层——怎么把一个AI能力从想法变成能跑、能维护、能迭代的工程系统——反而很少有人系统性地讲。
这个标题的核心价值就在这里。它瞄准的不是"AI理论"也不是"AI应用",而是"AI工程"(AI Engineering)。这三个字的差别很大。理论关心的是模型为什么有效,应用关心的是模型能做什么,而工程关心的是:怎么让模型在真实环境里稳定地产出结果,怎么管理数据流,怎么控制成本,怎么排查故障,怎么让整个系统可复现、可扩展。
适合看这个内容的人,我大致分成三类。第一类是有一定编程基础但没接触过AI系统搭建的开发者,比如写了几年后端或者前端,想往AI方向转,但不知道从哪下手。第二类是做过一些AI demo但没做过完整项目的学生或者研究者,模型能跑通,但一涉及到部署、监控、数据管道就抓瞎。第三类是做产品或者技术管理的人,需要理解AI工程的全貌,以便更好地做技术决策和团队协作。
这篇文章我会按照一个完整的AI工程学习路径来展开,从环境搭建到数据处理,从模型训练到服务部署,从监控告警到持续迭代。每个环节我都会解释为什么这么做、怎么做、以及我踩过哪些坑。你不需要有很深的数学背景,但需要会写基本的Python代码,了解命令行操作。读完你至少能知道一个AI工程项目从零到一需要经历哪些阶段,每个阶段的关键决策点在哪里。
2. 整体设计思路:为什么AI工程不能只学"调包"
2.1 从"能跑"到"能扛"的思维转变
很多人学AI的路径是这样的:找一份教程,跟着敲一遍代码,在Jupyter Notebook里看到准确率数字,觉得自己学会了。然后换一个数据集,发现各种报错,数据格式不对、内存爆了、训练不收敛,瞬间回到原点。这个问题的根源在于,教程展示的是"理想状态",而真实工程面对的是"混乱状态"。
AI工程的核心思维转变在于:你写的代码不是给自己看的,是给系统跑的。这意味着你需要考虑异常处理、日志记录、资源管理、版本控制、自动化测试。举个例子,在Notebook里你可能会写data = pd.read_csv('data.csv'),但在工程代码里,你需要考虑文件不存在怎么办、文件太大内存装不下怎么办、数据格式变了怎么办、读取速度太慢怎么办。这些"怎么办"就是AI工程要解决的问题。
我个人的经验是,从零搭建AI工程能力,最有效的路径不是先学某个框架,而是先理解一个完整AI系统的生命周期。这个生命周期大致包括:问题定义、数据采集与清洗、特征工程、模型选择与训练、评估与调优、部署上线、监控与迭代。每个阶段都有对应的工具和方法论,但更重要的是理解阶段之间的衔接关系。
2.2 技术选型的底层逻辑
在开始动手之前,有一个决策会直接影响后续所有工作:技术栈的选择。我见过太多项目因为早期选型不当,后期维护成本高得离谱。这里我给出一个基于常见实践的选型框架,你可以根据自己的实际情况调整。
编程语言层面,Python几乎是AI工程的默认选择,原因很简单:生态最全。从数据处理到模型训练到服务部署,Python都有成熟的库。但Python的性能问题在工程中确实存在,所以常见的做法是核心计算用C++或者CUDA写底层,Python做上层调度。对于大多数项目来说,纯Python足够用,遇到性能瓶颈再针对性优化。
深度学习框架层面,PyTorch和TensorFlow是两大主流。我个人的建议是:如果是研究导向或者需要快速实验,选PyTorch,它的动态图机制让调试更直观;如果是生产环境且需要成熟的部署工具链,TensorFlow的Serving和Lite生态更完善。不过最近两年PyTorch在部署侧也在快速补齐,所以这个界限越来越模糊。我的选择是PyTorch为主,因为社区活跃度和新模型的支持速度更好。
数据处理层面,Pandas适合中小规模数据,但数据量超过内存就需要换工具。Dask、Polars、Spark都是可选方案。我的经验是,如果数据能装进内存,Pandas加NumPy的组合最省心;如果装不下,优先考虑Polars,它的API和Pandas很像但性能好很多;如果数据量到了TB级别,那就得上Spark或者类似的分布式框架了。
服务部署层面,FastAPI是目前Python生态里做模型服务最顺手的选择,异步支持好、自动生成文档、性能也不错。如果对性能要求极高,可以考虑用Go或者Rust写服务层,Python只做模型推理。容器化方面,Docker基本是标配,Kubernetes看团队规模,小团队用Docker Compose就够了。
2.3 项目结构的设计原则
一个容易被忽视但极其重要的环节是项目结构。我见过很多AI项目,代码全堆在一个目录里,文件名从train.py到train_final_v2.py到train_final_v2_fixed.py,最后没人知道哪个是最新的。这种混乱在项目初期影响不大,但一旦需要协作或者回溯,就是灾难。
我推荐的项目结构是这样的:顶层分为data、src、configs、experiments、tests、scripts几个目录。data放原始数据和处理后数据,但注意不要提交到版本控制,用.gitignore排除。src放核心代码,按功能模块划分子目录,比如data、models、training、serving。configs放配置文件,所有超参数、路径、模型参数都从这里读,不要硬编码在代码里。experiments放每次实验的输出,包括模型权重、日志、评估结果。tests放单元测试和集成测试。scripts放一些辅助脚本,比如数据下载、环境初始化。
这个结构的好处是职责清晰。当你想复现某个实验时,只需要找到对应的config和experiment目录。当你想修改模型结构时,只需要动src/models下的代码。当你想部署时,只需要关注src/serving。这种模块化设计在项目规模变大后优势会非常明显。
3. 核心细节解析:数据、模型、服务三大支柱
3.1 数据管道的搭建与维护
数据是AI工程的基石,但也是最容易被低估的环节。我见过太多项目在模型上花80%的时间,在数据上只花20%,最后效果不好还找不到原因。实际上,一个设计良好的数据管道应该具备以下特征:可复现、可监控、可扩展。
可复现意味着给定相同的原始数据和代码版本,你能得到完全相同的数据集。这听起来简单,但实际操作中很容易出问题。比如你用了一个随机采样的步骤,但没有固定随机种子;或者你依赖了一个外部API获取数据,但API返回的结果随时间变化。解决方法是:所有随机操作固定种子,所有外部依赖做本地缓存,所有数据转换步骤记录版本。
可监控意味着你能知道数据管道是否正常运行。最基本的监控包括:数据量是否在预期范围内、字段是否缺失、数值分布是否发生漂移。我通常会在数据管道的关键节点加检查点,比如原始数据加载后检查行数和列数,清洗后检查缺失值比例,特征工程后检查特征分布。这些检查可以用简单的断言实现,也可以用更专业的工具如Great Expectations。
可扩展意味着当数据量增长时,你的管道不需要重写。这要求你在设计时就考虑并行化和增量处理。比如用Pandas的时候,如果数据量可能增长,就不要写一次性加载全部数据的代码,而是用分块读取。如果用Spark,就要考虑分区策略。我的经验是,宁可早期多花一点时间设计可扩展的方案,也不要等到数据量上来了再重构。
具体到实操,一个典型的数据管道包括以下步骤:数据采集、数据清洗、数据转换、数据划分、数据加载。数据采集阶段要注意数据源的稳定性和合法性,不要依赖不稳定的免费API。数据清洗阶段要处理缺失值、异常值、重复值,这里的关键是记录清洗规则,不要凭感觉处理。数据转换阶段包括归一化、编码、特征交叉等操作,注意训练集和验证集要用相同的转换参数。数据划分阶段要保证分布一致,分类任务要分层采样。数据加载阶段要考虑批量大小和预取,避免成为训练瓶颈。
注意:数据泄露是AI工程中最隐蔽也最致命的错误之一。任何在训练时用到了验证集或测试集信息的操作都属于数据泄露,包括但不限于:用全量数据计算归一化参数、在划分数据前做特征选择、用未来数据预测过去。排查方法是:假设你的模型效果特别好,先怀疑数据泄露。
3.2 模型训练与实验管理
模型训练是AI工程中最核心也最耗资源的环节。从工程角度看,训练不仅仅是跑一个model.fit(),而是涉及实验设计、资源调度、过程监控、结果记录等一系列工作。
实验设计的关键是控制变量。每次实验只改变一个因素,这样才能准确评估每个因素的影响。我通常会用配置文件来管理实验参数,每个实验对应一个配置文件,文件名包含时间戳和关键参数。比如20240115_lr0.001_bs32_resnet50.yaml。这样即使过了几个月,你也能从文件名快速了解实验内容。
资源调度方面,如果只有一台机器,要注意GPU内存管理。常见的问题是批量大小设得太大导致OOM(内存溢出),或者多个实验同时跑导致资源争抢。我的做法是:先用小批量测试模型能否跑通,然后逐步增大批量直到GPU利用率达到80%左右。如果有多台机器,可以考虑用Slurm或者Kubernetes做任务调度。
过程监控包括损失曲线、准确率曲线、学习率变化、梯度范数等。这些指标不仅要记录,还要实时可视化。我习惯用TensorBoard或者Weights & Biases来跟踪实验,这样可以在训练过程中及时发现问题。比如损失突然变成NaN,可能是学习率太大;验证集损失开始上升,可能是过拟合了。
结果记录要详细到可以复现的程度。除了最终的模型权重,还要保存:训练配置、数据版本、代码版本、环境依赖、训练日志、评估结果。我通常会在实验目录下放一个README.md,记录这次实验的目的、方法、结果和结论。这个习惯在后期写论文或者做项目汇报时特别有用。
模型选择方面,不要盲目追求最新最复杂的模型。我的经验是:先从简单的基线开始,比如逻辑回归或者小型的预训练模型,确认数据管道和评估流程没问题,再逐步尝试更复杂的模型。很多时候,数据质量的提升比模型复杂度的提升带来的收益更大。
3.3 模型服务化与API设计
模型训练完成只是第一步,让它能被其他系统调用才是工程价值的体现。模型服务化的核心问题是:如何以低延迟、高可用的方式提供推理能力。
最简单的服务化方式是用Flask或者FastAPI写一个HTTP接口,加载模型,接收请求,返回预测结果。这种方式适合流量不大的场景。但如果流量大或者延迟要求高,就需要考虑更专业的方案。比如用TorchServe或者TensorFlow Serving,它们支持模型版本管理、批量推理、GPU加速等特性。再进一步,可以用Triton Inference Server,它支持多框架、多模型、动态批处理,适合大规模部署。
API设计方面,有几个关键决策点。第一是输入输出格式,我推荐用JSON,因为通用性好,调试方便。第二是错误处理,要区分客户端错误(比如输入格式不对)和服务端错误(比如模型推理失败),返回合适的HTTP状态码。第三是版本管理,API路径里要包含版本号,比如/v1/predict,这样后续升级不会影响老用户。第四是限流和鉴权,防止滥用。
性能优化方面,常见的手段包括:模型量化(把FP32转成FP16或者INT8,减少内存占用和计算量)、模型剪枝(去掉不重要的权重)、知识蒸馏(用大模型教小模型)、批处理(把多个请求合并成一个批次推理)。这些手段各有取舍,量化可能损失一点精度,剪枝需要重新训练,蒸馏需要额外的训练过程。我的建议是先用最简单的方式上线,遇到性能瓶颈再针对性优化。
提示:模型服务上线前一定要做压力测试。用Locust或者wrk模拟并发请求,观察响应时间、吞吐量、错误率。我见过太多服务在测试环境没问题,一上线就被真实流量打垮的情况。
4. 实操过程:从零搭建一个完整的AI工程示例
4.1 环境准备与依赖管理
动手的第一步是环境准备。我强烈建议用虚拟环境隔离项目依赖,不要直接在系统Python里装包。虚拟环境可以用venv、conda或者poetry。我个人偏好conda,因为它在处理科学计算相关的依赖时更省心,尤其是涉及CUDA和cuDNN的时候。
创建环境的命令很简单:
conda create -n ai-engineering python=3.10 conda activate ai-engineeringPython版本我选3.10,因为它在稳定性和新特性之间平衡得比较好。3.11和3.12虽然更新,但有些库的兼容性还没完全跟上。3.9也可以,但3.10是目前的甜点版本。
依赖管理方面,我推荐用requirements.txt加pip-compile的方式。直接写requirements.txt的问题是版本不固定,今天装和明天装可能得到不同的版本。pip-compile可以从requirements.in生成锁定版本的requirements.txt,保证每次安装的依赖完全一致。
pip install pip-tools pip-compile requirements.in pip install -r requirements.txtrequirements.in里只写顶层依赖,比如torch、fastapi、pandas,pip-compile会自动解析出所有底层依赖并锁定版本。这个做法在团队协作和持续集成中特别重要。
CUDA版本的选择要看你的GPU型号和驱动版本。用nvidia-smi查看驱动支持的CUDA版本,然后安装对应的PyTorch。PyTorch官网有很清晰的安装命令生成器,选好版本复制粘贴就行。注意不要混用conda和pip安装PyTorch,容易出问题,选一种方式就好。
4.2 数据准备与预处理实操
我用一个图像分类任务作为示例,因为它的流程比较直观,而且涵盖了AI工程的主要环节。数据集我选CIFAR-10,它足够小,在普通机器上就能跑,但又足够真实,能体现工程中的各种问题。
数据下载和加载的代码如下:
import torch from torchvision import datasets, transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) train_dataset = datasets.CIFAR10( root='./data', train=True, download=True, transform=transform ) test_dataset = datasets.CIFAR10( root='./data', train=False, download=True, transform=transform )这里有几个工程细节值得注意。第一,download=True只在第一次运行时需要,后续应该设为False,避免重复下载。第二,transform里的归一化参数是CIFAR-10的均值和标准差,如果你用自己的数据集,需要先计算这些参数。第三,数据增强应该在训练集上做,测试集不要做随机增强。
数据加载器的配置:
from torch.utils.data import DataLoader train_loader = DataLoader( train_dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True ) test_loader = DataLoader( test_dataset, batch_size=64, shuffle=False, num_workers=4, pin_memory=True )num_workers控制数据加载的并行进程数,一般设为CPU核心数的一半到全部。pin_memory=True可以加速GPU传输,但会占用更多内存。shuffle=True只在训练时用,测试时不要打乱,否则评估结果不好对比。
注意:
num_workers设得太大反而会拖慢速度,因为进程间通信有开销。我的经验是从4开始试,逐步增加,观察GPU利用率,找到最佳值。
4.3 模型定义与训练循环
模型我选一个简单的卷积神经网络,结构清晰,便于理解:
import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.conv1 = nn.Conv2d(3, 32, 3, padding=1) self.conv2 = nn.Conv2d(32, 64, 3, padding=1) self.conv3 = nn.Conv2d(64, 128, 3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(128 * 4 * 4, 256) self.fc2 = nn.Linear(256, num_classes) self.dropout = nn.Dropout(0.5) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x这个网络有三层卷积加两层全连接,参数量不大,在CIFAR-10上能到70%左右的准确率。如果你想更高,可以加残差连接或者用预训练模型,但作为工程示例,这个复杂度刚好。
训练循环的工程化写法:
import torch.optim as optim from tqdm import tqdm device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(20): model.train() running_loss = 0.0 for inputs, labels in tqdm(train_loader, desc=f'Epoch {epoch+1}'): inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() avg_loss = running_loss / len(train_loader) print(f'Epoch {epoch+1}, Loss: {avg_loss:.4f}') # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in test_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total print(f'Validation Accuracy: {acc:.4f}')这段代码有几个工程要点。第一,model.train()和model.eval()的切换很重要,因为Dropout和BatchNorm在训练和推理时的行为不同。第二,torch.no_grad()在验证时使用,可以节省内存和计算。第三,tqdm提供进度条,方便观察训练进度。第四,每个epoch结束后做验证,可以及时发现过拟合。
模型保存和加载:
# 保存 torch.save({ 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': avg_loss, }, 'checkpoint.pth') # 加载 checkpoint = torch.load('checkpoint.pth') model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict'])保存完整的checkpoint而不仅仅是模型权重,这样可以从断点继续训练。如果只是推理,保存model.state_dict()就够了,文件更小。
4.4 模型服务化实操
训练好的模型需要包装成服务。我用FastAPI写一个简单的推理服务:
from fastapi import FastAPI, File, UploadFile from PIL import Image import io import torch from torchvision import transforms app = FastAPI() device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = SimpleCNN().to(device) model.load_state_dict(torch.load('model.pth', map_location=device)) model.eval() transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] @app.post('/v1/predict') async def predict(file: UploadFile = File(...)): image_data = await file.read() image = Image.open(io.BytesIO(image_data)).convert('RGB') tensor = transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) probabilities = torch.softmax(outputs, dim=1) confidence, predicted = torch.max(probabilities, 1) return { 'class': classes[predicted.item()], 'confidence': confidence.item() }启动服务:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4workers参数控制工作进程数,一般设为CPU核心数。如果模型在GPU上,多个worker会共享GPU,要注意显存占用。
这个服务已经具备了基本的生产可用性:异步处理请求、返回置信度、版本化API路径。但还缺少一些东西:输入验证(确保上传的是图片)、错误处理(模型推理失败时返回合适的错误)、日志记录(记录每个请求的处理情况)、限流(防止滥用)。这些可以根据实际需求逐步添加。
5. 常见问题与排查技巧实录
5.1 训练过程中的典型问题
问题一:损失不下降或者变成NaN。这是最常见的问题,原因可能有很多。首先检查学习率,太大容易震荡,太小下降慢。我通常从0.001开始试,如果损失震荡就降到0.0001。其次检查数据,有没有标签错误、有没有异常值、归一化是否合理。再次检查模型,初始化是否正常、有没有梯度消失或爆炸。最后检查损失函数,分类任务用交叉熵,回归任务用MSE,不要搞混。
问题二:训练集表现好但验证集表现差。这是典型的过拟合。解决方法包括:增加数据增强、加Dropout层、加权重衰减、减小模型复杂度、早停。我的经验是,先加数据增强,效果最明显;然后加Dropout,成本最低;如果还不够,再考虑减小模型。
问题三:GPU利用率低。可能的原因有:数据加载是瓶颈(增加num_workers)、批量太小(增大batch_size)、模型太小(换大模型)、CPU和GPU之间的数据传输慢(用pin_memory)。用nvidia-smi查看GPU利用率,用htop查看CPU利用率,定位瓶颈在哪里。
问题四:显存不够用。解决方法包括:减小批量、用梯度累积(多个小批量累积梯度再更新)、用混合精度训练(FP16)、用梯度检查点(用时间换空间)。梯度累积的代码如下:
accumulation_steps = 4 for i, (inputs, labels) in enumerate(train_loader): outputs = model(inputs) loss = criterion(outputs, labels) / accumulation_steps loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()5.2 部署过程中的典型问题
问题一:服务启动慢。模型加载是主要耗时。如果模型很大,可以考虑用更快的序列化格式(比如ONNX)、用内存映射加载、或者用模型预热(启动时先跑几次推理)。另外,如果用了多个worker,每个worker都会加载一份模型,显存占用会翻倍。可以考虑用共享内存或者模型服务器来避免重复加载。
问题二:推理延迟高。优化手段包括:模型量化、批处理、用更快的推理引擎(ONNX Runtime、TensorRT)、用GPU替代CPU。批处理的效果最明显,但要注意延迟和吞吐量的权衡。如果用户对延迟敏感,批处理窗口要设小;如果对吞吐量敏感,批处理窗口可以设大。
问题三:服务不稳定,偶尔报错。常见原因包括:内存泄漏(长时间运行后内存持续增长)、并发问题(多个请求同时访问共享资源)、输入异常(用户上传了非预期格式的数据)。排查方法是加详细的日志,记录每个请求的输入输出和耗时,用监控工具跟踪内存和CPU使用情况。
问题四:模型更新后服务异常。这是版本管理的问题。模型更新时,要确保新模型的输入输出格式和老模型一致,否则调用方会出错。我的做法是:API路径带版本号,新模型用新路径,老模型保留一段时间,等所有调用方迁移完成再下线。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 损失不下降 | 学习率不当、数据问题、模型问题 | 检查学习率、数据分布、梯度范数 | 调整学习率、清洗数据、换模型 |
| 过拟合 | 模型太复杂、数据太少 | 对比训练和验证曲线 | 加正则化、数据增强、早停 |
| GPU利用率低 | 数据加载瓶颈、批量太小 | 查看GPU和CPU利用率 | 增加num_workers、增大批量 |
| 显存不够 | 批量太大、模型太大 | 查看显存占用 | 减小批量、梯度累积、混合精度 |
| 服务启动慢 | 模型加载耗时 | 计时模型加载过程 | 用ONNX、预热、共享内存 |
| 推理延迟高 | 模型太大、无批处理 | 计时单次推理 | 量化、批处理、换推理引擎 |
| 服务不稳定 | 内存泄漏、并发问题 | 查看日志和监控 | 修复泄漏、加锁、输入验证 |
提示:排查问题的黄金法则是"二分法"。先确认问题是出在数据、模型还是服务,然后逐步缩小范围。不要一上来就改代码,先看日志、看指标、看数据。
6. 持续迭代:AI工程能力的进阶方向
6.1 自动化与CI/CD
当项目稳定运行后,下一步是自动化。持续集成方面,每次代码提交自动跑单元测试和集成测试,确保没有引入回归。持续部署方面,模型训练完成后自动评估,达标后自动部署到测试环境,人工确认后部署到生产环境。
模型版本管理可以用DVC或者MLflow。DVC适合管理大文件,它把模型文件存在远程存储,版本信息存在Git里。MLflow适合管理实验,它记录每次实验的参数、指标、模型,提供Web界面查看和对比。
自动化测试方面,除了常规的代码测试,还要加数据测试(确保数据质量)和模型测试(确保模型性能)。数据测试可以用Great Expectations,模型测试可以写一些行为测试,比如"给定这个输入,模型应该输出这个类别"。
6.2 监控与告警
生产环境的AI系统需要监控三个层面:系统层面(CPU、内存、GPU、网络)、服务层面(请求量、延迟、错误率)、模型层面(预测分布、置信度分布、特征漂移)。
模型层面的监控最容易被忽视但最重要。如果输入数据的分布发生了变化(数据漂移),模型的预测效果会下降。监控方法是:定期计算输入特征的统计量,和训练时的统计量对比,如果偏差超过阈值就告警。预测分布的监控类似,如果模型突然大量预测某个类别,可能是出了问题。
告警策略要合理设置,避免告警疲劳。我的经验是:系统层面的告警设得宽松一些,服务层面的告警设得严格一些,模型层面的告警设得敏感一些。因为模型问题往往更隐蔽,需要更早发现。
6.3 性能优化与成本控制
AI系统的成本主要来自计算资源。优化方向包括:提高资源利用率(用Kubernetes做弹性调度)、降低推理成本(用量化、蒸馏、剪枝)、优化数据存储(用列式存储、压缩)、合理选择实例类型(GPU实例贵,能用CPU就用CPU)。
我个人的经验是,成本优化要从架构层面考虑,而不是只盯着模型。比如,如果推理请求有明显的波峰波谷,用自动扩缩容比固定资源更省钱。如果模型推理不是瓶颈,数据预处理才是,那就优化数据管道而不是模型。
最后分享一个我踩过的坑:不要过早优化。我见过团队在项目初期就花大量时间做性能优化,结果后来需求变了,优化白做了。正确的做法是:先让系统跑起来,找到真正的瓶颈,再针对性优化。大部分情况下,80%的性能问题来自20%的代码,找到那20%比全面优化更有效。
这个方向后续还可以这样扩展:加入A/B测试框架,对比不同模型的效果;加入联邦学习,在保护数据隐私的前提下训练模型;加入边缘计算,把推理放到离用户更近的地方。每个方向都值得深入,但核心的工程思维是一样的:理解问题、设计方案、实现验证、迭代优化。