“ai-engineering-from-scratch”,看到这个标题的第一眼,我就想起了自己刚转行做AI的那段日子。那会儿以为AI工程就是“写个模型、跑个精度、完事”,真正从零开始把一个项目放到线上,才发现从数据清洗到部署监控,处处都是学问。这篇文章就把我从零做AI工程的完整经验梳理出来:学习路径、工具选型、项目拆解、常见坑点,尽量说人话。不管你是刚入门的学生,还是想转型的工程师,都可以把它当成一份比较务实的参考,省掉一些不必要的弯路。
1. 从零开始AI工程,先纠正几个“想当然”
1.1 模型只是其中一环,不是全部
很多人对AI工程的理解,是从“训练模型”开始的。这没错,但只看模型会吃大亏。
一个真实的上线项目,至少包含需求拆解、数据获取、数据清洗、特征构造、模型训练、效果评估、服务部署、线上监控、持续迭代这些环节。训练模型这一环通常只占整个项目时间的五分之一都不到。我做过一个工单自动分类项目,最终训练一个模型只花了两三天,但前期的数据整理和标签校准花了整整两周。如果你只盯着模型,第一周就会发现自己无事可做或无从下手,因为大部分时间其实都在和数据“缠斗”。
举一个更生活化的例子:模型像个厨师,数据是食材和菜谱,部署是餐厅的后厨流水线。三者必须配合好。厨师再厉害,食材不新鲜、后厨流程混乱,端到客人面前的菜一样会被差评。AI工程要的是整条链路稳定,而不是某一个单点出彩。
1.2 AI工程和算法研究是两种思路
这一点我花了不少时间才真正想明白。
算法研究追求的是“新”。一个新结构、一个新损失函数,能在某个benchmark上刷出更好的数字,就是有价值的研究成果。但AI工程追求的是“稳”和“可复现”。同一个模型,今天跑是这个结果,明天跑变成另一个结果,这在工程上是不能接受的;线上预测出错率高了0.5个百分点,如果不能定位到原因,那就是事故。
所以工程实现里,你会看到大量的“笨功夫”:固定的随机种子、统一的预处理函数、数据版本记录、模型版本管理、回滚机制。这些看起来很琐碎,但恰恰是AI工程和算法实验最大的分水岭。刚开始做项目的时候,我也会因为模型不稳定而怀疑自己代码写错了,后来才明白,很多时候问题在于实验环境不统一:依赖版本飘了、数据被悄悄改动、预处理逻辑在训练和推断时不一致。
1.3 “from-scratch”的三个不同起点
“从零开始”这句话其实有歧义。不同的人起点完全不同,我不想装作每个人都需要同一条路线。
如果你的起点是“几乎没有编程经验”,那第一条要补的是Python基础、SQL和命令行,而不是深度学习。因为这些是地基,地基不稳,后面每走一步都在晃。如果你的起点是“会写代码,但没做过机器学习”,那你的核心任务是理解数据到模型的全流程,以及评估指标背后的业务含义。如果你的起点是“已能复现一些模型,但不知道怎么上线”,那你要补的是服务封装、容器化、监控告警这些工程能力。
这三种起点对应完全不同的学习计划。我见过太多人一上来就啃《统计学习方法》、看Transformer源码,结果坚持不了两周。原因不是不够聪明,而是没有结合自己的现状去拆任务。真正的“from-scratch”,是先承认自己现有的位置,再一步步往AI工程这个目标挪。
2. 学习路径:怎么一步步建立AI工程能力
2.1 基础三件套:Python、SQL、Linux
不要小看这三样东西。它们决定了你处理数据和排查问题的效率。
Python是AI工程的主流语言,但你要学的不是语法,而是用Python处理脏数据的能力。我建议重点练pandas和numpy的操作,尤其是groupby、merge、apply这些高频用法。很多人跑模型很溜,遇到一个缺了很多值的表格反而手足无措,就是因为数据处理练得太少。
SQL也很重要。真实数据大多存在数据库里,你不可能每次都导出CSV用pandas读。会写SELECT、JOIN、子查询,必要时会用窗口函数,这基本就够用了。我很多时候都是先写SQL把数据从大库里捞成一份宽表,再交给pandas做清洗。这个组合非常顺手。
Linux主要在部署阶段体现价值。你会需要在服务器上装环境、跑训练、查日志、看资源占用。至少要熟悉conda、pip、systemctl或docker常用命令,以及grep/tail/sed这几个日志处理工具。如果连文件权限和进程管理都搞不清楚,部署时一定会焦头烂额。
2.2 先传统机器学习,再深度学习,这个顺序别跳
我见过不少人,一上来就想用BERT微调,或者直接上GPT开源模型。不是说不行,而是工程上风险太高。传统方法有解释性、有稳定性、对数据量要求低,很多业务问题用逻辑回归、随机森林、GBDT反而更快见效。
我自己的实践顺序是:先搞懂线性回归和逻辑回归,理解损失函数和梯度下降的基本直觉;然后学决策树和集成方法(随机森林、XGBoost、LightGBM),理解特征重要性和调参思路;掌握了这些,再过渡到神经网络。此时你会发现神经网络的很多概念,比如学习率、过拟合、正则化,其实在传统模型里都有对应概念,学起来会快很多。
深度学习不是不需要学,而是不要作为第一步。真实工程里,如果是图像、文本、语音这类非结构化数据,往往确实需要深度学习模型。但即使是这些场景,我也会先做一个简单的基线,比如TF-IDF加逻辑回归,或者直接用一个小的预训练模型。先让流程跑通,再谈效果提升。这个习惯帮我避开了很多“模型没跑通但自我感觉良好”的坑。
2.3 工程化能力:版本管理、实验追踪、环境隔离
这部分是很多自学路线里最容易漏掉的,却是AI工程最核心的护城河。
版本管理不只是Git管代码,还包括数据版本和模型版本。代码管理的意义大家都懂,但数据每天都在变,模型也在迭代。如果没有记录哪份数据配哪个模型,出了线上问题很难回溯。我用DVC记录数据集版本,用MLflow记录实验参数、指标和模型产物。刚开始觉得多余,等到需要回滚一个模型版本时,才发现这些记录有多重要。
实验追踪解决的是“我试过什么”的问题。以前我习惯把参数随手写在notebook里,过了一个星期完全想不起当时到底用了什么学习率、什么归一化方式、为什么结果好。后来改成每次实验都记一个配置,包括数据路径、预处理方式、模型结构、超参数、随机种子、评测指标,并把它们写入MLflow。这个习惯让协作和复现变得轻松很多。
环境隔离则是为了避免“在我电脑上明明能跑”的尴尬。用conda或poetry管理Python依赖,用Docker打包包含系统依赖的镜像。训练阶段可以略松,但部署阶段必须严格锁定环境。这一步做不好,模型只能活在你的电脑里。
3. 核心细节解析:一个完整项目怎么做
3.1 项目定位:从业务问题变成机器学习任务
很多从零开始的AI项目死在第一步:把业务需求拍脑袋转换成任务。
以我做的“工单自动分类”为例。业务方的需求是“帮客服更快处理工单”。这个描述很模糊,不能直接训练。必须继续拆:要处理的是新工单进入时自动分发给对应小组,所以任务定义为:给定工单标题和描述文本,输出预定义的工单类别。类别一开始定了五类:咨询、退款、投诉、物流问题、其他。这就是一个多分类文本分类问题。
任务定义直接决定了后续所有工作。如果分类定义不清晰,后半程一定会反复返工。比如“退货”和“退款”是两个类别还是同一个?如果一刀切,遇到“我退货了但退款还没到”这种文本,模型很难抉择。最终我们通过和业务方讨论,重新归类成六个类别,并补充了明确的判定规则。这个环节宁可慢一点,也要让每个参与的人都知道边界在哪里。
评估指标也要提前定好。我用的是宏平均F1,而不是简单准确率。原因是数据类别不均衡,“咨询”类样本很多,“投诉”类样本很少,如果只看准确率,模型全部预测成“咨询”也能拿到八成以上的正确率,但业务上这种模型没有任何价值。宏平均F1会对少数类更敏感,能逼着模型认真对待小类目。
3.2 数据采集与清洗:最耗时但最值钱的一步
工单数据来自历史客服系统,总共导出了十二万条记录。听起来数据量不小,但清洗完只剩不到六万条可用。常见的坑包括:重复提交、空白内容、测试工单、内部备注混在生产数据里,还有大量html标签和特殊符号。
清洗步骤我按固定顺序做:先按工单ID和标题内容去重,再过滤掉明显无意义的内容,然后统一文本大小写、去除多余空白和html标签,最后用正则把手机号、订单号等敏感信息做脱敏。这里必须强调,脱敏不只是合规问题,也能避免模型记住用户隐私而不自知。
数据中最麻烦的是标签不一致。同期有好几个客服团队在记录工单,同一个“快递没送到”,有的标成“物流问题”,有的标成“投诉”。如果不校准标签,模型学的边界就是乱的。我当时做了一件事:随机抽了一千条数据,让两个同学分别独立打标,再对比不一致的部分,跟业务方开会确认统一规则。前后花了两三天,但效果非常明显,模型训练直接稳了不少。
最后,数据划分也有讲究。我按时间顺序切分训练集、验证集、测试集,而不是随机切分。原因很简单:线上数据永远是“未来”的数据,随机切会高估模型在时间维度上的泛化能力。这个细节在工单、交易、风控等场景尤其重要。
3.3 基线模型:先从简单方案开始,别一上来就上深度学习
数据清完之后,我先用TF-IDF加逻辑回归做了个基线。这里想强调,基线不是敷衍,而是给大家一个参照系,也帮整个流程先跑通。
第一步是特征化。把每一条工单的标题和描述拼接起来,然后用TF-IDF转成向量。TF-IDF的做法我不展开讲数学了,可以简单理解成:如果一个词在这条文本里频繁出现,但在整个语料里很少出现,那么它更可能具备区分价值。它很朴素,但作为文本分类基线非常有效。
第二步是训练逻辑回归。逻辑回归的好处是训练快、可解释,每个词对应的权重可以直接查看。比如模型把“快递”这个词的权重分配在“物流问题”类别上,这很容易验证是否符合直觉。我用scikit-learn的Pipeline把特征化和模型串起来,整个训练过程不到一分钟。
第三步是用宏平均F1打分。这个简单基线在验证集上达到了0.82左右。看起来不惊艳,但已经比随机猜测好太多,更重要的是给我后续深度学习模型提供了对比锚点。如果深度学习模型连这个分数都超不过,那就说明问题不在模型,而在数据。
3.4 深度学习模型:微调预训练模型前后的关键控制点
基线稳定后,我尝试了一个较小的预训练模型,对工单文本做微调。这个过程比训练逻辑回归复杂得多,也更容易翻车。
先说数据格式。预训练模型需要把文本转成模型认识的输入序列,我使用的tokenizer会对句子分词,并转成input_ids、attention_mask。这里必须让训练阶段和预测阶段用同一个tokenizer对象,否则会因为词表不一致导致线上输入和训练完全不匹配。我曾经在这上面吃过亏,离线评估都是用自己的tokenizer,部署时一换版本,效果立刻崩了。
训练参数方面,我最开始常用的一组分段是:batch size为16,学习率2e-5,训练3个epoch。刚开始loss下降很快,后面两个epoch就陷入波动。后来加入了一个小技巧:前10%的steps先做warmup,把学习率从很小逐步升到预设值,再按余弦退火降到接近0。这样曲线稳定一些,最终验证集的宏平均F1达到了0.91,比逻辑回归高了不少。
另外一个很重要的控制点是冻结随机种子。训练前固定seed(比如42),保证每次重跑结果一致。深度学习训练有随机性,如果不固定种子,哪怕同一份代码和数据,两次训练出来的权重也会有差异。在工程上,这种不确定性会增加排查成本。
训练完成后,我做了错误分析。抽了几十条被预测错的样本,发现一个典型问题:模型把“退运费”猜成了“退款”,但业务方认为应该归为“物流问题”。这说明类别定义和模型学到的隐层规律存在不一致。最后我们在预处理层加了同义词映射,把“运费”“退货运费”归一化,又调了一版。这个小改动让模型F1提升了不少。所以不要只盯着模型结构,数据处理往往才是最后一个百分点提升的来源。
4. 部署与监控:让模型真正“活”在生产环境
4.1 为什么部署是AI工程的试金石
模型训练完毕,准确率也达标了,但项目还没结束。真正的考验在于:你能否让这个模型稳定地服务线上请求。
部署阶段最常遇到的问题是环境不一致。训练时你可能用了conda环境,里面装了很多兼容依赖;但生产服务器可能是干净的容器,Python版本、依赖版本、GPU驱动都可能不同。一个有半年经验的人会告诉你:从第一天训练就开始用Docker,至少把Python环境固定在同一个镜像里。这样训练和推理的基础环境基本一致,能减少大量莫名其妙的问题。
部署还有一个容易被忽略的点:模型加载时机。在FastAPI之类的服务框架里,模型加载应该发生在服务启动时,而不是每个请求进来后再加载。否则在高并发下,接口会慢到让业务方放弃使用。我当时是把模型加载和tokenizer加载放在启动事件里,一次性放进全局变量,推理请求只做前向计算。
4.2 我常用的部署方案:FastAPI加Docker
下面给出一段我常用的FastAPI推理服务骨架,代码不复杂,但把关键点都带出来了。
import pickle import torch from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class PredictRequest(BaseModel): title: str description: str def load_model(): tokenizer = torch.load("tokenizer.bin") model = torch.load("model.bin", map_location="cpu") model.eval() return tokenizer, model tokenizer, model = load_model() LABELS = ["咨询", "退款", "物流问题", "投诉", "其他"] @app.post("/predict") def predict(req: PredictRequest): text = f"{req.title} {req.description}" inputs = tokenizer(text, truncation=True, max_length=128, return_tensors="pt") with torch.no_grad(): logits = model(**inputs) idx = int(logits.logits.argmax(dim=1)[0]) return {"label": LABELS[idx]}这个服务的最大优点就是把“加载一次、接受请求”的结构做得很清晰。模型预热在启动时完成,CPU单机上也能跑,推理延迟大约在几十毫秒量级。线上部署时,我用Docker把它打包,组成一个独立镜像,再放到服务器上跑。日志直接打到标准输出,方便用日志系统去做持久化和监控。
有一点要注意:一旦涉及训练数据和模型文件,不要把它们塞进Git仓库。模型文件通常很大,Git会越拉越慢。更合理的方式是把模型文件放到对象存储或专门的模型仓库,部署时从远端拉取。配合MLflow的话,你还能直接记录哪一个模型版本是“生产版本”。
4.3 线上监控与迭代:模型不是上线就完事
部署完不等于结束,上线后还有三类监控要做。
第一类是系统资源监控。CPU、内存、延迟、请求量这些基础指标必须建立告警。否则一个异常流量高峰就可能把服务打挂,而你是最后一个知道的人。我用Prometheus加Grafana做基础监控,规则简单但有效。
第二类是输入特征监控。线上预测数据分布与训练数据不一致,我们叫数据漂移。比如工单文本里突然大量出现一个新词,模型可能完全没见过,预测自然不准。实现方式可以每天统计线上预测输入的长度分布、词汇表覆盖情况,和训练分布做对比,偏差超过阈值就报警。
第三类是业务结果监控。也就是模型预测的类别分布是否合理。我会保存每天每个请求的预测结果,按类别做统计,观察各类别占比是否发生突变。如果“投诉”类从5%突然涨到20%,大概率是业务出现了异常,也可能是数据分布变了。这类监控能让你在业务损失扩大之前发现问题。
监控不是只看的,还要接迭代流程。我给自己定的节奏是:每两周抽一批线上新增数据,人工标注后加入训练集,重新训练模型,再和当前线上模型做离线对比。如果新模型在验证集上保持稳定或提升,就会执行灰度上线。这样可以形成一个缓慢但持续改进的循环。
5. 常见问题与排查技巧实录
5.1 训练效果不错,上线就打折,为什么
这是最经典的问题。离线测试F1是0.91,上线后感觉经常误判。原因通常是三个:数据分布不一致、预处理不一致、推理模式不一致。
训练时的历史数据可能和你线上实时输入的结构不同,比如线上用户写的句子更短、更多口语词;预处理环节训练和推理用了不同版本的函数;训练时dropout在模型里,但推理时没关掉。前两个用上线后的日志对比就能发现,第三个则是必须显式地调用model.eval()。
我的建议是上线前写一个“冒烟测试”脚本,用十几条代表性的真实样本,分别调用本地推理和线上服务,逐条对比结果。如果发现输出不一致,立刻检查预处理链和环境版本,多数问题半天内可以定位。
5.2 数据漂移是怎么悄悄发生的
数据漂移可能不是突然的。一开始模型还好,几周后准确率慢慢下滑。最典型的表现就是预测类别的历史分布变化。拿工单分类来说,电商大促期间“物流问题”增多,你之前用日常数据训练的模型自然要受影响。
应对方式不只是重新训练。我会在线上预测日志里记录每一条文本的token数量、类别概率分布,按天汇总。如果发现某几个类别的平均置信度持续下降,就说明模型开始“不确定”了,主动触发一次再训练。与其等业务方投诉,不如让系统自己给出信号。
5.3 模型文件该不该放进Git仓库
一定不要。特别是深度学习模型,动辄几百兆甚至几个G。Git仓库放的是代码和配置,不是大文件。大文件上Git,不仅仓库膨胀,拉取代码也变得痛苦。模型文件应该放在专门的对象存储或模型仓库里,最好用带版本号的文件名,比如“cls_model_v3.bin”,配合训练日志记录对应关系。
我记得有一次队友把模型文件提交到仓库,整个仓库立刻涨到几个G,所有成员拉代码都要等很久。后来用Git LFS或外部存储才解决。这个问题很常见,early stage最容易踩。
5.4 样本很少的项目怎么起步
如果你只有几百条数据,不要绝望,但要调整策略。第一,别用大模型,用小模型或传统方法。几百条数据不够深度学习充分学习,逻辑回归或小型的预训练模型可以做。第二,想办法做数据增强或利用预训练语言模型的知识,但前提是你对业务理解足够深,不能瞎增强。第三,可以尝试人工规则加模型混合:规则负责把握高频场景,模型负责处理规则覆盖不到的边缘,这样整体效果更可控。
还要理性地告诉业务方“这是一个迭代型系统”。第一次上线70%的准确率,第二轮变成85%,这就是可接受的工程进步,比憋大招半年后再上线要实际得多。
啰嗦了不少,但我最想说的是:AI工程从零做起来,确实不轻松,可也没必要觉得它高不可攀。我在过程中最大的体会是,保持“细致”比保持“聪明”更重要。数据清洗时多检查一次,实验配置多记录一条,线上日志多看两眼,很多问题根本不会发生。如果你也正在走这条路,建议从一个小而完整的项目开始,先把它跑上线,再慢慢迭代。这个“先跑通,再跑好”的节奏,能帮你省下大量自我怀疑的时间。