做AI工程这行有个特别有意思的现象:真正能在生产环境里跑得稳的模型,往往不是算法最花哨的那个,而是把数据、训练、部署、监控这些脏活累活都捋顺了的那一套。很多人以为AI工程是从写模型开始的,实际上它更像是一场从数据到价值的接力赛。所谓ai-engineering-from-scratch,就是把这条链路从头到尾亲手搭一遍,不依赖现成的傻瓜平台,也不靠调包侠式的复制粘贴,而是理解每个环节背后的工程决策。这篇文章是我过去几年在AI工程落地过程中的经验沉淀,写给那些想从零开始、真正入行AI工程的人,也写给已经在做算法但总被工程问题纠缠的朋友。我会从整体思路开讲,再拆工具链,最后用一个完整的情感分析项目把流程串起来,顺带把那些光看文档绝对学不到的坑翻出来晒一晒。
1. 为什么说AI工程不是“调模型”那么简单
1.1 从“跑通notebook”到“生产可用”之间隔着一整套工程
很多初学者在Jupyter Notebook里跑通一个模型后,会觉得AI工程已经完成了大半。实际上,Notebook里的代码只是整条流水线的某一个片段,而且往往是受控条件下的片段。真实环境里有脏数据、不均衡分布、字段缺失、延迟约束、并发请求、模型漂移等一系列问题,Notebook里完全不会体现。
我之前接过一个项目,算法同事在离线评测集上把准确率做到了94%,高高兴兴上了线,结果线上效果一塌糊涂。后来排查发现,他训练时用的标签是用户手动反馈的“好评”和“差评”,这些标签本身就有滞后性,等模型上线时用户行为分布已经变了。这属于典型的训练服务不一致,根子上就是工程问题,不是模型结构能解决的。所以从零开始做AI工程,首先要把自己的视角从“模型”拉高到“系统”。模型只是系统里的一个组件,它前面有数据管道,后面有服务框架,旁边有监控告警,脚下有算力资源。
1.2 from-scratch的路线图:数据、模型、部署、运维四条腿走路
我见过不少自学AI工程的人,路线基本是:先啃完线性代数,再学Python,然后刷一遍吴恩达的机器学习课,接着碰一个Kaggle项目,就以为完成了。这套路不能说错,但漏掉了工程化最核心的部分。真正的from-scratch应该围绕四条主线并行推进:
第一条是数据线。包括数据采集、清洗、标注、特征工程、数据版本管理。这条线决定了模型的天花板,很多AI项目失败不是因为模型不好,而是数据质量太差。
第二条是模型线。包括基线选择、训练调参、实验记录、评估体系。这条线追求的是可复现和可对比,而不是一次性的“最佳分数”。
第三条是部署线。包括推理服务、接口封装、性能优化、资源调度。模型训练好只是半成品,得让它稳定地对外提供预测能力才算完工。
第四条是运维线。包括日志、监控、告警、模型版本回滚、A/B测试。模型上线不是终点,而是新一轮迭代的起点。
这四条线互相咬合,少一条都会在某个阶段卡住。我建议新手按这个顺序去实践:先手写一遍数据清洗,再训练一个简单模型,接着把它封装成HTTP接口,最后用监控工具盯一周。走完这一轮,AI工程的框架就立起来了。
1.3 先搞清楚边界:AI工程师、算法工程师、数据工程师各管什么
讨论AI工程之前,有必要把角色边界说清楚。否则很容易出现“什么都想学,什么都学不精”的焦虑。算法工程师的核心职责是设计模型结构、改进训练策略、提升评测指标。数据工程师的核心职责是把分散的数据收集、清洗、组织成可用的数据仓库或特征平台。AI工程师则处在两者中间,既要懂模型的训练逻辑,又要懂数据怎么流动,更要把训练好的模型变成可靠的线上服务。
实际项目里岗位是交错的,但知识结构应该有侧重。如果你定位在AI工程,那打磨工程能力优先于钻研新模型。我的做法是:模型方面掌握经典结构和最新趋势的差别就够了,重点花时间在端到端流程、服务质量、问题排查上。一个能两周上线一个稳定服务的AI工程师,比一个只会在论文里调transformer但一部署就挂的算法工程师更受市场欢迎,这话不好听,但很现实。
2. 核心技能与工具链拆解:每一环为什么这么选
2.1 Python之外,你还得会点数据基本功
AI工程对Python的要求不是“会写脚本”,而是“能处理内存放不下、结构混乱的数据”。首先得熟练pandas和SQL,这两个是吃饭的家伙。很多新手喜欢在pandas里做所有过滤和聚合,但数据量跑到几千万行时,内存直接爆掉。正确姿势是先落SQL做粗粒度筛选,再用pandas做精细处理。
然后是数据版本管理。模型要复现,光有代码版本不够,数据也得有版本。我早期吃过亏:同一个训练脚本,半年后重跑,结果完全不一样,因为底层数据源悄悄变了。后来引入DVC(Data Version Control),把每个实验对应的数据快照记录下来,再配合Git提交信息,才彻底解决可复现问题。你说这些技术听起来不“AI”,但它们才是AI工程真正的地基。
2.2 模型训练的关键不在模型结构,而在实验管理
模型训练环节最容易走偏的地方是“只关注最后那个分数”,不关注过程中的变化。你要知道调参时改了什么、数据切分方式是怎样的、随机种子是多少、哪一轮开始过拟合。这些信息如果只靠脑子和文件名管理,项目一复杂准出乱子。
我现在习惯用MLflow做实验追踪,每次训练跑完自动记录超参数、指标、模型产物和数据集hash。这样做有三个直接好处:一是对比实验时能精确知道哪个改动带来了提升;二是复盘时能快速定位是哪份数据或者哪个参数导致效果退化;三是交接给同事时,不需要口述几十条历史记录。
另外,训练环境的依赖管理要提前固定。Python的版本地狱大家都有体会,今天numpy更新一个版本,明天可能就冒出兼容性问题。所以每个项目必须用虚拟环境或者容器镜像锁定依赖。我是直接用Docker镜像来训练,Dockerfile里把Python版本、CUDA版本、pip依赖全部写死,镜像构建完推到私有仓库,谁跑谁拉,基本不会再出现“我本地能跑,你那边报错”的情况。
2.3 部署与推理优化:从torchserve到ONNX Runtime实测
模型训练只是前半程,推理部署才是工程的试金石。很多人以为把模型用Flask包一层就是部署了,但真实场景里要考虑批量推理和单条推理的差异、GPU显存占用、延迟波动、并发上限。
我常用的是TorchServe加ONNX Runtime的组合。TorchServe负责模型管理和HTTP接口,ONNX Runtime负责实际推理加速。导出ONNX时要注意动态轴设置,否则输入长度变了就得重新导出模型。比如处理中文文本时,每条样本的长度不一样,导出的模型必须支持动态维度,否则服务端一收到变长输入就报错。这一步属于“文档里不容易写清楚,但遇到一次就长记性”的细节。
部署架构上,我习惯把模型服务和无状态API网关分开。网关负责鉴权、限流、超时控制,模型服务专注推理,这样任何一个模型升级都不影响整个接口的稳定性。压测时重点看两个指标:P99延迟和最大QPS。不要只盯着平均延迟,因为平均值会被少数慢请求拉低,线上用户体验往往由P99决定。
2.4 监控与反馈闭环:没有监控的AI系统等于盲飞
很多团队把模型推上线就撤了,直到用户投诉才反应过来说模型出问题了。这种情况本质上是缺少监控。AI系统的监控跟普通后端服务不太一样,除了要盯CPU、内存、QPS这些基础设施指标,更重要的是盯模型预测的分布。
我分享一个最实用的做法:给每次预测请求记录输入特征和输出概率,然后按小时统计输出概率的均值、分位数和类别占比。一旦发现均值突然偏移、某个类别的输出占比骤变,立刻告警。这套逻辑不依赖真实标签,因为真实标签往往有延迟,而特征和输出分布几乎实时就能算。有了分布监控,很多模型退化的问题能在影响扩大之前就被发现。
监控之外还要有反馈闭环。预测结果要有地方落库,积累到一定量之后可以抽样人工复核,生成新的标注数据,再进入下一轮训练。我在项目里用Label Studio做标注平台,把模型预测得分低的样本自动推送给标注员,形成“预测-筛选-标注-重训”的循环。这套机制跑顺之后,模型效果提升会进入一个良性循环,而不是上线一次就等着效果老化。
3. 手把手做一个端到端项目:商品评论情感分析
3.1 项目背景与数据准备
为了不空谈理论,我用一个完整的案例来演示AI工程的最小闭环:商品评论情感分析。任务很简单,输入一段中文评论,输出正面或负面。看起来是一个入门级分类任务,但把工程链路跑完,就能覆盖前面讲的所有环节。
首先得弄数据。我用了开源的中文电商评论数据集,里面包含几万条已标注评论。拿到数据先别急着训练,先做一轮探索性数据分析。我写了一个简短的Python脚本来查看类别分布、文本长度和缺失值情况。这步不能省,因为如果你连数据的类别分布都不清楚,后面所有结论都可能是错的。
import pandas as pd df = pd.read_csv("reviews.csv", names=["label", "text"]) print(df["label"].value_counts(normalize=True)) df["length"] = df["text"].str.len() print(df["length"].describe())输出结果显示正负样本比例大约是7比3,有明显的类别不平衡。文本长度最短几个字,最长上千字,中位数在几十个字左右。粗糙地直接训练分类器,模型大概率会对多数类倾向严重,而且长文本和短文本的处理方式完全不同。我处理的方式是先做长度分桶,再考虑是否截断。最终把训练集、验证集、测试集按6比2比2随机切分,同时记录切分时的随机种子,保证实验可复现。
3.2 特征工程与基线模型
第一个模型不要一上来就上BERT,成本高、调试难,而且一旦出问题很难定位是数据问题还是模型问题。正确做法是先做一个简单可解释的基线。我用的基线是TF-IDF加逻辑回归。它在很多短文本分类任务上表现不差,训练速度快,还能给出每个词的重要性权重,方便排查问题。
特征工程这块,中文文本需要分词。我直接使用jieba的自带词典做精确模式分词。分词之后过滤掉明显无意义的停用词,但要注意情感分析场景下“不行”“不太好”这类带否定词的短语不能粗暴移除。这里有个技巧:我构造了一个二元词特征,把“不”和它后面的动词合并成一个特征,这样能部分保留否定语义。
逻辑回归训练用sklearn的一行代码就能搞定,但实际工程中我建议把数据预处理、特征抽取、模型训练串成一个pipeline,既方便交叉验证,也方便上线时保持一致。这一步对应的代码大概长这样:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline pipe = make_pipeline( TfidfVectorizer(tokenizer=jieba_tokenizer, ngram_range=(1, 2)), LogisticRegression(class_weight="balanced") ) pipe.fit(X_train, y_train)这里把class_weight设为balanced,是因为正负样本不平衡,通过调整类别权重让模型注意少数类。训练完在验证集上看到F1大概在0.84左右。基线模型达到这个水平,已经比瞎猜强很多了,后续所有尝试都拿这个结果当参照物。
3.3 训练调优与验证策略
有了基线,接下来可以尝试更复杂的模型。但从工程角度看,我不建议直接搓一个BERT然后傻等。先想清楚验证策略比选什么模型更重要。情感分析这类任务,如果训练数据来自不同时间段或者不同商家,直接随机切分会高估模型效果。只有按时间或按来源划分验证集,才能模拟真实上线后的分布漂移。
我用的是分组切分,把数据按评论来源商家分组,确保同一个商家的评论不会同时出现在训练集和验证集里。这样评测结果更接近线上真实水平。之后我尝试了用HuggingFace的预训练中文BERT做微调,训练时只跑了3个epoch,学习率设置为2e-5,批大小16。这一步想提醒的是,预训练模型微调不等于把数据喂进去就行,还必须做学习率预热,否则模型容易在初期震荡。微调之后F1从0.84涨到了0.92,提升明显,但训练耗时也从几十秒涨到了十几分钟。这就是一个典型的工程取舍:如果你有足够的GPU资源,用BERT会有收益;但如果推理环境很紧张,TF-IDF加逻辑回归的效果或许已经够用。
调优记录要全部落到MLflow里。我记录的内容包括数据集路径、切分种子、模型类型、关键超参数、验证F1、训练耗时、显存占用。这样当你想复现“那个效果最好的模型”时,不用翻聊天记录或者凭记忆猜测。
3.4 部署为HTTP服务并做性能压测
模型训练完,接下来把它变成一个能对外提供服务的接口。我采用FastAPI封装服务,因为它的异步支持和请求体验比Flask更适合AI推理场景。模型加载只在服务启动时做一次,后续请求直接复用,避免每次推理都加载权重导致延迟爆炸。预测函数接收文本,把分词、向量化、模型预测这些步骤串联起来,返回标签和置信度。
from fastapi import FastAPI from pydantic import BaseModel import joblib app = FastAPI() pipe = joblib.load("sentiment_pipeline.joblib") class Review(BaseModel): text: str @app.post("/predict") def predict(review: Review): label = pipe.predict([review.text])[0] prob = pipe.predict_proba([review.text])[0].tolist() return {"label": int(label), "confidence": max(prob)}部署时我直接用uvicorn启动服务,然后用locust做压测。压测的目标是看清楚这个服务在单机、双核CPU下能扛多少并发。实测下来大概200 QPS时P99延迟在85毫秒左右,再往上压就会出现超时。后来我把TF-IDF向量器里面的tokenizer从jieba换成更精简的自定义正则,压缩了特征维度,P99降到了55毫秒。这个优化思路很简单:线上环境不需要和离线训练时完全一致的特征,只要保证映射逻辑一致,可以在服务层面做轻量化替换。
部署上线前还应该做一轮“模型服务联调”,确保请求字段和返回字段与前端约定一致。很多人上线后才发现前端传过来的字段名和后端对不上,这就是联调没做透。我习惯在预发布环境跑一遍全链路的模拟请求,从API网关到模型服务再到日志落库,完整走一圈才能确认系统是真的通了。
4. 真实项目中的坑与排查技巧
4.1 数据泄漏:最隐蔽也最致命的错误
数据泄漏是AI工程里最让我头皮发麻的问题。它不像代码报错那样有大堆堆栈可以排查,而是模型指标高得离谱,上线就露馅。常见泄漏模式包括:用全量数据做标准化后再切分,导致验证集信息混入训练集;做特征工程时用到了未来时间的数据;重复样本没有去重,同一批数据前后出现在训练集和验证集。
我遇到过一次至今难忘的泄漏:一个推荐项目的离线准确率高达98%,但上线后点击率反而没有显著提升。最后查出来是因为训练数据里包含了用户是否点击了某商品这一“未来行为”,而线上预测时这个字段根本不存在。这类问题只能靠严格的数据切分规范来预防。我的经验是先把所有样本按照实际预测时刻的时间排序,再用时间窗口切分,同时保证任何特征都不包含预测时刻之后的信息。这条规则应该刻在每个AI工程师的脑门上。
4.2 训练与服务时的版本地狱
模型训练和部署之间的版本不一致,是团队协作时最常踩的坑。比如离线训练用的sklearn版本是1.2,部署环境却用的是1.0,两个版本的TfidfVectorizer内部实现有细微差异,导致同样的输入产出不同的特征向量,模型预测自然就飘了。
解决这个问题的唯一靠谱办法是把训练环境和服务环境做成同一个容器镜像。我在这个情感分析项目里就是用一个Dockerfile同时打包训练依赖和服务依赖。如果训练机器是GPU环境、服务机器是CPU环境,那就要格外注意。深度学习框架在GPU和CPU上的算子实现并不完全一致,浮点数计算也可能有微小差异。我的做法是在导出模型前先做一致性测试:拿一条固定输入,分别在训练环境和部署环境跑一遍,比较输出结果。如果误差在可接受范围内,再打包上线。
4.3 冷启动与长尾问题
新上线的AI系统往往面临冷启动问题。你没有线上真实数据,模型是在离线数据上训练的,而离线数据分布和线上初始流量分布很可能不一致。我处理冷启动的方法是分阶段放量:先在5%的流量上运行,观察输出分布与训练分布是否一致,确认稳定后逐步放量到10%、50%、100%。这一步看起来保守,但能有效避免“一上线就把所有用户都体验了坏结果”的灾难。
长尾问题是另一个容易被忽视的坑。模型在头部高频样本上表现很好,在低频但影响大的样本上可能非常差。比如情感分析里可能有大量“假阳性”样本,像“这个商品用起来还行,但是客服态度极差”这种混合情绪,模型很容易判错。应对长尾的思路是专门收集这部分样本,做针对性标注并加入训练数据。我在项目里用模型对回答不确定的样本进行主动抽样,每周抽一批出来人工复核,再补充进训练集,持续迭代三个月后,长尾效果有了明显改善。
4.4 排查套路速查表
实际排查AI系统问题时,我会遵循一套固定的套路,推荐给所有从零上手的朋友。首先看数据,确认请求和训练数据格式是否一致,字段有没有变化。第二看模型输出分布,统计当前的类别占比和得分均值是否和训练时接近。第三看特征对齐,把线上请求日志里提取的特征与训练时的特征做对比,检查有没有缺失或畸形。第四看环境一致性,确认依赖版本、模型文件hash、GPU驱动是否与训练时一致。最后看资源瓶颈,如果延迟突然升高,大概率不是模型变笨了,而是CPU打满或内存占用了。
为了把这套方法固化成可执行的机制,我在团队里建了一个简单的“模型健康检查表”,每次发布新模型之前,必须跑一遍检查表上的所有项目,全绿才能放量。刚开始走这个流程会觉得繁琐,但坚持两三个项目之后就会明白,这些步骤救过你太多次了。
结尾
做AI工程这些年,我最大的体会是:真正难的不是模型训练,而是把一个模型稳定地跑在真实业务里,并让它持续产生价值。从零开始的意义在于,你亲手把数据、模型、部署、监控每个环节都踩过一遍,那些文档里一带而过的细节才会真正长成你自己的经验。比如我到现在还能记得第一次因为数据泄漏被坑到失眠的夜晚,也记得第一次把简单的逻辑回归模型上线后看到实时日志里出现预测结果时的那种踏实感。这些经历比任何教程都值钱。如果你也正在从零摸索AI工程,希望这篇文章能让你少走一些弯路。最后再分享一个小技巧:当你陷入某个bug毫无头绪时,先别急着改代码,把数据、环境、日志从头到尾分别看一遍,答案往往就藏在你不经意忽略的那一层。