ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零构建 AI 工程知识体系:路线图、实战闭环与避坑指南

从零构建 AI 工程知识体系:路线图、实战闭环与避坑指南

从零开始整理 AI 工程知识体系这件事,我断断续续做了一年多。项目名字就叫 ai-engineering-from-scratch,一开始只是因为自己带团队时发现一个尴尬的问题:组里算法工程师写模型没问题,但一聊到部署、监控、数据管道就露怯;后端工程师能把服务写得滴水不漏,但面对模型训练时的种种玄学现象,又常常摸不着头脑。我身边几乎所有人都在用各种零碎的方式补课,但补得很痛苦,因为市面上要么是纯算法理论,要么是纯工程实操,很少有人能把中间那一大块"AI 工程"的拼图完整拼上。于是我把自己的经验沉淀成了一个从零到一的知识仓库——这篇博文就是想把这个项目从头到尾拆开,讲讲我为什么这么设计、里面到底放了哪些东西、踩过哪些坑,以及如果你也想系统性进入 AI 工程这个方向,有哪些路是已经验证过走得通的。

这个项目最适合两类人:一类是已经有软件工程基础、想往 AI 方向延伸的开发者,另一类是算法基础不错、但始终搞不定"上线"这件事的研究型同学。我会把整个知识地图摊开,尽量把每条路径背后的选择逻辑讲清楚——为什么先学 A 而不是先学 B,为什么这里选这个工具而不是那个工具,遇到问题时真实的排查思路是什么。都是些我自己趟过的经验,不会有太多教科书式的废话。

1. AI 工程到底是什么——先搞清楚游戏规则

1.1 AI 工程和"调模型"完全是两回事

很多人一听到 AI 工程,第一反应就是"训练模型"。这个理解不能说错,但严重不完整。AI 工程如果把整个链路画出来,训练可能只占两三成的工作量,剩下的大头全在数据管道、特征工程、模型评估、上线部署、监控告警、资源成本这些"软件工程肌肉"上。这也是为什么我会给这个项目起名叫 from scratch——不是从线性代数开始学起,而是从一个普通工程师能理解的起点开始铺路。

打个比方:训练模型就像是做一道菜。算法工程师可能是个很厉害的菜谱研究者,能告诉你盐放几克、火开多大,但 AI 工程负责的是整家餐厅怎么运转——食材从哪里采购、怎么清洗切配、菜品怎么标准化出品、客户反馈怎么收集、出品的质量怎么保持稳定。你光会研究菜谱,是开不了餐厅的。这就是 AI 工程和"调模型"之间最本质的区别。

我见过太多团队栽在这个认知差上。模型在离线测试集上效果很好,指标漂亮得像假的一样,但一上线就崩——不是模型本身崩了,而是线上数据和训练数据分布不一致,特征的字段没对齐,上游数据延迟导致缺失率暴涨,推理速度跟不上业务要求,线上环境缺了某个依赖库。这些问题,没有一个是在 Jupyter Notebook 里面能发现的。所以我把项目的第一章节就定为"AI 工程全景认知",先把游戏规则讲透,再谈具体技能。

1.2 这个方向的核心技能栈图谱

既然 from scratch 是给零基础的人铺路,那技能栈图谱就是整个项目的骨架。我设计了一份"AI 工程师能力模型",分成了四条主线并行推进:

  • 数据线:数据采集、清洗、标注、版本管理、特征存储。这一块最容易被轻视,但实际业务里花的时间最多。
  • 模型线:经典机器学习、深度学习、大模型微调、评估与调优。这是"厨师"的基本功。
  • 工程线:Python 工程化、容器化、CI/CD、模型服务化部署、推理优化。
  • 运维线:监控、告警、日志、模型版本管理、数据与概念漂移检测。

四条线不是先后关系,而是螺旋式上升的关系。我的建议是:用一个小项目把四条线都走一遍,哪怕是最小可行版本,也比只在一条线上钻三个月更有收获。等你走完一个小闭环,再回来深入某一条线的细节,理解和记忆都会扎实得多。

2. 从零起步盘根式学习——课程与路线选型背后的为什么

2.1 第一步不是学 PyTorch,而是把 Python 工程化补扎实

我观察过很多半路转 AI 的工程师,最容易犯的一个错误就是一上来就啃 PyTorch 官方教程。结果就是:张量操作会用一点,Dataset和DataLoader看懂了,但一写实际项目就露馅——代码全是脚本式堆叠,没法复用,没法测试,参数硬编码在代码里,跑一次要手动改五个地方。

