☰
AI工程从零开始:数据管道、模型部署与监控全攻略
2026/9/30 8:31:12 网站建设 项目流程

最近在整理自己这大半年从零开始折腾AI工程的实践,项目代号就叫ai-engineering-from-scratch。名字很直白,就是想记录一个没有深厚算法背景、也不是科班出身的工程师,到底是怎么一步步把AI项目真正"工程化"跑起来的。这篇内容不是课程,也不是完整教程,更像是我把自己踩过的坑、走过的弯路、最终沉淀下来的方法论摊开给你看。

先说结论:AI工程的难点,从来不在"调参"或者"跑通一个模型",而在于把模型放进真实的业务系统里,让它稳定、可维护、能迭代。这中间隔着数据管道、训练环境、模型服务、监控反馈、版本管理这一大堆平时在Notebook里根本看不见的问题。如果你正准备从零开始,或者已经在路上,希望这篇内容能帮你少走一些我走过的弯路。

1. 先搞懂"AI工程"和"AI研究"不是一回事

1.1 我在这个项目里最想解决的问题

很多人看到from scratch,第一反应是"从零写一个神经网络"。但我理解的from scratch,是把你从"会用框架调模型"的状态,拉到"能独立交付一个AI系统"的状态。这两个状态之间的距离,比想象中大得多。

我最初的状态很典型:能在Jupyter Notebook里跑通FashionMNIST分类,能调一些超参让Accuracy往上走一点点,知道卷积池化Dropout这些概念大概是什么意思。可真要让我把这些东西部署到服务器上,提供一个HTTP接口让别人调用,支撑每天几万次的请求,我整个人是懵的。

所以这个项目的第一个目标很朴素:我要亲手搭出一个端到端的AI系统,从原始数据采集开始,到模型训练、评估、打包、部署、监控,全部走一遍。

1.2 从Notebook到生产系统的四个跨越

我用一个比较粗的框架总结了一下,从Notebook到生产系统,至少隔着四道坎:

维度Notebook原型生产系统
数据本地文件直接读入管道化采集、清洗、校验、版本化
训练交互式跑一个cell可复用、可复现、可回滚的训练流程
模型形态内存里的一个对象打包后的静态产物,带版本、带元信息
服务方式到model.predict()结束API接口 + 性能保证 + 监控告警

这四道坎,每一项单独拎出来都不算难,但合在一起就是一座山。我花了大半个月才把第一版完整走通,中间无数次想放弃,但走通之后再看以前的东西,确实有种降维打击的感觉。

1.3 能力和心态的双重转变

除了技能,心态上也要转个弯。做工程和做实验有一个本质区别:实验追求的是"能不能成",工程追求的是"稳不稳定"。

做实验的时候,模型效果不好,换参数再跑一次就是了。做工程不一样,你上线一个模型,它效果不好不只是你一个人的事,是业务方、用户、你一起承担。所以你必须在模型效果和系统稳定性之间找一个平衡点。

我自己的体会是,AI工程师一半是数据科学家,一半是全栈开发者,外加一小部分运维。不要抗拒这些"杂活",它们才是工程能力的主体。

2. 从零搭起工具链和项目骨架:第一周先别急着写模型

2.1 环境管理:Python版本、依赖、GPU驱动的三角关系

我踩的第一个大坑就是环境。有一阵子我发现requirements.txt在某台新机器上怎么都装不上,报错信息五花八门。后来才发现问题根源是Python版本不一致——我在本地用的是3.10,那台机器是3.8,很多新版本的库根本装不上。

如果你是从零开始,我强烈建议环境管理这件事从第一天就规范化。具体做法:

  • 统一用conda管理Python环境,一个项目一个环境,不要混用。
  • 锁定Python大版本,比如python=3.10,小版本也尽量锁死。
  • 依赖至少区分requirements.txt(直接依赖)和requirements-lock.txt(全量锁定版本)。进阶用法是用uv lock或poetry这类工具做依赖解析,省心很多。

