ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零开始构建AI工程:手写模型到部署监控全链路实战

2026/9/28 8:51:14 拓冰建站 浏览量
从零开始构建AI工程:手写模型到部署监控全链路实战 1. 为什么“从零开始”反而是AI工程最该走的路线我见过太多人拿着“AI工程师”的title上线一调接口就露馅——模型不会选、数据不会洗、评估不会做、挂了不会查。市面上到处是七天速成GPT应用开发教你怎么调OpenAI的接口、把聊天窗口糊一个壳然后用完即走体系全无。而“ai-engineering-from-scratch”这种思路反而是现在最稀缺、最该认真对待的一条路不借助任何封装好的框架从神经网络最底层的计算图开始把数据、模型、训练、评估、部署、监控这条完整链路亲手搭一遍。我最早听到这个项目名时第一反应是“何必呢”毕竟现在有现成的MLOps平台、有AutoML、有各家的零代码训练平台按几个按钮模型就跑起来了为什么还要从零写后来踩的坑多了才明白所谓“AI工程”根本不等于“能把模型跑起来”而是“模型出了问题你知道去哪里找原因”。而这恰恰是无法靠按钮学会的。这个项目适合什么样的人我建议三类人重点看第一类是算法工程师天天用TensorFlow/PyTorch但被问到“梯度消失为什么发生”就开始含糊第二类是后端/全栈工程师公司突然让你搞AI落地你手里除了会调API没有别的底牌第三类是准备走AI方向的学生学校教的理论和工业落地之间隔着一整条工程链这个项目就是中间的桥梁。这篇文章我把自己梳理的完整路线、实操中的代码细节、踩过的坑和排查经验全部写出来。不写那些花里胡哨的概念只讲动手怎么搭、每一步为什么这么走。2. 从零构建AI工程能力的核心设计思路2.1 先明确AI工程和算法岗到底差在哪过去两年我面试过不少候选人简历上都写着“熟练使用PyTorch”结果问两句就发现所谓“熟练”就是会调用model.fit()一旦让写一个DataLoader的采样逻辑、或者解释一下学习率调度器内部做了什么就答不上来。这不是个例而是行业通病工具越来越抽象大家对底层机制的感知越来越弱。ai-engineering-from-scratch这个项目思路最聪明的地方是刻意把“工程实现”和“算法原理”放在同一条横向链路里对齐。你不会只学“怎么用PyTorch”而是从数学原理出发手写一个两层的反向传播验证梯度计算正确后再切换到框架实现。这样当你面对框架报错时底层发生了什么、哪里可能断链你跟没写过的人完全是两种状态。我个人的理解就是从零开始不是目的建立“可迁移的判断力”才是目的。你用PyTorch写了100次nn.Linear不代表你能判断一个业务场景该不该用线性模型但你手写过一次矩阵求导、理解了参数更新的全部过程之后这种判断力就有了。2.2 项目设计里的三阶递进把整个路线拆开我认为核心分三个阶段分别对应“能写”“能用”“能扛”三个层次第一阶模型实现层。不调用任何深度框架用NumPy手写一个简单的MLP多层感知机完成前向传播、反向传播、梯度检查。这一步干掉的是“黑盒恐惧”。你亲手写了dW np.dot(X.T, dz)之后再也不会觉得神经网络是什么玄学。第二阶框架实战层。这一层再做三件套用PyTorch实现一个图像分类模型、一个文本分类模型、一个时序预测模型覆盖CV/NLP/序列三类主业务形态。要做的事情包括自定义Dataset、构建DataLoader、实现训练循环、加入早停和模型检查点。比代码本身更重要的是理解训练曲线loss下降太慢、验证集震荡、过拟合提前到来每一类症状对应什么操作。第三阶工程封装层。这一层真正把AI当软件工程来做模型服务化用FastAPI封装推理接口、离线评估分类指标、回归指标、置信度分析、日志与监控记录推理时延、输入分布漂移、数据异常、以及模型版本管理与A/B测试。很多人学会了训练但压根没想过部署后模型会“悄悄变笨”这一阶段就是补上这块能力拼图。这三阶段加在一起恰好形成一条“从数学原理到生产环境”的完整链路。我自己带项目时一直强调每一层都不要急着往下跳前一层的产出物比如NumPy的MLP代码留着不要删后面做框架实现时反复对照价值极大。2.3 为什么说这条路能避开“只会调包”的死循环现在网上充斥着“大模型实战训练营”两周让你做出一个聊天机器人。这些课程不教你embedding层到底做了什么也不教attention里的QKV从哪来。结果就是模型换一个场景、换一个数据分布你立刻抓瞎。从零开始的项目相当于给了你一张“故障地图”。比如线上模型精度下降你会按这个顺序排查数据分布是否漂移预处理逻辑是否在离线/在线不一致训练/推理的随机种子是否固定模型版本是否被意外回滚这些能力的根基正是你对“整条链路”的熟悉程度而不仅仅是某一层框架的API熟练度。3. 核心细节拆解与实操要点3.1 环境准备与依赖管理到底怎么做才规范很多新手第一步就翻车。上来就在全局Python环境里pip install torch两个月后项目多了一堆奇奇怪怪的依赖冲突不得不重装系统。我强烈建议从第一天就使用虚拟环境加依赖锁定。我的标准操作是# 创建项目目录 mkdir ai-engineering-from-scratch cd ai-engineering-from-scratch # 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate # 安装核心依赖 pip install numpy pandas matplotlib scikit-learn pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install fastapi uvicorn pydantic # 导出依赖锁定文件 pip freeze requirements.txt这里有两个细节容易被忽视一是PyTorch的CPU版本和GPU版本安装源不同如果本机没有NVIDIA显卡千万不要直接pip install torch拉默认源否则装下来的版本会用不了CUDA后面白折腾半天二是requirements.txt要提交到Git里这是可复现性的第一步。我们组里考核项目第一件事就是看这个文件能不能在干净环境里直接复现不行的一票否决。3.2 手写MLP的完整代码与梯度检查我先展示一个最小可用的MLP实现。这里不用PyTorch只用NumPy目的是把神经网络的骨架露出来import numpy as np class MLP: def __init__(self, input_size, hidden_size, output_size, seed42): rng np.random.default_rng(seed) # 关于初始化均值为0、标准差为1/sqrt(fan_in)的随机初始化 # 不要用全0初始化否则所有神经元对称更新模型根本训不动 self.W1 rng.normal(0, 1/np.sqrt(input_size), (input_size, hidden_size)) self.b1 np.zeros(hidden_size) self.W2 rng.normal(0, 1/np.sqrt(hidden_size), (hidden_size, output_size)) self.b2 np.zeros(output_size) def sigmoid(self, x): # 数值稳定性对x大于0和小于0的情况分开处理防止exp溢出 return np.where(x 0, 1 / (1 np.exp(-x)), np.exp(x) / (1 np.exp(x))) def forward(self, X): self.z1 np.dot(X, self.W1) self.b1 self.a1 self.sigmoid(self.z1) self.z2 np.dot(self.a1, self.W2) self.b2 # 二分类输出层用sigmoid self.a2 self.sigmoid(self.z2) return self.a2 def backward(self, X, y, lr0.1): m X.shape[0] dz2 self.a2 - y # 交叉熵 sigmoid 的导数正好是预测-真实 dW2 np.dot(self.a1.T, dz2) / m db2 np.sum(dz2, axis0) / m dz1 np.dot(dz2, self.W2.T) * self.a1 * (1 - self.a1) dW1 np.dot(X.T, dz1) / m db1 np.sum(dz1, axis0) / m self.W2 - lr * dW2 self.b2 - lr * db2 self.W1 - lr * dW1 self.b1 - lr * db1写完之后千万别直接拿去训练先做“梯度检查”。这是整个环节里最容易被跳过的关键操作。原理是数值梯度逼近解析梯度def numerical_gradient(model, X, y, epsilon1e-5): # 对W1做数值梯度近似 params [(W1, model.W1), (b1, model.b1), (W2, model.W2), (b2, model.b2)] grads {} for name, param in params: grad np.zeros_like(param) it np.nditer(param, flags[multi_index]) while not it.finished: idx it.multi_index old_val param[idx] param[idx] old_val epsilon loss_plus compute_loss(model, X, y) param[idx] old_val - epsilon loss_minus compute_loss(model, X, y) param[idx] old_val grad[idx] (loss_plus - loss_minus) / (2 * epsilon) it.iternext() grads[name] grad return grads比较解析梯度和数值梯度之间的相对误差小于1e-4基本默认为正确。这一步能帮你提前发现反向传播里的公式错误、维度不匹配、以及“该转置的没转置”这类低级问题。很多人在这个环节就挂了——因为写出来的梯度数值差了两三个数量级。我的经验是梯度检查效率极低但价值极高相当于给你的反向传播做一次全身体检尤其在你从零手写的时候这是唯一能确定自己写对了的方法。3.3 切到PyTorch之后真正差别不是代码行数从NumPy切到PyTorch很多人以为这只是“换个库调用”大错特错。框架真正给你的不是“少写几行”而是三样东西自动微分、GPU加速、分布式能力。但代价是你必须理解它背后的图计算机制。一个最容易踩坑的点是Tensor的requires_grad和detach()。我见过无数人在训练循环里忘了对输入数据做.detach()导致计算图越积越大内存直接爆掉。正确姿势是在把NumPy数组转成Tensor时明确.float()在循环中把中间结果需要隔绝梯度的地方果断.detach()。举一个典型训练骨架import torch import torch.nn as nn import torch.optim as optim class SimpleNet(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes): super().__init__() self.embedding nn.Embedding(vocab_size, embed_dim, padding_idx0) self.rnn nn.LSTM(embed_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, num_classes) def forward(self, x): embedded self.embedding(x) output, (h_n, c_n) self.rnn(embedded) # 取最后一个时间步的隐状态 return self.fc(h_n[-1]) def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0 for batch_idx, (x_batch, y_batch) in enumerate(dataloader): x_batch x_batch.to(device) y_batch y_batch.to(device) optimizer.zero_grad() outputs model(x_batch) loss criterion(outputs, y_batch) loss.backward() # 梯度裁剪RNN/LSTM训练必加防止梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / max(len(dataloader), 1)这里面的padding_idx0就是文本处理里的经典坑不同长度的句子在batch里要补齐而补位符的位置在Embedding里如果不指定padding_idx模型会把padding当真词学习产生严重偏差。这些细节光看教程根本看不到都是跑废几个实验才记牢的。3.4 训练过程中的“玄学”其实全是工程问题很多人训练模型时明明代码看起来都对Loss就是不降。这时候你该查的不是模型结构而是一堆“工程细节”第一个是学习率。我见过最多的低级错误是学习率设成0.1甚至1.0。跨数量级的学习率几乎不可能把模型训出来。经验法则Adam优化器从3e-4起步SGD从1e-2起步。第二个是数据归一化。输入特征值域相差几十倍不处理梯度更新方向就会被大特征主导模型训练效率极差。第三个是标签是否均衡。正负样本比100:1的模型你不管它它学到的就是永远预测负样本准确率还高达99%看起来跟完美模型一样实际上毫无用处。我建议在训练早期做一个“喂一个batch”的调试只取8条数据塞进模型看Loss是否能正常下降。如果8条数据都训不动那问题大概率出在模型/数据的接口衔接上而不是优化器或超参数上。这一步能帮你把“调试范围”缩小80%。4. 实操过程与核心环节实现4.1 完整的数据处理流水线怎么搭在AI工程里“模型代码”永远是最后那20%的工程前面的80%都是在处理数据。我踩过最大的坑就是边写模型边改数据处理逻辑结果两边互相纠缠出了问题根本定位不到根因。我最终的解决方案是搭一条独立的数据流水线数据处理与模型训练解耦。核心流程是原始数据 - 清洗 - 特征化 - 切分 - 缓存。每一步产出的中间结果都落盘可以随意检查。以文本分类为例import pandas as pd from sklearn.model_selection import train_test_split from collections import Counter # 1. 原始数据读取 df pd.read_csv(raw_data.csv) # 2. 清洗去重、去空、去除异常长度 df df.drop_duplicates(subset[text]) df df.dropna(subset[text, label]) df df[df[text].str.len() 5] # 太短的文本信息量不足 # 3. 标签分布检查 print(标签分布:, Counter(df[label])) # 4. 分层切分保持训练/验证/测试的标签比例一致 train_df, temp_df train_test_split(df, test_size0.3, stratifydf[label], random_state42) val_df, test_df train_test_split(temp_df, test_size0.5, stratifytemp_df[label], random_state42) # 5. 缓存清洗结果 train_df.to_csv(train_clean.csv, indexFalse) val_df.to_csv(val_clean.csv, indexFalse) test_df.to_csv(test_clean.csv, indexFalse)这三个csv文件就是后续所有实验的唯一输入。模型代码不允许直接读raw_data.csv。为什么要这么设计因为当你调整模型超参数、特征表示、模型结构时数据必须保持一致否则你没法判断是模型变好了还是数据变了。再一个细节stratify参数很容易被忽略。分类问题里如果不做分层切分验证集和训练集的标签分布可能差很多模型在验证集上的表现就不可信。所谓“验证集Loss突然暴涨”有一半情况不是模型崩了而是数据切分没做好。4.2 模型服务化部署完整代码训练完成之后模型躺在磁盘上是没有价值的你得让它“接客”。我用FastAPI做的服务化封装代码就几十行但背后有不少工程考量import uvicorn import joblib import numpy as np from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI(titleModel Serving API) # 模型加载这里要用全局变量不能每次请求都加载 model joblib.load(model.joblib) label_map joblib.load(label_map.joblib) class PredictRequest(BaseModel): features: list[float] return_proba: bool False class PredictResponse(BaseModel): label: str proba: list[float] app.post(/predict, response_modelPredictResponse) def predict(req: PredictRequest): try: arr np.array(req.features).reshape(1, -1) proba model.predict_proba(arr)[0] pred_idx int(np.argmax(proba)) label label_map[pred_idx] # 置信度阈值低置信度请求直接标记方便后续review confidence float(np.max(proba)) if confidence 0.6: # 这里可以接一个告警逻辑日志里标记低置信度样本 pass if req.return_proba: return PredictResponse(labellabel, probaproba.tolist()) return PredictResponse(labellabel, proba[]) except Exception as e: raise HTTPException(status_code500, detailstr(e)) if __name__ __main__: uvicorn.run(app, host0.0.0.0, port8000)这个部署代码里有三个关键技术决策我展开说说为什么第一个是模型加载放全局。很多人第一次写服务会把模型加载写进请求函数里结果每个请求都加载一次模型接口时延秒级起步并发一高直接把机器打挂。模型加载必须只做一次。第二个是输入校验用Pydantic。这里的list[float]类型约束不是摆设如果线上有人往接口传字符串FastAPI会自动挡掉并返回400。没有这层校验模型推理函数接收到非法输入报错信息会让调用方一脸懵。第三个是低置信度标记。这是很多教程根本不会讲的线上策略不是所有预测结果都值得直接信任。置信度低于0.6的样本按概率很可能预测错我们在工程上要做的是把这些样本单独记录下来留给人工审核或后续补充训练数据。这一层逻辑一加你的系统立刻从“demo”变成“可上线的系统”。4.3 模型评估不做评估等于闭着眼睛上线一个模型训完了你到底怎么判断它行不行很多人只看accuracy这在绝大多数业务场景里都远远不够。我把评估拆成三层对应三种问题分类指标层准确率之外必须看Precision、Recall、F1。一个抓垃圾评论的系统如果误伤正常评论precision低损失的是真实用户如果漏掉垃圾评论recall低损失的是社区环境。具体到业务场景你需要在Precision和Recall之间做取舍F1是平衡点参考。分层评估层光看整体指标没用要看模型在不同子群体上的表现。一个情感分析模型在长文本上F1是0.85在短文本上只有0.6那问题就出在短文本处理上。评估时按文本长度、数据来源、时间等维度分开测才能找到模型的真正弱点。置信度校准层模型说它有0.9的置信度实际上预测对的概率真有0.9吗如果不一致就需要做校准。这个问题在线下业务中特别常被忽略结果就是模型的概率输出完全不可信下游决策系统跟着遭殃。我的评估代码里一定会包含这一部分from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score import matplotlib.pyplot as plt # 预测 y_true, y_pred, y_proba [], [], [] for batch in test_loader: outputs model(batch[input_ids]) proba torch.softmax(outputs, dim-1) _, pred torch.max(proba, dim-1) y_true.extend(batch[labels].tolist()) y_pred.extend(pred.tolist()) y_proba.extend(proba[:, 1].tolist()) # 综合评估报告 print(classification_report(y_true, y_pred)) # 混淆矩阵可以可视化 cm confusion_matrix(y_true, y_pred) # AUC评估排序能力 auc_score roc_auc_score(y_true, y_proba) print(fAUC: {auc_score:.4f})4.4 模型监测和告警体系模型上线不代表事情结束恰恰是运维的开始。模型在真实环境里会“变笨”这几乎是必然的只是时间问题。我在生产环境里做的监控包括三个维度基础性能监控每小时的请求量、平均时延、P99时延、错误率。这四个指标像人的体温血压一样重要。数据漂移监控记录线上请求输入的特征分布和训练时的特征分布做对比。最常用的指标是PSIPopulation Stability IndexPSI超过0.25说明分布变化明显需要警惕。比如一个电商推荐模型顾客消费习惯在节假日和平日完全不同不做漂移监控你根本不知道模型在节日期间的预测已经不可靠了。预测分布监控统计模型输出的类别分布是否发生异常偏移。一个垃圾邮件过滤模型平时垃圾邮件占比5%某天突然变成30%不管模型指标是否正常这个信号就必须触发告警。一个简单的监控记录代码长这样import json import datetime import redis r redis.Redis(hostlocalhost, port6379, db0) def log_prediction(features, pred_label, confidence): record { timestamp: datetime.datetime.now().isoformat(), pred_label: pred_label, confidence: confidence, features: features } r.lpush(prediction_logs, json.dumps(record)) # 保留最近10万条防止内存无限增长 r.ltrim(prediction_logs, 0, 99999)这个模块在每次预测时都记录输入和输出一方面支持离线回溯分析另一方面为数据漂移检测提供数据来源。很多团队忽略这个简单动作等到线上模型出了问题连“问题从什么时候开始”都不知道排查难度直接翻倍。5. 实操中最高频的坑与排查方法5.1 训练阶段常见问题速查我把从零开始做的过程中最容易踩的坑整理成一张表每一行都是真实的教训现象根本原因排查方向解决方案loss为NaN学习率过大打印每层梯度值降低学习率加梯度裁剪loss不下降数据没归一化检查特征取值范围做标准化使均值为0方差为1验证集准确率低于训练集很多过拟合观察训练曲线距离加正则化Dropout、L2增加数据量训练时间异常长DataLoader加载瓶颈看GPU利用率是否低增加num_workers用pin_memoryTrueGPU内存溢出计算图不断累积检查detach()使用正确切分计算图降低batch size模型效果时好时坏随机种子未固定检查random_state和手动种子固定环境变量、Python和库的随机种子这里面我想重点展开“loss为NaN”这个场景。新手最容易慌的情况就是这个。我排查的经验顺序是先打印每一层的梯度和权重看哪层先出现NaN通常这个位置就是“爆点”。如果很靠前接近输入层很可能是输入数据里有NaN值如果很靠后接近输出层大概率是学习率太大导致梯度爆炸。用这套排除法90%的NaN问题都能定位。5.2 部署阶段最容易翻车的三个大坑部署阶段的坑和训练阶段完全不在一个量级因为报错发生在用户面前修复是在压力之下进行的。我总结了三个翻车概率最高的点坑一训练/推理的预处理逻辑不一致。这个坑我称之为“AI工程第一杀手”。训练时你对文本做了全角转半角、小写化、去停用词部署上线时忘了加其中一步模型效果立刻打折。而且这个bug极其隐蔽线上不像训练环境没有可视化让你去对比你只会觉得“模型上线效果怎么差了”。解决办法只有一个把预处理逻辑做成一个独立函数训练和推理共用同一份代码禁止复制粘贴。复制粘贴就是埋雷。坑二模型文件管理与版本混乱。多人协作时A同事昨天训练的模型文件放在服务器某个路径今天B同事部署时用了另一个路径的旧模型线上跑了一周才发现是旧版本。我的解决方案是给每个模型训练产物加一个元信息记录包括训练时间、数据版本、超参数、指标结果并且把模型文件和元信息打包成一个版本。任何一次部署都必须先查看这个版本信息再上线。坑三并发请求下的线程安全问题。深度学习模型在GPU上推理时如果不加锁或使用正确的batch策略多线程并发会导致结果错乱甚至程序崩溃。我遇到过线上服务8个线程同时调GPU推理结果预测结果乱掉的情况。后来在推理函数里加了threading.Lock()把并发请求串行化或者用小batch批量推理问题解决。这个问题在教程里基本没人提但在真实线上场景里几乎必现。5.3 监控告警的黄金指标清单做AI工程的人容易陷入一个误区只写训练代码不管在线系统。实际上AI系统的生命力全在运维我整理了上线前必须确保能看到的一系列监控指标做成了清单式备忘请求量QPS及其时间趋势平均时延、P50、P95、P99时延错误率4xx、5xx、模型推理异常输入特征缺失率、异常值比例特征分布漂移指标PSI、KL散度预测类别分布变化模型服务的内存使用率和GPU利用率低置信度预测的比例变化这8项指标每一个都有意义但我见得最多的团队是前两个都没有。一个模型服务如果连QPS和时延都不监控等于全然不知道自己系统的健康状况出了问题也只能像无头苍蝇一样乱撞。如果你只能先加三项先加时延、错误率、预测分布这三个指标能覆盖六成以上的线上问题场景。6. 经验收尾从零到一之后这条路还能走多远做完整条ai-engineering-from-scratch路线之后我最大的体会是这个项目的终点并不是“我学会了AI工程”而是“我再也不怕AI工程了”。碰到任何新的模型、新的框架、新的部署工具底层的那份原理认知会自然而然地迁移过去。你不再需要重新学一遍因为你看到的是同一个骨架换了不同的皮。给正在走这条路的朋友三个实际的建议第一别急着追求“最新最热”的技术先把“最基础最稳定”的原理吃透。大模型确实火但如果你连标准分类任务的训练循环都能手写再学大模型的微调、部署你上手的速度会快得惊人。反过来一上来就怼大模型基础概念全是浆糊翻车了都不知道去哪查。第二找到一个真实的小项目跑通整条链路。哪怕只是一个最最简单的文本分类、图片分类重要的是完整体验“数据清洗-模型训练-评估-部署-监控”这五个环节而不是只做其中一个。五个环节里至少有三个会在真实项目中给你“惊喜”这些惊喜积累起来就是你的核心竞争力。第三建立一个“排错笔记本”把每次遇到的报错、排查过程、最终解决方案记下来。这些在正规文档里是永远查不到的。半年之后回头看你会发现这本笔记随便翻一页都能变成一篇价值极高的技术分享。我的个人体会是AI工程这条路上真正的门槛不是数学不是编程语言而是那种“敢于从零开始面对未知”的心态。如今这个快餐时代愿意把基础反复打磨的人越来越少反而是这些人能在关键时候站出来扛住问题。希望这篇梳理能帮你减少一些独自摸索的时间把你直接可以拿去用的经验和坑提前踩平。就像网上的热门比喻说的你看见的是别人两分钟调好的接口看不见的是背后几千个失败的训练日志。从零开始的过程不性感但它给你的能力是实打实的。