所以我给项目定的第一个里程碑,反而是"Python 工程化能力":类与抽象、类型注解、虚拟环境管理、代码格式化与 lint、单元测试、依赖管理。很多人觉得这些东西枯燥,不如训练模型有成就感,但实际经验告诉我,这些恰恰是 AI 项目能不能持续迭代的命门。

举个例子。我做一个数据处理管道时,需要写一个特征处理模块,给每个特征做缺失值填充和标准化。如果 Python 工程底子不牢,十有八九会写成一个大函数,把所有特征名硬编码在字典里。一旦特征列表变了,代码就要跟着改,还容易出现线上和线下处理逻辑不一致的 bug。而如果从一开始就设计一个抽象的特征转换接口,用统一的配置驱动,后续新增特征就不会恐慌。这中间花的小半天时间,后面能帮你省出好几天。

2.2 课程资源怎么选——以用带学,拒绝囤积焦虑

现在网上的 AI 课程资源实在太多了,Coursera 上有吴恩达的经典课,Fast.ai 有实战导向的教程,Hugging Face 有免费的 transformer 课程,还有各种付费训练营。我的观点可能和主流不太一样:不需要在选课上花太多时间,选定一条主线,然后以项目为锚点边做边学。

我自己当时定的主线很简单,就三个层次:

  • 第一层:机器学习基础,重点是理解损失函数、梯度下降、过拟合这些核心概念,不求全但求透。
  • 第二层:深度学习框架实操,选定了 PyTorch,主攻数据加载、模型构建、训练循环这些日常高频动作。
  • 第三层:大模型时代的工程化,包括 Prompt 工程、RAG 架构、微调策略、评估方法。

每一层我都配了一个立即可做的小项目。学机器学习基础时,我找了个公开数据集,自己从零实现一遍线性回归和决策树,体会推导和代码的对应关系;学 PyTorch 时,我把之前的模型用框架重写,并用 MLflow 记录实验;学到大模型工程化,就直接拿一份内部文档库做 RAG 检索问答系统。这种"以用带学"的方式效率极高,因为你学的每一点知识都能在项目里看到反馈,正向激励很强,也不会陷入"看了一堆视频却一个项目都拿不出手"的困境。

硬件层面的问题也顺便说一句。很多人觉得没有一块像样的 GPU 就没法学 AI,其实不完全对。训练小模型、跑通整个工程链路,用 CPU 完全可行,就是慢一点。而且只要你习惯用好实验记录工具,同等迭代次数下,CPU 上多跑几小时也不算什么。等到了大模型微调阶段,再考虑租云 GPU 实例或者用 Kaggle/Colab 的免费额度,成本完全可以控制在可接受范围内。

2.3 选型背后的核心原则:先跑通,再优化

我在项目里反复强调一条原则:任何技术选型,一开始都要选你最容易跑通的方案,而不是理论上最优的方案。这条原则几乎贯穿了整个学习路径。

举一个具体的选型案例。在模型部署这个环节,初学者面前有超多选择:直接用 Flask 起一个 HTTP 服务,用 FastAPI 加 Pydantic 做参数校验,用 Triton 做高性能推理服务,或者直接用 Hugging Face 的 pipeline 封装。如果照着"最佳实践"来选,大概率会一开始就上 Triton,然后被配置文件、模型仓库、动态批处理这些概念折腾得怀疑人生。

我的建议是,先做减法。第一版就用 FastAPI 把模型封装成一个 POST 接口,能接收文本、返回预测结果就算成功。跑通以后,再逐步加批处理、加缓存、加多模型管理。等你真正理解了当前方案的瓶颈在哪,再决定要不要上 Triton 或者 vLLM 这类专业推理引擎。这个"先跑通再优化"的路径,能让学习曲线平滑很多,而且在每一步你都能清楚知道优化的收益来自哪里,而不是为了用某个工具而用某个工具。

3. 实操练兵场——从数据到上线的一次完整闭环

3.1 用一个最小项目把整条链路串起来

理论说得再多,都不如一次完整的实操。我在 ai-engineering-from-scratch 项目里设置了一个贯穿始终的实操项目:做一个"技术文章自动分类器"。这个项目的选择有几个考虑:数据容易获得、任务难度适中、可以平滑地从传统的 TF-IDF 模型过渡到深度学习模型,再到如今的大模型范式,非常适合示范工程化流程。