GPU环境更折磨人。CUDA、cuDNN、PyTorch的版本必须匹配,否则就是无穷无尽的undefined symbol错误。我后来学乖了,就是用官方的基础镜像或者直接从nvidia/cuda镜像往上叠,再固定PyTorch的版本。

2.2 项目目录结构:一个能长远的骨架比想象中重要

以前写代码都是随便放,一个文件夹里堆着一堆.py和.ipynb。这次我特意设计了一个目录结构,后面所有迭代都在这个骨架上进行:

ai-engineering-from-scratch/ ├── configs/ # 参数配置,yaml格式 ├── data/ # 原始数据 + 处理后数据 ├── src/ # 源码 │ ├── data/ # 数据采集与清洗 │ ├── features/ # 特征工程 │ ├── models/ # 模型定义与训练逻辑 │ └── serving/ # 模型服务化 ├── experiments/ # 实验记录 ├── scripts/ # 脚本,如数据下载、训练启动 ├── tests/ # 单元测试 └── README.md

这个结构一开始看着挺空,但每个模块的边界很清晰。数据、模型、服务的代码不混在一起,后面重构的时候特别爽。目录不是摆设,它是你项目可维护性的第一道保障。

2.3 数据先行:先把数据管线跑通再谈模型

我以前的习惯是赶紧把模型跑起来看效果,数据这块是先凑合用。后来被现实教育了。数据干净与否,直接决定模型训练能不能进行、效果能有多好。

在ai-engineering-from-scratch项目里,我选了一个文本分类任务来练手。数据是一批用户反馈文本,需要分类到几个预定义标签里。数据管线的核心逻辑:

# 伪代码示例,展示数据管线的各环节 def build_dataset(raw_path, output_path): # 1. 读取原始数据 df = load_raw(raw_path) # 2. 简单清洗:去重、去空、长度过滤 df = df.drop_duplicates(subset="text") df = df[df["text"].str.len() > 5] # 3. 标签编码 label_map = {label: idx for idx, label in enumerate(sorted(df["label"].unique()))} df["label_id"] = df["label"].map(label_map) # 4. 按时间排序,切分训练/验证/测试集 df = df.sort_values("timestamp") train, val, test = split_by_time(df) # 5. 保存为tfrecord或parquet,方便后续加载 save_as_parquet(train, output_path / "train.parquet") ...

我特别要强调一点:数据切片要用时间切,不要用随机切。因为真实业务里,模型要预测的是"未来"的数据,如果训练集和测试集是从同一批数据里随机分的,等于考试时偷偷看了一眼未来的答案。用时间切分,模型才是在真正模拟上线后的情况。

3. 第一个端到端项目:从原始数据到可调用的服务接口

3.1 选一个"小而完整"的任务:靠谱的起点

练习项目最忌讳一上来就整大的。什么多模态、大模型微调,那是后面的事。我当时选了文本多分类,是因为它数据好找、模型不用太深、评估指标直观,但又能覆盖一个端到端项目所需要的全部环节。

选任务可以按这个标准来判断:

  • 数据规模适中:几千到几万条最好,既能体现数据工程的作用,又不会让你陷在数据处理里出不来。
  • 模型复杂度可控:用一个不是特别深的模型就能跑出不错的效果,比如入门级的就是几层Transformer或者经典的TextCNN。
  • 有清晰的业务场景:比如"自动对用户反馈打标签",这样你做的每一步都能对应到实际的业务价值上,而不是纯粹为练习而练习。

我当时选的是用户反馈分类:输入是一段文本,输出是若干个预定义标签中的一个。这个任务看起来简单,但已经足够帮我建立完整的工程闭环。

3.2 模型训练的关键配置与实验记录

训练阶段,我最大的改变是开始用配置文件和实验记录。

以前调参就是改代码里的变量,改完就跑,跑完就忘。这次我把所有关键参数抽到configs/train.yaml里:

