☰
AI工程从零到上线:数据、训练、部署与监控全链路实战解析
2026/10/4 1:21:41 网站建设 项目流程

这两年"AI engineering"这个词被频繁提起,但我发现很多人对它的理解其实是"调用一下大模型API,prompt写得好一点"。真正意义上的AI工程,是把数据、模型、训练、评估、部署、监控这条链路完整地握在自己手里,而不是当一个接口搬运工。

我在业余时间做了一个叫ai-engineering-from-scratch的仓库项目,初衷很朴素:不依赖任何现成的推理服务,从零开始训练一个小模型,然后把它做成一个真正能上线使用的服务。这个项目走下来,踩了非常多坑,也把很多"文档里不会告诉你"的细节摸清了。这篇文章我就按照项目的实际推进顺序来写,把我认为最有价值的思考过程、代码结构、踩坑记录,以及如果重来一次我会怎么安排学习路线,都摊开聊一聊。

如果你是刚入行、想从"会跑教程"过渡到"能独立构建AI系统"的人,或者你已经在做AI应用开发但总感觉底层不扎实,这篇内容应该能帮你在动手前建立一张完整的地图。

1. 先别急着写代码:AI工程的边界和"从零"的真正含义

很多人一听到"from scratch"就会以为要从写神经网络反向传播开始,其实这是对AI工程最大的误解。工程的目标是稳定交付系统,不是复现论文。你不需要手写矩阵求导,但你需要搞清楚数据从哪里来、特征怎么处理、模型怎么训练、怎么评估、怎么部署、怎么监控,这一整条流水线才是AI工程的核心。

我给自己定的边界是这样的:模型层面可以基于开源预训练模型做微调,但推理、服务、部署、数据链路全部自己搭建,不调SaaS接口。为什么是这样一个边界?因为对于绝大多数实际业务场景,完全从随机初始化训练一个大模型既不经济也没必要,但如果你整个流程都是黑盒,出了问题连排查方向都没有,这才是工程上最危险的事情。

1.1 三个容易混淆的层级:ML研究、AI工程、软件工程

我在项目早期一直没分清自己到底在做什么,导致时间浪费了不少。复盘之后我把工作分为三个层次。

第一个层次是ML研究层,关注模型结构创新、Loss设计、训练技巧,核心指标是论文里的Benchmark分数。第二个层次是AI工程层,关注数据质量、训练效率、评估可靠、部署稳定,核心指标是系统的可复现性和服务可用性。第三个层次是软件工程层,关注接口设计、模块划分、测试覆盖、日志监控,核心指标是代码可维护性和团队协作效率。

ai-engineering-from-scratch这个项目的定位非常明确:主要聚焦第二层,同时用第三层的方法论来保证工程质量。第一层我只需要做到能理解,不需要做出创新。这个定位帮我砍掉了很多不必要的纠结,比如"要不要自己设计一个新模型结构"——答案是不要,先用成熟架构把链路跑通。

1.2 选择第一个项目的三个原则

很多新手想练AI工程,第一个想到的就是"复现MNIST手写识别"。但说实话,MNIST这种数据集太"干净"了,数据下载下来就是规整的图像,标签不会错,类别很均衡。真实业务里的数据永远是脏的、偏的、动态变化的,而这些恰恰是工程中最耗时、最考验功力的部分。

我建议按照三个原则来找第一个项目:数据不能太干净,最好有一定的清洗工作量;任务结果要容易评估,不要那种主观性极强的任务;训练成本要控制在单卡几小时以内,让迭代速度快起来。我最终选择的是中文情感二分类,数据用标注平台导出的历史记录。这个场景很合适:文本数据噪声大、标签有倾向性、训练快、评估也直观。

2. 最小可用闭环:从数据预处理到第一个模型训练跑通

项目启动后,我做的第一件事不是写训练脚本,而是先把数据链路完整梳理一遍。我给自己定的目标是:用一周时间跑通一个"数据集进、模型出"的完整闭环,哪怕模型效果很烂,也要保证每个环节是通的。