整个项目的闭环可以拆成六步:

  1. 数据采集:写爬虫抓取一批技术文章,或者用公开的新闻分类数据集替代。
  2. 数据清洗与标注:做去重、去噪,统一格式,构建验证集和测试集。
  3. 特征工程与建模:先用 TF-IDF + 逻辑回归做一个基线,再用预训练语言模型做升级版。
  4. 模型评估:不止看准确率,还要看各类别的精确率和召回率,做错误分析。
  5. 服务化部署:用 FastAPI 封装模型,打成 Docker 镜像,本地跑通后再上云。
  6. 监控与迭代:记录推理日志,统计特征分布变化,建立简单的告警机制。

每一步我都写了详细的笔记、代码模板和踩坑记录。下面重点讲讲几个容易翻车的环节的实操细节。

3.2 数据管道:清洗环节决定你后面的幸福指数

数据管道的核心目标是产出干净、可复用的分析数据集。我踩过最大的一个坑,就是把清洗逻辑全写在 Notebook 里,一段一段跑,没有固化成一个可重复执行的脚本。第一次跑完,模型效果不错,还挺开心。结果第二天重新执行,发现由于临时改过一段清洗逻辑,导致特征顺序变了、部分数据被重复处理,生成的数据集和前一天完全对不上。那一次教训让我记住了:处理数据的每个环节,都必须代码化、可复现、可测试。

现在我的数据管道脚本长这样(简化版):

# data_pipeline.py import pandas as pd from sklearn.model_selection import train_test_split def load_raw_data(path: str) -> pd.DataFrame: df = pd.read_json(path, lines=True) return df def clean_text(df: pd.DataFrame) -> pd.DataFrame: # 去 HTML 标签、统一小写、过滤超短文本 df["clean_text"] = df["text"].str.replace(r"<[^>]+>", "", regex=True) df["clean_text"] = df["clean_text"].str.lower() df = df[df["clean_text"].str.len() > 20] return df def split_data(df: pd.DataFrame, test_size: float = 0.2, seed: int = 42): train, test = train_test_split( df, test_size=test_size, random_state=seed, stratify=df["label"] ) return train, test if __name__ == "__main__": df = load_raw_data("data/raw/articles.jsonl") df = clean_text(df) train, test = split_data(df) train.to_json("data/processed/train.jsonl", orient="records", lines=True) test.to_json("data/processed/test.jsonl", orient="records", lines=True)

注意两点。第一,train_test_split里我用了stratify参数按标签分层抽样,保证切分后训练集和测试集的类别分布一致,这个细节直接影响评估结果的可信度。第二,我用seed=42固定随机种子,确保每次切分结果一致——我见过有人跑实验时随机种子没固定,每次训练集都不一样,结果连"哪个版本的模型效果更好"都判断不了。

3.3 模型实验管理:别让你的实验记录活在聊天记录里

模型实验不记录,就等于白做。很多人在本地随便跑几轮实验,发现某个参数效果好,就留在了一个叫model_v2_final_真的最终版.pth的文件里。等过几天想复现这个结果,发现自己完全不记得当时用了什么学习率、什么 batch size、什么数据版本——这种状态做 AI 工程,基本等于盲人摸象。

我在项目里很早就引入了 MLflow 做实验管理。它的核心功能就三个:

  • 实验参数记录:把学习率、batch size、模型名称等超参数自动记录。
  • 指标记录:把每个 epoch 的 loss、accuracy、f1 等指标记录下来。
  • 模型产物管理:把训练好的模型权重和预处理配置打包管理。

实际用起来非常顺手。我训练时只需要在代码里加几行:

import mlflow with mlflow.start_run(): mlflow.log_param("learning_rate", 0.001) mlflow.log_param("batch_size", 32) mlflow.log_param("model_name", "bert-base-chinese") # training code ... mlflow.log_metric("val_accuracy", 0.91) mlflow.log_artifact("model.pt")

这样每次实验都会有完整的档案。等评估阶段发现某个模型效果好,可以直接通过 MLflow 的 UI 找到当时的运行记录,看到所有参数、指标和产物。这一点对于团队协作尤其重要——你可以很清楚地告诉同事"我调了什么、为什么效果好",而不是甩一个文件过去就完事。

3.4 训练环节的黄金法则:先建立基线,再叠 buff

建模环节最常见的错误是上来就整大模型。我的建议永远一样:先花半天时间做一个最简单的基线,再逐步升级。对于文本分类任务,最简单的基线就是 TF-IDF + 逻辑回归。它可能不是效果最好的,但它有几个不可替代的价值:跑得快、逻辑透明、容易 debug。如果这个基线就已经达到 85% 的准确率,那你就知道后面所有复杂模型的空间大概只有 10 个百分点左右,值不值得花那个成本就心里有数了。

基线跑通后,再开始升级模型。我在项目里用了 Hugging Face 的transformers库来做微调,代码结构也相对清晰:

