
先说一个我观察到的现象这两年“ai-engineering”这个词越来越火但很多人的理解还停留在“会用几个深度学习框架、能跑通一个训练脚本”的层面。跑通一个 notebook 和交付一个能稳定运行、可维护、可迭代的 AI 系统中间差着十万八千里。这也是我今天想聊的核心——从零开始把一个 AI 项目做成真正的“工程”到底要过哪些坎、学哪些东西、避哪些坑。我本来想用一个具体项目串起这篇文章后来发觉不如把“ai-engineering-from-scratch”当成一条完整的学习与落地路径来拆。这样不管你是准备转行做 AI 工程还是已经接了一个实际业务项目但感觉心里没底都能从中找到自己能上手的那一步。文章不会堆概念按我实际做项目时的顺序从学习路径、最小工程搭建、评估、部署到长期维护一层层剥开。1. 先搞清楚AI 工程和机器学习到底差在哪1.1 模型训练只是冰山一角不夸张地说很多人第一次做 AI 项目时都会掉进同一个陷阱拿到数据后立刻开始调模型训练集准确率刷到 95% 就觉得胜利在望。等真正上线才发现数据格式变了、接口调用超时、日志完全看不懂模型效果也不行。这不是模型的问题而是工程化缺失。我理解里的 AI 工程不是“把模型训练好”这一个环节而是一条完整链路数据获取与清洗、特征加工、实验管理、模型训练、离线评估、服务封装、部署上线、实时监控、持续迭代。模型训练在这条链里只占一小块。这也解释了为什么很多学术背景很强的人到了真实业务场景反而寸步难行——因为他们缺的恰好是链条前半段和后半段那些“不高级但极其磨人”的能力。1.2 从零开始的工程化到底要学什么“从零开始”并不是指从数学推导开始。就我个人的经验真正零基础的人在 AI 工程方向上最值得先抓住的是以下五块基础Python 与数据处理基本功这不是简单会写 for 循环而是要能熟练处理各种脏数据、缺失值、类型混乱的表格能写清晰的脚本而不是永远依赖 notebook。命令行与 Linux 基本功模型训练几乎都在服务器上跑不懂基本的 SSH、文件管理、GPU 面板查看寸步难行。Docker 与容器化这是解决“在我机器上是好的”这个千古难题的唯一靠谱方案。模型服务化至少能用 FastAPI 或 Flask 把一个模型封装成 HTTP 接口知道如何处理请求、优雅地做异常返回。监控与持续集成的基本意识模型上线不是终点能发现问题、能快速迭代才算闭环。如果你已经具备其中一两块剩余的部分按项目去补会非常快。怕的是学了很久却没有一个完整的项目把自己按在真实环境里摩擦。1.3 这套东西解决的是真实世界的什么痛点说白了AI 工程化解决的是“一个模型如何稳定地变成一项服务”的问题。业务方不会关心你用的是 BERT 还是 ResNet他们只关心调用你的接口时响应够不够快、结果可不可信、系统挂了能不能自动恢复。这些全都要靠工程能力来兜底。我在实际项目中见过太多“模型 demo 惊艳、产品化翻车”的案例。一个做客服工单分类的模型离线测试 F1 很高一上线却被运营吐槽“分类结果完全没法用”。后来排查发现线上传过来的文本里有大量网页标签和 emoji而训练数据里几乎没见过这种输入。这种情况你调再好的模型架构都没用必须靠工程手段在数据处理和服务层把它接住。2. 零基础的整套学习路径与技术栈选型2.1 先打地基Python 与数据处理的正确姿势网上有一堆“50 天精通 Python”之类的教程但对 AI 工程而言你不需要成为 Python 语言专家重点是把数据处理的几个库用熟。pandas 是绕不开的第一个DataFrame 的索引、分组、合并、透视表以及各种各样的 dtype 陷阱。我在训练第一个文本模型时就踩过一个大坑csv 读进来某个字段的 dtype 变成了 float原因只是那一列里混了几个空值导致后面文本序列化时频繁报错。这种问题看起来低级排起错来却能耗掉大半天。要提升数据处理能力光靠刷文档不行最有效的办法是找一份脏数据去清理。比如拉一份公开的电商评论数据自己做标签映射、去重、过滤无效文本、划分训练集验证集测试集。这套流程你完整走一遍比看十遍 pandas 教程都有用。有一点想特别提醒尽快养成“把数据处理写成脚本而不是 notebook”的习惯。notebook 适合做探索性分析但一旦数据量大了、步骤多了notebook 的单元格顺序依赖很容易让人翻车。我在项目里通常的做法是先用 notebook 做探索确认逻辑后马上固化成一个 .py 脚本保证整个清洗流程是可复现、可重跑的。2.2 模型侧框架怎么选现在这个时间点做 AI 工程绕不开深度学习框架选型。我个人的建议是除非你的团队已经深度绑定了 TensorFlow 生态否则优先选 PyTorch。倒不是 PyTorch 比 TensorFlow 强多少而是它的调试体验更符合工程直觉动态图模式下每一步张量是什么形状、什么值都可以直接用 print 或 debugger 打出来。工程上排查问题时这种“所见即所得”极其重要。当然青铜选手不要一上来就手写 Transformer。现在成熟的生态已经把模型能力高度封装了你可以直接基于 Hugging Face Transformers 加载预训练模型用 Trainer API 做微调。我最近做一个小型文本分类项目从加载模型到完成微调核心代码可能不到 100 行。工程的重心早就从“怎么训”转移到了“数据怎么弄干净、服务怎么封好”。下面是一个最简单可行的文本分类微调伪代码注意这只是一个骨架实际项目里你还得处理数据加载、验证、存储等逻辑from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(bert-base-chinese, num_labels5) def tokenize_fn(examples): return tokenizer(examples[text], truncationTrue, max_length128) train_dataset raw_train.map(tokenize_fn, batchedTrue) val_dataset raw_val.map(tokenize_fn, batchedTrue) training_args TrainingArguments( output_dir./checkpoints, num_train_epochs3, per_device_train_batch_size16, per_device_eval_batch_size32, evaluation_strategyepoch, save_strategyepoch, logging_dir./logs, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_datasetval_dataset, ) trainer.train() trainer.save_model(./final_model)这里我想特别提一个细节输出目录和日志目录一定要从一开始就规范好。很多项目训练到一半checkpoint 散落在各个临时目录日志也被覆盖最终想复盘实验时完全找不到记录。这看起来是小问题在真正工程化时却是扯后腿的大问题。2.3 工程化四件套Docker、API 框架、存储、部署从零搭建 AI 工程有四样东西是躲不开的越早学越好。Docker 不是可选项是必选项。模型训练和推理的环境依赖极其脆弱Python 版本差一个小版本都可能让某个 C 扩展编译失败。我见过最夸张的一次一个同事的 TensorFlow 装完能 import但在特定机器上跑训练时 CPU 100% 但梯度完全不更新最后发现是系统底层库冲突。Docker 把依赖和环境一起打包直接消灭这一整个类别的报错。API 框架目前最推荐 FastAPI。选它不是因为异步特性虽然异步确实带来高并发收益而是因为它的数据校验和文档生成对 AI 工程太方便了。你定义好入参模型FastAPI 自动帮你校验请求格式、生成 Swagger 文档前后端联调时省掉大量沟通成本。Flask 当然也能用但 FastAPI 在工程规范上更省心。存储方面普通业务先用 PostgreSQL 或 MySQL 就够了但做自然语言处理或图像相关的 AI 项目强烈建议了解向量数据库。比如做语义搜索、做 RAG检索增强生成你需要把文本/图片转成 embedding 存入向量库。Milvus 是社区比较活跃的选择pgvector 作为 PostgreSQL 插件也可以适合数据量没有大到一个专库的量级时使用。选型逻辑就一条数据量小、想省事就用 pgvector数据量大、检索复杂就上 Milvus。部署层面从小到大可以分三步走第一步用 Docker 把服务跑起来手动执行 docker run第二步引入 docker-compose把服务、依赖的数据库、缓存一起编排起来第三步等规模大了再考虑 Kubernetes。很多新手一上来就奔着 K8s 去结果被各种概念淹没项目反而推进不下去。从最简单的部署方式起步是工程上很成熟也很实用的策略。3. 从零搭建一个最小 AI 工程实操演示3.1 项目定义先从一个能跑通闭环的小任务开始纸上谈兵没有意义我拿一个实际的小项目来走完整条链路客服工单自动分类。业务方手里有几千条工单文本需要按照五个类别咨询、投诉、维修、退换货、其他自动打标减少人工分组的工作量。选择这个项目有原因。首先它是文本分类任务数据好理解、模型不复杂适合演示完整的 AI 工程链路其次它的业务边界足够小从头到尾一个人一周内可以跑通不会让人在中途失去耐心。在动手之前先明确整个系统的组成部分模块用什么解决什么问题数据存储PostgreSQL存放原始工单与标注结果数据处理pandas 自定义清洗脚本把脏文本变成模型可用的干净文本模型训练PyTorch HuggingFace训练文本分类模型服务封装FastAPI把模型变成 HTTP 接口容器化Docker docker-compose一键启动整套环境监控日志 基础指标记录发现上线后的问题3.2 数据管线的搭建与细节数据管线是整个工程最容易出问题、也最容易被忽略的环节。我们假装数据已从业务系统导出成 csv字段包括工单编号、用户描述文本、人工分类结果。第一步永远不是训练模型而是先花时间把数据看透。清洗逻辑我一般遵循以下几条经验先把文本中的网页噪声清掉客服工单里经常混入 HTML 标签、URL 链接、特殊符号。这类内容对分类模型几乎都是噪声正规化方法是用正则或者其他清洗函数先剥离。统一全半角与大小写中文文本里英文和数字的全角半角混乱非常常见。“”和“ABC”在模型看来是两个完全不一样的 token不统一会让效果波动。处理缺失与重复有些工单是空文本有些工单是同一用户重复提交的复制粘贴。先做过去重和空值丢弃再去划分数据集。标签一致性检查有时候人工标注存在细微差异比如有人写“售后”有人写“售后问题”标准化成同一套标签体系再喂给模型。我自己的习惯是数据清洗脚本里每一步都加print或者写日志记录清洗前后条数变化。走到后面要排查线上效果差时这些记录能帮你快速判断“是数据出了问题不是模型出了问题”。划分数据集时我坚持用 sklearn 的train_test_split并固定random_state。这不是迷信而是为了实验可复现。同一个项目换一次随机划分结果可能面目全非到时候你根本无法判断模型改动到底是好是坏。3.3 模型训练与迭代注意那些没人提醒你的细节数据准备好以后训练阶段反而简单。这里分享三个我踩过很多次才记住的细节第一学习率和 batch size 直接决定成败。用预训练模型微调时学习率一般设置在 2e-5 到 5e-5 区间batch size 在显存允许的情况下从 8、16、32 逐步尝试。不要一上来就学新手的做法直接学习率 1e-3这会让预训练模型学到的知识被快速破坏。第二checkpoint 保存策略要提前想清楚。我建议每个 epoch 保存一次同时保留“best model”根据验证集指标选定。这样如果训练后期过拟合了还能回退到中间那个更好的版本不至于只能用一个已经坏掉的最终版。第三实验记录比模型本身还重要。最原始的方式是在每次实验前把参数、数据版本、代码 commit 号写进一个实验记录表进阶一点用 MLflow、Weights Biases 这些工具。我自己最开始不用这些工具结果有一次同时开三组实验两天后完全记不清哪组是哪个配置损失了整整一个周末的时间。3.4 封装成可调用的服务训练出的模型最终要变成一个接口否则业务方没法用。用 FastAPI 封装一个“工单分类”接口的骨架如下from fastapi import FastAPI, HTTPException from pydantic import BaseModel from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch app FastAPI() model_dir ./final_model tokenizer AutoTokenizer.from_pretrained(model_dir) model AutoModelForSequenceClassification.from_pretrained(model_dir) model.eval() LABELS [咨询, 投诉, 维修, 退换货, 其他] class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str confidence: float app.post(/predict, response_modelPredictResponse) def predict(req: PredictRequest): if not req.text.strip(): raise HTTPException(status_code400, detailtext must not be empty) inputs tokenizer(req.text, truncationTrue, max_length128, return_tensorspt) with torch.no_grad(): outputs model(**inputs) probs torch.softmax(outputs.logits, dim-1) idx int(torch.argmax(probs, dim-1)[0]) confidence float(probs[0][idx]) return PredictResponse(labelLABELS[idx], confidenceconfidence)有几个细节想强调。首先模型一定要先设置model.eval()否则 dropout 层在推理时还会生效结果就带随机性。其次接口层要自己做基础的输入校验空文本、超长文本都要能给出明确的错误信息而不是抛出晦涩的栈追踪。封装完成后用 Docker 把它变成镜像FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]注意requirements.txt 里一定不要把 torch 写成不限定版本建议锁定主次版本比如torch2.1.*否则基础镜像更新后可能拉到一个不兼容的新版本导致程序启动时崩溃。构建镜像后运行容器用 curl 做一次冒烟测试docker build -t ticket-classifier . docker run -d -p 8000:8000 ticket-classifier curl -X POST http://localhost:8000/predict -H Content-Type: application/json -d {text: 我的空调不制冷了想报修}完成这一步一个最简单的 AI 工程闭环就打通了数据进、模型出、接口可调用。4. 模型上线前必做的评估与保障工作4.1 离线评估和线上表现为什么经常不一致这是所有做 AI 工程的人都绕不开的痛点。你在测试集上跑出了 0.97 的准确率上线后业务反馈却只有六成可用中间到底发生了什么最常见的原因是数据漂移。训练数据是从历史工单里来的但线上用户的问题方式和业务活动都在变。比如夏天一到空调维修类工单暴涨而这个场景在训练集里占比并不高模型自然应对不了。另一个常见原因是预处理流程不一致训练前你做了清洗、去噪但线上调用时文本直接传给接口完全没有走同一套清洗特征分布自然不同。我在实际项目里的教训是**训练时的预处理逻辑和上线服务时的预处理逻辑必须复用同一份代码。**最稳妥的做法是把清洗函数抽成一个公共模块训练脚本和推理服务都从同一个入口 import。两处各写一份迟早分叉。这是一个非常低级的错误但在我见过和做过的项目里反复出现。4.2 回归测试与鲁棒性检查模型也是一种代码代码需要测试模型更需要。只不过模型的测试跟普通单元测试不一样你关心的不是“逻辑对不对”而是“行为稳不稳定”。我在项目里通常准备两份额外测试集。一份是对抗样本集专门挑那些容易让模型翻车的输入比如很短的文本、带错别字的文本、包含业务黑话的文本。另一份是边界样本集比如超长文本、空字符串、纯标点符号等。模型在这两份测试集上的表现比在干净测试集上的分数更能反映真实线上能力。这个步骤看起来麻烦但它帮我在上线前发现过不少问题。有一次客服工单里大量出现“???”这样的纯 emoji 文本模型在训练集里从未见过类似样本全部被分到了“其他”类。如果不做鲁棒性检查这个问题会等到上线后被业务方吐槽许久才发现。4.3 可解释性与日志审计AI 工程一旦进入生产环境就不只是技术问题还涉及合规和信任。尤其是涉及到用户数据处理的场景用户可能投诉模型分类错误这时候没有日志佐证问题会变成“公说公有理婆说婆有理”。我的基本做法是所有推理请求都落日志包括请求时间、输入文本摘要、返回的 label 和置信度、响应时长。输入文本涉及用户隐私时日志里做脱敏处理。记录模型版本号同一个服务升级模型后要知道线上跑的是哪个版本。上线前把模型文件的 hash 一并记录下来排查问题时能准确定位。置信度阈值要设不要把所有预测结果无脑返回分类不明确时可以在接口里返回“无法确定”而不是一个硬性标签。这个逻辑在实际业务里能减少很多错误标签带来的连锁问题。可解释性方面LIME 和 SHAP 这类工具确实能给出特征贡献度但在生产环境里通常太慢不适合高频调用。我更推荐的做法是在离线分析阶段用它们抽检典型样本理解模型大概在依赖什么特征线上服务本身不做实时可解释计算但保留原始输入与输出事后需要时再离线分析。5. 部署上线后的运维与监控5.1 模型更新先别急着学在线学习很多教程都在讲在线学习、持续学习鼓吹模型要实时更新。但我的建议是新手阶段不要碰这些先从定期重训开始。比如固定每周从业务库里拉取新数据重新训练模型灰度上线。这么做的好处是流程简单、可控性高。只有当你发现每周重训都跟不上数据变化速度了再考虑引入更复杂的机制。而且即便要做在线学习也要设计好防遗忘策略和回滚机制否则模型可能因为某一天的数据异常直接被带偏。这是很多资深团队都踩过的泥潭新手完全没必要在起步期就给自己上这么高的难度。5.2 监控指标怎么设定监控不是看 CPU 和内存就够了AI 服务要额外关注以下三个维度服务健康指标例如响应时间TP50、TP99、吞吐量、错误率。模型推理接口如果响应时间恶化通常意味着模型输入或并发出了状况。业务效果指标分类准确率、用户满意度、误判率。这类指标在线上没有“真实标签”的情况下获取成本较高通常靠抽检或用户反馈间接估算。数据漂移指标监测输入文本长度分布、关键词频率、类别预测分布。如果输入分布和训练期差异很大说明业务环境变了需要触发重新训练流程。想快速落地监控不一定非得上大型监控系统。我早期只用了一个很“土”的方案在 FastAPI 中间件里把每次请求的信息写入日志再用一个小脚本定时扫描日志并统计指标。后来数据量大了才逐步迁移到 Prometheus Grafana。这里想传达的思路是先有监控意识再谈监控工具。5.3 版本管理与回滚模型也是有版本的代码不想办法管好版本迟早出事。我见过太多次这样的场景深夜上线新模型白天业务方反馈效果异常但团队成员已经想不起来这个服务用的是哪个目录下的模型权重了。我的经验是比较老派的模型目录带上日期和实验标识例如final_model_20250512_exp03。同时接口配置里写明当前线上的模型路径在上线流程里记录从旧版本切换到新版本的日志。一旦发现问题回滚就是改一行配置再重启容器的事。如果你有 CI/CD 的实践经验可以把模型发布纳入标准流程例如用 git 管理配置文件模型文件放到对象存储发布时通过脚本同步到服务器。这些听起来很繁琐但在出现事故时可以极大缩短恢复时间。6. 实操过程中的几个高频坑与应对思路坑 1数据泄漏。最常见的情况是在构建训练集时文本的某些 token 其实在不经意间携带了标签信息。比如工单分类里文本本身包含了“已标记为投诉”这样的字段信息。遇到这种情况模型分数高得离谱不是好事。排查方式只有一个随机抽查模型预测正确但测试集标准答案也正确的样本人眼过一遍看是否有明显泄漏线索。坑 2资源估算不准。新手训练模型时最容易忽略显存占用。一个 BERT-base 模型batch size 设为 3212G 显存可能直接爆掉。建议从一开始就习惯用torch.cuda.memory_summary()或者 NVIDIA 的nvidia-smi实时观察显存占用。训练前先用小 batch size 冒烟测试一遍再逐步增大。坑 3过度优化模型结构。很多新手喜欢在各种模型架构之间反复横跳今天换这个预训练模型明天试那个 trick。其实对大部分业务项目来说先把一个经典模型跑通、把工程链路建好价值远大于刷那 0.5 个点的 F1。模型好不是靠结构炫酷是靠数据、训练技巧和工程保障。7. 给同样从零开始的人一些心里话我自己的体感是做 AI 工程和做普通后端开发的差别在于它有非常大的不确定性。普通后端的 bug 是逻辑错误查一查栈就能定位AI 工程的 bug 可能是数据问题、特征问题、模型退化问题甚至玄学一样的环境问题。焦虑是正常的排查排查着就习惯了。有些人会觉得从零开始要学的东西太多了数据、模型、后端、运维、监控样样都得懂一点。但换个角度想正是这些广度要求让这个方向的工程师变得稀缺。你不需要在某一项上成为专家你需要的是能把整条链路串起来的人。最后分享一个我坚持了很久的习惯每个小项目结束后把踩过的坑整理成 checklist。下一次做新项目时拿出来对照一遍。因为 AI 工程里的很多坑真的是一模一样的今天踩过改天换个项目还会再踩。这份 checklist 攒到五十条之后你会发现自己的项目推进速度明显比周围人快一截。从零开始并不神秘踏实做完一个完整的项目比收藏多少份学习资料都更有用。