2.1 数据管线的第一版:不要用Pandas一把梭

这里有个非常常见的误区:拿到数据之后直接写一个Jupyter Notebook,用Pandas做清洗、过滤、合并,一气呵成。Notebook在处理小样本探索时很方便,但一旦数据量上来、清洗规则变复杂,这种方式的弊端就暴露出来了:步骤不可复用、中间结果不可追溯、切到生产环境全部要重写。

我的做法是建立了一个data_pipeline/目录,每个阶段单独成脚本,用明确的输入输出衔接:

data_pipeline/ ├── 01_fetch_raw.py # 从数据库/文件抽取原始数据 ├── 02_clean_raw.py # 去掉HTML标签、URL、无效字符 ├── 03_build_labels.py # 规则+人工复核打标签 ├── 04_split_dataset.py # 按时间切分训练/验证/测试集 └── 05_make_features.py # 分词、构建词表/编码

每个脚本只做一件事,输出统一存成parquet格式。为什么要用parquet而不是csv?因为列式存储体积更小,而且自带类型信息,不会出现CSV那种"所有字段读进来都变成字符串"的恶心事。

2.2 数据分割里藏着的魔鬼:时间泄漏

拆分数据集是我前期踩过最深的一个坑,值得单独拿出来说。一开始我用train_test_split(random_state=42)随机切分,训练完发现模型效果非常好,准确率接近95%。但我高兴了没两天就意识到不对:这是时间序列数据,同一条用户会话被打散到了训练集和测试集,模型其实"见过"答案了。

正确的做法是严格按时间切分:比如前80%的时间段作为训练集,后20%作为测试集。这样模拟的才是线上真实场景——模型永远只能看到过去的数据来预测未来。改完之后准确率一下子掉到了87%左右,但这个87%才是真实可信的。AI工程里最忌讳自我欺骗,一个虚高的离线指标比一个诚实但偏低的指标危险得多。

2.3 训练脚本的结构:照着这个骨架改就行

我踩完了数据坑,开始写训练脚本。下面这个骨架是我反复迭代之后定下来的,虽然不是最精简的版本,但每一个组件都有它存在的理由:

# train.py 核心结构 import torch from torch.utils.data import Dataset, DataLoader from transformers import AutoTokenizer, AutoModelForSequenceClassification from sklearn.metrics import classification_report import wandb # 1. 固定所有随机种子,保证可复现 def set_seed(seed: int = 42) -> None: torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) import numpy as np np.random.seed(seed) # 2. Dataset类:职责单一,只负责“取样本” class ReviewDataset(Dataset): def __init__(self, df, tokenizer, max_len=128): self.texts = df["text"].tolist() self.labels = df["label"].tolist() self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.labels) def __getitem__(self, idx): enc = self.tokenizer( self.texts[idx], max_length=self.max_len, truncation=True, padding="max_length", return_tensors="pt", ) return { "input_ids": enc["input_ids"].squeeze(0), "attention_mask": enc["attention_mask"].squeeze(0), "labels": torch.tensor(self.labels[idx], dtype=torch.long), } # 3. 训练循环:每个epoch记录loss和评估指标 def train_one_epoch(model, dataloader, optimizer, device): model.train() total_loss = 0 for batch in dataloader: batch = {k: v.to(device) for k, v in batch.items()} outputs = model(**batch) loss = outputs.loss loss.backward() optimizer.step() optimizer.zero_grad() total_loss += loss.item() return total_loss / len(dataloader)

这个骨架里我想强调三件事。

第一,Dataset类里不要写任何数据清洗逻辑,清洗是data_pipeline/中已完成的事,不要在训练时再过滤一遍。第二,每一个epoch结束必须打印或上报验证集指标,而不是只看训练loss,否则你无法判断模型是在学习还是在死记硬背。第三,所有超参数一定要抽成argparse或配置文件。我第一版图省事把学习率写死在代码里,结果后面每次调参都要改代码,改完还忘了改了什么。

