
如果你正在搜 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 python3.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_features5000, stop_wordsenglish)), (lr, LogisticRegression(max_iter1000, random_state42)) ]) 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排序能力模型对正负样本区分度AUC3.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_seed42 就一定能完全复现尤其在 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 震荡第一反应查学习率看到线上指标下降第一反应查预处理和切分逻辑。直觉不是天赋是踩坑后留下的痕迹。不要迷信复杂模型也不要迷信复杂架构。先用最简单的手段跑通全链路再逐步升级你会避免大量的无效加班。项目本身并不复杂但如果你真的从空白目录一路走到模型上线你获得的不只是一个模型而是一整套解决问题的肌肉记忆。