ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建AI工程体系:避开调包陷阱,构建稳定生产系统

从零搭建AI工程体系:避开调包陷阱,构建稳定生产系统 1. 从零搭建AI工程体系为什么我劝你别一上来就调包“ai-engineering-from-scratch”这个标题第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地但绝大多数都在教你import torch之后怎么调API、怎么微调现成模型真正愿意从工程地基开始讲起的内容少得可怜。我自己带过几个刚入行的同事发现一个很普遍的现象他们能跑通一个demo但一旦模型效果不对、推理速度上不去、部署到线上出问题就完全不知道从哪里下手。根子就在于他们跳过了“工程”这两个字最核心的部分。所谓AI工程不是算法研究也不是数据科学而是把AI能力变成稳定、可维护、可扩展的生产系统的全过程。它涵盖的东西比大多数人想象的要杂得多数据管道的搭建、特征存储的设计、训练流程的编排、模型版本管理、推理服务的性能优化、监控告警、灰度发布、成本控制……每一项单独拎出来都够写一本书。而“from scratch”的意思不是说你要手写一个Transformer而是说你要理解每一层抽象下面到底发生了什么这样出了问题你才有能力往下钻。这篇文章适合谁看如果你是一个已经会用PyTorch或TensorFlow跑通模型但对“怎么把它变成一个靠谱的线上服务”感到迷茫的开发者那这篇内容就是写给你的。如果你是一个后端工程师想转方向做AI基础设施同样适用。甚至如果你是技术负责人需要评估团队AI工程的成熟度这里面的思路也能帮你建立一套判断标准。我会尽量用大白话把每个环节讲透该给代码给代码该给参数给参数该说坑的地方绝不藏着。2. 整体架构设计先想清楚数据怎么流再动手写代码2.1 为什么“先跑通再说”是最大的陷阱我见过太多项目是这样起步的拿到一个需求比如“做一个文本分类服务”然后立刻打开Jupyter Notebook加载数据集调个预训练模型跑出个准确率截图发群里大家鼓掌。然后呢然后就没有然后了。因为这个Notebook里的代码离一个能上线的系统差了十万八千里。从零搭建AI工程体系第一步不是写代码而是画数据流图。你需要明确几个问题数据从哪里来是批量导入还是流式接入数据需要经过哪些清洗和转换步骤训练数据和推理数据是否走同一套预处理逻辑模型训练完成后产物是什么是权重文件还是完整的计算图推理服务如何加载模型是常驻内存还是按需加载请求进来之后经过哪些环节才能返回结果这些问题听起来很基础但每一个如果没想清楚后面都会变成技术债。举个例子训练时你用Pandas做特征工程推理时你用NumPy重新实现一遍结果因为浮点数精度或者缺失值处理逻辑不一致导致线上线下效果对不上。这种问题排查起来极其痛苦因为模型本身没问题数据也没问题就是工程实现有偏差。我的建议是在写第一行代码之前先用一张纸画出端到端的流程图。不需要多漂亮但要包含这几个关键节点数据源、数据校验、特征计算、训练触发、模型评估、模型注册、服务部署、请求处理、结果返回、监控埋点。每个节点标注清楚输入输出格式和负责人。这张图会成为你后续所有工作的地图。2.2 分层架构把变化的部分和稳定的部分隔离开AI系统和传统后端系统最大的区别在于AI系统的“核心逻辑”是不断变化的。今天用BERT明天可能换LLaMA今天用全量训练明天可能改增量更新。如果把这些变化直接耦合到业务代码里每次模型迭代都是一次伤筋动骨的重构。所以我推荐的分层架构是这样的最底层是基础设施层包括计算资源、存储资源、网络配置这部分尽量用云服务或者容器化方案保持稳定往上是数据层负责数据的采集、清洗、存储和版本管理这一层的关键是保证数据可追溯、可复现再往上是训练层包括训练脚本、超参数配置、实验跟踪这一层要支持快速迭代和并行实验然后是模型层负责模型的序列化、版本管理、元数据记录最上面是服务层处理推理请求、负载均衡、限流降级。每一层之间通过明确定义的接口通信。比如训练层输出一个模型产物这个产物必须包含权重文件、配置文件、预处理逻辑的代码快照、训练数据的版本号。服务层加载模型时只需要根据模型ID去模型仓库拉取这些产物不需要关心训练时用了什么框架、什么超参数。这样当你想把训练框架从PyTorch换成JAX时只要保证输出的模型格式兼容服务层完全不用动。这种分层带来的另一个好处是团队分工更清晰。数据工程师专注数据层算法工程师专注训练层后端工程师专注服务层大家通过接口契约协作减少了很多沟通成本。2.3 技术选型的几个关键决策点在从零搭建的过程中你会面临大量技术选型。我挑几个最容易踩坑的地方说一下。第一个是实验跟踪工具。很多人觉得用TensorBoard就够了但TensorBoard只适合单机、单实验的场景。一旦你开始并行跑几十组超参数或者需要对比不同时间段的实验结果TensorBoard就力不从心了。我推荐MLflow或者Weights Biases前者开源可自部署后者体验更好但需要付费。关键是要养成习惯每次训练都记录完整的配置、指标曲线、产物路径。否则一个月后你根本记不清哪个模型对应哪组参数。第二个是模型服务框架。TorchServe、Triton Inference Server、ONNX Runtime、还有各种自研的Flask/FastAPI封装选择很多。我的经验是如果只是简单的PyTorch模型用FastAPI自己封装反而最灵活如果需要支持多框架、动态批处理、GPU共享那就上Triton。不要一上来就追求“企业级方案”很多复杂度是你现阶段不需要的。第三个是数据版本管理。DVC是这方面的老牌工具但学习曲线不低。如果数据量不大用Git LFS加上良好的目录命名规范也能凑合。关键是每次训练都要记录数据集的哈希值或者版本标签保证实验可复现。3. 核心模块拆解数据管道、训练编排、模型服务3.1 数据管道别让脏数据毁掉你的模型数据管道是AI工程里最不起眼但最重要的部分。我见过太多模型效果不好最后排查下来是数据问题标签错了、特征泄漏了、训练集和测试集有重叠、类别极度不平衡但没做处理。从零搭建数据管道我建议分成四个阶段采集、校验、转换、缓存。采集阶段要解决的是数据来源问题。如果是离线数据通常是从数据仓库或者对象存储拉取如果是实时数据可能需要接消息队列。这个阶段的关键是做好幂等性设计避免重复消费或者漏消费。我习惯给每条数据打上时间戳和来源标识方便后续排查。校验阶段是很多人忽略的。你需要定义一套数据质量规则比如字段是否完整、数值是否在合理范围、类别分布是否异常、文本长度是否超标。这些规则可以用Great Expectations或者自己写脚本实现。校验不通过的数据要隔离出来不能直接进入训练流程。我踩过的坑是某次训练时混入了一批爬虫抓取的脏数据导致模型学会了一堆乱码模式线上效果直接崩了。从那以后我强制要求所有数据必须过校验。转换阶段是把原始数据变成模型能吃的格式。这里最大的坑是训练和推理的预处理逻辑不一致。我的做法是把预处理逻辑封装成一个独立的Python模块训练时和推理时都调用同一个模块。这个模块的输入是原始数据输出是模型输入张量。模块内部可以包含分词、归一化、特征拼接等操作。为了保证一致性这个模块的版本号要和模型版本号绑定。缓存阶段是为了加速训练。如果每次训练都重新做一遍全量预处理时间成本太高。我通常会把预处理后的数据存成Parquet或者TFRecord格式放在高速存储上。缓存要设置合理的过期策略数据更新后要及时失效。3.2 训练编排让实验可复现、可对比、可追溯训练编排的核心目标是任何一次实验结果都能被完整复现任意两次实验都能被公平对比。要做到可复现你需要固定随机种子、记录环境依赖、保存数据版本。随机种子包括Python的random、NumPy的random、框架的random、甚至CUDA的随机性。环境依赖用requirements.txt或者conda env export导出但要注意有些库的版本差异会导致结果不同。数据版本用哈希值或者标签记录。要做到可对比你需要统一的评估指标和评估数据集。我见过团队里每个人用自己的测试集结果A说他的模型准确率90%B说他的模型准确率85%但两人根本不在一个测试集上对比毫无意义。正确的做法是维护一个固定的验证集和测试集所有人用同一套评估脚本。训练编排工具方面小规模可以用Makefile或者Shell脚本中等规模用Airflow或者Prefect大规模用Kubeflow或者Metaflow。我的建议是不要过度设计先从简单的开始。一个训练脚本加上一个配置文件配合MLflow做实验跟踪就能满足大部分场景。等到任务依赖变得复杂、需要分布式训练时再引入编排工具。这里分享一个实用技巧把训练配置写成YAML文件包含模型结构、超参数、数据路径、输出路径等所有可变项。训练脚本只负责读取配置并执行。这样你可以用不同的配置文件跑不同的实验而不需要改代码。配置文件也要纳入版本管理和代码一起提交。3.3 模型服务性能、稳定性、可观测性一个都不能少模型服务是AI工程里离用户最近的部分也是最容易出问题的部分。我总结下来核心要关注三点性能、稳定性、可观测性。性能方面关键指标是延迟和吞吐。延迟包括网络延迟、预处理延迟、推理延迟、后处理延迟。很多人只关注推理延迟忽略了预处理可能才是瓶颈。比如文本分类任务分词和截断可能比模型前向传播还慢。优化手段包括使用更快的分词库、把预处理放到GPU上、做动态批处理、使用量化模型、启用TensorRT等推理加速。动态批处理是提升吞吐的利器。原理很简单把多个请求攒在一起凑成一个批次送给模型充分利用GPU的并行能力。Triton Inference Server内置了这个功能自己实现的话需要维护一个请求队列设置最大批次大小和最大等待时间。等待时间太短批次凑不大吞吐上不去等待时间太长延迟增加用户体验下降。这个参数需要根据实际流量压测来确定。稳定性方面要考虑模型加载失败、推理超时、内存泄漏、GPU OOM等情况。我的做法是服务启动时做健康检查确保模型能正常加载推理请求设置超时时间超时后返回降级结果监控GPU内存使用接近阈值时触发告警定期重启服务防止内存泄漏累积。可观测性方面至少要记录这些指标请求量、延迟分布、错误率、GPU利用率、内存使用、模型版本。日志要包含请求ID、模型版本、输入输出摘要注意脱敏、耗时分解。这些数据不仅能帮你排查问题还能为容量规划提供依据。4. 实操过程从零搭建一个文本分类服务的完整记录4.1 环境准备与依赖安装我以文本分类任务为例完整走一遍从零搭建的流程。操作系统用Ubuntu 22.04Python版本3.10GPU是单卡RTX 3090。首先创建虚拟环境这一步别偷懒我见过太多因为全局环境混乱导致的问题。python -m venv venv source venv/bin/activate pip install --upgrade pip然后安装核心依赖。这里我选择PyTorch作为训练框架FastAPI作为服务框架MLflow做实验跟踪DVC做数据版本管理。pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers datasets fastapi uvicorn mlflow dvc scikit-learn pandas pyarrow安装完成后用一个小脚本验证GPU是否可用import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果输出显示CUDA可用并且能看到显卡型号说明环境没问题。如果显示False检查驱动版本和CUDA版本是否匹配。4.2 数据准备与预处理模块我用的数据集是一个公开的中文情感分类数据集包含约5万条标注数据。数据格式是CSV两列text和label。首先定义数据校验规则。我写了一个简单的校验脚本检查以下几点text字段不能为空、长度在1到512之间、label只能是0或1、类别分布不能超过7:3。import pandas as pd def validate_data(df): assert df[text].notna().all(), 存在空文本 assert df[text].str.len().between(1, 512).all(), 文本长度异常 assert set(df[label].unique()) {0, 1}, 标签值异常 ratio df[label].mean() assert 0.3 ratio 0.7, f类别分布异常: {ratio} return True校验通过后进行数据划分。我按照8:1:1的比例划分训练集、验证集、测试集并且固定随机种子。from sklearn.model_selection import train_test_split train_df, temp_df train_test_split(df, test_size0.2, random_state42, stratifydf[label]) val_df, test_df train_test_split(temp_df, test_size0.5, random_state42, stratifytemp_df[label])接下来是预处理模块。我把它封装成一个类训练和推理共用。from transformers import BertTokenizer class TextPreprocessor: def __init__(self, model_namebert-base-chinese, max_length128): self.tokenizer BertTokenizer.from_pretrained(model_name) self.max_length max_length def __call__(self, texts): return self.tokenizer( texts, paddingmax_length, truncationTrue, max_lengthself.max_length, return_tensorspt )这个类的关键点是tokenizer的配置model_name、max_length要作为模型元数据保存推理时加载相同的配置。否则训练时截断到128推理时截断到256结果肯定对不上。4.3 训练流程与实验跟踪训练脚本我设计成配置驱动。配置文件config.yaml如下model: name: bert-base-chinese num_labels: 2 max_length: 128 training: batch_size: 32 learning_rate: 2e-5 epochs: 3 warmup_ratio: 0.1 weight_decay: 0.01 data: train_path: data/train.csv val_path: data/val.csv test_path: data/test.csv output: dir: outputs/exp_001 model_name: text_classifier训练脚本读取配置初始化模型、优化器、数据加载器然后开始训练。每个epoch结束后在验证集上评估记录指标到MLflow。import mlflow import yaml from transformers import BertForSequenceClassification, AdamW def train(config_path): with open(config_path) as f: config yaml.safe_load(f) mlflow.set_experiment(text-classification) with mlflow.start_run(): mlflow.log_params(config[training]) model BertForSequenceClassification.from_pretrained( config[model][name], num_labelsconfig[model][num_labels] ) optimizer AdamW( model.parameters(), lrconfig[training][learning_rate], weight_decayconfig[training][weight_decay] ) for epoch in range(config[training][epochs]): train_loss train_one_epoch(model, optimizer, train_loader) val_metrics evaluate(model, val_loader) mlflow.log_metrics({ train_loss: train_loss, val_accuracy: val_metrics[accuracy], val_f1: val_metrics[f1] }, stepepoch) model.save_pretrained(config[output][dir]) mlflow.log_artifacts(config[output][dir])这里有个细节mlflow.log_artifacts会把模型文件上传到MLflow的artifact存储。如果模型很大上传会很慢。可以配置MLflow使用本地路径或者S3兼容存储。我一般用本地路径然后定期备份。训练完成后在测试集上做最终评估。如果指标达标就把模型注册到模型仓库。MLflow提供了Model Registry功能可以给模型打上版本标签标记为Staging或Production。4.4 模型服务封装与部署服务端我用FastAPI封装。核心逻辑是启动时加载模型和预处理器请求进来后先预处理再推理最后返回结果。from fastapi import FastAPI from pydantic import BaseModel import torch app FastAPI() class Request(BaseModel): text: str class Response(BaseModel): label: int confidence: float model None preprocessor None app.on_event(startup) def load_model(): global model, preprocessor model BertForSequenceClassification.from_pretrained(outputs/exp_001) model.eval() model.cuda() preprocessor TextPreprocessor() app.post(/predict, response_modelResponse) def predict(request: Request): inputs preprocessor([request.text]) inputs {k: v.cuda() for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) probs torch.softmax(outputs.logits, dim-1) confidence, label torch.max(probs, dim-1) return Response( labellabel.item(), confidenceconfidence.item() )启动服务uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1注意这里workers设为1因为每个worker都会加载一份模型到GPU多worker会导致显存爆炸。如果需要更高吞吐应该用动态批处理或者多卡部署而不是简单增加worker数量。部署到生产环境时我用Docker打包。Dockerfile的关键是基础镜像要包含CUDA运行时并且把模型文件复制进去。FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04 RUN apt-get update apt-get install -y python3.10 python3-pip WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]构建和运行docker build -t text-classifier:v1 . docker run --gpus all -p 8000:8000 text-classifier:v14.5 监控与日志埋点服务上线后监控是必不可少的。我在FastAPI里加了中间件记录每个请求的耗时和状态。import time from fastapi import Request app.middleware(http) async def add_metrics(request: Request, call_next): start time.time() response await call_next(request) duration time.time() - start # 记录到日志或监控系统 print(fpath{request.url.path} duration{duration:.3f}s status{response.status_code}) return response更完善的做法是接入Prometheus暴露metrics端点然后用Grafana做可视化。关键指标包括QPS、P50/P95/P99延迟、错误率、GPU利用率、显存使用。日志方面我建议记录请求ID、模型版本、输入文本长度、预测标签、置信度、耗时。输入文本本身要不要记录如果涉及用户隐私建议脱敏或者只记录哈希值。如果数据不敏感记录原文有助于排查bad case。5. 常见问题与排查技巧实录5.1 线上线下效果不一致这是最经典的问题。原因通常有三个预处理逻辑不一致、模型版本不一致、数据分布不一致。排查方法首先确认线上服务加载的模型版本和离线评估的模型版本是否一致。然后拿一批线上请求的真实数据在离线环境用相同的预处理和模型跑一遍对比结果。如果离线结果和线上不同那就是预处理或模型加载有问题。如果离线结果和线上相同但和训练时的评估结果不同那就是数据分布问题。预防措施预处理代码必须共用模型元数据必须包含预处理配置上线前做一次影子测试。5.2 推理延迟过高延迟高可能是预处理慢、模型大、批处理没做好、GPU利用率低。排查方法在代码里加耗时打点分别记录预处理、推理、后处理的耗时。如果预处理占大头考虑换更快的分词库或者把预处理放到GPU上。如果推理占大头考虑模型量化、剪枝、或者换更小的模型。如果GPU利用率低考虑开启动态批处理。我遇到过一个案例预处理用了Python的循环逐条处理改成批量处理后延迟从200ms降到了50ms。5.3 显存溢出显存溢出通常发生在批次太大、模型太大、或者多个进程共享GPU时。排查方法用nvidia-smi查看显存占用用torch.cuda.memory_summary()查看PyTorch的显存分配。如果是批次太大减小batch size。如果是模型太大考虑混合精度训练或者模型并行。如果是多进程确保每个进程只加载必要的模型。一个容易忽略的点是PyTorch的缓存分配器会保留已释放的显存导致nvidia-smi显示的占用比实际需要的高。可以用torch.cuda.empty_cache()手动清理但不要频繁调用会影响性能。5.4 模型更新后效果下降新模型上线后效果变差可能是过拟合、数据泄漏、或者评估指标不具代表性。排查方法对比新旧模型在同一个测试集上的表现确认新模型确实更差。然后检查训练数据是否有问题比如混入了脏数据、标签错误、或者训练集和测试集有重叠。还要检查评估指标是否合理比如类别不平衡时准确率会误导应该看F1或者AUC。预防措施每次模型更新前做A/B测试或者灰度发布先放小流量观察效果确认没问题再全量。5.5 常见问题速查表问题现象可能原因排查手段解决方案线上线下效果不一致预处理不一致、模型版本不一致对比离线复现结果共用预处理代码、绑定模型版本推理延迟高预处理慢、模型大、无批处理分段耗时打点优化预处理、量化模型、动态批处理显存溢出批次大、模型大、多进程nvidia-smi、memory_summary减小批次、混合精度、单进程加载模型更新后效果下降过拟合、数据泄漏、指标不合理对比测试集表现灰度发布、检查数据、换评估指标服务启动失败模型文件缺失、依赖版本冲突查看启动日志检查模型路径、固定依赖版本请求超时推理慢、队列积压查看队列长度和延迟分布限流、扩容、优化推理5.6 几个我踩过的坑和对应技巧第一个坑用torch.save保存整个模型对象而不是state_dict。这样保存的模型和代码强耦合换一个目录结构就加载失败。正确做法是保存state_dict加载时先实例化模型结构再加载权重。第二个坑在Docker里用latest标签的基础镜像结果某次构建时基础镜像更新了导致CUDA版本不匹配。后来我固定了镜像的digest保证每次构建环境一致。第三个坑训练时用了shuffleTrue但推理时忘了设置model.eval()导致Dropout和BatchNorm行为不一致结果波动很大。这个错误很隐蔽因为不会报错只是结果不稳定。第四个坑日志里记录了完整的输入文本结果日志文件暴涨磁盘写满。后来改成只记录文本长度和哈希值需要排查时再根据请求ID去查原始数据。第五个坑模型服务没有设置超时某个请求因为输入异常导致推理卡死整个服务被拖垮。后来加了超时机制超时后返回默认结果并记录告警。6. 工程化之外的思考成本、迭代与团队协作6.1 成本控制GPU不是免费的AI工程和传统后端工程最大的成本差异在于GPU。一块A100每小时的成本可能是几十块钱如果利用率不高烧钱速度非常快。控制成本的手段有几个第一训练任务用竞价实例或者抢占式实例成本能降一半以上代价是可能被中断需要做好checkpoint。第二推理服务用自动扩缩容流量低时缩到零。第三模型压缩用蒸馏、量化、剪枝把大模型变小减少GPU需求。第四共享GPU多个小模型跑在同一块卡上用MPS或者Triton的模型集成功能。我自己的经验是训练阶段用云上竞价实例推理阶段用预留实例加自动扩缩容整体成本能控制在预算的70%左右。6.2 迭代速度从想法到上线要多久AI工程的迭代速度直接影响业务价值。如果一个模型从想法到上线需要两周那很多机会就错过了。提升迭代速度的关键是自动化。数据校验自动化、训练触发自动化、模型评估自动化、部署自动化。我通常用CI/CD流水线来实现代码提交后自动跑单元测试合并到主分支后自动触发训练训练完成后自动评估评估达标后自动部署到Staging环境人工确认后部署到Production。这套流程搭建初期需要投入一些时间但长期来看回报巨大。我们团队从想法到上线的时间从两周缩短到了两天。6.3 团队协作算法和工程的边界在哪里算法工程师和工程工程师的协作是AI项目里最容易出问题的地方。算法工程师关注模型效果工程工程师关注系统稳定性两者的目标不完全一致。我的建议是算法工程师负责训练脚本、模型结构、超参数调优输出是模型产物和评估报告。工程工程师负责数据管道、服务框架、部署运维输入是模型产物。两者通过模型格式和接口契约协作。模型产物必须包含权重文件、配置文件、预处理代码、依赖列表、评估结果。工程工程师拿到这些就能独立部署不需要算法工程师介入。算法工程师也不需要关心服务怎么部署只需要保证模型产物符合规范。这种分工的前提是有一套清晰的模型规范。我们团队维护了一个模型模板所有模型都必须按照模板输出产物。模板包含model.py模型定义、preprocess.py预处理逻辑、config.yaml配置、requirements.txt依赖、metrics.json评估指标。这样工程侧可以写通用的加载和部署逻辑不需要为每个模型定制。6.4 后续扩展方向这套从零搭建的框架后续可以往几个方向扩展。一是支持更多任务类型比如目标检测、语音识别、推荐系统每个任务类型定义自己的预处理和评估规范。二是引入特征存储把特征计算从训练和推理中抽离出来统一管理。三是做模型集成把多个模型的预测结果融合提升效果。四是接入在线学习让模型能够根据实时反馈持续更新。但我要提醒一句不要为了扩展而扩展。每增加一个组件就增加一份维护成本。只有当业务确实需要时才引入新的复杂度。我见过太多团队一开始就追求大而全的架构结果半年过去了核心功能还没上线。我个人在实际操作中的体会是AI工程的核心不是技术有多先进而是每个环节都扎实可靠。数据不出错、训练可复现、服务不宕机、问题能排查这四点做到了就已经超过大部分团队了。至于那些花哨的新技术等基础打牢了再考虑也不迟。
返回列表