2.4 评估不是准确率一个指标

如果你的项目也是分类任务,建议从第一次训练起就直接打印完整的、分类报告和混淆矩阵,而不是只看一个准确率。尤其是情感分析这类任务,类别往往不平衡。比如我的数据里正面样本占62%,那一个"永远预测正面"的蠢模型准确率也有62%,你光看准确率根本发现不了模型已经废了。

更好的做法是定义三个指标:precision、recall、f1,并且重点关注少数类的F1。我额外做了一个很土但很实用的操作——把错分样本的原文打印出来,人工看20条。这个习惯帮我发现了两个严重问题:一是数据里有大量"讽刺语气"的句子标签标反了,二是清洗正则误删了句子的否定词。这两件事任何指标都发现不了,只有人工看错例才能发现。

3. 工程化逃不掉的那几件事:实验管理、版本化与回归验证

模型在Notebook里跑通只是开始,真正让项目变得"工程化"的,是后面这三件事。它们不像训练那么有成就感,但不做的话,项目一旦复杂起来就会彻底失控。

3.1 实验管理:记录一切,包括失败的实验

我写训练脚本时同步接入了实验跟踪工具。当时在MLflow等工具里考虑。我的诉求很简单:每次训练的代码版本、超参数、评估指标、模型权重位置,都要能一键查到。因为如果不记录,你会发现三天前那个效果不错的模型再也复现不出来了。

跟踪项具体内容失败教训
代码版本Git commit hash第一次运行没记录,导致好模型无法重建
超参数learning_rate, batch_size, max_len, seed多个seed取平均才是真实效果
数据版本训练/验证/测试集文件hash数据更新后没有版本标记,新旧结果无法对比
指标loss、acc、F1、每类precision/recall只看acc掩盖了少数类崩塌

这里给一个非常实用的建议:如果项目比较小、不想引入重型工具,那就自己写一个experiments.csv,每次训练一行记录,至少把 commit hash、数据版本、超参数、指标这四列信息填完整。关键不在用什么工具,而在于"可追溯"这个习惯。

3.2 让实验可复现的几个关键点

可复现是AI工程里最容易崩盘的一环。我看过太多人抱怨"昨天跑的模型今天复现不了了",绝大多数情况下是下面这几个点没守住。

随机种子必须固定。训练代码、数据加载器、模型初始化都要设置种子。我更进一步,在set_seed里连DataLoader的worker_init_fn也设置了种子,因为多进程打乱数据也会引入随机性。然后是依赖版本锁定。transformers一个小版本升级,模型结果就可能变。我的做法是项目根目录放了requirements.txt,里面精确到小版本号,并且每次跑实验都记录pip freeze到日志目录。第三个点很反直觉,就是固定数据版本。数据是会被"更新"的,今天补了几条标注、明天删掉几条噪声,如果数据本身没有版本号,一切实验对比都失去了意义。我用的是一个非常轻量级的方案——data_pipeline/里每个产出文件同时生成一个md5校验文件,训练脚本启动时校验当前数据hash是否等于实验计划里的hash,不一致直接报错。

3.3 自动回归验证:给模型上个"保险"

模型训练完成后,我加了一个看似多余但极其有用的环节:若干个最小化的回归测试。它不需要多复杂,但要能抓住那些"模型看似正常实则退化"的情况。我用的是pytest,测试用例只有十几个,但覆盖很关键的行为。比如构造几个明显含否定词的样本,模型必须判断为负面;互换两个样本的标签顺序,推理结果不允许变化;同一输入重复推理10次,输出必须完全一致——这个能抓出训练/推理阶段随机性未对齐的问题。

这个回归测试还有一个非常重要的场景:每次换新模型版本之前,先在老数据上跑一遍测试集,看F1有没有下降超过1个百分点。它能挡住很多"看着换了新数据集训练后效果提升,其实是因为测试集也换代了"的假象。