model: name: "bert-base-uncased" # 基于预训练模型做微调 max_length: 128 dropout: 0.1 training: batch_size: 32 learning_rate: 2e-5 epochs: 3 warmup_ratio: 0.1 weight_decay: 0.01 data: train_path: "data/processed/train.parquet" val_path: "data/processed/val.parquet"

每跑一次实验,我都会用时间戳记录一个实验目录,里面包含:当前用的配置文件、训练日志、模型checkpoint、最终的评估指标。这样任何一次实验结果都能追溯回去,知道是哪个参数组合产生了这个结果。

这里我要推荐一个工具,mlflow。它做实验追踪太合适了,能自动记录参数、指标、模型产物,还带一个简单的UI。如果你不想引入额外依赖,自己用一个日志文件记录也行。但记录本身绝对不能省,否则你做的实验都是"一次性实验",下一次就忘了上一次怎么跑的了。

3.3 模型服务化的三种方案对比

模型训练完了,怎么把它变成服务接口?我对比了三种方案:

方案优点缺点适合场景
Flask/FastAPI 自写接口灵活、可控、轻量要自己处理并发、生命周期管理个人项目、小流量
TorchServe专为PyTorch设计,支持模型版本、监控指标配置略复杂,学习成本高中大型项目、需要模型版本管理
Triton Inference Server性能强,支持多模型、动态batch、GPU调度上手门槛最高高并发、生产级GPU部署

我自己当时因为还是起步阶段,选了FastAPI。方案很朴素:加载模型到内存,然后用FastAPI暴露一个/predict接口。

from fastapi import FastAPI from pydantic import BaseModel import torch from transformers import pipeline app = FastAPI() # 这个classifier在模块加载时初始化,避免每次请求都重新加载模型 classifier = pipeline( "text-classification", model="./models/bert-classifier-v1/", device=0 if torch.cuda.is_available() else -1 ) class TextRequest(BaseModel): text: str @app.post("/predict") def predict(req: TextRequest): result = classifier(req.text) return {"label": result[0]["label"], "confidence": result[0]["score"]}

这个接口跑通的那一刻,我有点激动。模型不再是Notebook里只能自己玩的东西,它变成了一个可以被任何系统调用的服务。"能用接口调用"和"能跑通"是完全两种体验。

3.4 把离线评估和在线监控补齐

服务上线前,我还做了一件事:写了一个离线评估脚本。用之前切分出来的测试集,批量调用模型的predict接口(或者直接调用模型的批量预测方法),算出准确率、F1值、混淆矩阵。这一步是为了确保模型的"线上表现"和"训练时表现"是一致的。

除了离线评估,在线监控也是必须的。我最开始只记录CPU/内存使用率,后来发现不够。你更需要监控的是模型的预测结果分布。如果某个标签突然占比猛增,或者平均置信度骤降,那大概率是输入分布变了,模型可能需要重新训练了。

我自己加了一个极简监控:每处理100条请求,记录一次标签分布和平均置信度,配合日志一起打出来。这样有问题可以快速发现,不至于用户都开始投诉了你还蒙在鼓里。

4. 绕不过去的踩坑记录:环境、数据和显存的三重考验

4.1 依赖地狱:如何让环境"可复现"

这是我整个项目中最崩溃的一段。当时我刚把训练好的模型部署到一台新的服务器上,按requirements.txt装完依赖后,模型推理速度慢了三倍,甚至还有随机报错。

排查了一整个下午,最后发现:本地的transformers是4.36,服务器上的是4.30,模型输出的logits分布有一点点差异,导致某些样本的预测结果变了。这让我意识到"在我的机器上能跑"完全不算数,环境必须可复现。

后来我做了三件事:

  1. 用conda env export导出完整的环境文件。
  2. 用pip freeze > requirements-lock.txt锁死所有传递依赖的版本。
  3. 再进一步,直接把整个环境打成Docker镜像,镜像带标签存到私有仓库里,部署时只做镜像拉取。

