如果你正在搜 ai-engineering-from-scratch 这类关键词,大概率是已经在 AI 这个领域吃过亏了,或者正打算入坑但不想走弯路。我自己就是从拿着 Kaggle 代码跑通 notebook、到被生产环境毒打、再到慢慢建立起一套相对完整的工程方法论走过来的。这个标题在我看来其实写得很准:AI engineering 不是"学几个模型",from scratch 也不等于"从零开始造轮子",而是要求你能独立把一个 AI 项目从数据到上线完整串起来。这篇文章就按我实际走通的路径来写:先拆思路,再搭环境,然后完整做一个文本分类项目,最后聊工程化进阶和常见的坑。这篇内容比较适合准备转 AI 工程方向的人、已经会调包但缺工程经验的人,也适合那些想让模型在线上稳定跑而不是只在汇报里跑的业务团队。
1. 先别急着写代码:拆解标题背后的三层工程含义
1.1 "AI" 这两个字背后是完整的闭环
很多人一提 AI 就想到神经网络、Transformer、大模型,但真正在业务里碰过 AI 的人都明白,算法只占整个闭环的一小部分。一个完整的 AI 系统,大概包括:业务问题定义、数据采集与治理、标签体系设计、特征工程、模型训练、离线评估、在线 A/B 测试、部署上线、监控告警、定期重训。你只要在任何一个环节掉链子,前面模型的"高性能"都救不回来。
我见过太多类似场景:同学或者同事拿着公开数据集跑到 99% 的准确率,觉得自己已经会 AI 了,结果一接真实业务数据就发现,需求方的数据格式和你假设的完全不同,标签里充满了噪声,用户行为随时在变,模型预测速度跟不上接口延迟要求,运维团队根本不知道你这个模型是干嘛的,出了问题也不知道该看哪个日志。这些才是 AI engineering 真正要解决的问题。
1.2 "engineering" 的核心是可控、可重复、可维护
如果你只是自己跑着玩,不讲究工程化完全没事。但只要一个项目涉及多人协作、长期迭代、线上产品,工程化就是生死线。工程化听起来很虚,但落到具体动作上就三件事:可控、可重复、可维护。
可控,是知道每个实验用了什么数据、什么代码、什么参数,能说清线上模型是哪个版本。可重复,是换一台机器、换一个人,跑同一套代码能得到一致或近似一致的结果。可维护,是代码结构清晰,有测试,有文档,不管以后是要加功能还是换算法,都能低成本接住。这些东西看着朴素,但绝大多数翻车的 AI 项目,都不是模型不够好,而是这三件事没做到。
1.3 from scratch 的真正含义:不靠抄,但可以站在巨人肩上
有人把 from scratch 理解成连深度学习框架都不用,自己写反向传播,那纯属是自虐。工程意义上的 from scratch,我理解是从一个空白目录开始,明确自己要解决什么问题、用什么数据、怎么评估、怎么部署,然后一步步把系统搭起来。你可以用 sklearn、PyTorch、Transformers、FastAPI、Docker,这些都是常识范围内的工具,用它们不丢人。关键是你不能只是"调包跑通",而是知道每一层在做什么、为什么这样做、出了问题去查哪里。
所以这里有一点提示:如果你只是想把"AI工程"当作简历里的一句描述,那你可以找个人带你跑一遍;但如果你想真的具备独立交付 AI 系统的能力,就得老老实实从空白目录开始,自己把这条链路走一遍。这个过程不会快,但收益是长期的。
2. 从零搭环境:先把"能复现"的底座打牢
2.1 Python 环境管理:我为什么推荐 miniconda
现在做 AI 项目,Python 版本和依赖库的兼容问题永远是第一个坑。如果你直接在系统 Python 里 pip install 一堆包,很容易出现:今天装了一个库,明天另一个库被迫升级,然后之前的代码就崩了。所以我强烈建议一开始就用隔离环境。
我的选择是 miniconda,而不是 Anaconda 全家桶,因为 Anaconda 里面预装的东西太多,很多你用不上但会占用管理心智。miniconda 只带 conda 和 Python,干净。用 conda 创建环境的好处是,它可以比较方便地指定 Python 版本和 CUDA 相关的包,这对后面跑 PyTorch 很重要。如果你更喜欢 venv 也没问题,但遇到 Python 版本切换时还是要靠 conda 或者 docker。
新手第一次可以这样建环境:
conda create -n ai-engineering python=3.11 conda activate ai-engineering pip install --upgrade pip这里要强调一下:环境名不要用 test、env 这种含义不明的词。我见过不少同事因为懒得想名字,最后所有项目都挤在一个环境里,依赖互相踩踏,报错的时候根本不知道是哪一方引入的,升级一个库可能就要连带重建。尤其当你同时维护三四个项目时,环境一混就是灾难。管理环境的目的本来就是隔离风险,不要让它成为新的风险源。环境名与项目名保持一致,看起来是小事,实际能省很多事。
2.2 项目目录:一个可以长大的结构
先给出一个我用下来很顺手的结构:
ai-engineering/ ├── config/ # 配置参数,yaml/json ├── data/ │ ├── raw/ # 原始数据,只读 │ └── processed/ # 清洗后的数据 ├── src/ │ ├── data/ # 数据下载/清洗 │ ├── models/ # 模型定义 │ ├── train.py │ ├── predict.py │ └── api.py # 推理服务 ├── tests/ # 测试 ├── notebooks/ # 探索性分析的草稿本 ├── models/ # 训练好的模型文件 ├── requirements.txt ├── docker/Dockerfile └── README.mdsrc 里只放正式代码,notebook 只是草稿纸,不要想偷懒把主线逻辑全写在 notebook 里。数据目录区分 raw 和 processed 是为了保证原始数据不被破坏,raw 是只读的,清洗脚本的输出统一放到 processed。config 集中管理超参数,不要散落在代码里,更不要写死在各个脚本中。这样无论谁接手项目,都能一眼看出什么东西放在哪里,比读一百行代码理解结构快得多。
2.3 Git 和 DVC:代码进 git,数据和模型单独管
从第一天就用 git,先别管分支策略多高级,至少做到:代码有历史,能回滚,能看清改了什么东西。AI 项目和普通软件开发有一个明显不同:模型文件动辄几百 MB,数据集可能几个 G,这些不能直接塞进 git 仓库。通常的做法是 .gitignore 把 models/ 和 data/ 忽略掉,然后另外通过 DVC 或云存储管理大文件。
DVC 的逻辑很像 Git,但它管理的是文件指针,实际的大文件放在对象存储里。这样团队克隆代码时就只拉小指针,真正需要数据时再看需要拉取。如果项目还没到多人协作规模,你也可以用最简单的方式:模型文件统一放 models/,命名带日期和实验序号(比如 model_20250607_v2.pkl),然后用 README 或 CSV 记录它来自哪次实验、用什么参数。简单,但有效。
2.4 实验记录:最简单的方式也胜过不记录
这里要真心建议:从第一个实验开始,就养成记录实验的习惯。不用上来就上 MLflow 这种平台,一个 Excel 或 CSV 也可以:实验编号、日期、数据版本、模型类型、关键参数、验证集指标、备注。等到你一个月后回头看,这个 CSV 会救你的命。
我第一回做项目没记录,后来要复现最佳模型,硬是把几十个小时浪费在猜参数上,最后发现是数据清洗版本没存。从那以后我再也不敢不写记录了。如果你想更省事,可以早点接 MLflow,它会把参数、指标、模型文件都串在一起,查询起来很方便。但工具不是重点,记录的习惯才是。
3. 完整项目实操:做一个文本分类系统(从数据到部署)
3.1 选一个经典题目:垃圾短信分类
作为从零开始的第一个项目,我不建议一上来就做那种超大规模、多模态的东西。先做一个经典且闭环的小项目:垃圾短信分类。数据用公开的 SMS Spam Collection,大约几千条短信,标注为 spam 或 ham,规模不大,几分钟就能训练一个 baseline,但五脏俱全,可以把整条 AI engineering 链路走一遍。
第一步是下载数据,简单看一眼格式,确认没有编码问题,然后做一些基础的探索性分析(EDA):类别分布是否均衡?短信长度分布怎么样?样本里有没有明显的噪声比如空值、乱码?这些分析会直接影响后面的建模决策。例如,垃圾短信里普遍包含"抽取""点击链接""赚钱"这些词,而正常短信更口语化,TF-IDF 加上线性模型就能取得不错的效果。如果你的数据类别分布差异很大(比如 90% 是正常短信),那评估指标就不能只看准确率,否则模型只需全预测成"正常"就能骗到高分。
3.2 数据预处理与划分:训练和推理必须共用同一套逻辑
实际项目里,数据清洗和预处理往往比模型调参更费时间。这里的处理大致包括:去重、去空值、小写化、移除 HTML 标签、特殊符号处理。要注意的是,清洗逻辑一定要同时作用在训练和推理阶段,否则你训练时用的文本经过清洗,线上推理时却直接喂了原始文本,效果必然打折扣。我见过有团队把清洗函数写死在训练脚本里,部署时根本没调用,线上数据全是另一样子,指标直接崩。
预处理之后,按分层抽样把数据切成训练集、验证集、测试集。所谓分层抽样就是保持每个集合里 spam 和 ham 的比例和全量一致,避免随机切分造成分布偏移。常用 70/15/15 的比例,用 sklearn 的 train_test_split 设置 stratify 参数即可。这里再补一句:如果数据本身带时间属性,通常要按时间切分,而不是随机切分,否则会把未来的信息泄漏进训练集。
3.3 Baseline:先跑通一个简单模型,建立参照系
做模型的第一步,不是直接上 BERT,而是先跑一个简单、快速、可解释的 baseline。我一般先用 TF-IDF 特征加逻辑回归:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline model = Pipeline([ ("tfidf", TfidfVectorizer(max_features=5000, stop_words="english")), ("lr", LogisticRegression(max_iter=1000, random_state=42)) ]) model.fit(X_train, y_train) print(model.score(X_val, y_val))这段代码几分钟就跑完,先拿到一个可用的分数。它的价值在于:给后续所有复杂模型一个参照物。如果一个 BERT 模型的验证分数还不如这个 baseline,那说明你的数据或预处理有问题,或者模型配置不对,而不是模型能力不够。做 baseline 还有一个好处:你可以用它快速验证部署链路,等确认整条链路通了,再回来换更强的模型,风险会小很多。
3.4 升级模型:从线性模型到预训练语言模型
如果 baseline 已不能满足要求,可以引入预训练语言模型。以 transformers 库为例,用 BERT 类模型做文本分类需要:加载 tokenizer、编码文本成 input_ids 和 attention_mask、构造 DataLoader、定义训练循环。这里有几个容易踩的细节:预训练模型的文本处理必须和 tokenizer 保持一致,比如最大长度设置、特殊符号处理;学习率一般要设得比训练从头开始的模型小,常见 2e-5 到 5e-5 这个范围;batch size 受显存限制,通常 16 或 32。
另外,这么小的数据集上微调 BERT 很容易过拟合,所以要设早停(early stopping),监控验证集 loss,一旦连续几个 epoch 不下降就停止训练。我自己实际调过很多次,对小规模文本分类来说,TF-IDF 加线性模型经常已经够用,不是每个项目都值得上大模型。判断标准是:简单模型是否能满足业务指标?如果能,就不要引入更多的部署和运维复杂度。
3.5 评估:别只用 accuracy 打分
分类任务的评估,很多人默认只看 accuracy,但垃圾短信这种类别不均衡的场景,accuracy 会骗人。假设 88% 样本是正常短信,模型只要全部预测为正常,accuracy 就是 88%,看起来还不错,但它对垃圾短信的召回是 0,用户照样会被骚扰。所以我建议至少同时看 precision、recall、F1 和混淆矩阵。对垃圾短信来说,更要关注"真正垃圾短信被识别出来的比例",也就是召回率,同时兼顾误杀率,毕竟误杀正常短信的代价是用户收不到重要消息。
| 评估维度 | 关注的问题 | 常用指标 |
|---|---|---|
| 总体表现 | 整体预测正确比例 | accuracy |
| 少数类识别 | 垃圾短信漏掉了多少 | recall |
| 误报情况 | 正常短信被杀掉多少 | precision |
| 综合平衡 | 两个维度都要兼顾 | F1 |
| 排序能力 | 模型对正负样本区分度 | AUC |
3.6 把模型变成一个接口服务
模型训练好之后,要能对外提供服务。最简单的做法是用 FastAPI 包一个 HTTP 接口,代码大致长这样:
# src/api.py from fastapi import FastAPI from pydantic import BaseModel import joblib, re app = FastAPI() model = joblib.load("models/model_20250607_v2.pkl") class TextRequest(BaseModel): text: str @app.post("/predict") def predict(req: TextRequest): text = clean_text(req.text) prob = model.predict_proba([text])[0][1] label = "spam" if prob > 0.5 else "ham" return {"label": label, "spam_probability": round(float(prob), 4)} def clean_text(text: str) -> str: text = text.lower() text = re.sub(r"<[^>]+>", "", text) return text.strip()注意这里我把 clean_text 定义在接口脚本里,和训练脚本保持一致。这是上面提到的"训练推理同逻辑"的一个具体落地。启动服务用uvicorn src.api:app --host 0.0.0.0 --port 8000。先用 curl 或者 requests 随便发一条"恭喜您获得大奖,点击链接领取",检查返回结果是否符合预期,再考虑接流量。
3.7 用 Docker 锁住整套环境
到这一步,本地服务已经能跑,但要交付给其他人或者上部署平台,最好把运行环境一起打包。一个简单的 Dockerfile 长这样:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ COPY models/ ./models/ EXPOSE 8000 CMD ["uvicorn", "src.api:app", "--host", "0.0.0.0", "--port", "8000"]Docker 解决的最核心问题是"可复现":不管在哪台机器上,只要镜像一样,跑出来的行为就一样,不再有"我这里是好的呀"这种经典甩锅。关于 Dockerfile,建议把复制顺序按变更频率排列,最不常变的 requirements.txt 先拷,代码和模型后拷,这样每次重新构建镜像时可以充分利用缓存,构建速度快很多。模型文件比较大的话,也可以不打进镜像,改成运行时挂载卷,具体取舍看团队基础设施。
4. 工程化进阶:从"能跑"到"能上线并活下来"
4.1 用实验追踪避免"我也不知道当时怎么调出来的"
当你开始认真迭代模型以后,最难受的事莫过于:一周前跑出一个高分,但你已经记不清用了什么参数、清洗了什么数据、是第几个 epoch 的权重。这时候实验追踪工具就派上用场了。MLflow 是目前很主流的选择,它的 tracking 功能可以记录参数、指标、模型产物,界面也直观。如果你不喜欢引入太重的东西,至少做到三点:每次实验有唯一编号;把参数写进配置文件而不是散落在脚本里;把模型文件命名带上实验编号。这些都能让你在事后复盘时不用靠猜。注意这说的不是给老板看的形式主义,而是保护你自己的时间。我自己不止一次靠实验记录救回一个本来已经找不回来的结果。
4.2 数据漂移与概念漂移:模型失效的头号元凶
模型上线不是终点。真实环境的数据会随着时间变化:用户习惯变了、业务策略变了、甚至数据采集渠道变了,都会让线下辛辛苦苦拟合的分布失效。这类变化可以粗略分成两种:数据漂移指的是输入特征的分布变了,比如新增了一批手机用户,短信文本长度和用词风格明显不同;概念漂移指的是同样的输入,对应的业务标签含义变了,比如某段时间平台把"营销通知"也归为 spam,决策边界就移动了。
应对漂移的常规手段包括:监控线上请求的特征分布,比如平均值、方差、分位数;监控模型输出的置信度和预测类别占比;设定告警阈值,并在飘得厉害时触发人工审查或者重训流程。这个环节很多小团队会忽略,但它恰恰是 AI 工程和学术实验最大的区别。
4.3 可复现性:随机种子、依赖锁定、数据版本一个都不能少
要让实验可复现,光有代码不行。至少要控制四个维度:随机种子、代码版本、数据版本、依赖环境。随机种子影响模型初始化、数据 shuffle、dropout;代码版本用 git 记录;数据版本用 DVC 或者简单地把数据文件按日期来源归档;依赖环境用 requirements.txt 或 poetry、pip-tools 锁定具体版本,最好在 Docker 里固化,避免在别人机器上跑出不同结果。
这里有一个常见误区:不是设了 random_seed=42 就一定能完全复现,尤其在 GPU 上,某些运算存在非确定性。但设了总比不设强,它是工程上可复现的第一道保险。如果你想追求极致的可复现,还需要在文档里写明硬件类型、驱动版本、CUDA 版本,这些都是黑盒,但搞过一次你就知道它们有多重要。
4.4 把模型变快:推理性能是用户体验的一部分
再准的模型,如果接口响应 5 秒,业务方大概率不会用。文本分类这种场景,常见的优化手段有:把多条请求合并成一个 batch 一起推理,利用矩阵运算的并行能力;对重复输入做缓存;如果需要进一步压榨性能,可以把模型导出成 ONNX 格式,再配合 ONNX Runtime 做推理,速度通常能提升不少;在硬件允许的情况下也可以尝试量化,比如 INT8。
不过这些优化有一个前提:先做基准测试,测量一下当前服务的 P99 延迟和吞吐量。不要凭感觉优化,用数据说话。我见过不少人一上来就搞量化,结果业务量明明不大,完全没必要,还白白增加复杂度。优化的目标是满足业务要求,不是把数字压到极限。
4.5 从模型指标到业务指标:最后要对业务结果负责
最后想提一个经常被忽略的点:模型指标好,不代表业务指标好。垃圾短信识别项目,用户关心的不是 F1 多高,而是自己收到的垃圾短信变少了、重要短信没有被误杀。上线前最好定义清楚业务目标:比如垃圾短信投诉率下降、用户关键短信拦截率低于某个阈值。这要求 AI 工程师不只跟数据打交道,还要和产品、运营对齐目标。
我最初做项目也是只管"模型掉点没",后来才意识到,费了半天劲把准确率提升了 0.5%,业务上没有任何感觉;而改成只拦截置信度大于 0.9 的垃圾短信后,误杀少了,用户投诉反而降得更明显。工程不是孤岛,AI 工程尤其不是。
5. 常见问题排查:这是踩坑实录,不是标准答案
先把最常见的现象和排查方向放在一张表里,后面再逐个展开。
| 现象 | 大概率原因 | 优先排查方向 |
|---|---|---|
| 训练 loss 不降或反而升 | 学习率过大、数据预处理错误 | 先查学习率,再查标签和文本 |
| 离线指标好,线上崩 | 预处理不一致、数据泄漏 | 检查推理链路,检查数据切分 |
| 别人机器跑不出你的结果 | 环境依赖不一致 | 锁版本,用 Docker |
| 显存 OOM 或训练太慢 | batch 太大、序列太长 | 调小 batch,梯度累积,混合精度 |
| 模型文件太大,仓库放不下 | 缺少模型管理方案 | 用 DVC 或对象存储 |
5.1 训练 loss 不降反升怎么办
这个问题我一个月能遇到三次。第一步别慌,先看是不是学习率太大。学习率过大时 loss 会在某个值附近震荡甚至一路上涨,这时候把学习率调小一个数量级试试。第二步检查数据预处理:标签是否从 0 开始连续编号?文本有没有被清成空串?有没有在训练代码里用到未来数据?第三步是从小处验证:喂一个 batch 的数据,看看前向传播和 loss 能不能正常计算;如果单 batch 都不正常,先解决数据管道,再谈训练。
我遇到过一次特别诡异的情况,loss 在前几个 step 正常,之后立刻变 NaN,结果发现是某一行文本里有异常字符,预处理没处理干净,转成 id 后出现了 0 值。排查了整整一晚上,最后靠逐步 debug 输入输出才找到。所以如果你也碰到 NaN,优先检查数据里有没有极端值和异常字符。
5.2 验证集指标好,上线就拉胯
这是 AI 项目最主流的翻车原因。常见原因包括:训练和推理的预处理不一致,比如线下清洗了线上没清洗;训练集和线上真实数据分布不一致,可能是采集渠道变了;评估时用随机切分产生了数据泄漏,特别是时间序列数据没有按时间切分;模型过拟合到了训练集包含的特定模式。
排查思路也很直接:先拿一批线上真实请求打到模型上,人工标注一小部分,和线上返回结果做对比;然后再看训练集里的同类样本是不是明显不同。通过分层对比,很快能定位是数据问题还是评估方式问题。不要一上来就重训模型,先找到根因再动刀。
5.3 环境依赖版本不一致
AI 项目对依赖版本极其敏感,PyTorch 版本一变,某些算子的结果就会略有差异。要避免这类问题,最好的办法是第一天就锁定环境。requirements.txt 里直接写死版本号,不要用>=这种宽松范围;进一步是使用 poetry 或 pip-tools 生成完整依赖树;再进一步是 Docker 镜像,把 Python、CUDA、依赖库全部固化。
真到了排查阶段,先看报错栈,再对比你机器和目标机器的python -c "import torch; print(torch.__version__)"以及 CUDA 版本。我见过同事 debug 了一下午,最后发现是两台机器的 numpy 版本不同。这件事没什么好办法,就是锁环境。
5.4 显存不够、训练太慢怎么办
碰到 OOM,最直接的办法是把 batch size 调小,但有时 batch size 太小会影响训练的稳定性,这时可以使用梯度累积,把小 batch 的梯度累积若干步再更新参数,等效地换来"虚拟大 batch"。另外,混合精度训练在支持 GPU 上能同时降低显存占用和加快速度。对于文本数据,还可以考虑序列最大长度截断,很多人默认设 512,如果你的统计显示样本大多在 128 以内,完全可以直接截到 192,显存和速度都会好很多。
这些优化按需使用,不要为了用而用。先把最简单的配置跑通,再用 profiling 工具看瓶颈在数据加载还是模型计算,定点优化。
6. 一些最后的体会
6.1 工程能力比算法技巧更能决定项目天花板
做完这套 from scratch 项目后,我最深的感触是:工程能力比算法技巧更能决定项目的天花板。你会调大模型,但要真把一个 AI 系统稳定交付,需要的是对数据、代码、环境、监控每一个环节的控制力。很多人模型调参很厉害,但一遇到工程链路就崩溃,那这个模型永远只能活在 notebook 里。
你不需要成为一个全栈工程师,但至少要理解全链路。知道数据怎么来、模型怎么跑、服务怎么部署、日志怎么查,这是 AI 工程师和算法研究员的重要区别。如果你正处在转行或起步阶段,我建议你先不要追求新算法,而是把一个简单项目完整地做到底。
6.2 记录、复盘和简单的方案,是长期复利的来源
任何记录在当下看起来都是浪费时间,事后都是救命稻草。哪怕只是用 CSV 记下每次实验的参数和结果,长期价值都远超想象。反复复盘的另一个好处是,你会慢慢形成自己的排查直觉:看到 loss 震荡第一反应查学习率,看到线上指标下降第一反应查预处理和切分逻辑。直觉不是天赋,是踩坑后留下的痕迹。
不要迷信复杂模型,也不要迷信复杂架构。先用最简单的手段跑通全链路,再逐步升级,你会避免大量的无效加班。项目本身并不复杂,但如果你真的从空白目录一路走到模型上线,你获得的不只是一个模型,而是一整套解决问题的肌肉记忆。