恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
从零搭建AI工程链路:数据、训练、推理与监控全解析
首页
资讯中心
/
从零搭建AI工程链路:数据、训练、推理与监控全解析
从零搭建AI工程链路:数据、训练、推理与监控全解析
发布时间:2026/10/1 12:08:17
开头为什么我会回头重造AI轮子最近手头一个项目搞得我有点焦虑模型训练出来精度看着还行可是一旦要上线各种问题全冒出来——数据管道偶尔断流、推理接口半夜超时、显存莫名其妙涨了又掉、日志打了一堆根本没法看。说白了我发现自己对“AI工程”这四个字的理解还停留在“能跑通notebook”的层面。于是我给自己定了一个硬性任务把整个AI工程链路从数据清洗、模型训练、推理优化到服务部署监控全部从零到底亲手搭一遍。这个项目我命名为ai-engineering-from-scratch——不是用现成的MLOps平台一键拉起也不是套一个大而全的框架交差而是每一层都自己动手实现、自己写清楚每一步“为什么这样做”。今天这篇就是我这个“重造轮子”项目的完整复盘想看结论的直接拉到各部分小节标题想跟着走的建议从头读因为我踩过的坑你大概率也会踩。1. 为什么非要从零开始搞AI工程1.1 只调API和“能写模型”之间差着一条完整的工程管线很多朋友一开始接触AI路径基本都是这样装个深度学习框架跑一遍官方MNIST示例再把预训练模型拿来微调一下跑通几个demo就觉得“我会AI了”。但等到真实场景里数据分布一变、流量一起来、推理延迟动不动就要P99小于100毫秒的时候百分之百翻车。“from-scratch”这个思路本意不是让你重复发明轮子而是让你把轮子拆开看一次。我自己列了个检查清单发现至少有五件事是以前根本不会主动去管的数据从哪来、怎么保证每一次训练和评估用到的数据切分逻辑一致训练脚本可不可以复现换一台机器结果是否还一样模型推理时真正耗时在哪一步是算子慢、显存搬运慢还是接口序列化慢一套监控体系能否24小时盯住线上模型的预测分布漂移出了问题能不能快速定位是数据问题、模型问题还是基础设施问题如果这五条你答不上来那不管模型涨了几个点都算不上工程化。1.2 从零搭建给我带来的三点认知刷新第一AI工程的核心不是“模型”而是“数据流动的稳定性”。以前我总觉得调参炼丹最酷其实真实项目里最花时间的恰恰是清洗脏数据、对齐特征口径、保证线上和线下计算逻辑一致这些看不见的活。你模型精度再高上线时特征少传一个字段预测结果直接崩给你看。第二工程上的每一层都必须用“可观测”的方式埋点。说白了就是每一步都留日志、留版本、留样本快照。传统软件开发有print调试大法AI工程里更关键的是把每个阶段的输入输出都记录下来否则出了诡异问题根本不知道在哪一环爆的。第三性能瓶颈很多时候不在GPU算力而在数据加载和通信。我一个简单的文本分类模型训练时GPU利用率只有不到40%后来发现是我的数据管线里反复做正则和分词CPU忙成狗GPU等着吃数据。这个坑后面再细聊。用生活类比传统软件开发像是盖楼图纸清晰、材料标准AI工程更像是开饭店——菜谱模型很重要但买菜数据、洗菜切菜预处理、后厨出餐推理服务、食客反馈监控哪个环节断了饭店都得关门。2. 从零构建AI工程的完整技术栈拆解2.1 数据层特征逻辑、清洗规范与切分一致性数据在整个AI工程里的地位怎么强调都不过分。我在这个项目里设计了一套数据管线的骨架核心原则只有一条所有对数据做的操作都必须是可复现、可版本化的。实操上我建议把数据管线拆成三步原始数据落盘从任何数据源拿到的原始数据先原样存档不打任何标记不做任何修改。这是为了后面排查问题时有据可查。清洗与特征工程统一处理缺失值、去重、格式归一化、文本清洗等。这里有个关键点清洗规则必须用一个独立模块管理不能散落在训练脚本里。训练/验证/测试切分切分逻辑必须固定通常做法是给每个样本生成一个哈希ID然后按哈希值范围划分数据集。这样每次执行同样的代码拿到的划分始终一致不会出现训练集和测试集样本串味。2.2 模型层从手写参数更新到理解框架设计逻辑模型这一层我的做法比较“笨”先不用成熟框架的自动求导用NumPy手写了一个两层的MLP分类器包括前向传播、反向传播、梯度下降更新。这一步看起来绕远实际上收益巨大。当你手动写过dW np.dot(X.T, delta)之后你才能真正理解框架里的loss.backward()在帮你干什么。举个具体例子我们做分类任务时经常看到交叉熵损失函数但很多同学不知道它求导之后得到的梯度恰好就是模型预测概率与真实one-hot编码的差。这个直觉在手写代码时特别清晰而且调模型时能少走很多弯路。后来切回PyTorch发现只是把底层的自动求导交给框架核心的训练循环逻辑和手写版本几乎一一对应。2.3 推理与服务层模型只有跑出延迟指标才算数模型训练完不是终点推理性能才是工程落地的生死线。训练时你可以batch大一点因为反正都在GPU里跑推理时却要面对单条或小批量请求延迟和吞吐必须同时好看。我在这个项目里做了一个实验把训练好的模型分别用三种方式跑推理统计各自的延迟表现。方式一原始PyTorch模型动态图推理灵活但开销大方式二转为ONNX格式用ONNX Runtime推理静态图优化后延迟明显下降方式三转为TensorRT仅限N卡显存和延迟进一步优化表格看起来像这样推理方式单条延迟ms显存占用MB灵活度备注PyTorch动态图38512高调试方便生产环境不推荐直接用ONNX Runtime9128中兼容性好CPU/GPU均可TensorRT486低极致性能但模型层操作受限实测下来ONNX Runtime的性价比最高。TensorRT性能虽然猛但图优化比较挑算子模型稍微复杂一点就可能要手动处理插件工程成本并不低。2.4 部署与监控线上模型需要“体检报告”部署层面我选择用FastAPI包一层推理服务每个请求进来先做预处理再进模型推理最后把结果和特征快照写进结构化日志。监控方面我重点盯三个指标服务健康度请求成功率、延迟P50/P95/P99、GPU显存水位。预测分布漂移每个小时统计预测类别的分布跟训练集的类别分布做对比一旦偏差超过阈值就告警。特征口径漂移线上接收到的特征均值和方差跟训练集做对比防止某一天上游数据源格式悄悄变了模型却被蒙在鼓里。有了这三份“体检报告”模型出了问题不会像无头苍蝇一样乱猜。这也是我认为AI工程和算法研究最大的区别——算法研究追求精度提升AI工程追求稳定和可维护性。3. 实操过程与核心环节实现3.1 设计一个最小可用但从零手写的端到端项目我给自己设计了一个具体到能跑的任务做一个文本的情感三分类正面/负面/中立但不用现成的预训练模型整个链条都自己搭。项目结构我按工程化思路分成五个目录ai-engineering-from-scratch/ ├── data/ # 原始数据 处理脚本 ├── features/ # 特征工程代码 ├── models/ # 手写模型 训练脚本 ├── inference/ # 推理服务代码 └── monitoring/ # 监控与日志脚本数据我用的是公开的影评文本做了一些简单的清洗去掉HTML标签、统一小写、过滤超长句。特征层面我选择构建一个词频向量维度5000只保留出现频率最高的5000个词然后用TF-IDF做加权。这一步是纯手工特征工程完全不用深度学习。3.2 手写训练循环的完整代码解析模型结构很简单两层都是Linear层。关键在于参数更新的每一步都要自己用NumPy实现import numpy as np class MiniMLP: def __init__(self, input_dim, hidden_dim, output_dim, lr0.01): # Kaiming初始化避免梯度消失或爆炸 self.W1 np.random.randn(input_dim, hidden_dim) * np.sqrt(2.0 / input_dim) self.b1 np.zeros((1, hidden_dim)) self.W2 np.random.randn(hidden_dim, output_dim) * np.sqrt(2.0 / hidden_dim) self.b2 np.zeros((1, output_dim)) self.lr lr def softmax(self, z): e_z np.exp(z - np.max(z, axis1, keepdimsTrue)) return e_z / np.sum(e_z, axis1, keepdimsTrue) def forward(self, X): self.z1 np.dot(X, self.W1) self.b1 self.a1 np.maximum(self.z1, 0) # ReLU激活 self.z2 np.dot(self.a1, self.W2) self.b2 self.a2 self.softmax(self.z2) return self.a2 def backward(self, X, y, probs): m X.shape[0] delta2 probs - y # 交叉熵的梯度就是预测与真实值的差 dW2 np.dot(self.a1.T, delta2) / m db2 np.sum(delta2, axis0, keepdimsTrue) / m delta1 np.dot(delta2, self.W2.T) delta1[self.z1 0] 0 # ReLU反向传播 dW1 np.dot(X.T, delta1) / m db1 np.sum(delta1, axis0, keepdimsTrue) / m return dW1, db1, dW2, db2 def step(self, dW1, db1, dW2, db2): self.W1 - self.lr * dW1 self.b1 - self.lr * db1 self.W2 - self.lr * dW2 self.b2 - self.lr * db2 def train(self, X, y, epochs, batch_size32): n X.shape[0] for epoch in range(epochs): indices np.random.permutation(n) total_loss 0.0 for i in range(0, n, batch_size): batch_idx indices[i:i batch_size] X_batch, y_batch X[batch_idx], y[batch_idx] probs self.forward(X_batch) loss -np.mean(np.sum(y_batch * np.log(probs 1e-8), axis1)) dW1, db1, dW2, db2 self.backward(X_batch, y_batch, probs) self.step(dW1, db1, dW2, db2) total_loss loss print(fepoch {epoch 1}/{epochs}, loss: {total_loss / (n // batch_size):.4f})这里有几个点值得展开说为什么用Kaiming初始化因为ReLU激活函数会把负数直接清零如果用标准正态初始化方差不当会导致深层信号逐层衰减。Kaiming初始化让每层输出的方差保持在合理范围内。为什么梯度是 probs - y这是交叉熵损失和Softmax组合之后的漂亮结果。手写一遍之后你会发现深度学习框架里的backward也没那么神秘。batch_size为什么选32太小则梯度噪声大训练不稳太大则单步更新太慢。32是工程上的经验折中值后续可以按显存和收敛速度微调。3.3 训练结束后如何用ONNX把模型“搬”到推理环境训练完模型下一步是导出和部署。我建议从一开始就养成一个习惯训练脚本只负责产出模型权重文件和一个独立导出的脚本导出格式统一用ONNX。import torch import torch.nn as nn # 这里演示把NumPy权重导入PyTorch模型再导出ONNX的逻辑 class TorchMLP(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super().__init__() self.fc1 nn.Linear(input_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, output_dim) def forward(self, x): x torch.relu(self.fc1(x)) return self.fc2(x) dummy_input torch.randn(1, 5000) model TorchMLP(5000, 128, 3) torch.onnx.export(model, dummy_input, text_classifier.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})注意这里做了两个关键设置dynamic_axes声明第0维是动态batch这样线上服务可以自由控制batch大小。导出时用的是dummy input所以特征维度的顺序必须和训练时完全一致否则模型输出就是垃圾。导出完成后用ONNX Runtime加载推理整体延迟从38毫秒降到9毫秒。主要收益来自图优化和算子融合比如把某些逐点操作合并成一个kernel减少显存读写。3.4 用FastAPI包一层推理服务顺手解决动态batch推理服务我写得比较朴素核心逻辑就三步from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort import numpy as np app FastAPI() session ort.InferenceSession(text_classifier.onnx) class TextItem(BaseModel): text: str def preprocess(text: str) - np.ndarray: # 这里调用之前的特征工程代码把文本转成5000维向量 return vectorize([text]) app.post(/predict) def predict(item: TextItem): vec preprocess(item.text) result session.run(None, {input: vec.astype(np.float32)})[0] label int(np.argmax(result, axis1)[0]) conf float(np.max(result, axis1)[0]) return {label: label, confidence: conf}真实项目里我还会在服务层加上缓存、限流、请求ID追踪等能力但在干活之前先跑通这条最朴素的链路比什么都重要。另一个工程化细节是不要直接在线做分词和特征向量化太慢了。正确做法是把预处理模块也独立编译成静态函数或者用缓存池预计算热门样本的特征。3.5 日志与监控给推理服务装上仪表盘服务跑起来之后我每收到一个请求都会记录一条结构化日志{request_id: xxx, label: 1, confidence: 0.92, feature_dist_mean: 0.003, latency_ms: 9.8}这些日志攒到一定量就可以拿去做分布监控。我用一个简单的脚本每小时跑一次把最近一小时的预测类别分布和训练集基线对比。如果“中立”类占比突然从20%涨到60%大概率是上游输入文本发生了数据漂移这个告警往往能先于用户投诉发现问题。我还额外记录每个小时的confidence均值。正常情况下模型对大部分样本都比较笃定如果某天confidence均值明显下滑说明模型遇到了分布外的数据这时候就该考虑补充训练样本或者重训了。4. 常见问题与排查技巧实录4.1 训练损失下降但验证集指标纹丝不动这个现象十有八九是过拟合。我在训练时加了L2正则把learning rate降到0.002同时加early stopping也就是验证集指标连续三个epoch不涨就停掉。工程上还有一个隐形坑训练和验证的样本切分必须完全隔离我见过有人洗数据时把全量数据先归一化再做切分这等于把验证集的信息泄漏进了训练过程导致验证指标虚高。4.2 GPU利用率低到爆炸瓶颈却在CPU刚才说过我的GPU利用率只有不到40%。用profile工具查了一下数据加载和预处理占了一大半时间。解决手段有三个把预处理尽量在训练之前批量完成直接存成npy文件用多线程DataLoader异步预取数据图像类任务可以把解码和裁剪操作放到GPU上做文本类则尽量减少正则的复杂度4.3 推理接口偶尔会超时根因不是模型而是Python的GIL有一次线上P99延迟突然飙到800毫秒查了半天发现是推理服务里混了一段纯Python的文本清洗逻辑大文本进来的时候占死了GIL。后来我把清洗逻辑用正则库重写了一遍某些场景下还会用多进程隔离CPU密集和GPU推理任务延迟稳定下来。这里我的经验是推理服务里尽量别写复杂的Python循环能向量化就向量化能不处理就不处理。4.4 模型上线后准确率下降原来不是模型的问题模型上线第一周准确率掉了5个点一开始以为概念漂移后来一看是上游数据源增加了一个新的字段特征拼接逻辑变了。排查过程全靠训练和线上特征的分布对比否则根本发现不了这种隐蔽问题。这个案例给我的教训是监控不止要看预测结果还要盯输入特征的口径一致性。下面把这些高频问题整理成速查表方便直接排查现象可能原因排查思路解决建议训练loss下降但验证指标不动过拟合 / 数据泄漏检查训练验证切分、观察训练验证曲线间距增加正则、早停、确保切分隔离GPU利用率低数据加载慢 / CPU瓶颈profile数据管线各环节耗时预计算特征、异步加载、减少复杂预处理推理延迟高动态图开销 / 模型冗余对比ONNX/TensorRT实测延迟导出ONNX并启动图优化P99偶发超时Python GIL / 伴随资源竞争日志排查耗时分布压测并发场景清洗逻辑向量化、多进程隔离上线后准确率下降特征口径漂移 / 上游数据变更对比线上特征分布与训练基线加入特征分布告警、重构特征版本管理5. 一些我现在特别相信的工程体会跑完这个从零项目我心里最强烈的感受是AI工程不是模型训练的延伸它是一条独立的生产链路。从数据入口到监控告警每一环都要以“可复现、可追溯、可快速定位问题”为标准来设计。前阵子我帮一个朋友排查他的项目他的模型在测试集上Accuracy有0.93结果一上线被用户喷成筛子。我一看他线上推理的特征标准化参数用错了——训练时用的是全局均值方差线上却用了实时流式统计的均值方差。这个问题恰恰是我从零搭建时踩过的同一个坑。特征处理逻辑不一致模型再强也在吃哑巴亏。如果你也想试一遍这个项目我的建议是别贪大。先选一个自己能掌控的小任务把数据、训练、部署、监控四个环节都跑通你获得的工程手感远比把某个SOTA模型微调出来更值钱。后来我在这个手写版的基础上逐步替换成成熟的MLOps组件心里已经很清楚每一块组件在扮演什么角色、失效时会有什么症状这比一开始就用黑盒平台踏实得多。最后再分享一个小习惯从第一天就给你的每条日志打上request_id给你的每个模型权重打上版本号给你的每一份特征配置打上commit hash。这些事特别琐碎但是在凌晨两点线上出问题时能救命的正是它们。