做到第三件事之后,环境问题基本绝迹了。Docker在这里的核心价值是:把"环境的构建过程"变成了"环境的运行快照"。你训练时用的环境和线上跑推理的环境完全一致,所有依赖版本、系统库、GPU驱动行为都一模一样。

4.2 数据漂移:训练时99%准确率上线后崩了

这个坑让我對数据漂移产生了生理性敬畏。我的模型在测试集上准确率97%,我信心满满地部署上线。结果上线第三天,线上的真实数据准确率只有78%,而且持续下跌。

为什么?仔细一查,训练数据里有大量以"我们"开头、格式规整的正式文本,但线上用户打出来的反馈短、乱、口语化严重,还经常有错别字。模型在训练数据上看到的分布和线上实际遇到的根本不是一个分布。

这就是数据漂移(Data Drift)的典型表现。我当时的应对分成两部分:

  • 短期:收集线上数据,人工标注一部分,混合到训练集里重新训练,让模型尽快"见过"真实世界的数据。
  • 长期:把数据采集逻辑修改为"持续采集 + 定期采样人工复核",并且增加一个简单的分布监控。比如统计输入文本的平均长度、词汇表覆盖情况,一旦发现跟训练集偏差很大就告警。

4.3 显存管理:一次OOM引发的架构调整

训练时显存溢出的经历应该每个做深度学习的人都遇到过吧。我用一张12GB的显卡训练Bert模型,batch_size调到32,直接OOM,程序崩掉。

一开始我以为是代码问题,后来意识到这是工程问题。显存管理有几个常规手法:

  • 减小 batch_size,配合梯度累积(gradient accumulation)保证等价效果。
  • 使用混合精度训练(AMP),显存占用能砍掉将近一半。
  • 在数据处理阶段把max_length从512降到128,对文本分类任务来说足够用了。
# 训练配置中,混合精度相关设置 training: batch_size: 16 # 显存不够,降下来 grad_accum_steps: 2 # 累积两步,等价于batch_size=32 fp16: true # 混合精度,显存减半,速度还更快

另外还有一个很容易被忽略的点:Dataloader的pin_memory和num_workers。这两个参数设置不当,不仅影响显存,还会严重拖慢训练速度。训练时打开pin_memory=True,num_workers设为CPU核心数的一半左右,是基本操作。

4.4 版本管理:模型文件、代码、数据三者如何联动

代码有Git管,模型文件怎么办?数据怎么办?这三者的版本如果不对应,拿到代码也复现不出结果来。

我的做法是建立了简单的三方联动规则:

  • 代码版本定一个Git tag。比如v1.0.0。
  • 模型目录命名带版本和实验ID,比如bert-classifier-v1-20250120。
  • 数据文件用单独的目录,每个版本一个子目录,并写一个manifest.yaml记录版本信息。
# data/processed/manifest.yaml version: "20250120" source: "user_feedback_2024Q4" description: "首次清洗后的训练数据,已按时间切分" label_map: bug_report: 0 feature_request: 1 complaint: 2 other: 3

这样做的核心目的是:任何一个时间点,你都能找到"这套代码 + 这份数据 + 这个模型"的完整组合,回去看当时的实验结果。

5. 从"能跑"到"好用":AI工程能力的分水岭

5.1 性能优化:先从瓶颈分析开始

当我的服务稳定运行之后,我开始关心性能。一开始请求量小不觉得,流量起来后发现单个请求平均耗时400ms,高峰期还有大量超时。

优化之前先做瓶颈分析。我用cProfile和py-spy看了一下CPU时间分布,发现大部分时间花在:

  • 文本预处理重复做的部分,比如每次都重新分词。后来把它放进缓存。
  • 模型推理无法并发,FastAPI的订单化处理。后来改用gunicorn多worker。
  • GPU利用率很低,因为单条请求推理时batch只有一个。后来做了简单的动态batching,多个请求合并成一个batch再推理。
