我最早接触ai-engineering这个词,是在一次技术分享会上。当时台上的人讲了一个观点:现在缺的不是能训练模型的人,而是能把模型变成稳定服务的人。那会儿我正卡在一个瓶颈期——模型在笔记本上调得风生水起,一到线上就被延迟、内存、并发这些问题打到怀疑人生。于是我建了一个叫ai-engineering-from-scratch的仓库,打算把自己从零到一补全工程能力的过程完整记录下来。这篇文章,就是我把那段经历重新梳理后的一份总结,里面有环境配置的教训、数据处理的坑、部署上线的撕扯,也有我认为最该避开的认知误区。如果你也想系统性地进入AI工程这个领域,或者已经在做模型训练但总觉得缺了点什么,这篇应该能给你一条可落地的参考路径。
1. 别被“AI工程”这个词吓到:它和算法建模是两个物种
1.1 模型只是AI系统里的一块拼图
很多初学者会把AI工程等同于训练模型,但实际上一套能稳定运行的AI系统,模型代码通常只占很小一部分。你还需要处理数据管道、训练平台、模型仓库、推理服务、监控告警、版本回滚,甚至要多租户资源调度。这些模块之间相互耦合,任何一个环节出了问题,模型再准也白搭。
我见过一个很典型的反例:团队花了两周调出一个准确率不错的文本分类模型,但上线时发现数据预处理脚本和模型训练时用的版本不一致,导致线上预测结果完全乱套。问题不在模型,而在工程链路缺少统一的数据处理封装。所以从零开始学AI工程,第一件事不是去背框架API,而是建立“系统”视角——你构建的不是单个模型,而是一条能持续运转的数据生产流水线。
1.2 “从零开始”的真正含义
大部分人理解的从零开始,是指从线性代数、Python语法、深度学习原理学起。这当然没错,但真正的从零开始还应该包括:零运维经验、零服务化概念、零监控意识。我建仓库的时候给自己定了一个目标:不管用什么工具,最终都要形成一个可以端到端跑通的闭环,训练、评估、部署、监控缺一不可。这个过程比单纯跑模型要痛苦得多,但走通一次后,你对AI工程的理解会完全不同。
所以如果你问我要从哪里开始,我的答案是:先定一个足够小但完整的项目,比如“做一个实时情感分析API”,然后逼着自己把数据、训练、部署、监控全都走一遍。别急着追求模型的SOTA,那个阶段不重要。
2. 启动阶段最消耗耐心的事:环境、基础与版本地狱
2.1 环境配置:值得花两天,但别超过两天
我早期浪费了无数时间在装环境上。今天装个TensorFlow,明天换成PyTorch,CUDA版本和cuDNN对不上,pip依赖冲突,最后连conda都给我整懵了。后来我给自己定了一条铁律:所有项目一律用Docker固化环境,宿主机上不装任何深度学习框架。
一个我在实践中验证过的组合是:用conda管理Python虚拟环境,用Docker打包运行时,用requirements.txt锁定精确版本。比如部署一个PyTorch服务,我的Dockerfile里会固定python:3.9-slim作为基础镜像,然后安装指定版本的PyTorch和torchvision,避免使用latest标签。这样做的好处是,过两个月再回来看项目,你依然能启动起来。
如果你只是在本地学习,也可以用conda+GPU版本对应表来减少踩坑。记住一件事:环境问题不是你的失败,而是标准工程实践的一部分。但也不要无限期困在里面,一个合理的时间预算是一到两天,超时就直接切到云GPU或预配置镜像。
2.2 数学基础学到什么程度才够用
很多人被“AI需要大量数学基础”这句话吓退。我的实际经验是:除非你要发论文或研究新架构,否则90%的落地场景只需要三样东西——线性代数里的矩阵乘法与维度变换、概率论里的条件概率与贝叶斯思想、微积分里的梯度理解。你不需要会手动推导复杂的损失函数,但必须知道梯度下降在做什么,为什么学习率太大会震荡。
我在学习时发现一个很有效的类比:训练模型就像是开车下山,梯度是当前路面的倾斜方向,学习率就是你踩油门的幅度。步子太大会冲下山坡,步子太小会一直原地挪。理解这种直觉比记忆公式重要得多。如果你已经会NumPy的基本操作,再看深度学习代码就不会太吃力。
2.3 选一个主战场:PyTorch还是TensorFlow
今天再讨论框架选型,已经不像几年前那么难。我的选择是PyTorch,主要原因是它在动态图下的调试体验更自然,社区里新论文的开源实现也大多以它为主。不过我也建议至少了解TensorFlow Serving或ONNX Runtime是怎么把事情做对的,因为在企业环境里,你可能会碰到老项目用TensorFlow。
不要在这上面花太多时间纠结。框架只是工具,你真正要学的是数据流、张量操作和训练循环背后的逻辑。选一个用得顺手的,把精力花在工程链路上,价值更大。
3. 核心流水线实战:从数据到训练的可复用闭环
3.1 数据工程才是AI项目里最重的活
如果让我统计从零到一的项目时间分配,数据获取、清洗、标注、版本管理大概占了70%。这听起来夸张,但真实项目里数据问题永远比模型问题多。比如我做命名实体识别时,第一个版本的数据里有大量标注不一致——同一个地名一个人标成“地点”,另一个人标成“组织”。模型怎么训都过不了85分,后来花了一周重新清洗数据才解决问题。
数据版本管理也是很容易被忽视的环节。代码可以用Git回滚,但数据如果变了,模型复现就会很困难。我推荐用DVC(Data Version Control)来管理数据集版本,它能把数据和代码放在同一个仓库里做关联,想要回溯到某个时间点的数据,一条命令就能搞定。
另一个关键点是数据增强和采样策略。不要想当然地认为样本均衡就万事大吉,真实业务场景里,你需要针对性地对少数类做过采样或合成,还要小心避免增强带来的数据泄漏。比如在文本分类里,如果对同一条句子做简单的同义词替换,但替换后的句子又出现在验证集里,那你评估出来的指标就会虚高。
3.2 训练代码规范化:从“能跑”到“可复现”
我最早写的训练脚本就是一堆顺序执行代码,变量名混乱,超参数直接硬编码在文件里。后来发现不仅别人看不懂,连我自己隔两周就忘了当时的参数。后来我改成三件套:配置文件、固定随机种子、自动指标记录。
配置文件我推荐用YAML或JSON。把所有超参数、数据路径、模型参数、输出目录都放进去。训练时读取一个config文件,这样每次实验的配置都有了记录。固定随机种子为什么重要?因为很多深度学习框架的随机性来自多个源(Python随机、NumPy随机、PyTorch/CUDA随机),如果不全部固定,同样的代码跑两次结果不同,你根本没法判断某个改动到底有没有效果。
自动指标记录我强烈推荐MLflow。它不仅能记录超参数和指标,还能存储模型产物。我第一次用的时候震惊于一个细节:它把每个实验的git commit id都自动记下来了,这样哪个代码版本跑出这个指标,一查便知。示例代码很简单:
import mlflow with mlflow.start_run(): mlflow.log_params({"lr": 0.001, "batch_size": 32}) mlflow.log_metric("f1", 0.876) mlflow.pytorch.log_model(model, "model")这样跑完,所有日志都进入MLflow的追踪服务,对比实验时一目了然。
3.3 评估阶段的经典翻车场景
很多人觉得评估就是划分训练集和测试集,算一下准确率。但在真实的AI工程中,评估是一个需要精心设计的过程。我最常犯的错误是线下指标好看,线上效果一塌糊涂。
原因通常有三个:第一,数据分布偏移,比如训练数据是新闻文本,线上却来了大量口语化评论;第二,验证集划分方式不合理,比如没有考虑时间先后顺序,导致未来信息泄漏;第三,线下的离线评估和线上的业务指标不是同一种东西,离线F1高不代表用户满意度高。
一个比较稳妥的做法是:在建模之前先确定好离线评估指标和线上观测指标,同时对数据做分层采样,保证验证集与训练集在类别分布上一致。如果涉及时序数据,一定要按时间切分,不能用随机切分。
4. 从模型到服务:部署环节才是真正的照妖镜
4.1 模型序列化与格式选择
训练出来的模型文件不能直接扔给生产环境,你需要考虑序列化格式和运行时的匹配问题。PyTorch的model.pt格式只在PyTorch环境中能加载,而ONNX格式则可以对接不同推理引擎,甚至能做一些图形化部署。
我的经验是:如果服务是用Python写的,直接加载.pt文件最快;如果对性能有要求,或者需要跨语言调用,建议导出成ONNX,再用ONNX Runtime或TensorRT推理。ONNX导出的坑不少,尤其是动态轴、算子在兼容性上的限制。导出后一定要用测试集做一遍对齐验证,确保输出和原始模型一致。
我在某个项目里把BERT模型从PyTorch导出为ONNX后,CPU推理时间从120ms降到了30ms,效果完全一致,这个优化肉眼可见。如果你用的是Transformer家族,HuggingFace的optimum库提供了更简洁的导出接口,值得一试。
4.2 用FastAPI包装模型服务的正确姿势
部署的最基础形式是把模型封装成一个HTTP接口。FastAPI是新一代里很合适的选择,因为它的async支持、自动文档和依赖注入都让工程实现变得干净。但有一个非常容易踩的坑:预处理和后处理必须在服务端完成,不能在客户端做一半。
举个例子,如果你在训练时先把文本做了分词、截断、转成token ids,那么线上请求进来时,服务端也必须执行一模一样的步骤。如果这部分逻辑被写在客户端的代码里,那一旦有新的调用方接入,很可能忘了做预处理,直接把原始字符串扔给模型,结果自然错得离谱。
一个标准做法是把预处理函数放到服务的启动阶段加载,在请求处理函数中调用同一个预处理对象。比如:
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class TextRequest(BaseModel): text: str @app.post("/predict") async def predict(req: TextRequest): inputs = tokenizer(req.text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): outputs = model(**inputs) label = outputs.logits.argmax(-1).item() return {"label": label}这里我们假设tokenizer和model在服务启动时已经加载到全局变量,请求处理只做推理和结果映射。这样逻辑清晰,也方便测试。
4.3 压测与性能调优:别被单次推理耗时骗了
很多人在部署后只看单次推理的耗时,觉得很短就以为没问题。实际上服务的吞吐量受限于并发请求、显存/内存占用、CPU上下文切换和网络IO。我第一次上线一个分类服务,单次推理只要5ms,但用压测工具跑了一下,发现200并发时p99延迟飙升到2秒,原因是Python的GIL导致多线程推理没有真正并行。
针对这个问题,有几种常见解法:第一,使用多进程部署(比如Uvicorn的--workers参数,但要注意模型在不同进程间的内存复制问题);第二,把推理放到异步任务队列里去处理,客户端先拿到任务ID,之后轮询结果;第三,用模型推理专用引擎,比如TorchServe或Triton Inference Server,它们内置了动态批处理(dynamic batching)和模型并发管理。
压测工具我曾经用locust,后来发现简单场景用wrk更轻量。重点要看p99和错误率,而不是平均延迟。平均延时会被少数快请求拉低,掩盖长尾问题。
4.4 监控与回滚:模型也会“生病”,你得有应急预案
上线不是终点,而是起点。模型服务和传统服务不同,它的输入分布是可能漂移的。用户行为一旦变化,你训练时的数据分布就会失效,预测结果自然跟着变歪。所以你必须对服务做监控,至少要盯三个指标:请求量、平均延迟、预测置信度。更进一步,可以做特征分布漂移检测,比如统计线上输入文本长度分布与训练时的偏差,或者某些类别的预测概率是否持续走低。
当监控发现异常时,最重要的能力是快速回滚。如果你在部署时采用了金丝雀发布或蓝绿部署,回滚就只需切换流量,而不用重新构建服务。我习惯把每个模型版本打上唯一的标签,部署时只开放一小部分流量,验证一段时间后再全面放量。这样即使新模型出问题,影响的也只是小范围用户。
5. 真实案例复盘:零基础搭一个命名实体识别服务
5.1 场景与目标
为了把前面的知识点串起来,我复盘一个真实做过的项目:给一个法律文书系统提供命名实体识别能力,需要从文本中抽出人名、地名、机构名、时间等几类实体。当时的硬性要求是:平均响应时间在300毫秒以内,支持20并发,服务需要长时间稳定运行。
这个项目完美踩中了从零到一的所有关键节点,很适合拿出来做体检式复盘。
5.2 数据准备与标注的一致性
我们当时拿到的是几万份脱敏后的法律文书,但没有任何标签。最终采用了人工标注和主动学习相结合的方式:先用少量标注数据训练一个弱模型,用它对未标注数据做预测,只把模型置信度低的样本交给人工标注员。这样标注效率提高了不少。
标注过程中最大的问题就是标签边界不一致。一个案子里的“张三”有时标成人名,有时因为上下文变成“当事人”,导致训练时模型学出矛盾决策。解决方法是做了一次严格的标注规范文档,并安排了两轮交叉校验。虽然花了额外时间,但后期模型效果提升特别明显,线下F1从0.71直接跳到0.83。
5.3 模型选型与训练中的显存困境
模型一开始我们尝试了一个公开的RoBERTa中文预训练模型,效果很好,但GPU显存被显卡限制拖住了。batch size稍微调大一点就OOM,最后只能用梯度累积来模拟更大的batch。过程虽然凑合能训练,但训练速度确实慢了很多。
后来发现另一个工程优化方式:使用混合精度训练(AMP),把部分操作从FP32降到FP16,显存占用减少将近一半,训练速度也提升了。对于很多场景来说,这个技巧远比换大显卡来得快。
训练时我们还发现,实体识别的标签分布极度不均衡,最常见的“时间”类别占了70%,而“法律条款”类别只有2%。如果不做处理,模型会偏向把所有实体预测成时间。我用了加权损失函数,同时结合简单的数据过采样,最终各类型F1基本平衡在了0.8左右。
5.4 部署与性能优化:从“能跑”到“稳跑”
模型训练完后,我们用ONNX导出了模型,并使用ONNX Runtime进行CPU推理。初始单次推理约40ms,还没有到目标;但经过算子融合和线程优化后,单次降到了18ms,20并发下p99在200ms左右,满足要求。
真正的坑是服务生命周期管理。我们一开始只是用uvicorn拉起一个脚本,结果每隔几天就会出现显存/内存缓慢增长的问题。后来排查发现是推理引擎内部的线程池没有释放。换成了在服务内复用引擎实例,并定时做健康检查,才解决了稳定性问题。
上线后的监控我们也做了一个简单版:记录每个请求的文本长度、预测置信度、响应延迟,每天做一次分布对比。如果某一天“法律条款”这个类别的置信度整体走低,说明数据分布可能变了,系统会报警提醒重新训练。
这个案例给我最大的启示是:模型训练只是项目的一小部分,真正为业务创造价值的,是那根从数据到服务、从监控到迭代的完整链条。
6. 过来人给出的工具清单与避坑建议
6.1 我整理的一份零基础起步工具箱
基于自己的实际使用体验,我把值得优先上手的工具按环节列一下,避免你在海量选择里迷路:
| 环节 | 推荐工具 | 用途说明 |
|---|---|---|
| 环境管理 | conda + Docker | 依赖隔离、环境固化 |
| 数据版本 | DVC | 给数据集做版本标签 |
| 实验追踪 | MLflow | 记录参数、指标、模型版本 |
| 训练框架 | PyTorch | 动态图、社区资源丰富 |
| 推理优化 | ONNX Runtime | 跨平台、CPU友好 |
| 服务API | FastAPI | 高性能、简单易用 |
| 压测 | locust 或 wrk | 评估吞吐与延迟尾点 |
| 监控 | Prometheus + Grafana | 指标采集与可视化 |
这些并不是唯一正确的选项,但它们组合在一起,能让你用最小的成本覆盖AI工程的典型链路。不要试图一次性全掌握,我建议按“环境-训练-部署”顺序,每完成一个环节再用笔记沉淀。
6.2 新手最容易踩的四个认知坑
第一个坑:以为必须拥有超贵的GPU才能学。其实很多入门项目用CPU就能跑,甚至可以在云上按小时租用,比一次性购买划算得多。第二个坑:认为模型越复杂越高级。实际上真实场景中往往是简单模型加好数据更可靠。我见过有人用BERT做“是否包含敏感词”的简单分类,效果不如一个规则引擎,还平白增加算力损耗。第三个坑:只关注离线指标,忽略线上行为分析。用户根本不是按测试集的表现来使用的。第四个坑:轻视日志和监控。没有日志和监控,你根本不知道线上发生了什么,只能靠用户投诉来发现问题,那时已经晚了。
6.3 学习方法论:输出倒逼输入
从我自己的经历看,最高效的学习方式就是持续输出。每次做完一个项目,我都会强迫自己写一篇技术笔记,或者做一个示例仓库。写作会逼你把模糊的知识点理清楚,也会暴露你理解不深的细节。ai-engineering-from-scratch这个仓库对我的价值,不是里面有多少代码,而是它记录了我从混乱到清晰的思维演进过程。
你可以从一个小得不能再小的项目开始,比如今天做一个JSON字段解析处理的AI工具,明天给这个工具加个日志,后天再给它加一个模型推理。一步一步把地基打牢,比一开始就要做一个完整平台靠谱得多。
如果你也想自己建一个类似的from-scratch仓库,我强烈建议你从“踩坑记录”开始,而不是从“知识点目录”开始。因为踩过的坑才是真正属于你的经验,也是你把知识转化为能力的地方。把这些坑写下来,半年后再回头,你会看到自己有多明显的成长。