from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments model_name = "bert-base-chinese" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) training_args = TrainingArguments( output_dir="./results", num_train_epochs=3, per_device_train_batch_size=16, per_device_eval_batch_size=64, evaluation_strategy="epoch", save_strategy="epoch", logging_dir="./logs", load_best_model_at_end=True, metric_for_best_model="f1", ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, ) trainer.train()

这里有一个关键细节:load_best_model_at_end=True配合metric_for_best_model="f1",可以让 Trainer 在训练结束后自动加载验证集上表现最好的 checkpoint,而不是用最后一个 epoch 的权重。这对最终效果的影响往往非常明显——最后一个 epoch 可能已经过拟合了,验证指标不升反降,如果直接拿它去测试,结果会比最佳 checkpoint 差不少。

3.5 部署上线:从 FastAPI 到 Docker,一次讲透

模型训练完、评估完,就到了让模型"见客户"的环节。我们的分类器部署方案分两层推进:本地先跑通 FastAPI 服务,再打包成 Docker 镜像。

FastAPI 封装模型的服务代码,核心逻辑如下:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() model = load_model() # 启动时加载一次,全局复用 class Item(BaseModel): text: str @app.post("/predict") def predict(item: Item): label = model.predict(item.text) return {"label": label, "confidence": float(score)}

这里有几个容易踩的坑,逐个说一下。

  • 模型要启动时加载,而不是每次请求都加载。这是性能的第一道坎。如果每次请求都去from_pretrained,一次推理可能要多花好几秒,在并发场景下服务很快就会崩。
  • 入参校验要用 Pydantic 模型,而不是手动传 dict 再循环取值。一方面代码更简洁,另一方面非法请求会在入口就被拦下来,不会脏到业务逻辑层。
  • 预测函数要包含异常处理。模型服务线上可能会碰到各种奇奇怪怪的输入,空字符串、超长文本、格式异常等,都需要有兜底逻辑。我在代码里加了try-except,兜底返回一个默认类别和极低的置信度,并记录日志。

Docker 配置这部分也顺手贴一下,经验是依赖文件能精简就精简,镜像体积直接影响部署速度。

FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

注意--host 0.0.0.0这个配置,很多新手漏掉它,结果在容器里启动服务后外部访问不到,白白消耗大量排查时间。

3.6 监控与告警:你不盯的指标,早晚会在深夜咬你一口

模型上线不等于事情结束,恰恰是新的开始。很多 AI 项目在 demo 阶段效果惊艳,上线一个月后效果肉眼可见地下降,其中一个核心原因就是数据漂移而没有任何监控机制。

我在项目里做了一个最小可行的监控方案:记录每次请求的输入特征和预测结果,按天聚合统计分布变化。重点盯两类指标:

  • 业务指标:预测结果的类别分布、平均置信度、请求量、响应延迟。
  • 数据质量指标:输入文本长度分布、缺失率、关键词频率等。

监控落地的技术栈选型上,我用的是 Prometheus + Grafana。服务里暴露一个/metrics端点,Prometheus 定时抓取,Grafana 做可视化。这个组合虽然老套,但稳定、生态好、资料多,适合作为学习样板。

如果发现某一天"置信度均值"明显下降或"类别分布"发生跳变,就触发告警,提醒模型可能需要重新训练。这个机制在我实际项目里真的救过命——有一个业务线的用户群体在某段时间发生了明显变化,线上模型准确率从 90% 掉到了 80%,如果不是监控及时告警,可能三周之后才会被用户差评反推回来,那时候损失已经造成了。

4. 常见问题与排查技巧实录——那些文档里不会写的坑

4.1 数据处理不一致:训练时香,上线时崩

这是 AI 工程里最经典、也最致命的坑之一。训练时你做了一堆预处理——分词、去停用词、特殊符号清洗、长度截断——但如果推理服务里的处理逻辑跟训练时不一致,模型效果就会瞬间崩塌。比如训练时你给文本做了小写化,但线上服务的预处理漏掉了这一步,模型拿到的是大小写混合的文本,特征分布变了,效果当然会掉。

排查起来的痛苦之处在于:模型文件是同一个,代码逻辑看起来也没问题,怎么结果就差这么多?我现在的做法是,把数据预处理封装成一个独立的类或模块,训练和推理的代码共用同一份实现,这样就从根上避免了不一致的可能。另外,每次上线前都要做一次"黄金测试"——用训练集里的一条样本,分别跑训练时的预处理和线上服务的预处理,对比输出是否一致。这一步只要做了,能省掉后面十倍以上的排查时间。

4.2 装了 GPU 版 PyTorch,但显存就是吃不满

