“AI Engineering”这几年被喊得震天响,各种课程和文章满天飞,但从零开始真正把它落地成自己的东西,我发现很多教程都避重就轻。这个项目名字叫 ai-engineering-from-scratch,说白了,它的核心不是教你调一个某某模型的接口,而是从头到尾把AI工程化这条链路完整走一遍——数据怎么处理、模型怎么选和训、服务怎么部署、上线后怎么迭代,环环都要扣上。我最初也是被铺天盖地的“十分钟入门大模型”吸引进来的,但真正踩过坑之后才明白,那种文章除了能带来一点虚假的成就感,对实际干活儿毫无帮助。这篇博客我会把当初从零折腾这条链路的过程、关键节点的决策思路、那些书本上不会写但实测很要命的细节,全部摊开来聊一聊。你要是刚入门想找一条踏实的路径,或者已经写了几个脚本但总觉得不成体系,这篇文章值得你花五分钟慢慢看。
1. 从零开始前,先建立完整的AI工程地图
1.1 为什么“会调库”不等于“懂工程”
很多人在第一步就走偏了,以为学AI工程就是多跑几个开源模型的Demo,把网上的代码粘贴复制以后能出结果就算会了。我最早也是这么干的:拿BERT跑了个情感分析,觉得差不多入门了。结果到了真正需要把模型接到业务里时,才发现模型训练脚本是单独的,数据处理是另一套逻辑,部署服务还得重新写接口,日志和监控完全没有,模型一更新离线流程就崩。这就是典型的“会调库”和“懂工程”之间的鸿沟。
工程化要解决的是模型从“能跑”到“能持续稳定地跑”的问题。一个完整的AI工程系统至少包含五个环节:数据管道、模型实验、训练与评估、服务化部署、监控与迭代。这五个环节每个都有独立的工具和方法论,就像一栋房子的地基、框架、水电、装修和后期维护缺一不可。大多数人只接触过“框架”和“装修”,对地基和维护的概念几乎为零,所以一涉及真实项目就露馅了。
1.2 优先搭建认知体系,而不是急着写代码
我在搞这个项目的过程中,体会最深的一件事是:先别急着写代码,先把整个链路在脑子里跑一遍。所谓认知体系,就是知道每一步要解决什么问题、有哪些主流方案、各自的优缺点是什么,这样遇到具体场景时才知道该查什么、怎么选型。
我建议新手拿到任何AI工程需求时,先画一张数据流向图:原始数据从哪里来、经过什么清洗、如何标注、怎么划分训练集和验证集、模型训练完存到哪里、在线推理走什么协议、预测结果怎么反馈给用户、又怎么回收新的样本。这张图不需要用什么专业工具,纸上画也行。但只有把这十来个节点想清楚,你写代码时才知道每个函数是为了哪一环服务的,而不是东拼西凑。
1.3 对这个项目制定一条清晰的路线图
我的路线图是按“四段式”走的,每一段都有明确目标,不急不躁。
- 第一段:搞定Python、SQL和Linux基础,能写清晰的数据处理代码,能在服务器上部署环境运行程序,目标是一个月内能独立把一份脏数据清洗成可训练的结构化数据。
- 第二段:掌握机器学习基础理论和常用模型的适用场景,包括线性模型、树模型、神经网络的基本原理,能拿着sklearn和PyTorch做标准的分类、回归任务,目标是不看教程也能复现经典模型。
- 第三段:进入深度学习与Transformer体系,理解词向量、注意力机制、预训练和微调的完整流程,会用Hugging Face工具链跑通一个完整的NLP任务,后端打通模型训练与推理接口。
- 第四段:补上工程化能力,包括Docker打包、模型服务框架选型、CI/CD流程、监控告警和模型版本管理,目标是模型从训练到上线再到回滚,全程可追踪。
这条路线我花了大约四个月走完,中间踩了不少坑。如果你时间紧张可以压缩,但顺序不建议跳。后面所有内容,我基本就是按这条路线围着我自己的实战项目展开的,每一步的细节和坑都会提到。
2. 核心细节解析:四个关键环节的实操拆解
2.1 数据工程的“脏活累活”远比想象中重要
做AI工程最容易被低估的就是数据环节。我在项目里接手过一份用户行为日志,字段七零八落,时间戳有的精确到秒有的到毫秒,同一个用户ID在不同表里格式还不一致。如果直接扔给模型训练,模型大概率学到的全是噪声。数据工程的本质是把非结构化的原始材料转化为模型能理解的高质量训练样本,这一步直接决定模型效果的上限。
我实践下来的标准流程是三段式:抽取、清洗、特征构造。抽取阶段主要解决数据来源问题,可能是从数据库拉取、接消息队列、读离线文件,关键是搞清楚字段含义和更新频率。清洗阶段处理缺失值、异常值、格式统一化,这里推荐pandas配合pydantic做数据校验,比一堆if-else靠谱得多。特征构造阶段则把业务理解编码进数据里,比如用户行为序列要切成固定窗口、文本要做分词和去停用词、数值要做归一化或分桶。
这部分我在实操中总结出的最大教训是:每一步清洗操作都必须留痕。你清洗了多少条、丢弃了多少条、为什么丢,全都要能追溯。否则模型效果不好时,你根本分不清是特征工程的问题、数据质量的问题还是模型结构的问题,排查起来就像大海捞针。建议每完成一个环节就输出一份数据质量报告,哪怕只是一张统计表,对后续定位问题都极有帮助。
2.2 模型选型不是越复杂越好
模型选型是另一个容易走极端的环节。新手倾向于追新追大,觉得参数越多、结构越复杂效果就越好。但实际上在绝大多数场景下,先用简单模型建立基线,是成本最低、最容易定位问题的做法。我个人的经验准则是:能用逻辑回归解决的问题就不要上GBDT,能用GBDT解决的问题就不要上深度模型。模型复杂度每提升一档,数据需求、训练成本、调试难度都是成倍增加的。
具体来说,如果你的数据是纯表格型的,特征之间关系不算太复杂,几千条到几万条样本,LightGBM和XGBoost其实是最优选,训练快、可解释性也好。如果你处理的是文本、图像、语音等非结构化数据,或者表格数据里包含大量高维稀疏特征和序列关系,这时候再引入深度模型才划算。Transformer系列在文本领域几乎是事实标准,但Vision Transformer在中小规模图像数据上的优势并没有想象中那么大,ResNet的变体往往更省心。
还有一个很多教程不提但实际很重要的点:模型选型要同时考虑推理成本和维护成本。上线之后要持续维护的不是模型本身,而是它的依赖环境、输入输出格式和监控指标。一个在性能上领先2%但推理速度慢一倍、部署方式奇特的模型,在实际业务里很可能就是个灾难。我后来做选型时都会加一张表,把离线指标、推理延迟、显存占用、部署难度全部列出来,再综合决策。
2.3 训练与评估环节要建立自己的“实验账本”
训练这个环节看似就是写个训练脚本跑起来,但真正专业和业余的分水岭,在于你有没有一套规范的实验追踪体系。我刚开始训练模型时,改一个参数就重跑一遍,跑完把结果记在脑子里,过两天忘了上次用的什么配置,又得翻代码历史记录,效率极其低下。后来我把MLflow引入工作流,每个实验的名称、参数、数据集版本、评估指标、模型文件地址全部自动记录,才彻底摆脱这种状态。
评估环节同样有很多细节。很多人只看准确率或F1,这在样本不均衡时很容易被表面数字欺骗。我做个分类任务时,训练集里正样本占比只有5%,模型全部预测负样本准确率也能到95%,但从业务角度看这个模型毫无价值。所以我在项目里强制自己同时关注precision、recall、ROC-AUC和PR-AUC,根据业务对误报和漏报的容忍度来定最终模型选择标准。
评估时还有一个容易被忽略的操作:单独留出一份“最后验证集”。就是从头到尾不参与训练、不参与调参、只在所有实验结束后测一次的数据。这样做是为了防止实验过程中你对验证集产生了隐式过拟合——调参次数多了以后,你其实是在拟合验证集,而不是在提升模型的真实泛化能力。这个教训我是吃过大亏的,当时调了一个多星期的参数,验证集指标一直涨,最后换成线上真实数据直接掉点严重。
2.4 部署上线:从Python脚本到生产服务的最后一公里
部署环节是ai-engineering-from-scratch这个项目里最容易卡住新人的地方。训练好的模型只是一个包含参数的文件,它要变成一个生产环境里稳定服务用户的系统,中间有一大段路要走。我最常用的方案是:先把模型导出成通用的格式,然后用容器把运行环境连同模型一起打包,最后用专门的推理框架对外提供服务。
具体来说,PyTorch模型我一般会用TorchScript或ONNX格式导出,这样脱离Python原始定义也可以加载推理。然后写一个封装了预处理、推理、后处理逻辑的服务类,用FastAPI暴露HTTP接口。最后整个服务打包成Docker镜像,部署到服务器上,前面挂Nginx做负载均衡。这套组合已经被验证了很多次,对于中小型项目足够稳定可靠。
模型部署有一个细节很多人一开始注意不到:训练环境和推理环境要保持一致。我吃过一次亏是本地Python 3.8 + PyTorch 1.10,服务器上是Python 3.10 + PyTorch 2.0,模型加载后前向传播的结果直接对不上。虽然模型结构和权重都是一样的,但底层算子实现有细微差别,输出就变了。所以建议在requirements.txt里锁定所有关键依赖的精确版本,并用容器锁定整个系统环境。
3. 实操过程:从0到1搭建一个可运行的AI服务
3.1 明确场景和数据集,不泛泛而谈
纸上谈兵没有意义,我在这个项目里选定的实操场景是一个文本多分类任务:对客服工单自动打标签。这个场景足够贴近业务,数据不需要额外采集,标注难度也不高,而且整个AI工程链路的每个环节都能涉及。数据集我用的是公开的中文语料,包含购物、物流、售后、投诉等几个类别的文本,总共差不多一万条,训练测试约八二开。
选定场景以后,第一步是编写数据预处理脚本。这步看起来不难,但我在处理中文文本时还是踩了些坑,比如全角半角符号混用、URL和数字要不要保留、分词器的选择。这些决策没有绝对的标准答案,但一定要基于你对下游模型的理解来做。我最终采用的处理方式是:去除HTML标签和无效字符,全角转半角,保留中文、数字和部分英文字母,再用对应模型的分词器做切分,不额外用jieba分词。原因是后来的模型都是基于子词切分的,提前用词级分词反而会割裂语义单元。
3.2 基线和进阶模型两步走,效果差距一目了然
第一步我用TF-IDF加逻辑回归作为基线。这个组合的好处是训练快、解释性强、对硬件要求极低,能快速验证“数据这块有没有大坑”。当时跑完测试集F1大概是0.82左右,对多分类任务来说已经算不错的起点。通过分析错误样例,我发现“退款”和“退货”两个类别经常互相混淆,于是回头检查数据,发现很多标注本身就存在歧义,同一句话在不同标注员眼里归类不一样。这就是数据清洗阶段难以完全避免的问题,后续处理方式是在特征层面加了关键词匹配规则来辅助区分。
第二步换成基于预训练的中文BERT模型,用Hugging Face的transformers库做微调。数据量一万条对微调BERT来说偏少,所以我采用了分层采样保证每个类别在训练集和验证集中的分布一致,训练轮次控制在3轮,学习率设为2e-5,batch size设为16。微调后测试集F1提升到0.91,提升效果非常明显。这个对比过程很重要,它量化了“换更强模型”到底能带来多少增益,而不是凭感觉说“深度学习必然更好”。
3.3 部署脚本与服务封装的核心代码解析
模型训练完成以后,真正的工程化才刚刚开始。我先用transformers的save_pretrained把模型权重和配置文件保存下来,再额外写一个预处理模块,包含文本清洗函数和分词器初始化逻辑。推理服务我封装成了一个类,核心方法包含load_model、preprocess、predict和postprocess四步,返回的JSON结构统一包含预测类别、置信度和原始输入,方便前端直接对接。
服务封装时有个容易被忽略的细节:要学会做批量推理而不是单条循环。刚开始我为了省事在接口里一条一条调模型,结果高并发压测时延迟直接爆炸。后来改成内部先攒batch再统一推理,延迟从几百毫秒降到几十毫秒。如果你是CPU环境跑BERT,建议开启torch.no_grad()并把模型切到eval模式,同时考虑用ONNX Runtime提升推理速度。这些优化点细节虽小,但实际生产中差别非常大。
3.4 用Docker锁定环境,实现一键部署
环境一致性是AI工程部署中最痛的问题之一。我的Dockerfile设计思路是:基础镜像选用官方Python 3.9-slim,安装PyTorch CPU版本(因为推理机没有GPU,选CPU版可以大幅减小镜像体积),再安装transformers、fastapi、uvicorn、pydantic等依赖。代码和模型文件通过COPY指令打进去,最后用CMD启动uvicorn服务。这样设计下来镜像体积控制在2GB左右,比带CUDA的版本少了接近一半,冷启动速度也快不少。
Docker部署的实际操作中有一个小坑必须注意:默认情况下容器里没有健康检查机制,服务挂了Nginx还在继续转发流量,用户就会看到长时间无响应。所以我额外加了healthcheck,设置成每30秒请求一次服务的/healthz接口,连续失败三次就自动重启容器。这个机制上线后帮我挡了至少两次因为内存泄漏导致的服务假死事故,属于花小钱办大事的典范。
4. 常见问题与排查技巧实录
4.1 训练时显存溢出和训练速度过慢的解法
显存溢出是训练环节最常见的问题。新手一上来就爱把batch size调大,结果显存直接爆掉。我的经验是:batch size从2开始指数级往上试,2、4、8、16,找到能跑的最大值再往回调一档,给梯度更新留点余量。如果batch size已经很小了还爆显存,就检查是不是序列长度过长,有些文本几百上千个token,配合大模型确实很吃显存,可以先用截断策略限制最大长度。
训练速度过慢则要先分清楚瓶颈在数据加载还是模型计算。数据加载瓶颈的典型特征是一轮训练中GPU利用率频繁掉到0,这时用DataLoader的num_workers参数开启多进程预读,同时开启pin_memory和prefetch_factor,往往立竿见影。模型计算的瓶颈则需要看是不是模型太大、算子效率低或者CPU在跑GPU的活,这种情况只能从模型结构和硬件配置下手调整。
4.2 线上推理结果和离线评估不一致是怎么回事
这个问题我碰到过不止一次,表现是离线验证集F1很高,上线后用户反馈却很差。可能的原因很多,最常见的是线上和离线的数据分布不一致。比如离线训练用的是清洗过、格式规整的文本,线上用户输入却五花八门,表情、错别字、口语化表达全都有。解决思路是在预处理阶段做得足够稳健,把线上真实输入流抽出一部分样本,人工核对模型输出,再决定要不要补充训练数据。
另一个原因是离线评估指标的选取和线上业务目标脱节。离线看F1没问题,但如果线上真正关注的是“用户问题能被正确解决的比例”,F1再高也没用。比较好的做法是在设计离线指标时,多找业务方对齐,把业务目标翻译成至少一个可离线计算的代理指标。这样你做的模型调优方向才是对的,否则就是在自嗨。
4.3 服务频繁崩溃和响应延迟飙升的排查清单
服务崩溃和延迟问题,我一般按照由外到内的顺序排查。先看基础监控,CPU、内存、磁盘、网络有没有异常,再看应用日志中有没有报错堆栈,最后看模型推理耗时和接口调用量是否有明显关联。有一个很典型的案例:服务上线后每两个小时内存占用就翻一倍,最后定位到是模型推理时缓存了所有请求的中间张量,一直没有释放。解决方案是把缓存清理解析和推理逻辑解耦,改成定时清理。
响应延迟飙升的原因更多。我先看是不是高并发导致线程阻塞,代码里有没有加锁的地方或者同步的网络请求。再看是不是模型推理变慢,比如CPU被其他进程抢占,或者输入文本长度异常大。最后看数据库和外部接口,如果服务依赖了第三方接口,对方响应慢也会拖垮整个请求链路。我建议把服务拆成多个阶段记录耗时并打日志,这样哪一段变慢一眼就能看出来,不用每次都靠猜。
4.4 模型效果一直不达标的调试方向参考
如果数据量不小、模型结构也合理,效果一直不达标,我会按这个方向去排查。第一,看是不是标签噪声太大,随机抽样一百条训练数据人工重新标注,计算一致率,如果低于85%,先解决标注质量和标注标准问题。第二,看是不是特征工程丢信息,比如文本模型输入时把数字全部归一化了,结果“一个月”和“一年”完全无法区分。第三,看是不是训练配置不合理,学习率过大或者过小都会导致不收敛或收敛到局部最优点。
调试模型这件事,本质上是一个假设驱动的过程。每次只改变一个变量,记录结果,对比分析,再设计下一个实验。千万不要同时改三个超参数、换一种模型结构又换了数据清洗方式,跑完发现效果变好了,但你根本不知道是哪个改动起了作用。这种严谨性,恰恰是AI工程和AI实验最本质的区别。
4.5 快速参考:典型问题排查速查表
| 问题现象 | 可能原因 | 推荐排查动作 |
|---|---|---|
| 训练Loss震荡不下降 | 学习率过大、数据噪声多 | 调低学习率1个数量级,检查数据标签质量 |
| 验证集效果好但线上差 | 数据分布偏移、评估指标失配 | 采集线上样本对比分布,调整代理指标 |
| GPU利用率忽高忽低 | 数据加载速度跟不上 | 加大num_workers、开启pin_memory |
| 推理延迟持续升高 | 缓存膨胀、输入过长 | 检查缓存清理机制,限制输入最大长度 |
| 服务内存缓慢增长 | 张量未释放、连接未关闭 | 用内存分析工具抓快照,修复资源释放逻辑 |
| 模型更新后效果回退 | 实验配置不一致、数据版本混乱 | 用MLflow追踪所有实验参数和数据版本 |
5. 工具链的选择与实践心得
5.1 核心工具清单与选型理由
整个项目里我用到最多的工具集中在数据处理、实验追踪、模型开发和部署运维四个方向。数据端我用pandas和pydantic,实验追踪用MLflow,模型开发用PyTorch和transformers,部署用FastAPI加Docker加Nginx。这套组合的好处是每个环节都有成熟的社区生态,遇到问题时能搜到大量解决方案,不推荐在这个阶段追求冷门工具。
选型还有一个很重要的考量:团队协作和未来维护。如果团队里其他人只会用某个工具,你再喜欢一个更“优雅”的方案也要慎重。工具最重要的属性是让团队用最低成本完成目标,而不是技术上的炫酷。我见过为了一个并不复杂的定时任务引入整套工作流框架的,最后维护成本高到没人愿意碰,这是典型的过度设计。
5.2 环境与依赖管理的实操建议
Python的依赖管理是AI工程里最容易翻车的地方。我吃过最大的亏是直接在服务器全局环境pip install,结果一次更新把另一个项目的依赖搞坏了。后来强制自己每个项目用独立的虚拟环境,并且用requirements.txt或conda环境文件锁定精确版本。代码仓库里要求必须包含完整的依赖说明文档,新同事克隆下来以后照着文档能一键复现环境,这才算标准化。
版本锁定要细到传递依赖。有时候你直接依赖的包没变,但它引用的底层依赖被pip自动升级了,行为就可能变化。一个比较稳妥的做法是用pip freeze把当前环境里的完整依赖列表导出并提交到仓库,而不是只手写几个主要包名。虽然看起来笨重,但排查“环境不一致”类问题时,这份完整列表能省下大量时间。
5.3 我反复使用的几个命令行技巧
命令行是AI工程里最容易提升效率的地方。我常用的几个技巧:用tmux在服务器上跑长时间任务,断网也不影响训练进程;用rsync做模型文件和数据集的增量同步,比scp快很多;用nvidia-smi配合watch命令实时盯GPU状态;用du和df排查磁盘空间被什么文件占满。这些都不是什么高深技术,但确实能在关键时刻救命。
另外一个很实用的习惯是:把训练和评估的日志同时输出到终端和文件,用tee命令就能实现。日志文件按日期命名存放,方便以后回溯问题。我甚至会在训练脚本里主动记录硬件信息、依赖版本、git提交号,这样任何一次实验结果都能完整复现。这些细节做多了以后,你会发现调试和复盘都变得轻松很多。
6. 从项目到能力:这套方法能迁移到哪里
6.1 AI工程思维在不同业务场景下的复用
这套从零到一的方法论,本质上跟具体的业务场景解耦。不管是文本分类、图像识别、推荐排序还是预测性维护,数据管道、实验追踪、服务部署、监控迭代的框架是完全一致的。业务场景换掉以后,变的只是数据形态和模型结构,工程化的骨架可以原样搬过去。这也是这个项目叫from scratch的真正价值所在——学的不是某个模型的用法,而是AI落地的通法。
我在换过三个不同领域的项目后,越来越确信这一点。第一个项目是客服工单分类,第二个是图像质检,第三个是用户流失预测。三个项目数据源完全不同,模型也从BERT换成了卷积网络又换成了树模型,但整体的流程、工具链和排查思路几乎是平移复用。这种迁移能力,才是AI工程师真正值钱的地方。
6.2 持续学习的方向:MLOps和LLM应用的新变化
如果从scratch这条链路已经走得比较顺了,接下来值得投入的方向我认为有两个。第一个是MLOps,即把机器学习全生命周期进一步平台化、自动化和规范化,包括特征平台、模型仓库、自动重训、A/B测试、模型监控报警。这些能力能让你从“做一个模型”进化到“管理一批模型的持续运行”。
第二个是LLM应用工程化。大语言模型带来了新的应用范式,比如RAG、Agent、Function Calling,它们让AI系统的能力边界大幅扩展,同时也带来了新的工程挑战:上下文管理、工具调用可靠性、成本控制、幻觉抑制、评测体系搭建。这些是当前AI工程领域里最前沿也最缺人的方向。如果你已经把传统模型的工程链路吃透了,再往这两个方向延伸会非常顺滑。