4. 从训练机到服务器:部署与推理优化绕不开的三个大坑

模型在训练机上"能跑"和线上服务器"稳定服务"完全是两码事。这个阶段我花的时间比训练还多,踩的坑也非常有代表性。

4.1 模型导出与量化:不只是model.save()那么简单

PyTorch训练完的模型,保存方式有很多种,但部署场景下请直接选择TorchScript,这是可落地的首选方案。刚开始我直接保存了完整的state_dict和整个模型类,结果放到服务器上发现依赖的环境版本不一致,加载直接报错。后来我改用torch.jit.trace导出:

# 导出为TorchScript model.eval() example_input = { "input_ids": torch.zeros(1, 128, dtype=torch.long), "attention_mask": torch.ones(1, 128, dtype=torch.long), } traced_model = torch.jit.trace(model, example_input) traced_model.save("model/traced_model.pt")

这样做的好处是,模型结构被固化成了计算图,部署时只需要一个兼容的libtorch运行环境,不再需要训练代码的完整依赖。如果对模型体积有要求,还可以在导出后做动态量化。对于情感分类这种对精度损失不太敏感的模型非常划算。但要注意,量化后一定要在回归测试集上跑一遍,不能只看体积变小就上线。

4.2 推理延迟瓶颈不在模型,在"输入处理"

上线之后我发现单次推理的P99延迟达到150毫秒,但模型本身前向计算只要20毫秒。问题出在哪?我的推理服务每次请求都在实时调用中文分词器,而这个分词器加载了一份很大的词典到内存,每次初始化一下就要几十毫秒。

优化思路很朴素:把所有能提前初始化的对象,全部提到服务启动阶段完成。分词器、模型、热词缓存都在进程启动时加载好,请求进来只做查表和矩阵运算。改完之后P99延迟降到了45毫秒。这个现象在AI服务里非常普遍——大家总觉得延迟高是模型不够快,其实大部分耗时在数据处理、序列化、网络IO这些"看起来不起眼"的地方。排查延迟先Profile,不要凭感觉优化。

4.3 并发与吞吐:单次快不如批量稳

在线服务的另一个常见痛点是并发一上来,延迟就开始剧烈抖动。我最初给推理接口加了同步锁,一次只处理一个请求,逻辑上没有问题,但QPS稍高就会积压。后来我改成了动态批处理模式:

# 伪代码:动态batch核心逻辑 # 请求进入队列,累积最多32条或等待20ms,然后一次性推理 while True: batch = [queue.get()] while len(batch) < 32 and time.time() < start + 0.02: try: batch.append(queue.get_nowait()) except queue.Empty: break results = model(batch) for result, fut in zip(results, futures): fut.set_result(result)

GPU或者CPU推理都有一个特点:批量处理多个样本的单条平均耗时远低于逐条处理。动态批处理就是把这个特性最大化利用。这个方案在实际压测里把吞吐提升了将近四倍,而单条延迟只增加了一点点。如果你的场景也是高并发低延迟,这个思路非常值得尝试。

4.4 监控与告警:不只是看进程活没活

部署之后我一度以为只要服务进程不挂就万事大吉了。直到某天线上反馈说结果变得很奇怪,我去看监控面板,CPU、内存、QPS全部正常,模型服务也没重启过,完全看不出问题。排查了很久才发现,是因为上游数据源的文本格式发生了变化,大量输入都变成了无意义的字符串,模型强行给这些样本做推理,输出自然乱了套。

从那之后,我在推理服务里加了一层非常关键的数据质量监控:统计每条输入的文本长度分布、词汇覆盖率、预测概率的均值方差,把这些指标定期发送到监控系统。当预测分布和历史基线发生明显偏移时,告警就会触发。这个机制相当于给模型装了一个"体检仪",数据漂移、模型退化都能更早暴露。仅仅监控进程存活、机器CPU这些基础设施指标,远远不够。

