做AI工程和做AI模型是两回事。拿我自己举例,跑通一个Notebook里的图像分类demo,或者把HuggingFace上的模型下载下来做推理,这些都算不上“AI工程”。真正的AI工程,是从数据采集那一刻开始,一直到模型稳定运行在线上、还要能监控它“什么时候会变差”、出问题了能快速定位——这整条链路,才叫工程化。ai-engineering-from-scratch这个项目,我理解为就是围绕“从零开始把AI模型做成一个可靠、可维护、可复现的系统”这个目标来设计的。我自己搭建过不止一套这样的全流程环境,踩过很多坑,这篇就把我的完整路径写下来,给同样想从“调参选手”过渡到“AI工程从业者”的朋友做一个参考。
适合看这篇文章的人,我认为有两类:一类是已经会用PyTorch或TensorFlow训练模型、但对“部署”“监控”“数据版本管理”这些词还有点发怵的算法工程师;另一类是想往AI工程方向转的纯后端开发,想了解整个AI落地链路上到底有哪些绕不开的环节。文章不吹概念,只讲我在实际项目中验证过的东西,包括技术选型、代码结构、部署方案、避坑技巧,全程都有具体的文件和命令示例。
1. 项目整体设计与路径规划
任何一个叫from-scratch的项目,最重要的不是代码多炫,而是路线图是否清晰。我在开始项目之前,先把从零到线上服务的完整链路拆成了四个阶段:数据工程 → 模型训练与实验管理 → 模型部署与服务化 → 监控与持续迭代。这四个阶段不是孤立的,它们彼此之间有明确的输入输出关系,比如数据工程产出的“版本化数据集”是训练阶段的输入,训练阶段产出的“模型产物+评估报告”是部署阶段的输入。这样规划的好处是,你心里永远有一张地图,不会在某个环节里钻牛角尖出不来。
1.1 核心需求拆解:模型精度不是唯一的KPI
很多人刚接触AI工程时,会本能地把注意力放在“怎么把模型精度刷高”上,这是典型的算法思维。工程思维要求你换一套KPI:可复现性(三个月后还能不能重建出一模一样的结果)、可维护性(模型出问题时,能不能快速定位是数据变了、还是代码变了、还是环境变了)、可观测性(线上模型的延迟、吞吐、预测分布是否正常)。带着这套KPI去审视整个项目,你会发现很多以前忽略的环节变得极其重要,比如数据版本管理、实验日志记录、依赖锁定。从我的实践经验看,精度固然重要,但一个可复现、可监控、虽然精度稍低一个点但在线上稳定运行半年的模型,价值远大于一个精度刷到SOTA却没人敢动的“艺术品模型”。
1.2 技术栈选型:为什么坚持自建全流程
市面上现成的AI平台很多,从云端托管训练到端到端AutoML都有,理论上可以省很多事。但既然是from-scratch项目,我的建议是核心链路尽量自建,理由有三个:
- 可控性:托管平台通常是黑盒,超参数怎么调度、数据怎么缓存你都不清楚,出了问题只能提工单,排查效率极低。
- 成本:个人学习项目或小团队场景下,用托管平台跑大量实验的费用并不划算,自建一套基于开源组件的方案基本只有服务器成本。
- 认知深度:只有自己亲手处理过数据版本化、模型导出、服务部署、监控告警这些问题,你才真正理解AI工程每一环的原理。这个认知价值是无法替代的。
我用的是这套组合:Python 3.10 + PyTorch 2.0 + HuggingFace Transformers作为模型训练底座;DVC做数据版本管理;Hydra做配置管理;MLflow做实验跟踪;ONNX Runtime加FastAPI做模型服务;Prometheus加Grafana做监控。选型逻辑很简单——都是社区活跃、文档完善、遇到问题能搜到答案的组件,避免在冷门工具上浪费时间。
1.3 项目目录结构设计
目录结构是工程化的第一张名片,一个好的目录结构能让任何人接手项目时快速找到对应的模块。我在项目里采用了这样的布局:
ai-engineering-from-scratch/ ├── configs/ # Hydra配置目录 │ ├── data.yaml │ ├── model.yaml │ ├── train.yaml │ └── inference.yaml ├── data/ # 数据目录(DVC管理) │ ├── raw/ # 原始数据,只读 │ ├── processed/ # 清洗后的数据 │ └── augmented/ # 增强后的数据 ├── src/ # 核心代码 │ ├── data/ # 数据采集、清洗、增强 │ ├── features/ # 特征工程与预处理 │ ├── models/ # 模型结构定义 │ ├── train/ # 训练脚本与逻辑 │ ├── evaluate/ # 离线评估脚本 │ └── serve/ # 推理服务与API接口 ├── experiments/ # 实验记录与日志 ├── models/ # 模型产物导出 ├── notebooks/ # 探索性分析Notebook(只做探索,不做生产) ├── tests/ # 单元测试与集成测试 ├── scripts/ # 部署、监控等运维脚本 └── pyproject.toml # 项目依赖管理这个结构我打磨过好几次,核心原则就两条:数据和代码分离、配置和逻辑分离。不要把数据目录嵌套在代码目录里,也不要在代码里硬编码路径,所有路径统一从配置层读取。这样做的好处,在我后续切换到新服务器做部署时体现得淋漓尽致——打包代码后,只需要修改配置文件的路径和参数,不需要动一行逻辑代码。
2. 环境准备与工程化基建:复现性的地基
如果说模型是AI项目的“上层建筑”,那环境依赖就是它的地基。地基不稳,上层建筑随时可能塌。很多同学在本地跑通代码后,部署到服务器上却报出一堆错误,根本原因就是环境没有做严格的工程化管理。
2.1 Python环境与依赖锁定
我建议使用pyenv加Poetry的组合来管理Python版本和项目依赖。pyenv可以轻松安装指定版本的Python,避免系统Python被污染;Poetry则通过pyproject.toml声明依赖,通过poetry.lock锁定依赖的精确版本。这个组合比单纯的requirements.txt强很多,因为requirements.txt通常只锁定顶层依赖,而Poetry会解析并锁定完整依赖树。
这里有一个非常关键的工程细节:必须把poetry.lock文件提交到代码仓库。很多人忽略这一点,以为有了requirements.txt就够了。但实际上,如果子依赖的版本在三个月后有更新,即使顶层依赖不变,pip install -r requirements.txt也可能装出不同版本的依赖树,导致模型结果无法复现。Poetry的lock文件保证了每次安装的依赖树完全一致,这是可复现性的第一道保障。
另外,从我个人经验看,GPU环境相关的依赖(CUDA、cuDNN、PyTorch版本)一定要在文档中明确记录,光有lock文件还不够。因为CUDA版本直接影响算子行为,某些情况下甚至会让模型结果产生细微差异。我习惯在项目根目录放一个environment.md,把CUDA驱动版本、PyTorch版本、显卡型号全部记录下来,方便后期排查复现问题。
2.2 数据版本管理:Git不能替代DVC的四个理由
AI工程和传统软件工程最大的区别之一,就是数据在系统里属于一等公民。传统项目用Git管理代码就够了,但AI项目中数据动辄几十GB甚至几百GB,而且数据会随着业务持续更新,这就需要一个专门的数据版本管理工具。我用的是DVC(Data Version Control)。
为什么不用Git LFS?原因很简单:Git LFS虽然能管理大文件,但它没有“数据血缘”的概念。DVC的核心价值,不只是存大文件,而是帮你建立“代码版本+数据版本 → 模型实验结果”的完整关联。每次训练实验,DVC会自动记录用的是什么版本的数据集、什么版本的代码,这样你现在想复现三个月前的某个实验,只需要切换Git分支、然后dvc checkout一下,数据集就会自动恢复到对应版本,整个过程像给数据打了一个和代码同步的标签。
在实践中,我深深体会到DVC还有一个隐藏优势:它天然支持增量同步。每次数据集更新,DVC只会上传变更的部分,而不是整个数据集。对于频繁迭代数据项目的团队,这能省下大量存储和带宽成本。曾经有一次我的数据集更新了10%,用DVC推送只用了原先完整推送的五分之一时间,而团队里的同事用scp重传整个数据集,花了四倍时间。
2.3 配置管理:把所有可变参数收进YAML
我见过太多项目,超参数和路径散落在训练脚本的各个角落,改一个参数要到处搜索。这个问题的标准解法是用Hydra做配置管理。Hydra允许你用一个主配置加多个子配置来组合训练参数,支持从命令行覆盖任何参数,还能自动记录每次运行用的完整配置。这样每次实验的配置都会生成独立的输出目录,极大方便了实验对比。
一个我踩过的坑是:配置层和代码层之间的“命名漂移”。最初我的模型配置里有个参数叫learning_rate,后来为了跟论文对齐改成了lr,但配置文件和代码里的引用没有同步替换,导致训练时某次实验悄悄用回了默认值0.001,而我以为用的是0.0001。这个问题直接导致了半天的调参时间被浪费。从那以后,我养成了一个习惯:无论代码还是配置文件,所有变量命名以一处定义为准,并且在代码启动时严格校验配置文件里的每个参数是否被实际读取。Hydra有一个hydra.job.config.override_dirname参数,配合日志记录可以明确看到每个配置项被谁消费了,强烈推荐大家用起来。
3. 数据工程:决定模型上限的隐形关卡
很多人以为数据工程就是“把图片标个类、把文本洗一下”,直到自己跑通全流程才明白,这个环节承担了项目中超过40%的工作量。模型能力的天花板,很大程度在数据进入模型之前就已经定死了。
3.1 数据采集与清洗的实操细节
我做的第一个文本分类项目,从网上抓了大约20万条语料,原以为模型训练会很顺畅,结果第一个版本就给了我一个下马威——验证集准确率只有64%,明显偏低。排查到最后发现,抓下来的文本里有大量重复内容(同一个网页被链接抓取脚本重复收录)和大量无意义的模板片段(比如“登录后查看更多”“点击下载”这类页面底部的固定文案)。这个教训告诉我,数据清洗的第一步不是“删掉空行”,而是去重和模板过滤。
- 去重:我用的是MinHash加SimHash做近重复检测,而不是简单的字符串精确匹配。真实采集的数据里,两条文本可能只有几个字符的差异,精确去重抓不到这种重复,近重复检测才能有效压缩冗余。
- 清洗:统一编码(强制转为UTF-8)、去除空值、过滤长度异常样本、修正标签错误。标签错误是最隐蔽的问题,一套有效的办法是做“置信度交叉验证”——训练一个初步模型,把预测置信度极低但模型很确定的样本挑出来人工复检,我曾经用这个办法找回了约2%的错标样本。
- 脱敏:涉及用户数据时,要在清洗阶段完成敏感信息替换,我用的是基于正则表达式的规则引擎加人工抽检,保证脱敏覆盖率。
3.2 数据标注与质检流程
如果项目要从零标注数据,建议把标注任务拆分得足够细,同时一定要建立“质检集”机制。质检集是指预先标注好的、标注团队不知道标准答案的样本,混在待标注任务中,用来估算标注准确率。我见过不止一个项目,标注团队在赶进度时疯狂刷任务,质量肉眼可见地下降,如果没有质检集及时发现,等模型训练完才发现数据质量不行,返工成本极高。
质检集占比我不建议低于5%,上线标准是准确率不低于95%。这里还有一个容易被忽略的细节:标注规范要写死,不能用口头约定。比如“这条评论是负面情绪”的定义,可能不同标注员理解完全不同,提前写清楚边界案例,能减少大部分返工。
3.3 数据增强与标签平衡
数据增强是提升模型鲁棒性的有效手段,但要强调:增强策略不要和业务逻辑脱节。做文本分类时,我用的是同义词替换加随机删除;做图像项目时用的是随机裁剪、翻转和色彩抖动。关键原则是让增强后的样本仍然“看起来像”真实业务数据,而不是制造一堆模型在线上根本遇不到的虚构样本。
标签平衡问题也很重要。如果类别分布严重失衡,我建议第一步不要急着用复杂的Loss函数(如Focal Loss),而是先尝试简单的过采样/欠采样,观察模型表现,同时保留原始分布作为对照实验。因为重采样策略和模型结构之间通常存在交互效应,简单方案往往比复杂方案更稳健。
3.4 数据版本化与血缘关系
数据血缘在工程落地中主要体现在:每次模型训练结果能反向追溯到“用的哪一份数据、哪个版本”。DVC在这里起到关键作用——训练脚本里记录当前dvc.lock文件的哈希值,实验管理工具自动关联数据集版本。这样线上模型效果变差时,你排查的第一个动作就是对比当前线上数据版本和训练数据版本的分布差异,快速锁定是不是“数据漂移”导致的问题。
数据血缘还能帮你避免一个经典大坑:测试集泄漏。如果不做数据版本管理,经常会在切分完训练集和测试集之后,又用全量数据(包含测试集部分)做了一次标准化,或者不小心把增强后包含测试集内容的样本混进训练集。有了血缘记录,你就能清晰看到每个切分文件的来源,从流程上堵住泄漏的口子。
4. 模型训练与实验管理:让每一次实验都可追溯
训练实验是整个项目里迭代最频繁、最容易产生混乱的环节。没有实验管理的AI项目,通常表现为“跑了很多实验,但没人说得清哪个实验是最优的,也没人能复现”。
4.1 训练脚本的结构设计
我推荐的训练代码结构是把数据加载、模型定义、训练循环、评估逻辑拆成独立模块,通过一个主入口串联起来。具体到文件组织就是src/data、src/models、src/train、src/evaluate这几个目录。这种设计的好处是模块可以单独测试和替换,比如上线新数据集时只需要替换data模块的加载逻辑,不用动训练循环。
训练主脚本我一般控制在200行以内,核心逻辑清晰可见:读取配置 → 加载数据 → 初始化模型 → 循环训练 → 周期评估 → 保存checkpoint和指标。不要把所有代码堆在一个巨型Notebook里,那也许是算法探索时最舒服的方式,但绝不是工程交付时最稳妥的方式。Notebook适合做探索性分析,生产代码必须脚本化。
4.2 超参数管理与自动搜索
配置管理解决的是“参数存在哪”的问题,超参数搜索解决的是“参数从哪来”的问题。我用Optuna做贝叶斯搜索,核心思路是:固定一切可固定项,只搜索那些对模型效果有显著影响的参数(如学习率、批大小、层数、Dropout比例)。搜索前设定预算上限(比如最多跑20组实验),避免无节制地消耗时间和算力。
一个容易犯的错误是搜索时忽略“计算资源约束”。有些超参组合效果虽好,但推理耗时远超业务要求,比如模型层数加到很深导致单次推理延迟翻倍。我的习惯是在搜索的目标函数里,除了精度指标,同时加入延迟约束分数(比如延迟超过50ms直接扣分),这样搜索出的最优解才可能真正部署上线。
4.3 训练过程监控与Checkpoint管理
训练阶段的监控,我用的是TensorBoard加MLflow的组合。TensorBoard负责实时展示loss曲线、学习率变化、梯度分布,方便观察训练状态;MLflow负责做实验归档,记录每个实验的配置、指标、产物目录和代码版本。
训练阶段最实用的经验是**“早停+模型快照”策略**。不要只保存“最后一个epoch”的模型,因为训练后期很容易过拟合,最佳模型往往出现在中间某处。我会在每个epoch后评估验证集指标,同时保存两份文件:一份是最新checkpoint(用于中断恢复),一份是验证集表现最好的checkpoint(用于最终选择)。模型快照的元信息(epoch数、验证指标、超参哈希)通过MLflow自动记录,随时可以回溯。
4.4 模型评估:不要只盯着一个指标
模型评估最容易犯的错误,是只看单一的Accuracy或AUC。工程上,我要求评估报告覆盖四类指标:整体指标(准确率、F1等)、分面指标(按类别、按时间段、按用户人群分别计算,发现隐藏短板)、鲁棒性指标(对输入扰动、噪声样本的容忍度)、性能指标(推理延迟、吞吐量、显存占用)。只有综合这四类指标,你才能判断一个模型能不能上生产。
在结构化数据项目里,我曾发现一个模型整体准确率96%,看起来很优秀,但按时间段拆开一看,最近三个月的样本准确率明显下滑,而老数据样本准确率接近99%。这说明模型的实际表现远没有整体指标显示得那么光鲜,数据漂移问题已经存在。要不是分面评估,这个隐患可能要在线上跑一段时间才会暴露。
5. 模型部署与服务化:从模型产物到线上服务
训练完成只是万里长征的一半,把模型变成真正可调用、低延迟、高可用的服务,是另一个技术深水区。
5.1 模型导出:PyTorch到ONNX再到TensorRT
模型导出环节,最常见的做法是把PyTorch模型转为ONNX格式,然后在推理服务器上用ONNX Runtime加载,必要时再进一步转为TensorRT引擎获得更高性能。PyTorch模型本身部署不是不可以,但ONNX Runtime和TensorRT在推理性能上有明显优势,而且能脱离PyTorch环境运行,减小镜像体积和部署风险。
导出时我踩过一个坑:动态维度问题。最初训练时的输入固定是[batch=32, seq_len=128],导出ONNX时没设置动态轴,结果部署后客户端传入[batch=1, seq_len=200]直接报错。正确做法是在导出时显式标注动态维度,允许推理时的batch大小和序列长度变化。这需要在torch.onnx.export时传dynamic_axes参数,具体配置我建议写进脚本,而不是靠记忆。
5.2 推理服务框架搭建与性能优化
推理服务我选用FastAPI,因为它原生支持异步、自动生成OpenAPI文档、性能在Python框架中属于第一梯队。核心实现逻辑很简单:接收请求 → 文本预处理 → TensorRT/ONNX推理 → 后处理 → 返回结果。但性能优化的空间很大,我总结出几条关键经验:
- 批量推理:如果QPS较高,一定要做动态批处理(dynamic batching),把一段时间窗口内累积的请求合并成一个大batch,大幅提升吞吐。这在CPU和GPU场景中都有效,NV Triton里这叫
dynamic_batcher,自建FastAPI服务可以自己实现一个简单的批处理队列。 - 预分配缓冲区:避免每次请求都重新申请数组或拼接字符串,在服务启动时就预分配好最大尺寸的缓冲区,可以减少GC压力。
- 缓存预处理结果:在NLP服务中,分词和向量化通常是重复劳动,对高频请求可以加一层缓存。
服务端超时和熔断这个细节也值得专门提一句。线上服务不能因为某个请求卡死就拖垮整个进程,必须要给推理请求设置超时时间(比如300ms),超过直接返回错误;推理后端如果连续报错,要触发熔断,避免无意义的雪崩。这些内容虽然不复杂,但如果不做,线上事故往往就在这些不起眼的地方爆发。
5.3 性能压测与资源评估
上线之前,我强烈建议做一轮正规的压测。压测工具我用Locust,它支持分布式压测,能模拟多用户并发场景。压测的目标不是“跑通”,而是得出三个关键数据:P95延迟、最大吞吐量、资源瓶颈(是CPU打满、内存不够还是GPU利用率低)。有了这三个数据,你才能规划合理的副本数和流量入口配置。
压测过程中我发现的典型问题是:单次请求延迟不高(平均30ms),但并发一高,延迟迅速恶化到300ms以上,初步怀疑是Python GIL加上频繁的CPU密集操作导致的排队。解决办法是把预处理逻辑改成异步io,同时把高耗时的tokenizer部分做成批量预计算缓存,最终把高并发下的P95延迟压回了60ms。压测报告里我会附带一个建议副本数估算:按单实例100QPS、目标容量400QPS计算,至少需要4个副本,再额外预留20%冗余应对突发流量。
5.4 线上监控:漂移检测与告警体系
模型上线不是终点,监控才是“稳定运行”的隐形支柱。我的监控方案分三层:
- 指标监控:通过Prometheus采集请求延迟、QPS、错误率,Grafana展示大盘,超过阈值触发Alertmanager告警。
- 预测分布监控:记录线上模型预测值(比如分类概率分布)的统计量,周期性对比训练时的分布,用PSI(Population Stability Index)量化漂移程度。PSI超过0.25就触发告警,说明线上数据分布已经明显变化,模型可能需要重新训练。
- 服务质量监控:通过实时抽样的方式,把线上预测结果记录到日志系统,定期人工抽样评估预测质量。
这三层监控我是在一次线上事故之后才补齐的。当时模型运行了一个月,业务方反馈效果变差,但指标监控一切正常。排查后才发现,用户行为模式已经变了,而模型还在用旧分布做预测。从那以后,预测分布监控就成我部署模型的标配,强烈建议所有做模型服务的团队都加上。
6. 常见问题与排查技巧实录
最后这部分,整理下我在实际项目中反复遇到的高频问题,按问题表现、原因、解决办法列出来,方便你直接对照排查。
6.1 典型问题排查速查表
| 问题表现 | 可能原因 | 排查思路 | 解决办法 |
|---|---|---|---|
| 线下精度高线上效果差 | 数据分布漂移、预处理不一致 | 对比训练数据和线上数据特征分布;核对线上预处理逻辑是否和训练一致 | 重建线上样本分布画像;统一预处理代码为共享模块 |
| 模型跑一段时间后延迟变高 | 内存累积、显存碎片化、日志IO阻塞 | 查看资源监控曲线,分析是否有增量上涨 | 定期重启工作进程;优化缓冲区释放逻辑;异步写日志 |
| 训练无法复现 | 随机种子未固定、依赖版本不一致、GPU非确定性算子 | 检查种子、lock文件、对比环境变量 | 固定随机种子;使用torch.use_deterministic_algorithms(True);完整记录运行环境 |
| 推理服务启动报错“shape mismatch” | 模型导出的固定shape与实际输入不一致 | 用真实输入shape做一次推理验证 | 导出时配置dynamic_axes,用多样化shape测试 |
| 验证集指标高但业务转化低 | 离线指标与业务目标不一致 | 拆分业务漏斗指标,逐层对比 | 把业务指标纳入离线评估,做指标对齐分析 |
6.2 复现性陷阱:随机种子之外的隐患
随机种子是复现性的第一道门槛,但就算你每次都固定了random.seed(42)numpy.random.seed(42)torch.manual_seed(42),你依然可能遇到无法复现的情况。原因通常在以下几个方面:
- cuDNN的非确定性:某些算子(如卷积)在GPU上有非确定性实现,需要在代码里设置
torch.backends.cudnn.deterministic = True和torch.backends.cudnn.benchmark = False。注意benchmark关闭会影响少量性能,但换来的是可复现性,值得。 - 数据加载顺序:多进程
DataLoader的worker在不同机器上的shuffle行为可能不同,一定要固定generator和worker的随机种子。 - 浮点数累加顺序:分布式训练或并行reduce时,浮点数累加顺序不同会导致微小差异,累积起来可能影响结果对比。工程上接受极小波动即可,不必追求比特级复现。
6.3 资源不够时的工程解法
大多数个人项目和中小团队都会面临算力紧张的问题。就我自己的实践而言,较有效的方法按优先级排序是:
- 模型量化:Post-Training Quantization(如INT8量化)能把模型体积压缩到原来的四分之一,推理加速2到3倍。精度损失通常在1%以内,几乎不影响体验。
- 模型剪枝:结构化剪枝(如通道剪枝)能直接减少计算量,适合部署在边缘设备。但剪枝需要微调恢复,流程比量化长。
- 知识蒸馏:用大模型做teacher,小模型做student,能在模型体积大幅缩小时保留较多精度。适合那些基础模型能力强、但线上成本敏感的场景。
- 混合精度训练:FP16训练能显存减半、速度翻倍,配合
torch.cuda.amp实现非常简单,是训练阶段的默认选项。
这些手段单独用都有收益,组合拳效果更佳——我最近的实践是“FP16训练 + INT8量化 + 动态批处理”三件套,线上吞吐提升了近4倍,精度只掉了0.6%。当然前提是你要先做好性能基线测试,否则看不出每一步优化到底带来了多少收益。
6.4 我建议你在从零开始时优先做的三件事
根据个人经验,如果你要复制这个从零工程化的项目,最好的路径不是按部就班照着文章从头做到尾,而是先完成三个“里程碑式”的验证:
第一,用100条样本把“数据 → 训练 → 导出 → 部署 → 请求”这条全链路跑通,哪怕效果很差,但链路通了,后面所有工作都只是优化。第二,在10个场景下做数据版本管理加实验复现练习,确保自己完全搞懂DVC、MLflow和Hydra如何协作。第三,部署后立刻配上预测分布监控,哪怕规则简单一点,也要让“漂移”这个隐患在第一天就被你的系统感知到。
做完这三件事,你已经具备了一个合格的AI工程入门者的核心能力:不是某个模型的精度刷多高,而是整个系统在你手里是透明的、可靠的、可控的。我在实操中最大的体会是,AI工程化不是某一项技术,而是一种思维习惯——每个步骤都要问自己“这一环节挂了,我怎么发现、怎么恢复、怎么追溯”。把这个习惯带进所有项目里,远比学会任何单一框架都重要。