ARTICLE DETAIL

资讯详情

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

AI工程化从零到上线:数据、模型、部署与监控全链路实战

AI工程化从零到上线:数据、模型、部署与监控全链路实战 这几年我一直在做AI应用落地相关的工作经常被问到一个问题“教程看了不少模型也能跑通但真让我从零搭一个能上线的AI项目该从哪里下手”说实话市面上教“训模型”的教程一抓一大把但教“AI工程化”的太少。尤其是那种真正从零开始、把数据、模型、接口、部署、监控串成一条完整链路的东西几乎是稀缺品。所以当我看到“ai-engineering-from-scratch”这个标题的时候第一反应就是“对这才是AI工程最缺的那部分”。这篇文章不是我写给自己看的笔记而是把我从零搭AI项目的一套方法和踩坑记录摊开来讲清楚适合正在做毕业设计、刚进AI岗位实习、或者想把自己手头模型变成完整产品的同学参考。我要强调一点从零开始做AI工程不等于从线性代数开始重写一切。没人会教你徒手实现反向传播也没有必要。真正的“from scratch”是从零搭建一个可交付、可迭代、可维护的AI应用系统。你需要面对的是环境、数据、模型、接口、部署、监控这些工程问题而不是重新发明神经网络。下面我按实际做项目的顺序把整条链路拆解一遍。1. 项目概述AI工程化到底在“抠”什么1.1 从零开始不是重复造轮子很多人对“从零开始”有误解以为要自己实现一个深度学习框架、自己从第一性原理推导一遍Transformer。工程视角下完全不是这样。把“ai-engineering-from-scratch”理解成“从一个空白仓库开始搭建AI服务”更准确。你会面临的核心问题包括代码怎么组织、数据怎么管理、模型怎么训练、怎么把模型包成接口、上了生产之后怎么监控效果。这些问题的集合就是AI工程化。我见过太多项目死在“模型效果还行但没法落地”这一步。模型文件有演示的notebook也有可团队里没人说得清这个模型依赖的Python版本是多少、数据预处理用了哪套规则、跑一次推理要多少显存。工程化解决的恰恰是这些东西。它不是让你去造一个新的深度学习框架而是让你把你手头的模型变成一个别人能调用、能部署、能继续迭代的软件系统。我从一开始就给项目定了三条铁律环境可复现、流程可测试、服务可观测。后面所有选型和代码组织都是围绕这三条来的。还有一个关键思维转变先把MVP跑通再谈优化。MVP不只是“做出一个能用的模型”而是“从零到一打通全链路”。我通常会把75%的精力放在数据管线和接口工程上只留25%给模型调优。原因很简单模型没上线之前一切效果都是“实验室自嗨”。而全链路里最容易被忽视的恰恰是那些看起来平平无奇的数据处理和部署环节它们才是真正决定项目生死的地方。1.2 技术栈选型背后的取舍逻辑选型这个问题我看过太多人纠结。从零开始搭AI工程技术栈选得对不对直接决定你后面是“拾级而上”还是“寸步难行”。先看语言。Python在这个场景里几乎是无争议的选择模型生态、数据科学工具链、部署框架全都在这里。其他语言不是不行但会让你在每一个环节都多走弯路。如果追求极致性能可以用Rust或者Go写推理服务的外围但那是大厂优化到一定程度之后的事从零起步真没必要给自己上难度。再看服务框架。我会毫不犹豫地选FastAPI而不是Flask或Django。原因有三第一FastAPI原生支持异步撑并发比Flask轻松第二基于Pydantic做请求参数校验文档和质量都有保障第三自动生成OpenAPI文档前后端联调省很多事。做AI服务时模型推理往往不是瓶颈频繁的IO和无效等待才是。异步接口能让你在同一份硬件资源上多扛好几倍请求。数据存储方面我偏好先用PostgreSQL打底。它足够成熟、生态完整JSONB字段可以直接存预测日志对于中小项目的AI应用来说一个库就够了。很多人喜欢一上来就上各种大数据组件但我个人的经验是超过90%的AI项目在这个阶段用PostgreSQL加对象存储就能解决没必要徒增运维成本。下面是我在项目里反复权衡后常用的技术栈组合层级选型核心理由开发语言Python 3.11模型生态最全团队共识度高API层FastAPI Uvicorn原生异步、Pydantic校验、自动文档数据库PostgreSQL稳定可靠JSONB适配灵活存储模型管理MLflow Hugging Face Hub实验记录、版本追踪一目了然部署Docker Compose - Kubernetes先单机可复现再集群可扩展监控Prometheus Grafana开源标准组合方便接入告警一句话总结选型原则能少用一个组件就少用一个组件能在单机上解决就不上分布式。做AI工程不是在秀技术是在用最简单可靠的方案把模型变成服务。1.3 以终为始先定上线形态再反推架构我选型有个习惯先问一个问题这个服务最终怎么被人使用是Web API、离线批量任务还是嵌入式部署三种形态对架构的影响天差地别。如果是Web API你需要重点关注请求响应延迟、并发能力、鉴权限流如果是离线批量注意力就要放在任务编排、失败重试和批量吞吐上如果是嵌入式模型量化和推理引擎优化就成了核心矛盾。很多项目从零开始就搞偏了一上来就写模型训练脚本完全没想过模型后面要挂在什么形态上最后架构来回推翻。所以从零搭建一个AI项目第一步永远不是写代码而是把最终上线形态画清楚。这个“以终为始”的思维帮我避开了好几次大改重来的坑。2. 核心链路拆解与技术要点2.1 数据工程模型的地基也是踩坑重灾区AI工程里最“不性感”但最要命的部分就是数据工程。可以这么说模型能不能work七成靠数据三成靠算法。从零开始做AI项目数据是绕不开的第一道坎。先说数据采集。根据项目场景可能是爬公开数据集、调用API采集也可能是人工标注。这里最容易踩的坑是“采集数据之后直接拿来训练”。真实场景中你拿到的原始数据几乎是肯定带着各种问题的格式不统一、字段缺失、标签噪声、重复样本甚至不同来源的数据时间口径都不一样。清洗环节有几个必须盯死的点。一是编码问题中文数据经常遇到GBK和UTF-8混用读取时直接乱码训练出来的模型自然也是“半身不遂”。二是去重逻辑单纯的文本去重会导致本应保留的业务重复样本被删掉需要根据业务含义设计去重规则。三是异常值处理数值型特征里偶尔冒出一个“99999”或者负值会直接带偏归一化。四是标签清洗很多人工标注存在前后不一致同一个规则不同人标注结果不一样这种情况需要在清洗阶段就统一标准不然后面评估时你会发现连ground truth都是歪的。做完清洗接着是数据划分。数据划分是新手重灾区这里有三个经典问题泄漏训练集和验证集共享了来自同一用户的样本导致验证指标虚高。比如做文本分类时同一篇文章的多个段落被切分到了两侧。样本不平衡正负样本比例悬殊时直接按比例划分会让少数类在验证集中更少模型评估失真。需要分层采样。时间序列错乱如果是时间相关的数据不能随机划分必须按时间切分。否则模型会“偷看未来”上线后效果断崖式下跌。我建议在项目里固定写一份数据校验脚本划分完数据集后系统跑一遍。包括字段缺失率、标签分布、训练集与验证集之间的样本重叠度这三项检查。我在实际项目中吃过亏验证集AUC跑到98%上线一测直接掉到82%最后查来查去发现就是样本泄漏——同一个案件的描述文本被同时拆进了训练集和测试集。从那以后我再也不信手滑的划分方式。2.2 模型选择与训练先跑通再优化模型选择这件事本来可以很简单先从常用基线模型开始而不是一上来就追最新的大模型。很多人看到新模型发布就兴奋立刻想往上套结果训练时间、显存占用、推理延迟全都爆了。从零做AI工程尤其是第一个版本我建议优先考虑“够用、好训、易部署”的模型。以文本分类为例如果你的数据量在几万条以内直接用Hugging Face上的轻量级预训练模型做微调就够了比如BERT系列的中文小模型。这个选择性价比极高训练一个epoch只需要几分钟到十几分钟推理时CPU都能扛住。等确认项目值得投入更多资源再考虑更大的模型或者部署LLM也不晚。下面是一段基于Hugging Face transformers库做模型微调的代码骨架我把关键步骤和参数都写清楚# 基于 Hugging Face 生态做文本分类模型微调 from datasets import load_dataset from transformers import ( AutoTokenizer, AutoModelForSequenceClassification, TrainingArguments, Trainer, ) # 1. 加载数据集和模型 dataset load_dataset(json, data_files{train: data/train.jsonl, validation: data/val.jsonl}) model_name hfl/chinese-roberta-wwm-ext tokenizer AutoTokenizer.from_pretrained(model_name) def tokenize_function(examples): return tokenizer( examples[text], truncationTrue, max_length128, paddingmax_length, ) tokenized_datasets dataset.map(tokenize_function, batchedTrue) # 2. 关键训练参数配置 training_args TrainingArguments( output_dir./results, learning_rate2e-5, per_device_train_batch_size32, per_device_eval_batch_size64, num_train_epochs3, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modeleval_loss, ) # 3. 训练 model AutoModelForSequenceClassification.from_pretrained(model_name, num_labels2) trainer Trainer( modelmodel, argstraining_args, train_datasettokenized_datasets[train], eval_datasettokenized_datasets[validation], ) trainer.train()三个参数值得展开说。第一个是learning_rate迁移学习场景下2e-5到5e-5之间最安全大学习率会让预训练权重被快速冲掉直接灾难性遗忘。第二个是batch_size受限于显存但也不能一味调小太小的batch会让梯度噪声变大训练收敛不稳。第三个是epoch数预训练模型微调通常2-5个epoch就够了再多就会过拟合表现为训练loss持续下降但验证指标反而回落。顺带说一个显存估算的小技巧大致显存占用可以按“batch_size × 最大序列长度 × 模型权重参数量 × 一个经验系数”来粗算。以BERT-base为例大约1.1亿参数FP32训练时每个token需要的显存大约是1-2KB。如果你用的序列长度是128batch_size是32估算就是32×128×1.5KB约等于6GB再加上优化器和中间激活值的开销一张12GB的显卡基本就是极限。这个估算公式虽然粗糙但足够让你在启动训练前判断“这张卡到底撑不撑得住”不至于跑一半直接OOM。2.3 模型评估别只盯着准确率模型评估这件事99%的新手会犯同一个错误只看accuracy。二分类场景下正负样本比是9:1的时候模型啥都不学全预测成多数类准确率照样能到90%。这个数字不仅没有意义还会误导你做出各种错误判断。正确的姿势是按任务类型选择合适的指标。分类任务至少要看precision、recall和F1回归任务要看MAE或RMSE。如果正负样本严重不平衡还要加上AUC或者PR曲线。我给自己的项目定了个硬规矩每个模型上线之前必须提交一份包含三类指标的报告——整体指标、分人群指标、以及bad case分析。bad case分析是最花时间但最有价值的部分。你要随机抽取几十个预测错误的样本一条一条看模型为什么错了。十次里有八次你会发现问题出在数据而不是模型结构上。比如标注有误、存在样本噪音、以及模型学到了数据中的无关模式。常见的例子是因为采集到的正样本大多来源于某几个固定渠道模型实际上学到了“来自这些渠道的内容大概率是正样本”而并没有真正学会业务语义。这种情况下再加训练轮数也没有用必须回去清洗数据或者补充样本。2.4 服务化部署与API设计模型训练完接下来就是把它变成服务。AI工程和算法研究的分水岭就在这一步。设计推理API的时候有一条最重要的经验不要在接口层暴露原始模型输入输出格式。你需要对调用方提供业务友好的请求结构然后在内部完成转换。这样做的直接好处是你可以随意更换底层的模型版本而不需要让调用方感知到接口变化。比如前端传过来一段文本你不用让前端知道“这个模型需要先分词再拼接特殊token”这些全部封装在服务内部对外只需要一个clean文本输入。下面是FastAPI部署模型推理接口的骨架代码# 基于 FastAPI 的模型推理服务 from fastapi import FastAPI from pydantic import BaseModel from transformers import pipeline app FastAPI(titleAI Service, version0.1.0) # 使用 lifespan 管理模型生命周期避免每次请求重复加载 from contextlib import asynccontextmanager asynccontextmanager async def lifespan(app: FastAPI): # 启动时加载模型 app.state.classifier pipeline( text-classification, model./models/classifier, device0, # 0 表示使用GPU没有GPU就改为 -1 ) yield # 关闭时可释放资源 app FastAPI(lifespanlifespan) class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str score: float app.post(/predict, response_modelPredictResponse) async def predict(req: PredictRequest): result app.state.classifier(req.text)[0] # 返回前做标签映射与阈值判断 return PredictResponse( labelresult[label], scoreround(float(result[score]), 4), ) # 健康检查接口方便负载均衡和监控系统探测服务状态 app.get(/healthz) async def healthz(): return {status: ok}这里有一个特别容易踩的坑不要把模型加载写在全局作用域或者请求函数内部。写在全局作用域会导致子进程复制模型副本多进程部署时每个worker各存一份显存瞬间爆炸写在请求函数内部则是每次调用都加载一次模型推理延迟直接飙到几秒。正确的做法是用lifespan钩子在服务启动时加载到app.state里进程内全局复用。部署时的另一个关键是遵循“生产偏好”选择运行实例数。在单机场景下我会先把可用的GPU或CPU资源确认清楚再决定开多少个worker。如果只有一个GPU开一个worker就够了多开worker共享同一张显卡反而会因为显存和算力竞争导致延迟不稳定。3. 实操过程从零搭建一个可上线的AI架构3.1 项目目录设计与环境隔离一个标准的AI工程代码结构必须让任何接手的人一眼就能看懂。我常用的目录结构长这样ai-engineering-from-scratch/ ├── app/ │ ├── api/ # 接口层 │ │ ├── routes.py # 路由定义 │ │ └── schemas.py # 请求响应模型 │ ├── core/ # 核心业务逻辑 │ │ ├── predictor.py │ │ └── config.py │ ├── models/ # 模型权重目录可用软链指向对象存储 │ └── main.py # 服务启动入口 ├── data/ │ ├── raw/ # 原始数据只读 │ ├── processed/ # 清洗后数据 │ └── splits/ # 数据划分结果 ├── notebooks/ # 探索性分析只做分析不做训练 ├── scripts/ │ ├── train.py # 训练脚本 │ ├── evaluate.py # 评估脚本 │ └── data_prepare.py # 数据准备脚本 ├── tests/ # 单元测试与接口测试 ├── docker/ │ └── Dockerfile ├── pyproject.toml # 项目管理与依赖声明 └── README.md这个结构把接口、模型、数据、脚本分得很清楚。注意一个细节notebooks目录里的文件只用来做分析和可视化不应该承担任何训练或者数据处理任务。原因很简单notebook的单元格天然依赖执行顺序别人拿到你的notebook从头跑到尾大概率各种报错。任何可以写进Python脚本的内容就应该放进scripts目录。环境隔离方面我比较推荐用uv或者Poetry管理Python依赖而不是直接用pip install逐步安装。用pip的问题在于它默认不区分项目的环境边界装多了之后依赖冲突会把你折磨到怀疑人生。使用uv创建一个项目虚拟环境只需要一条命令生成锁文件锁定全部依赖版本后任何时候换一台机器都能一键复现环境。uv init ai-engineering-from-scratch cd ai-engineering-from-scratch uv add fastapi uvicorn[standard] transformers torch datasets pydantic-settings这个操作下来项目就有了独立的环境依赖版本都记录在pyproject.toml和锁文件里。等部署的时候在服务器上执行同样的uv sync装出来的环境跟本机一模一样这就跑通了“环境可复现”的第一环。3.2 训练与验证流水线项目启动之后第一版流水线通常长这样先用data_prepare.py把原始数据清洗并划分再用train.py完成训练且把训练参数和指标记录到MLflow最后用evaluate.py在测试集上跑出报告。我建议把数据准备脚本设计成“幂等”的。所谓幂等就是不管执行多少遍产出的数据都是同一个结果。这看似不起眼实则是整个工程可复现的基础。如果每次运行数据脚本都会产生不同的数据划分或者清洗结果后面所有实验对比都是空中楼阁。训练脚本里值得注意的一个组件是实验跟踪。不要用打印日志和手抄Excel的方式记录实验在项目初期就接入MLflow或者至少把每次训练的关键参数、指标、模型文件路径写成一个JSON文件存入experiments目录。否则两天后你就会开始怀疑当前这个模型效果到底是最好的还是上一次调的参数更好没有记录只能重新训练一遍。我在训练脚本中通常会记录这些字段数据集版本对应数据文件的哈希值或Git提交号模型名称与具体版本训练参数学习率、batch_size、epochs、seed各项评估指标F1、precision、recall等训练环境Python版本、CUDA版本、依赖锁文件哈希这样每次实验都留痕后面复盘或者排查线上问题时有据可查。3.3 接口实现与配置管理在实际写接口代码的时候还有几个细节值得展开讲。第一个是配置管理。模型路径、数据库连接串、阈值这些信息不应该硬编码在代码里而是统一放到环境变量或配置文件中由pydantic-settings做校验和读取。我见过太多人把模型版本号写死在路由文件中每次升级模型就要改代码重新发布。正确的做法是模型路径、版本号既然是运维和部署层面关心的事就应该交给配置文件管理。第二个是超时和重试。如果你给业务方提供的API触发了第三方接口那调用链路上的超时和重试机制就要做好设计。模型推理服务虽然本身是同步逻辑但外层网关的请求排队和上游服务的io等待不可控。一个简单的经验法则是调用模型推理接口时把超时设置为最长推理时间的1.5倍对于非关键路径上的辅助调用用超时兜底。宁可让当前请求失败也不要拖垮整个服务进程。第三个是批处理支持。分类模型单条推理和批量推理在延迟上差别很大。GPU推理时批量请求可以共享模型权重加载吞吐量提升非常明显。所以接口层面上我通常会设计一个batch接口接收一组请求批量加载数据再批量推理最后逐条返回。调用方可以根据自己的场景选择单个请求或批量请求。class BatchPredictRequest(BaseModel): texts: list[str] app.post(/predict/batch, response_modellist[PredictResponse]) async def predict_batch(req: BatchPredictRequest): results app.state.classifier(req.texts) return [ PredictResponse(labelr[label], scoreround(float(r[score]), 4)) for r in results ]3.4 部署、CI/CD与监控本地代码写得再优雅部署才是真正考验工程水平的地方。我的部署第一版通常从Docker Compose开始原因很简单单机上一条命令就能起全套服务数据卷和网络也配置直观。等并发要求真正上来之后再去考虑Kubernetes也不迟。写Dockerfile时值得注意的点有几个。首先是尽量使用多阶段构建先在一个包含完整编译工具链的镜像里安装依赖、构建产物再把运行所需的最小文件拷贝到新的精简镜像中。这样最终镜像体积能缩小一半以上。其次是在镜像中指定非root用户运行。这样做能降低容器逃逸风险而且现在很多平台默认拒绝root容器。第三是合理区分GPU镜像和CPU镜像。模型推理服务如果在GPU上运行需要在镜像中预装CUDA运行时而不是等到容器启动再装——那样又慢又容易版本不匹配。# Dockerfile 多阶段构建示例 FROM python:3.11-slim as builder WORKDIR /build COPY pyproject.toml uv.lock ./ RUN pip install --prefix/install uv uv pip install --prefix/install -r pyproject.toml FROM python:3.11-slim WORKDIR /app COPY --frombuilder /install /usr/local COPY app/ ./app/ COPY models/ ./models/ # 创建非root用户 RUN useradd -m appuser USER appuser EXPOSE 8000 CMD [uvicorn, app.main:app, --host, 0.0.0.0, --port, 8000]CI/CD是从零工程化和“算法工程师手搓notebook”最大的区别之一。我的要求很简单每次push到主干分支时自动跑一遍代码检查、单元测试和构建镜像打tag时再自动部署到测试环境。GitHub Actions或者自建的GitLab CI都可以关键在于让流程自动化而不是靠某个人在本地手动执行。我常用的一套CI流程包括代码格式检查ruff和类型检查mypy。单元测试与接口冒烟测试。用一个最小的模型跑一遍predict接口确认请求响应结构正常。构建Docker镜像推送到镜像仓库。在测试环境部署跑一遍完整的接口回归。监控这一环我强烈建议在第一天就接上而不是等出问题了再补。最基础的监控至少包含三类指标服务可用性接口错误率、性能P50/P95延迟、资源消耗CPU、内存、GPU显存。装上prometheus-fastapi-instrumentator之后FastAPI的延迟、请求量指标自动就暴露出来了Grafana配几个面板问题发生之前就能看到苗头。4. 常见问题与排查技巧实录4.1 环境类GPU明明买了模型就是跑不动这类问题在从零搭建项目的过程中出现频率极高。典型的症状是torch.cuda.is_available()返回False或者训练刚开始就报显存不足。先给排查路径。遇到GPU问题第一件事不是去看代码而是去跑nvidia-smi。看两个信息驱动版本和CUDA Version。驱动版本对应的CUDA能力决定了你能装哪个版本的PyTorch。比如驱动只支持CUDA 11.x那你硬装一个需要CUDA 12.x的torch版本大概率会出现CUDA initialization error。显存不足的问题也很好定位。nvidia-smi里可以看到哪些进程占着显存。如果发现其他进程占着好几GB显存可能是上一个训练任务没有正常退出需要先清理僵尸进程。如果是自己的模型一次前向直接OOM那就把batch_size减半再试。记住一个原则先确认环境再怀疑代码最后怀疑模型。4.2 模型类效果差、过拟合、预测结果诡异模型效果不行时大部分人会直接调模型结构。我个人的经验是先怀疑数据再怀疑训练流程最后才轮到模型结构。过拟合的信号是训练loss持续下降而验证loss回升。遇到这种情况处理办法优先级排序是加数据增强、加正则化和dropout、减小模型容量、提前停止。早停是我用的最多的方法在验证集指标连续几个epoch没有改善时就停止训练并保存最佳模型权重。transformers库里可以直接设置early_stopping_patience来实现。预测结果诡异的情况最常见的元凶是数据泄漏和标签噪音。如果同一个实体在训练阶段、验证阶段都出现过模型其实就是开卷考试。排查方式很简单随机抽一些预测错误的样本看。如果你的bad case里模型错误规律高度一致比如总是把某些特定词汇判成某个分类基本可以断定是数据里混入了不该有的模式。4.3 工程类延迟高、并发差、内存泄漏接口延迟高的时候别急着怪模型推理慢。先用日志和监控把耗时分段拆开是请求排队慢还是数据预处理慢还是模型推理慢还是返回序列化慢大部分时候问题出在前面两个环节。优化思路通常按性价比排序第一是加缓存。如果服务是短文本分类场景文本分布重复度又高完全可以加一层基于内容哈希的结果缓存命中率能到30%-60%。第二是异步化处理不依赖推理结果的任务放到后台队列执行请求先返回。第三才是模型优化比如量化、蒸馏或者改用更小的模型。内存泄漏的排查手段也很直接看服务进程内存随时间的增长曲线。如果是锯齿状上升且不断新高大概率是某个全局缓存没有释放或者加载了不必要的模型副本。跑一下pytest内存测试反复调用接口观察内存变化能很快定位是哪个模块的问题。4.4 监控类线上效果悄悄变差模型上线后效果变差是常态不是异常。一个刚上线的模型随着数据分布的变化效果会逐渐下滑。这里分两种情况输入分布漂移和概念漂移。输入分布漂移指的是模型接收到的数据跟上线时差别越来越远比如业务人群变了、文本风格变了。概念漂移则是“标准答案”变了同样一个输入以前是这个标签现在应该变成另一个标签。两种漂移的处理手段不一样前者需要补充相似分布的数据做增量训练后者往往意味着规则和业务语义在演变可能需要重新定义标签体系。监控漂移的简单方法我强调过很多次在推理日志里记录文本特征、模型置信度、标签分布。定期对比线上最近的预测置信度跟训练集的差距如果发现置信度持续下降基本可以判断输入漂移正在发生是时候该准备下一轮训练数据了。最后再分享一个我从零搭建AI工程时最深的体会能落地的从来不是某个模型而是一整个被组织得很好的系统。模型指标只是这条链路上的一个环节数据到位、接口流畅、监控完整模型效果哪怕稍弱一些整体方案依然是可靠的。反过来光有一个懂事的模型服务一崩就全白搭。所以我的建议是先别沉迷于追新模型先用最朴素的方式把这条链从零跑到通哪怕只有一个文本分类器也能让你体会到什么才是真正的AI工程。
返回列表