优化项操作效果
预处理缓存用lru_cache缓存高频片段耗时降低20%
多workergunicorn -w 4 -k uvicorn.workers.UvicornWorker吞吐提升3倍
动态batching请求积攒50ms内合并成batch单卡GPU利用率提升到70%

有一点值得提醒:动态batching是以延迟换吞吐。请求到达后需要等待一点点时间凑batch,所以单次请求延迟会略微升高,但整体吞吐大幅提高。看你的业务是更关心延迟还是更关心吞吐,自己权衡。

5.2 CI/CD for ML:我所理解的MLOps最小闭环

模型上线几次之后我意识到,AI项目也需要类似软件工程的CI/CD流程。模型本身就是一个"软件组件",它需要测试、构建和部署。

我搭的最小闭环长这样:

  1. 数据更新触发数据校验脚本。检查数据分布是否异常、数据量是否达标、标签是否完整。
  2. 训练脚本在干净的环境里跑,跑完自动导出模型产物并记录实验指标。
  3. 离线评估通过阈值(比如F1 >= 0.85)才允许继续。
  4. 模型注册:把模型产物推送到模型仓库,并打上版本号。
  5. 部署:更新线上的服务镜像,加载新版本模型。

这个流程我大部分用GitHub Actions加Shell脚本拼出来的。你没有看错,不一定要上Kubeflow、MLflow那样的大平台,哪怕是脚本,只要能流程化地把上面这几步跑通,就是一个可用的MLOps闭环。核心价值是把"手动跑模型、手动部署"变成"自动化流水线",减少人为失误。

5.3 成本意识:GPU很贵,算力要花在刀刃上

做AI工程和做学术研究还有一个很大的区别:你的每一分钱都要花得值。GPU一个小时就算几块钱,如果每次都把训练任务跑一遍又一遍,不做复用和断点续训,成本会让人肉疼。

我学到的两件事:

  • 训练开始前先在一个小样本上跑一个极短的实验,比如1/10的数据、1个epoch,确认代码没bug再启动全量训练。不然全量训练跑到第5个epoch才发现数据加载有问题,白白浪费时间。
  • 使用断点续训。每过几个epoch就保存一个checkpoint,万一中间挂了,直接从最近的checkpoint恢复,而不是从零开始。

还有一个小提示:在云上训练的时候,如果开启了spot实例,成本能降一半以上,但要做好随时中断的应对。结合断点续训,这个方案很适合个人和小团队。

5.4 下一步:从个人实践到团队协作

当我一个人能跑通整个流程之后,我逐渐开始思考团队层面的问题。个人的流程和团队的流程有很多不同点:

  • 个人项目里你可以自己决定一切,团队项目里每个环节都需要文档和review。
  • 个人项目里代码能跑就行,团队项目里代码需要被其他人理解、维护、扩展。
  • 个人项目里模型效果不好你自己知道,团队项目里需要一个客观的评估机制,让所有人对"模型当前处于什么状态"有一致的认知。

这也是为什么MLOps越来越受重视的原因——它不是某个人的事,而是让一个团队能协作起来交付机器学习系统的工程方法。

如果团队里有人分工协作,我的建议是把整个流程拆成清晰的模块:数据工程、实验管理、模型服务、监控告警、CI/CD。每个模块都有明确的接口和交付物,这样既能并行工作,又不会互相干扰。


回头看整个ai-engineering-from-scratch项目,最大的收获其实不是学会了某个具体工具,而是建立了一套完整的思考框架:怎么做数据、怎么训练、怎么部署、怎么监控、怎么迭代。这套框架是通用的,不会因为框架版本更新而过时。

最后分享一个小技巧:如果你也准备从零开始,不要一上来就瞄准大模型或者复杂的推荐系统,就找一个简单的分类任务,把整条链路跑通。这条链路本身才是最有价值的东西。链路通了,后面换成任何更复杂的模型、更大的数据量,都只是在某些节点上增加复杂度而已,你的主线不会断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询