
年初那会儿我在地铁上刷到一个仓库名字就叫 ai-engineering-from-scratch点进去翻了十来分钟第一反应是这年头什么都敢叫 from scratch。但把整个项目过完一轮之后我反而冷静下来了——AI 工程领域从来不缺教程缺的是那种把“从零到上线”每一环都拆给你看、还能让你照着不跑偏的资料。我自己带过几个从算法转工程、或者从纯后端转 AI 的新人太清楚这里面的断层在哪算法课教你调模型工程课教你写接口可真正落地一个 AI 项目你得同时应付数据、训练、部署、监控这一整条链路。这篇博文就把我自己做这类项目时的思路、工具和踩坑记录完整摊开适合刚入行的算法同学、准备转型 AI 工程的开发以及已经在做模型训练但总觉得缺工程味儿的人。1. 从零开始到底该怎么入门AI 工程的核心命题1.1 算法和工程的边界在哪里先说实话很多人问我的第一句话都是AI 工程和算法工程师有什么区别。我一般用开餐馆来打比方。算法工程师是研发新菜品的人他关心的核心指标是这道菜好不好吃换个专业说法就是模型的准确率、F1 这些。厨师把菜做出来味道对了就算完成。AI 工程师是那个要开连锁餐厅的人他要解决的是更头疼的问题一道菜在总店做得好吃怎么保证全国三百家分店做出来味道一致食材供应不稳定怎么办客人高峰期厨房会不会崩有一天厨师跳槽了菜谱和流程能不能完整交接。反映到技术上就是模型的可复现性、数据处理流程的标准化、服务的稳定性和延迟、以及整个项目的可维护性。算法题里常见的 bug 是 loss 不降、梯度爆炸这些在 AI 工程问题里当然也会遇到但工程现场更多是这种场景模型在测试集上 F1 有 0.92上线之后日志里跑出来的效果却只有 0.8训练的时候一切正常换台机器就完全复现不了昨天还能跑通的代码今天因为依赖库自动升级了一个小版本结果整个 pipeline 崩了。这些才是 AI 工程真正要解决的问题。所以如果你只盯着模型训练那一块觉得精度不提升就是天大问题那说明你还没进入 AI 工程的状态。AI 工程的核心命题是让一个模型靠谱地、持续地、规模化地产生价值精度只是其中一个环节。1.2 自建体系 vs 现成框架两条技术路线怎么选从零开始的另一个常见误解是既然是 from scratch那是不是要把神经网络也手写一遍。实话说如果你连反向传播都没手推过我当然建议你先去写一个简单的两层网络这是打基础的必经之路。但如果你的目标是一个能上线的 AI 产品那没必要也不应该从零实现 transformer 和梯度下降。from scratch 的重点是亲手搭建整个工程链路理解每一环为什么存在而不是拒绝所有工具。我列一个对比帮你判断自己该走哪条路。对比维度自建轻量体系采用成熟框架学习成本高所有环节自己趟低文档社区丰富灵活性高可以完全定制受框架限制但多数场景够用代码量多维护成本高少社区持续维护功能成熟度初期不稳定稳定、经过大规模验证适用场景学习原理、极度定制化的业务绝大多数实际项目我的实际建议是分阶段走第一波学习阶段尽量徒手实现数据加载、训练循环和评估逻辑理解每一步在做什么等项目复杂度上去之后再顺势引入 MLflow、DVC、FastAPI 这些工程工具。这样你既不会变成只会调包的调包侠也不会陷入重复造轮子的泥潭。说白了工具是帮你省时间的不是替你思考的。判断标准也很简单如果这个轮子已经非常成熟你没有特殊需求那就直接用它如果用着用着发现它在关键环节限制了你的业务你再考虑自己实现那部分。项目里自己写的那部分代码应该始终是“你理解最深、业务最核心”的部分而不是最外围的胶水代码。2. 环境基建别让第一块砖头绊倒你2.1 Python 环境与依赖管理实战我见过太多项目代码写得漂漂亮亮结果环境装不上、版本对不上光折腾就花掉半天。第一步就是给 Python 环境立规矩。目前这个阶段我推荐 Python 3.10 或 3.11原因很简单主流深度学习框架的兼容性做得最好过新的 3.13 反而容易在第三方库上碰壁。虚拟环境这块conda 和 venv 我都用。conda 的优势在于能顺手管理 CUDA 相关的底层依赖比如你用 conda 安装 pytorch 的时候它会自动处理 cudatoolkit 的匹配venv 则更轻量干净适合纯 Python 项目。具体用哪个不关键关键是每开一个新项目第一件事就是创建独立的虚拟环境并且把依赖固定住。我常用的做法是先把项目跑起来然后执行pip freeze requirements.txt但这个文件会带上很多传递依赖可读性一般。更好的做法是用 poetry 这类工具它通过 pyproject.toml 区分离线依赖和传递依赖你只需要维护自己直接引用的包锁定文件poetry.lock用来保证环境可复现。GPU 环境是另一个重灾区。先执行nvidia-smi看看驱动的 CUDA 版本再决定装哪个版本的 PyTorch。举个例子如果nvidia-smi显示最高支持 CUDA 12.1那么你装 PyTorch 的时候可以选cu121或更低的版本但不要选高于驱动支持的版本否则会报 CUDA 初始化失败。这块看着简单实际上我接手过的项目里至少有三分之一的问题都出在这里。注意换机器或者过几个月之后再跑老项目最容易出的问题就是依赖版本变了。所以 requirements.txt 或者 poetry.lock 一定要进 git 仓库不只是你自己用团队协作时这是救命的东西。2.2 数据、代码、模型三件套的版本管理一个 AI 项目里代码、数据、模型三者的变更频率和解耦程度不一样得分开管。代码用 git这没什么好说的。数据经常有几个 G 甚至几十个 G直接塞 git 仓库会把仓库撑爆所以我用 DVC 来管。DVC 的思路大致是把真实的文件存在本地目录或者对象存储里git 仓库里只放一个很小的 .dvc 文件它记录了文件的哈希和存储位置。这样你 git clone 下来一个仓库得到的是数据的“指针”需要用到数据的时候再通过 dvc pull 拉取。还能顺便保证数据版本和代码版本是一一对应的不会出现“代码是新的数据还是上个月的”这种尴尬。模型文件的处理更讲究。我的习惯是不把权重文件直接提交到 git而是用 MLflow 或者简单的模型仓库目录来管理。MLflow 能把每次实验的参数、指标、模型权重、甚至使用的代码版本一起记录下来界面上一目了然。如果你不想引入这么重的工具至少也要在模型文件名里带上日期、数据集版本和关键指标比如chinese_bert_f1_0.912_20250115.bin否则三个月后你面对十来个 model_final.bin 真的会崩溃。这里还要强调一个容易被忽略的点数据集本身的校验。很多团队只记文件名不记内容结果某个同事重新导出了一份数据表面上结构一样里面样本分布已经变了。我至少会在数据 pipeline 里计算一个数据集的哈希并在实验记录里写清楚用的是什么版本。这样即使模型指标出现异常你也可以快速回滚到之前的数据版本去排查而不是靠拍脑袋猜。3. 第一个端到端项目从零做一个文本分类服务3.1 数据准备与标签体系设计光讲理论没意思我带大家把一个完整的文本分类服务走一遍。为了简单起见我们用酒店评论情感分类作为案例目标是给定一段评论文本判断它是正面还是负面。这个项目麻雀虽小五脏俱全数据、训练、部署、排查全都包含了。拿到原始数据之后第一步永远不是急着建模而是先看数据长什么样。用 pandas 读进来看看字段、看看样本量、看看标签分布。这个环节我用一个很土但很有效的办法直接打印出每一类里随机抽取的二十条样本人工过一遍确认标签质量。你以为标注完的数据就没问题我至少遇到过三次这种情况标签是反的、标注标准前后不一致、有些样本明显是机器采集的重复数据。如果不在这步发现问题后面所有工作都建立在错误地基上。import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(hotel_reviews.csv) print(df[label].value_counts(normalizeTrue)) train_df, tmp_df train_test_split( df, test_size0.2, stratifydf[label], random_state42 ) val_df, test_df train_test_split( tmp_df, test_size0.5, stratifytmp_df[label], random_state42 ) print(train_df.shape, val_df.shape, test_df.shape)这段代码做三件事统计标签分布、按 8:1:1 划分数据、用分层抽样保证每一部分的正负比例一致。接下来是预处理。文本清洗方面我一般只做必要的处理去 HTML 标签、去掉多余空白、处理统一编码。情感分类这个场景我不建议做太激进的分词或者去停用词因为后面用 BERT 这类预训练模型时它会自己学习上下文特征你反而可能因为过度清洗丢掉信息。之后做标签编码positive 记作 1negative 记作 0再交给训练脚本。3.2 训练脚本与核心超参数数据准备好进入模型训练环节。这里我用 Hugging Face 的 transformers 库选一个中文预训练模型比如 bert-base-chinese。第一次跑的时候我强烈建议先用原生的 PyTorch 训练循环写一遍不要一上来就 Trainer API。虽然 Trainer 确实省事但它的封装太黑了出了问题你都不知道是哪里崩的。自己写一遍循环你才能看清 forward、loss 计算、梯度清零、backward、参数更新、验证评估这几步的完整生命周期。核心超参数上我直接给出一个我实测下来稳定可用的组合新手可以直接抄作业。超参数推荐值说明learning_rate2e-5微调预训练模型的黄金区间过大容易灾难性遗忘batch_size16根据显存调整显存不够就先减半epochs3一般 2-4 轮就够别死磕max_length128超过部分截断控制显存和速度warmup_ratio0.1前 10% 的步数做学习率热身稳定训练weight_decay0.01轻微正则减少过拟合训练循环里我固定这样几个习惯第一步设置随机种子random、numpy、torch 都固定并且把torch.backends.cudnn.deterministic置为 True第二步在验证集上评估记录准确率、F1同时做早停比如连续两个 epoch 验证指标没有提升就停止避免浪费算力和时间第三步每次保存最优模型我通常会保留 top-2 的 checkpoint然后记录当时的超参数和指标。训练信息用 JSON 格式存到同一个实验目录确保每一个结果都能追溯到当时的配置。训练完在测试集上做最终评测把那套数字作为模型合格与否的判断依据。同时我在这里提醒一下过拟合的判断。如果训练集 loss 一路下降验证集却在第二个 epoch 就开始反弹说明模型在死记硬背训练数据这时候早停机制就会触发。反过来如果训练集和验证集指标都很差则是欠拟合可能是学习率太低、模型太小或者数据量不够不要一开始就怀疑代码写错了。3.3 推理服务与上线部署模型训练好了紧接着的问题是怎么让别人也用上它。最直接的方式是用 FastAPI 封装一个 HTTP 接口。这里有一个非常典型的坑很多人把模型加载写在路由函数里导致每一个请求都重新加载一次模型慢到你怀疑人生。正确做法是在应用启动时加载一次放到全局变量或者依赖注入里之后所有请求共享同一份模型权重。# 启动时加载一次避免每个请求重复加载模型 model load_model(best_model.bin) tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) def predict(text: str): inputs tokenizer(text, max_length128, truncationTrue, return_tensorspt) with torch.no_grad(): logits model(**inputs).logits prob torch.softmax(logits, dim-1) return {label: int(prob.argmax()), score: float(prob.max())}接口设计上我一般用 Pydantic 定义请求体比如请求体里放text字段返回体里包含预测标签、置信度和处理耗时。模型预测之前要对输入做和训练时完全一致的预处理这也是最容易埋雷的地方训练代码里用的是 transformers 的 tokenizer推理代码也必须用同一个 tokenizer并且 max_length、truncationTrue 这些参数要保持一致。我见过一个团队训练时对文本先做了清洗再去 tokenize推理时却直接 tokenize结果线上效果崩盘查了半天才发现是预处理不一致。部署这一层最省心的路径是 Docker。一个干净的 Dockerfile 大概就是基于python:3.10-slim把依赖装进去把代码 COPY 进去模型文件通过外部挂载的方式挂进去不要在镜像里塞一个几个 G 的模型不然每次构建镜像都痛苦。启动命令用 Gunicorn 加 Uvicorn worker 去跑 FastAPI默认的 uvicorn 单进程在并发上不够用。压测的时候我习惯用 locust 或者简单的多线程脚本去测 QPS 和延迟先本地压一遍再上线避免线上被流量打穿。4. 工程化现场踩坑实录4.1 高频问题排查速查这些是我和团队在实际项目里反复遇到、且具有一定代表性的一批问题。我整理成一张表方便你快速对应。问题现象大概率原因处理方案训练时报 CUDA out of memorybatch_size 过大或显存碎片化减小 batch_size、开启梯度累积、减少 max_lengthloss 不降或下降极慢学习率太低、标签噪声大、模型没正确进入训练模式调大学习率先看趋势、检查数据标签、确认 model.train()验证指标波动剧烈学习率偏高、batch 太小、验证集样本太少调小学习率、加大 batch、检查验证集划分训练可复现但换机器复现不了GPU 型号不同导致算子精度差异、依赖版本不一致固定 CUDA 版本、固定依赖锁文件、记录 GPU 型号中文输出乱码编码不一致统一用 UTF-8代码头部明确 encoding检查数据库连接字符集线上推理结果和离线测试不一致预处理逻辑不一致、模型处于 eval/train 模式差异把预处理封装成统一函数训练和推理共用指标虚高但业务效果差数据泄漏检查是否有目标信息进入特征、样本去重是否充分显存占用高但 batch 不大动态 padding 缺失长短文本 batch 内被 pad 到最长用 DataCollatorWithPadding 做动态 padding4.2 几个隐蔽但会炸死你的坑第一数据泄漏。这是离线指标虚高、上线立刻打脸的经典元凶。最常见的一种泄漏是做文本分类时数据清洗阶段不小心把标签本身当作特征拼进去了。另一种隐蔽得多的情况是重复样本没有去重。如果同样一条文本在训练集和测试集各出现一次模型就等于提前看到了答案测试指标自然会虚高。我在实际项目里见过的极端案例是某团队因为有 15% 的重复数据F1 从 0.78 虚高到 0.91上线后直接被打回原形。所以数据 pipeline 里至少要有一步去重并且用样本哈希做记录。第二训练与推理的随机性控制。你可能会遇到这种情况同样一份代码同样一份数据第一次训练和第二次训练出来的精度就是不一样。原因大概率是某些库在部分算子用了非确定性算法比如 PyTorch 的某些 CUDA 卷积。固定种子能解决一部分问题但还不够必要时还得设置torch.use_deterministic_algorithms(True)。但注意这个开关不是万能的有些算子根本不支持确定性执行你只能在关键环节固定种子、固定依赖版本然后接受极小范围的波动这已经足够支撑大多数业务了。第三先跑冒烟测试再全量训练。这是我个人强烈推荐的习惯也顺带救过我很多次。拿到新数据、新模型后不要一上来就全量跑三个 epoch 五个小时而是先抽 100 条样本跑 10 个 step确认前向、反向、loss 计算、评估逻辑全部没报错再切回全量。很多低级错误比如维度不匹配、tokenizer 加载失败都能在两分钟内暴露而不是等到五小时后。你可以把冒烟测试当成一次“刹车检查”省下的时间远远大于多花的那几分钟。5. 写到最后一点个人习惯设备配置不高、数据集小的朋友别一上来就追大模型。我见过太多人抱着一个十几个 G 的预训练模型跑得磕磕绊绊最后连一个简单任务都没跑通。先把小模型在一个小数据集上完整跑通一遍理解了全流程再去叠加规模和复杂度这是最稳的学习路径。我做项目还有个雷打不动的习惯每个实验都记录日志不管多忙。跑完一个实验之后我会把超参数、数据版本、代码 commit 号、关键指标这四项记下来有时候只是写在项目根目录一个 README 里有时候是写到 MLflow。这个习惯坚持下来最大的好处是模型出现问题的时候你能快速定位是数据变了、代码变了还是参数变了而不是靠回忆。项目做得越久你越会发现AI 工程里最贵的不是 GPU而是团队里每个人的记忆和沟通成本。最后一个无关紧要的小技巧。你在训练脚本里随手打印几条样本的模型预测让每次跑完训练都能看到几条真实样本的模型输出哪怕只是丢到 stdout 里。等到模型上线出问题时回头看一眼当时的输出样例往往能最快定位是数据问题还是模型问题。这个动作成本几乎为零但它在关键时刻的作用绝对超出你的预期。