我建了一个公开路线图项目叫 ai-engineering-from-scratch,名字很长,但核心就一句话:把 AI 工程这件事从零到一完整讲清楚。这个仓库里没有“读完你就月薪三万”的噱头,只有我这些年从算法岗做到 AI 平台工程岗、从只会跑 notebook 到负责线上推理系统的真实经验。今天这篇不打算写一堆理论,而是把这个项目的设计思路、路线结构和我在实操中踩过的坑完整拆一遍。适合三类人看:想转行做 AI 工程的后端开发、刚入行的算法实习生,以及所有“只会调 API”但对模型内部一头雾水的朋友。
市面上教你“上车”的教程太多了,有教调接口的,有教三天跑通大模型的,但很少有人讲清楚从数据到模型、从模型到服务、从服务到监控这条完整链路到底该怎么走。我见过太多人学完一堆课,最后连一个完整的模型项目都交付不了,问题恰恰出在“从零到一”中间的断层。这篇文章就是把这个断层补齐。
1. 内容整体设计与思路拆解
1.1 为什么我坚持“从零开始”而不是“从框架开始”
这年头入门 AI 的第一反应都是找个 PyTorch 教程跑一遍,跑通之后就觉得自己会了。但等你真在项目里遇到 loss 不降、显存爆掉、线上效果比离线差一大截这些问题时,你会发现自己跟这个框架之间隔着一堵墙,因为你根本不理解它在替你做什么。
所以我在路线图里把“从零开始”定义成“从第一性原理出发”。你会调用model.fit()不算会,你能解释清楚这一个 fit 内部到底在拟合什么才算会。比如损失函数为什么存在、一次反向传播的梯度是怎么流动的、为什么 batch size 太大反而收敛不好,这些听起来基础,但恰恰是排查问题的底层地图。
整个路线图的第一个动手任务,不是装 PyTorch,而是用 NumPy 手写一个线性回归,再手写一次梯度下降。代码量不大,但当你能把 loss 对权重的导数一步步推出来、写出来,后面再用 PyTorch 训练大模型,你会非常清楚它在哪些环节帮你省事、哪些环节又可能在给你制造麻烦。这种“先懂原理再碰工具”的做法,比一上来就啃几百页框架文档靠谱得多。
1.2 这个路线图想解决什么实际痛点
我接触过不少带着奇怪卡点来找我的朋友,总结下来最典型的五类问题:
第一类是“学过但不会做”。课程看了很多,paper 也读了一些,但轮到自己去处理一份真实 dataset 时,连第一步清洗该干什么都想不清楚。第二类是“训练完不知道怎么交付”。模型在 notebook 里跑得挺好,领导一问“什么时候能上线”,瞬间不知道怎么答,因为保存模型、写推理接口、打包容器这些环节压根没练过。第三类是“上线了不知道怎么看”。接口是通了,但没人知道模型预测质量好不好、什么时候该重训,属于典型的“上线即失联”。第四类是“出了问题无从排查”。loss 不降说加数据,显存炸了说换显卡,永远是头疼医头脚疼医脚,没有一套系统性的排查思路。第五类是“团队协作混乱”。谁的实验跑过什么参数、线上跑的到底是哪一版模型,全靠聊天记录,三个月后想复现一个旧实验,比登天还难。
ai-engineering-from-scratch 这套路线图就是围绕这五个痛点设计的,每一个阶段都要求你交付一个可运行、可验证的产物,而不是“学完知识”就完事。
1.3 整体路线:五个阶段一张表
我在项目里把学习路径拆成五个阶段,每个阶段都有明确目标和产出物,你可以对照着看自己卡在哪一环。
| 阶段 | 核心主题 | 关键内容 | 产出物 |
|---|---|---|---|
| Phase 0 | 编程与数学基础 | Python、NumPy、Pandas、线性代数、概率 | 能熟练处理表格数据 |
| Phase 1 | 传统机器学习 | 线性模型、决策树、随机森林、评估指标 | 训练并评估一个完整基线模型 |
| Phase 2 | 深度学习与 Transformers | PyTorch、训练循环、分词器、微调 | 微调一个小型预训练模型 |
| Phase 3 | 数据工程与特征工程 | 数据清洗、样本设计、防泄漏、特征存储 | 一套可复现的数据处理流水线 |
| Phase 4 | 模型服务化与部署 | ONNX、FastAPI、Docker、GPU 推理 | 一个可访问的模型 API |
| Phase 5 | 监控与持续迭代 | 日志、漂移检测、A/B 评估、重训机制 | 一套监控看板与更新流程 |
看着是不是很清楚?但要注意,这五个阶段不是串行学完才叫完成。正确的做法是先用 Phase 1 和 Phase 2 的最小技能跑通一个端到端小项目,再用 Phase 3 到 Phase 5 去加固它。先跑通,再做扎实,这是我反复强调的原则。
2. 核心细节解析与实操要点
2.1 数据工程:决定模型上限的第一块基石
太多人觉得 AI 工程就是调模型,但以我的经验,真实项目里 70% 的时间都在跟数据打交道。模型选择再花哨,也弥补不了数据的脏、乱、缺、漏。一个老生常谈但极其重要的结论是:模型的效果上限由数据决定,模型只是逼近这个上限。
我在做一个用户流失预测项目时就栽过大跟头。用验证集评估,AUC 高达 0.99,我一度以为自己要发 paper了,结果上线后真实效果比随机猜好不了多少。排查了很久,发现是因为我把一条“用户已经流失之后”才产生的营销触达记录当作特征放进了训练集,模型学会了“看到营销触达就是流失用户”,这在离线数据里是“正确答案”,但在线上预测时这种特征还没发生,根本取不到。这就是典型的目标泄漏。
所以我在路线图的 Phase 3 里专门加了一节课:怎么系统性地检查数据泄漏。核心动作有三个:训练集和测试集之间做交叉查重;按时间顺序的业务数据必须按时间切分,不能随机切;所有特征必须确认在预测时刻是“已知的”。这三条做到,至少能躲开我当年踩的那个巨坑。
2.2 模型训练与调试的最小必备知识
训练模型这件事,技术细节多如牛毛,但真正决定你能不能把这个项目做完的,是几个最小必备知识点。第一个是损失函数的选择,分类问题优先交叉熵,回归问题用 MSE 这类距离损失;第二个是优化器,绝大多数情况 Adam 都能对付,但要理解学习率意味什么;第三个是梯度流动方向,理解网络层之间为什么会有梯度消失或爆炸;第四个是过拟合信号,训练 loss 持续下降但验证 loss 开始反弹时,就是典型的过拟合。
我自己有个习惯,训练一开始就画三条曲线:训练 loss、验证 loss、学习率变化。很多人只盯着训练 loss,等到最后才发现验证集一塌糊涂。记住一个判断规则:如果训练 loss 和验证 loss 都在高位不动,先看学习率和数据顺序,是不是没做 shuffle;如果训练 loss 在降但验证 loss 在涨,不要急着加数据,先考虑正则化、early stopping、dropout 这些手段;如果两个 loss 都震荡剧烈,十有八九是 batch size 太小或学习率太大。
这套排查顺序写进路线图之后,帮很多人省了至少一个月的瞎折腾。模型调试不是玄学,是一套有顺序、有假设、能验证的工程方法。
2.3 评估指标的坑:离线指标不等于业务指标
新手最容易犯的一个错误,是把离线准确率当成一切。我给你举个极端例子:垃圾邮件分类任务里 99% 是正常邮件,1% 是垃圾邮件。如果你无脑把所有邮件都判为“正常”,准确率直接就是 99%,看起来漂亮极了,但实际业务上等于没做。这时候要看的不是 accuracy,而是 precision(查准率)和 recall(查全率),甚至要看混淆矩阵里每一类错误带来的业务成本。
我见过很多团队的评估标准是“模型效果不错”,但仔细一问,“不错”是按什么指标定义的,没人说得清。所以在路线图里我特意强调:任何模型项目在开工之前,必须先写下一句可量化的成功标准。比如“把垃圾邮件误判率从 5% 降到 2%,同时保持正常邮件拦截率不超过 0.1%”。这句话写清楚了,后面所有调参和模型选择才有方向。
还有一点容易被忽略:离线评估的分布和线上真实分布经常不一致。训练数据是今年一月的,线上来的是三月的,分布早就变了。所以离线指标只能作为参考,真正的裁判永远是线上业务指标。
2.4 部署:从 notebook 到服务的最后一公里
模型训练完只是完成了一半,部署是另一半。很多人第一次把模型部署成 API 时,会直接从训练脚本里复制出一大坨代码,把Trainer对象整个 import 进来,结果接口还没跑起来,先下载了一堆模型权重。正确的做法是:推理代码必须和训练代码彻底解耦。训练代码负责“产出模型权重”,推理代码只负责“加载权重 + 前向计算”。
我在路线图里推荐的部署组合是:模型导出 ONNX,接口用 FastAPI,容器用 Docker,推理用 ONNX Runtime。这套组合的学习成本很低,性价比却极高。为什么用 ONNX?因为它能把模型从 PyTorch 里解放出来,小而快,跨平台部署非常方便。对于一个小型文本分类模型,导成 ONNX 之后在 CPU 上的推理速度通常能提升一倍以上。
另外,部署不是把代码扔给运维就结束了。你需要定义输入输出 schema、设置健康检查接口、配置资源上限、记录请求日志。这些细节我在第三节的实操部分会展开讲。
2.5 监控与反馈闭环:模型上线只是开始
模型上线那一天,不是项目的终点,而是运维的起点。我见过太多模型上线三个月后,预测分布早就偏得没边了,团队却毫无察觉,直到业务方投诉“最近结果怎么这么不准”才开始排查。
监控要抓的核心指标至少有这几类:接口层的延迟、吞吐、错误率;模型层的预测分布变化;业务层的最终效果指标。其中预测分布变化最容易被忽略,但往往最先出问题。有一个便宜且有效的做法:记录每次请求的输入文本哈希、模型版本、预测结果和延迟,每天算一下预测类别的比例,一旦发现分布偏移超过阈值,就触发告警。
在路线图里我还专门强调一个概念:模型没有“上线即永逸”这回事,只有持续更新。离线要维护一套重训流水线,线上要留好反馈数据采集的入口。哪怕一开始只是每天导出一份预测日志,也比什么都没有强得多。
3. 实操过程与核心环节实现
3.1 选一个恰到好处的练手项目
说再多理论也不如亲手干一个项目。在 ai-engineering-from-scratch 里,我建议的第一个完整项目是文本情感分类,用 IMDB 电影评论数据集,判断一条评论是好评还是差评。
为什么选这个?第一,数据公开好拿,不用自己吭哧吭哧爬半天;第二,文本数据不需要复杂的特征工程,很多坑可以直接暴露在数据清洗和模型评估环节;第三,计算资源友好,微调一个小模型在普通开发机上也能跑;第四,业务含义清晰,好评差评一眼就能评估。
如果你更偏好结构化数据,也可以把用户流失预测作为替代,但文本分类的全链路练习价值会更高,因为后面做更复杂的 NLP 应用时,这套流程可以直接复用。
3.2 基础版:从零手写逻辑回归练手感
我的要求很反直觉:第一个模型不准用 sklearn,不准用 PyTorch,就用 NumPy 手写。代码很简单,我来展示核心部分。
import numpy as np def sigmoid(z): return 1 / (1 + np.exp(-z)) def predict(X, w, b): return sigmoid(X @ w + b) def train(X, y, lr=0.1, epochs=100): num_features = X.shape[1] w = np.zeros(num_features) b = 0.0 for epoch in range(epochs): p = predict(X, w, b) grad_w = X.T @ (p - y) / len(y) grad_b = np.mean(p - y) w -= lr * grad_w b -= lr * grad_b return w, b每一行都值得你反复看。sigmoid的作用是把线性输出压到 0 到 1 之间,变成概率;grad_w = X.T @ (p - y) / len(y)这一步是从交叉熵损失推导出来的梯度表达式,它的形态简洁得惊人——预测概率减去真实标签,再乘上特征矩阵转置。当你亲手算出这一步、写出这行代码,再回头去看任何机器学习框架里的逻辑回归,就会发现它不再是黑盒。
这个练习的价值不是工程价值,而是认知价值。它会让你彻底理解梯度下降:我们不是在“调参”,而是沿着损失函数的负梯度方向一步步往下走,直到找到一个损失足够低的位置。
3.3 进阶版:用 Hugging Face 微调一个小模型
流程跑通、手感建立之后,就可以进入现代深度学习工作流了。我们用 DistilBERT 在 5000 条 IMDB 数据上微调一个情感分类模型,这是整个路线图里最关键的“现代范式”体验。
from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments import datasets train_ds = datasets.load_dataset("imdb", split="train[:5000]") test_ds = datasets.load_dataset("imdb", split="test[:1000]") tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") def tokenize(examples): return tokenizer(examples["text"], truncation=True, padding=True) train_ds = train_ds.map(tokenize, batched=True).select_columns(["input_ids", "attention_mask", "label"]) test_ds = test_ds.map(tokenize, batched=True).select_columns(["input_ids", "attention_mask", "label"]) model = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased", num_labels=2) training_args = TrainingArguments( output_dir="./results", num_train_epochs=2, per_device_train_batch_size=16, eval_strategy="epoch", save_strategy="epoch", logging_dir="./logs", ) trainer = Trainer(model=model, args=training_args, train_dataset=train_ds, eval_dataset=test_ds) trainer.train()注意几个细节。第一,padding=True会把序列补到 batch 内最大长度,truncation=True控制超长文本截断,两个参数缺一不可。第二,我把数据量限制在 5000 条和 1000 条,是为了让训练时间控制在几分钟级别,你完全不需要在练手阶段就上全量数据。第三,建议你自己试一下不同的num_train_epochs、学习率和 batch size,这会让你直观感受到超参对最终效果的影响。
关于序列长度我可以多说一句:DistilBERT 默认最大长度是 512,但 IMDB 评论大多不超过两百字,所以实践里我通常把max_length=256甚至 128。序列长度减半,显存占用几乎等比例下降,而精度损失很小。256 个 token 能覆盖绝大多数评论信息了。
3.4 导出 ONNX 并用 FastAPI 搭一个推理服务
模型微调完成之后,下一步就是把它从 PyTorch 里“解放”出来。我建议导出成 ONNX 格式,这样推理阶段不再依赖完整版 PyTorch,模型体积更小、启动更快、部署更干净。
from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch model = AutoModelForSequenceClassification.from_pretrained("./results/checkpoint-500") model.eval() tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") dummy_input = tokenizer("this movie is great", return_tensors="pt") torch.onnx.export( model, (dummy_input["input_ids"], dummy_input["attention_mask"]), "sentiment_model.onnx", input_names=["input_ids", "attention_mask"], output_names=["logits"], dynamic_axes={"input_ids": {0: "batch"}, "attention_mask": {0: "batch"}}, opset_version=14, )导出之后用 FastAPI 包一层 HTTP 接口,代码非常短:
from fastapi import FastAPI from pydantic import BaseModel from transformers import AutoTokenizer import onnxruntime as ort import numpy as np app = FastAPI(title="sentiment api") tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased") session = ort.InferenceSession("sentiment_model.onnx", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): inputs = tokenizer(item.text, return_tensors="np", truncation=True, padding=True) logits = session.run(None, {"input_ids": inputs["input_ids"], "attention_mask": inputs["attention_mask"]})[0] prob = 1 / (1 + np.exp(-logits[0][1])) return {"label": "pos" if prob > 0.5 else "neg", "confidence": float(prob)}这个接口做的事情很清楚:拿到文本,走同一个 tokenizer 转成张量,交给 ONNX Runtime 推理,把 logits 转成概率,最后返回标签和置信度。别小看这个简单的服务,它已经具备了一个生产级推理服务的基本骨架。后面要加鉴权、限流、监控,都是在这个骨架上扩展。
我在项目里专门提醒过一个点:在线服务的 tokenizer 版本必须和训练时保持一致。否则词汇表一变,同一句话可能被切分成不同的 token,预测结果就会莫名其妙地飘。
3.5 补上实验跟踪和 Docker 部署
从第一个实验开始,我就建议你用 MLflow 记录每次训练的配置、指标和产物。这习惯越早养成,后面越省心。
import mlflow mlflow.autolog() with mlflow.start_run(): trainer.train() trainer.evaluate()mlflow.autolog()会自动记录超参数、loss、评估指标以及模型文件,你什么都不用手动写。启动mlflow ui,就能在浏览器里看到每一次实验的记录,三个月后还能精确复现某一次实验用的代码版本和数据版本。
部署环节,我用一个极简 Dockerfile 把服务打成镜像:
FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]构建镜像后运行容器时,一定要记得加资源限制,这是我强烈建议的习惯操作:
docker build -t sentiment-api . docker run --rm -p 8000:8000 --memory=4g --cpus=2 sentiment-api给容器设置--memory和--cpus,是防止模型推理或突发流量把整台机器拖垮。这个习惯越早养成,线上事故越少。
3.6 部署后的基础验证与观察
接口起来之后,先用 curl 打一发验证:
curl -X POST http://localhost:8000/predict \ -H "Content-Type: application/json" \ -d '{"text": "this movie is fantastic"}'看到返回{"label":"pos","confidence":0.98}之类的结果,服务就算通了。但我还会再多做一步:压测。用 hey 或者 locust 简单打几百个请求,看 P95 延迟和错误率。你会发现,单个服务本地调用和多并发请求下的表现完全不同,这一步能提前暴露很多资源瓶颈。
如果你有 GPU,推理时用CUDAExecutionProvider;没有 GPU,就老老实实用 CPU。一个小型 DistilBERT 模型在 CPU 上用 ONNX Runtime 做单条推理,延迟通常在几十毫秒级别,完全够用。不要一上来就觉得“AI 服务必须要 GPU”,很多业务场景 CPU 就能扛,成本还低得多。
4. 工具选型与生产化细节:我的真实对比
4.1 实验追踪与模型注册:选 MLflow 还是 W&B
市面上的实验追踪工具非常多,我先把三个主流方案的利弊列成表:
| 工具 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MLflow | 开源、可自建、实验+模型注册+部署一条龙 | UI 相对朴素 | 中小团队、需要私有部署 |
| Weights & Biases | 交互体验好、可视化强、团队协作方便 | 免费版有限制、数据在 SaaS 端 | 快速原型、学术研究 |
| ClearML | 开源、支持大模型训练管理 | 部署略复杂 | 已有基础设施的团队 |
我给大多数中小团队的建议是:从 MLflow 开始。理由很简单,它一个工具覆盖了实验追踪、模型注册、产物存储三个需求,而且只用pip install mlflow就能启动,不需要额外花钱。模型注册这个概念很多新手不理解,你把它想成“模型版本的 Git”就懂了:每个模型版本有名字、标签、指标、文件路径,线上要回滚时直接切版本,不用翻聊天记录找你上传的第几个model_v7_final_real_final.pt。
4.2 数据版本管理:让实验真正可复现
很多人训练模型只用一份 data.csv,改来改去,最后根本说不清楚当前模型是用哪份数据训练出来的。数据版本管理和代码版本管理同样重要。
我常用 DVC 做数据版本管理,核心流程很简单:
dvc init dvc remote add -d storage s3://your-bucket/dvc dvc add data/train.csv echo data/train.csv.dvc >> .gitignore git add data/train.csv.dvc dvc.lock git commit -m "add training data"dvc add会把数据文件的信息写进一个.dvc文件,真正的数据文件推到远端对象存储里。Git 记录的是数据文件的哈希指针,而不是数据本体。这样仓库不会因为大数据文件而爆炸,同时你任何时候切到某个 commit,都能用dvc pull把对应版本的数据拉回来。
如果你做的是个人练手项目,也可以偷个懒不引入 DVC,但至少要养成习惯:在目录或文件名里带上日期,比如data_20240528.csv,或者把数据文件放进 Git LFS。数据版本管理这件事,平时感觉不到价值,一旦要回溯某个实验结果时,它就是你唯一的救命稻草。
4.3 推理服务框架:FastAPI、BentoML、Triton 怎么选
我经常被问到“上线模型该用哪个服务框架”,其实答案取决于你的团队规模和业务复杂度。
| 方案 | 优势 | 运维复杂度 | 推荐场景 |
|---|---|---|---|
| FastAPI + ONNX Runtime | 灵活、轻量、上手快 | 低 | 个人/小团队,接口逻辑简单 |
| BentoML | 内置部署编排、模型管理、插件丰富 | 中 | 团队已有一定工程能力 |
| Triton Inference Server | 高性能、多模型并发、动态批量 | 高 | 大规模 GPU 推理、多模型统一入口 |
我的真实建议非常直接:大多数团队不需要 Triton。Triton 的价值在高吞吐、多模型并发、以及 N 卡统一调度上,但它对运维能力的要求也高出一大截。如果你只有两三个模型、日均调用量几十万次,FastAPI 加 ONNX Runtime 是性价比最高的起点。等你真的有一天需要同时服务 10 个模型、每个模型都要求高吞吐时,再迁移到 Triton 完全不晚。
4.4 硬件规划与成本控制
最后聊聊钱。训练和推理的成本控制,是所有 AI 工程都绕不开的话题。
先说推理,小模型在 CPU 上跑完全可行,不要逢模型就上 GPU。以文本分类为例,把模型导出 ONNX 并量化成 int8,模型体积能缩到原来的四分之一,CPU 推理速度还能进一步上升。很多场景下,用 int8 CPU 部署一年省下的服务器成本,够你再招一个实习生。
再说训练,实验初期建议用小数据、小模型、多轮快速迭代,锁定方案后再上全量数据和大模型。这能省下大量算力开销。如果你用云 GPU,训练任务一定要保存 checkpoint,并且定期把 checkpoint 同步到对象存储,这样即使机器被回收也可以随时续跑,不至于从头再来。
5. 常见问题与排查技巧实录
这部分把我在实操中踩过、也帮别人排查过的高频问题整理成速查表,然后再逐个展开。
| 问题现象 | 常见原因 | 排查方向 | 快速修复 |
|---|---|---|---|
| 训练 loss 不降 | 学习率设置不当、数据未 shuffle、标签错位 | 打印首个 batch 的输入输出检查 | 调低学习率、开启 shuffle 并复检标签 |
| GPU 显存爆炸 | batch size 过大、序列过长、未用混合精度 | 逐层打印激活显存 | 减 batch、梯度累积、开 AMP |
| 离线分数极高但线上效果差 | 数据泄漏、分布偏移 | 检查时间泄漏和训练/测试交叉重复 | 按时间切分、去重、重采测试集 |
| 接口延迟超高 | 模型过大、推理框架未优化 | 压测看 P95、分析耗时分布 | 导 ONNX、量化、加长超时与 batch |
| 本地跑通但容器里报错 | 依赖版本不一致、环境缺少系统库 | 对比 requirements 与镜像内版本 | 锁定依赖版本、固定基础镜像 |
5.1 训练 loss 不降或震荡
这是新手问得最多的问题,原因其实大多是同一个方向:信息流断掉了。我的排查顺序固定是:先看数据,再学学习率,最后看网络结构。
先打印一个 batch 出来,肉眼确认输入和标签是不是一一对应。我犯过的最离谱的错,是 dataloader 忘了设置shuffle=True,模型每天按顺序学到一类样本,loss 看似在降,验证集却一塌糊涂。再看学习率,有的教程给的默认学习率对别人合适,但对你自己的任务可能太大或太小,我通常把 learning rate 从1e-3开始往下扫。最后检查网络,是不是激活函数用了 ReLU 导致死亡神经元,或者输入特征没做归一化导致梯度计算异常。这个顺序能覆盖九成以上的“loss 不降”问题。
5.2 GPU 显存爆炸
显存炸了不代表你要马上换卡,很多时候是使用姿势的问题。可以考虑的顺序是:调小 batch size,改用梯度累积来模拟更大的 batch;打开混合精度训练,显存占用能直接减半;检查序列长度是不是被人为拉长了,比如把 512 个 token 的模型硬喂 1024 的文本。对于一个小型 Transformer 模型,显存占用的大头往往不是参数本身,而是中间激活值和梯度,它们跟 batch size、序列长度、层数几乎成正比。
有个粗略的经验公式可以帮你估算:训练一块模型需要的显存大约是推理的三到五倍,推理时如果你能接受,先用 CPU 加 int8 量化跑通业务,再评估要不要上 GPU,这是成本最稳的路线。
5.3 离线分数高但线上效果差
这个现象背后,第一嫌疑就是数据泄漏。除了我之前说的目标泄漏,还有两种常见情况:时间泄漏和重复泄漏。时间泄漏最常见于时序业务,你随机切分了数据集,导致模型在训练时“偷看”了未来的信息;重复泄漏则是因为同一实体在训练集和测试集里都出现过,模型相当于“见过答案”。排查方法很直接:业务数据按时间点切分,并做一次 ID 级查重。
另一种可能是采样偏差。比如你收集的训练数据来自某一类用户,但线上流量画像完全不同。这种问题最有效的缓解办法是上线后做影子评估:让新模型和旧模型同时接收线上流量,记录各自预测结果和后续业务反馈,来判断谁真的更好。
5.4 推理延迟高
接口延迟超标,先量清楚时间都花在哪了。把一次请求的耗时拆成三段:网络传递、tokenizer 处理、模型推理。大部分时候你都会发现瓶颈在模型推理。解决办法从便宜到贵排列:导成 ONNX 并用 int8 量化,通常能获得一到三倍加速;如果还不行,尝试减小序列最大长度,这能直接降低计算量;再不行,考虑换更小的模型,比如从 BERT 降到 DistilBERT;最后如果业务允许,把同一批请求合并成一个 batch 推理,吞吐量提升非常明显。
缓存也值得做。如果业务里有大量重复或相似查询,Hash 一下输入文本,命中缓存直接返回,这是几乎零成本的优化。
5.5 训练环境和推理环境不匹配
这是我在生产环境见过最多的“幽灵 Bug”。某个模型在训练时好好的,部署到容器里预测结果全变了。原因往往不是代码逻辑,而是依赖版本不一致。最典型的是 transformers 库:训练用的是 4.30 版本,推理容器里装的是 4.40 版本,中间某个 tokenizer 的默认行为变了,同样的文本进去,出来的 token 序列完全不同。
解决办法只有一条路:锁定版本。requirements.txt 里的所有关键依赖,包名后面必须写上精确版本号,比如transformers==4.30.2、torch==2.1.2、onnxruntime==1.17.1。同时把训练和推理放进同一个基础镜像,至少保证它们跑在同一个 Python 版本下。这些细节不会让你的模型变聪明,但能让你在凌晨三点被线上告警电话叫醒的频率大幅下降。
6. 最后分享三条我最深的体会
说了这么多,最后讲三件我在实际项目中体会最深的事。
第一,AI 工程的核心不是模型,而是数据流和反馈流。你花一周调模型结构,不如花一天把数据到模型的链路理顺、把线上反馈数据采集起来。模型只是这条流水线上的一个零件,流水线不通,换再好的零件都没用。
第二,基线模型永远是你最可靠的朋友。我每接一个新任务,第一版永远是最简单的逻辑回归或者决策树。它不追求效果,只负责把整个数据链路、评估链路、部署链路完整跑通。基线的效果就是你的下限,后面换成大模型,你能清楚知道提升到底来自哪里。
第三,从第一天起就写日志、记录实验。哪怕只是在本地跑一个几十行的脚本,也顺手把参数、数据版本、结果指标记到 CSV 里。三个月后再回来看,你会感激当时的自己。我见过太多人实验跑了几十次,最后问“上次最好的效果是哪个配置”,答案永远是最经典的三个字:不记得了。
如果让我现在回去重做这个项目,我最想补的一课是检索增强生成(RAG),因为它是目前把大模型落地到业务场景最实用、也最容易踩坑的技术方向。这部分我已经在整理,等方案稳定后,会直接作为新章节更新到 ai-engineering-from-scratch 里。