1. 为什么我把“从零学AI工程”当成一项系统工程来做
先说个挺反直觉的现象:这两年身边不少朋友学AI,路径惊人的一致——打开某教程、装好Python、跑通一个手写数字识别,然后就开始刷Transformer源码、背Attention公式。结果是什么呢?三个月后问他能不能把一个模型接到公司现有系统里,多半愣住。模型训练是一回事,AI工程是另一回事,这个差距就是标题里“from scratch”的真正含义。
我自己是从传统后台开发转过来的,最早接触AI工程时也走过弯路。当时接到一个需求:把几千份合同里的关键条款自动抽出来,做成结构化字段给业务系统用。我花了大半个月调BERT模型,F1从0.71磨到0.83,自己觉得不错了,结果模型一部署到测试环境就出问题——内存占用超限、推理延迟不稳定、标注数据来回变更导致版本混乱,最后被运维兄弟按在工位上聊了两个小时什么叫“可用性”。那段经历给了我一个很深的教训:AI工程不等于建模,它是一条从数据到模型再到系统的完整链路,每一环的坑都不比训练模型少。
如果你也想从零开始掌握AI工程,我建议你先接受三个前置认知,这三个认知决定了你后续的学习方式和踩坑密度。
第一,AI工程的核心矛盾不是“模型不够聪明”,而是“系统不够稳定”。你调模型能刷分,但推理服务的稳定性、数据管道的健壮性、模型版本的可追溯性,才是工程侧真正烧时间的地方。算法岗和AI工程岗的最大差异就在这:算法岗面对的是“怎么把分刷高”,工程岗面对的是“怎么把高分模型安稳送上线”。
第二,AI工程需要的技能栈是“T型”的。竖着的那一杠是深度学习基础,必须有,不要求你能手推所有公式,但至少得明白损失函数怎么反向传播、Embedding干了什么事、Transformer为什么能并行。横着的那一杠才是工程人的护城河——Docker、Kubernetes、FastAPI、SQL、数据管道、监控告警、CI/CD,这些传统后端技术你掌握得越扎实,AI工程对你来说就越不是高门槛的事。反过来,如果只会训练模型,你在工程体系里基本寸步难行。
第三,学AI工程最好的方式不是按章节啃书,而是从头到尾做通一个小项目。书可以告诉你Encoder和Decoder是什么,但没人告诉你模型量化之后精度掉了多少、推理服务怎么优雅处理并发请求、模型文件放对象存储还是走Git LFS、生产环境里数据分布漂移了该怎么办。这些知识只有在你完整经历一个项目后才会真正长在身上。
这篇文章我会用一条主线来讲:从环境搭建开始,到数据管道、模型训练、服务化部署、线上监控,完整走一遍一个文本分类项目的工程化过程。中间穿插我踩过的坑、验证过的方案,以及每个环节“为什么这么选”的思考。你跟着这条线走完,再回头看那些AI教程,视角会完全不一样。
2. 动手前的技术选型:哪些工具值得从零学,哪些别浪费时间
很多从零开始的教程让你先装TensorFlow,我建议你现阶段别碰这个坑。我刚入门那会儿也是一股脑跟着老教程装了TensorFlow 1.x,然后被各种Session、placeholder、graph折腾到怀疑人生。到了2024年你再从零开始,技术选型应该务实很多。
2.1 核心框架:PyTorch是默认选择,没有第二个选项
PyTorch现在基本是AI工程领域的事实标准。原因很简单:动态计算图让调试变得直观,你可以在前向传播中间print任意张量的shape和值,这对新手理解模型行为极其友好。TensorFlow的GradientTape虽然也改进了,但生态和社区活跃度已经明显落后。HuggingFace Transformers也几乎只提供PyTorch权重的优化版本。如果你要参与开源项目或者看最新的论文复现,PyTorch是绕不开的。
安装的时候有一点要注意:不要直接pip install torch了事。PyTorch的CUDA版本需要和你的显卡驱动匹配,最好去官网的install页面,用它的命令生成工具选择对应的CUDA版本。我自己吃过亏——有一次图省事直接装默认版本,结果跑训练的时候报“CUDA driver version is insufficient”,查了半天才发现是驱动的CUDA版本和PyTorch编译时用的版本对不上。
2.2 数据与特征工程:Pandas是必须的吗?未必
传统数据科学教程一定会让你学Pandas,但我见过太多新手在Pandas的join、groupby、apply里浪费一整个晚上。AI项目里的数据处理,尤其是非结构化数据(文本、图片、音频),核心操作是“加载—清洗—Tokenize—存TFRecord/Parquet”,Pandas反而用得不多。如果你已经会Pandas,那就用;如果不会,不用特意学,从Python原生的列表推导和字典操作开始就够了,等遇到真正的表格型数据再补充不迟。
更值得从零学的是HuggingFace的datasets库。它对数据做了内存映射和缓存,处理几GB的数据不会把内存打爆,还内置了train_test_split、shuffle、filter这些常用操作。我后来做多语言语料清洗,全是靠datasets扛下来的,Pandas反而成了辅助工具。
2.3 服务化:FastAPI是我的第一选择,没有犹豫
模型训完之后要对外提供服务,框架选择上我踩过Flask的坑。Flask的异步支持需要额外挂gunicorn和gevent,各种线程模型看得头大。FastAPI从设计上就是异步优先,天然支持并发请求,配合uvicorn跑起来,压测结果比Flask默认配置稳定得多。更重要的是FastAPI自带OpenAPI文档,前端、测试、运维拿到URL就能看到所有接口定义,沟通成本直线下降。
有一件事必须提醒:模型加载要放在模块级别,不要在请求处理函数里load_model()。我见过有人把torch.load写在predict函数里,结果每个请求都要重新加载一遍模型文件,第一次调用等5秒,后续请求慢到发指。正确做法是进程启动时加载一次模型到全局变量,请求处理时只调用forward。
2.4 容器化与编排:直接上手Docker Compose,Kubernetes看情况
从零开始不需要一上来就啃Kubernetes,Kubernetes的学习曲线陡峭,概念又多,容易把你绕晕。先用Docker把环境固化下来,让任何一个同事clone代码后都能docker compose up跑起整套服务,这比什么都重要。环境一致性问题解决了,后面再上Kubernetes就是水到渠成的事。
我建议你学Docker的时候重点关注三件东西:镜像分层机制(理解为什么基础镜像要用python:3.11-slim而不是python:3.11)、多阶段构建、以及 .dockerignore 文件。很多人把 .dockerignore 漏了,结果把几百MB的node_modules或者cache都打进镜像里,构建一次慢到怀疑人生。
3. 从零做一个AI工程项目的完整拆解:文本多分类实战
理论说太多没用,我拿我最近做的一个“工单自动分类”项目来走一遍完整流程。这个任务的业务背景是:客服部门每天收到大量工单,需要按照“网络故障、账号问题、账单咨询、投诉建议”等十几个类别归档。之前全靠人工标记,效率低且容易漏,现在要用AI自动化分类。这是一个非常典型的AI工程项目,麻雀虽小五脏俱全,涵盖了数据、模型、部署、监控的全部环节。
3.1 第一步:数据管道——先解决“有数据可训”的问题
项目拿到手第一件事不是选模型,而是盘数据。当时的原始数据是客服系统导出的Excel表格,一万多条工单记录,字段包括工单编号、提交时间、工单描述文本、人工分类标签。乱得离谱:描述文本里夹杂着表情符号、URL、银行账号脱敏后的星号,还有人把“发票”“fa piao”混着写,标签也有同一个类别多种叫法的情况。
我做了三件事。第一,写脚本统一编码为UTF-8,把Excel转成CSV,顺便把空行、只有标点符号的垃圾工单过滤掉。第二,用正则+人工抽查的方式清洗文本,把URL、邮箱、手机号替换成特殊占位符,避免模型学到这些无关模式。第三,把标签做标准化映射,“发-票”“发票问题”“账单发票”统一归到“发票相关”。这一步花了大概一天半,看起来慢,但后面训练效果好不好,一半取决于这里。
清洗后的数据重新划分:80%训练集、10%验证集、10%测试集。这里要强调一个容易犯的错误——划分时必须按类别做分层抽样,保证训练集和测试集里各个类别的比例大致一致。如果不做分层,某些稀有类別可能全都落进测试集里,模型压根没见过,测试分数就会虚高或者虚低。
3.2 第二步:模型选择——为什么小模型往往比大模型更合适
工单分类这个任务,很多朋友第一反应是上GPT或者微调大模型。但我坚持用了小模型路线——基于BERT的中文预训练模型,参数量大概1.1亿,加上一个分类头。原因有三点:推理成本低得多,几个毫秒出结果;部署简单,不需要额外的大模型推理服务;效果完全够用,这个任务的难点在数据质量上,不在模型容量上。
选择预训练模型时我比较了三个:BERT-base-Chinese、RoBERTa-wwm-ext、以及一个轻量的TinyBERT。实测下来RoBERTa-wwm-ext在验证集上的F1最高,比普通BERT高了3个点左右,TinyBERT虽然速度快但准确率掉了6个点,不划算。这个对比你可能不需要反复做,直接用社区口碑好、同类型任务常用的模型作为baseline就够。
Tokenization环节有一个容易被忽略的细节:必须统一在训练和推理时使用同一个tokenizer配置。有人训练时用max_length=128,推理时因为输入更长就调成了256,结果模型预测效果立刻下跌。原因在于BERT的位置编码是训练时固定的,推理时如果序列长度超出了预训练覆盖范围,位置向量就是模型没学过的区域,表现自然崩。
3.3 第三步:训练工程——早停、学习率、随机种子这些小决策决定最终分数
训练代码不长,但里面有几个不起眼却关键的选择。我直接用HuggingFace的Trainer API,省去自己写训练循环的麻烦,但它默认的行为需要调整。
第一是学习率。默认的5e-5对BERT类模型通常是安全的,但我用3e-5,配上一个线性衰减warmup,效果更稳。第二是早停,我设了patience=3,验证集损失连续3个epoch不降就打住,避免过拟合。第三是随机种子,设置了seed=42。很多人不重视这个,但如果你网格搜索时发现同样的参数跑出不同结果,大概率就是没固定种子。第四是混合精度训练,显存占用和训练速度都会有明显改善,A100上训练时间能缩短一半还多,T4上也有效果。
训练过程中每跑完一个epoch,记录验证集的loss、accuracy、macro-F1。这里我特别关注macro-F1而不是accuracy,因为这个多分类任务里“投诉建议”类别的样本量只有“网络故障”的四分之一,accuracy会被多数类主导,macro-F1能让少数类别也被公平对待。最终训练了8个epoch,验证集macro-F1到了0.87,测试集0.85。说实话,这个分数不是我调参调出来的,数据清洗和标签统一贡献了一大半。
3.4 第四步:模型评估——光看一个F1远远不够
模型训练完,很多人就急着部署了,我建议再做一个步骤——错误分析。把测试集里所有预测失败的样本捞出来,一条条看模型把什么分错了。这个过程极其耗费耐心,但价值巨大。
我在这个工单项目里发现了一个挺有意思的错误模式:模型经常把“我登录不了账号,一直提示密码错误”这种密码类问题分到“网络故障”里。原因是这类文本里出现了“网络”“连不上”等强特征词。怎么解决?不是去换模型,而是增加数据——我去历史工单里专门捞了一批“账号”和“网络”边界模糊的样本补充训练集,还加了一条规则后处理:当模型预测“网络故障”且置信度低于0.6时,自动转人工复核。这个阈值调了两版,第一版设0.7,误报少了但漏报多了,最后压到0.6平衡得最好。
评估环节我还做了一张混淆矩阵可视化,每个格子都标了数字。这张图后来成了跟业务部门沟通的利器——他们一眼就能看到哪个类容易混淆,而不是听我解释什么是F1。
4. 模型上线:服务化部署中绕不开的工程细节
这部分是全项目里真正“工程”含量最高的环节,也是从零开始的人最缺经验的环节。我把部署拆成三个层次:本地封装成推理服务、容器化跑起来、以及生产环境的监控与告警。
4.1 推理服务的封装方式
接口设计上我走单入口模式——POST一个文本进去,返回类别ID、类别名和置信度。结构大概是:
from fastapi import FastAPI from pydantic import BaseModel import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification app = FastAPI() class TextRequest(BaseModel): text: str class PredictResponse(BaseModel): label_id: int label_name: str confidence: float model_name = "path/to/your/finetuned-model" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name) model.eval() @app.post("/predict", response_model=PredictResponse) async def predict(req: TextRequest): inputs = tokenizer(req.text, truncation=True, max_length=128, return_tensors="pt") with torch.no_grad(): logits = model(**inputs).logits probabilities = torch.softmax(logits, dim=-1) confidence, label_id = torch.max(probabilities, dim=-1) return PredictResponse(label_id=label_id.item(), label_name=str(label_id.item()), confidence=confidence.item())这里有几个工程上的细节值得你注意。第一,模型加载放模块层,进程启动只加载一次,这是性能的基础。第二,推理时一定要包torch.no_grad(),不包的话PyTorch会默认保留计算图,内存泄漏的直接原因就在这。第三,文本长度要用truncation=True兜底,生产环境的输入不像测试集那么规矩,一条超长文本就能把tokenize时间拖到几秒。第四,统一走/model接口做健康检查,Kubernetes或者云平台的探针会定期请求这个接口判断服务是否存活,对模型服务来说这个探针逻辑要比静态HTTP更可靠。
4.2 并发与性能调优
刚开始跑起来,我拿ab压测工具打了一波,QPS大概50多,对于一个内部分类服务其实够了,但如果流量翻倍肯定会出问题。后面我做了三个优化,把QPS提到了150以上。
第一,把推理从同步改成批量推理。简单说就是攒一批请求一起过模型,利用GPU的并行能力摊薄单请求开销。FastAPI里可以自己实现一个简单的请求队列,或者用PyTorch的Server来做。第二,关掉PyTorch的默认GIL竞争。模型推理阶段其实不太吃Python代码,瓶颈在矩阵运算,所以多线程部署反而比多进程好用,进程切换开销大。我验证下来是uvicorn + 多worker = 每个worker加载一个模型副本,显存会翻倍;不如单worker + 批量推理划算。第三,给推理服务加了LRU缓存,对完全相同的请求直接返回缓存结果。工单描述里确实有大量重复的模板话术,比如“我的账号登不上了”,缓存命中率大概能到20%。
4.3 容器化部署的完整配比
Dockerfile这部分我用了多阶段构建思路。基础镜像选择python:3.11-slim,装完依赖后清理缓存,最终镜像体积控制在1.5GB以内(模型文件占大头,代码依赖反而不多)。
FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt FROM python:3.11-slim WORKDIR /app COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY . . EXPOSE 8000 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]一个坑:基础镜像里要装gcc、g++和python3-dev,否则那些需要编译的依赖(比如pydantic-core)会在构建时报错或者变成纯Python模式导致性能下降。slim镜像默认没有编译工具链,我第一次构建就踩了这个坑,后来改成在builder阶段加编译依赖,最终运行阶段只拷贝产物,镜像就干净了。
docker-compose.yml里我把模型文件挂载成volume而不是打进镜像,这样模型更新只需要替换挂载目录里的文件再重启容器,不用重新build镜像。模型服务、依赖的Redis(如果有)、日志采集器,我用一个compose编排起来,本地一条命令跑全套。
5. 模型不是终点:上线之后的监控、漂移与迭代闭环
这是整个项目里最容易被新手忽略、也是最决定工程水平的一环。模型上线那一刻不是交付完成,而是运维挑战的开始。你说模型在测试集上F1有0.85,但是线上真实数据长什么样,测试集永远模拟不了。
5.1 日志埋点:唯一能还原线上状况的线索
我从上线第一天就要求所有推理请求记录结构化日志,每一行日志包括:工单编号、输入文本的hash值、预测类别、置信度、模型版本号、响应时间。文本本身不落日志,防止用户隐私问题和日志膨胀,但用hash值做去重和追踪绰绰有余。
这条日志的用处非常大。举个例子,上线两周后我收到业务反馈说“最近分得有点不准”,我没有直接改模型,而是先拉日志做数据漂移分析——把线上输入文本的特征分布(比如平均长度、高频词、标签预测分布)和训练集做对比。结果发现线上工单里出现了一批新的描述词,类似“小程序打不开”这种,而训练集里这个场景几乎没覆盖。这是非常典型的概念漂移。
5.2 监控指标:别只盯着QPS
AI模型服务的监控和普通Web服务有很大区别。除了常规的QPS、延迟、错误率,你还要监控模型层面的指标:平均置信度、各类别预测占比、被拒样本数量。平均置信度连续下降是漂移的早期信号,预测占比异常偏斜说明线上数据分布变了。
我在Grafana里配了三个面板:服务面板(QPS、P99延迟)、系统面板(GPU利用率、显存占用)、模型面板(置信度分布、类别占比)。告警规则设了三条——置信度均值低于0.8持续5分钟、P99延迟超过300毫秒持续5分钟、错误率超过1%。这套监控在大模型时代同样适用,只是观察对象从分类头置信度换成了logprobs和幻觉率。
5.3 模型迭代的闭环机制
模型总要更新,怎么把更新过程做到可控,这本身就是一门工程课。我推行了一个简单的版本管理流程:每版模型生成时记录训练数据版本、代码commit号、超参数配置、训练时长、测试集F1,并以标签形式推到模型注册表。线上服务只加载指定tag的模型权重,切换时通过发布平台一键替换,如果新模型上线后监控指标异常,可以秒级回滚到上一个版本。
数据反馈链路也必不可少。我在业务系统里加了一个“AI分类纠错”按钮——人工客服如果发现模型分错了,点一下按钮提交正确分类,这些样本自动进入一个待标注池,每个月抽取一批补充到训练集里重训模型。这个闭环让模型不是上线即静止,而是越用越准。说实话,这套机制比我当时调的任何超参数都管用,第二个月的f1比第一版直接涨了4个点。
6. 留给后来人的几条实在经验
整个项目走下来,我最大的感受是:AI工程从零开始,最大的障碍不是理论深奥,而是没人告诉你每一步有哪些“看起来小但致命”的细节。这篇文章没有把全部代码展开,我把那些无法用代码表达的经验总结在这几条里,你实操时大概率用得上。
第一,明确你的项目边界。先定义清楚“完成”的含义——是F1达标、是延迟可控、还是业务方认可。不同的目标导向会让你的工作重心完全不同。我见过太多人把一个月时间耗在调模型上,最后被运维同学一句“你这个服务内存占用怎么这么高”打回原形。工程项目的完成标准应该是:数据通、能训练、能部署、能监控、能迭代,五者缺一不可。
第二,遇到问题先查日志和数据,别急着重训模型。模型行为不符合预期时,80%的原因在数据侧——标注错误、分布偏移、预处理不一致。把错误的样本一条条看下来,比换一版更大的模型有用得多。
第三,保持一个“最小可运行系统”的意识。哪怕第一版只做一个最简单的规则分类器,第二步再上BERT,也比你憋一个月的完美模型更靠谱。最小系统能让你尽早拉通数据管道、部署流程、监控告警这整条链路,这些基础设施一旦通了,后面换再强的模型都只是替换其中一个组件的事。
我在做这个工单项目的同时,也搭了一套模板化的脚手架,把常用的数据处理、评估脚本、部署配置都固化了下来。后面的新项目基本都是在那个框架上换数据和换模型,一两天就能跑通第一版。这就是工程化的复利——今天的每一步基础设施建设,都是在为下一个项目摊薄成本。