我见过不止一个同事装了 GPU 版 PyTorch 后,训练时发现 GPU 利用率只有个位数,显卡风扇动都不动,任务慢得像在 CPU 上跑。排查这个问题的第一步永远是看 CUDA 是否真的可用:

import torch print(torch.cuda.is_available()) print(torch.cuda.device_count()) print(torch.cuda.get_device_name(0))

如果输出False,那基本可以断定是驱动和 PyTorch 版本不匹配。解决思路很简单:到 PyTorch 官网查对应 CUDA 版本的安装命令,重装即可。如果第一步就发现 torch 里 CUDA 不可用,后面的高性能推理优化全都白搭。

但还有另一种更隐蔽的情况:CUDA 可用,但训练时每个 batch 太小、数据加载太慢,导致 GPU 大量时间在空等,看起来像"没在用 GPU"。这种问题需要从数据加载策略上下手,比如增加num_workers、开启pin_memory,或者加大 batch size。这个排查思路可以帮助你快速定位性能瓶颈到底是在"模型算不动"还是在"数据喂不上"。

4.3 模型服务延迟高:先量化,再想别的

模型部署上线后,响应延迟是大家最焦虑的指标之一。文本分类这种任务,如果用的是 BERT 级别的小模型,延迟通常还好。但如果用更大的模型,或者在上生产环境时遇到并发冲击,延迟和服务稳定性就成了大问题。

我给的优化顺序很明确:

  1. 先看有没有冗余计算:模型有没有被反复加载?有没有做批次推理?请求之间能不能复用 tokenization 结果?
  2. 再做模型量化:把 FP16/FP32 的权重转成 INT8,体积直接缩小四分之三,推理速度提升两倍以上,精度损失通常在一个点以内。
  3. 最后再考虑换更小的模型或者蒸馏:不是万不得已不建议走到这一步,因为换模型意味着要重新评估和验收。

以 BERT 分类模型为例,用 PyTorch 自带的量化接口做动态量化,只需要几行代码:

import torch model = load_model() model.eval() quantized_model = torch.quantization.quantize_dynamic( model, {torch.nn.Linear}, dtype=torch.qint8 ) torch.save(quantized_model.state_dict(), "model_quantized.pt")

这个简单的改动就能让推理速度有明显提升。而量化之后一定要重新跑一遍测试集,确认精度下降在可接受范围内,不然就是拿准确性换速度,得不偿失。

4.4 模型版本管理一团乱:靠文件名和"最终版"注释是行不通的

模型不像普通代码,你没法用一个git tag就管理好所有内容——因为一个模型除了代码,还涉及数据版本、预处理配置、超参数、训练框架版本、权重文件。我见过最离谱的一次,同事把模型文件命名为best_model_final_v3_真的不改了.pkl,结果训练时发现加载这个"不能改"的模型会报错,因为依赖库版本对不上。

我现在的模型版本管理策略是三层配合:

  • MLflow 管理实验与产物:记录参数、指标、模型文件,每个实验都有唯一的 run_id。
  • 模型注册表管理正式版本:把要上线的模型从 MLflow 注册到模型仓库,标注版本号、状态(Staging/Production/Archived)。
  • 代码仓库只存代码不存权重:权重文件一律走对象存储,代码里只记录模型版本的引用名称或 hash 值。

这样做之后,任何一次线上模型的生成过程都完全可追溯。而且回滚变得特别简单——模型仓库里翻出上一个版本的 hash,改一个配置就能切回去,不需要翻聊天记录找文件名了。

4.5 数据漂移的早期预警:在指标崩之前发现问题

前面提到过监控,这里单独铺开来聊聊怎么做"早期预警"。线上模型效果变差,往往不是突然发生的,而是被两类漂移慢慢侵蚀的:

  • 数据漂移:输入分布慢慢偏离训练分布。比如你的模型是在商品标题上训练的,但后来商家上架了一批新的标题风格,包含大量表情符号和特殊字符,模型的输入空间悄悄变了。
  • 概念漂移:输入分布没变,但"正确答案"变了。比如用户对推荐内容的喜好发生转变,同样的特征组合,标签已经不再适用。

应对策略是建立两个层面的哨兵。第一层是输入特征分布监控,用训练集的均值/分位数作为基准,线上实时计算当前特征分布,超过阈值就告警。第二层是抽样人工标注,定期从线上请求里抽样一批数据,找人标注,对比模型预测和人工标注的一致性。精度下降超过 5% 就触发重新训练流程。

这套机制听起来复杂,但最小实现其实不费事:每天跑一个 Python 脚本,算一下当天的 ps

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进