4.5 成本意识:先估算,再动手

作为一个自己承担算力成本的个人项目,我特别能体会为什么企业会关心推理成本。训练阶段用一张消费级显卡跑两小时也就十几块钱电费,但线上服务如果长期挂一个112M参数的模型,每个月成本就很可观了。所以我在项目收尾阶段做了一轮优化:模型从原来的bert-base切换成了更小的蒸馏版本,精度只掉了不到两个点,但体积和推理耗时都降了很多,线上成本直接砍掉一半以上。我的建议是,先想清楚业务对精度的最低要求是多少,再选模型。很多业务场景根本不需要最大最聪明的模型,性价比往往比SOTA分数更重要。

5. 复盘:如果我重新开始,会怎样安排学习与实践顺序

做完这个项目回头再看,很多弯路其实完全可以避免。如果让我重新走一遍ai-engineering-from-scratch,我会用下面这个顺序来安排,这里也一并分享给准备入坑的朋友。

5.1 我在项目里犯过的几个"低级但致命"的错误

第一个错误是数据整理阶段太依赖Notebook,导致清洗逻辑完全没有被版本化。某次我手动执行了某个单元格,直接覆盖了原始数据文件,后面整个项目的基线都乱了,最后不得不花了两天重新整理数据。如果你也在做数据处理,请务必把每一步都写成脚本,并且做好输入输出文件的备份,不要给"手动操作"留空间。

第二个错误是训练脚本没有从一开始就接入实验管理和固定随机种子。导致我第一次跑出还不错的结果之后,怎么都复现不了,反复怀疑代码有问题,消耗了大量不该消耗的精力。请记住,实验管理不是后期优化,而是项目第一天就要做的事。

第三个错误是上线后没有及时的监控数据漂移,直到用户反馈质量下降才被动去查。这提醒我,一个AI系统上线不等于结束,数据分布会变、业务定义会变,你需要一套持续监控的机制,让我真正意识到"模型上线只是开始"这句话的含义。

5.2 给零基础入坑者的一份精简路线图

如果你现在完全零基础,但目标是成为一个合格的AI工程师,我建议不要一上来就狂背深度学习理论。你先用两周时间跑通这个最小闭环:用pandas处理一份真实数据,用scikit-learn训练一个简单的逻辑回归模型,用Flask包一个HTTP接口,再写一个评估脚本来衡量它的效果。完成这一步,你就理解了AI应用的最小骨架长什么样。

接下来再往前推。先了解基础的Python、SQL、Linux命令,然后学习机器学习的基础概念,不需要深挖数学推导,但要理解偏差方差、过拟合、交叉验证这些核心思想。之后再碰深度学习和具体的模型架构,最后才是工程化工具:Docker、CI、监控、实验管理。这个顺序的核心思路是"先跑通,再深入",和我在ai-engineering-from-scratch里实际走完的路径一致,但省掉了那些因为认知不足而绕的大远路。

5.3 一个实用的学习心态

最后想聊聊学习心态。AI工程里有一个残酷的事实:你学到的知识会以极快的速度过时。我这次项目里用的模型版本和工具链,可能过半年再看就不是最佳实践了。所以比起背住某个具体工具,更重要的是建立一套属于自己的学习构造:每接触一个新组件,先问三个问题——它解决什么问题,它适合什么场景,它的局限是什么。把这三个问题搞清楚了,工具本身怎么用,看文档就足够。这个习惯帮我避免了最典型的无效学习:看教程的时候都懂,合上教程就无从下手。

如果让我用一句话总结这个项目的收获,我会说:AI工程的核心不是模型,是你围绕模型建立的一整套可控制、可观测、可持续迭代的系统。从零开始走一遍,你获得的不是某个工具的使用熟练度,而是对整个系统每一个环节的掌控感。这种感觉,才是AI工程师真正值钱的地方。

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

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

立即咨询