
如果让我用一个词来概括做 AI 工程最深的体会那就是“地基”。市面上讲 AI 的教程不少教你怎么调用现成接口的一抓一大把可你真要拿它去解决一个实际业务问题——比如给电商平台做商品评论的意图识别或者给内容社区做一个轻量级的推荐服务——你会发现真正决定项目成败的根本不是那几行模型代码而是模型之外的一整套工程体系。数据怎么管理、训练怎么跑、模型怎么上线、上线之后怎么迭代这些环节有一个掉链子整个项目就卡在那里。“ai-engineering-from-scratch”想做的就是把这套体系从头到尾走一遍。它不是高深的研究课题而是一条务实的技术扫盲加实战路线。适合刚转行 AI、想系统性建立工程思维的人也适合那些已经在用现成工具跑模型、但对数据管线、训练循环、部署上线这些环节仍然模糊的开发者。1. 项目定位AI 工程到底在解决什么问题1.1 从标题拆解核心含义先说说这个标题本身。“ai-engineering-from-scratch”直译过来是“从零开始做 AI 工程”。很多人一看到“from scratch”第一反应是“要从零手写一个深度学习框架”。真要这么做确实很硬核但多数业务场景根本不需要。我理解的“from scratch”有两层意思第一层是不依赖现成的全栈解决方案比如不直接套用某个 AutoML 平台也不拿别人的训练代码改几个参数就交差第二层是所有关键环节都要亲手搭一遍数据、训练、评测、部署、迭代每一步都知道底层在干什么出了问题知道去哪查。以电商评论的意图识别为例。业务方给的需求是“把评论里用户吐槽的点自动分类比如电池、屏幕、物流、售后”。表面看这是典型的多分类任务拿个预训练模型微调一下就能交差。可真做起来你会发现光是“定义清楚什么是吐槽”这件事就能和业务方开三次会。这还没有算上数据标注要怎么做、标注质量怎么把控、模型预测结果怎么跟人工标注对齐。AI 工程的核心不是“训练模型”而是“交付一个可运行的、效果可衡量的系统”。1.2 AI 工程思维与普通软件开发的区别传统软件开发讲究的是确定性代码逻辑是明确的输入输出是可预测的Bug 是可以复现和修复的。AI 工程不一样它引入了一个“不确定的中间层”就是模型。模型的输出不是代码逻辑直接决定的而是由“数据分布 模型结构 训练过程”共同决定的。这就带来一个根本性的差异你没法通过“读代码”来定位一个预测错误的原因你必须通过“看数据”来排查。我见过太多从传统开发转过来的同事第一反应是“模型效果不好那我看看模型的代码哪里写错了”盯着 loss 函数看半天其实问题出在训练数据里有大量重复样本验证集和训练集有重叠。另一个区别是迭代方式。普通软件上线后只要不出 Bug 就是稳定的。AI 系统上线后效果一定会随着线上数据分布的变化而衰减这不是 Bug是常态。所以 AI 工程必须内置一套反馈闭环线上日志怎么埋点、预测结果怎么回流、新数据怎么定期评估、模型怎么重训。这套闭环做得越顺系统就越健康做得越随意模型就会一步步变成废铁。1.3 项目的目标拆解与范围界定任何工程都怕“什么都想做”。做这个项目的时候我给自己划定的边界非常明确不做研究不做平台只做一条能跑的端到端管线而且一定要小。我选的是“电商评论意图识别 轻量级分类上线”数据规模控制在几万条以内模型用中小规模的预训练模型做微调部署端用一个轻量级 HTTP 服务。为什么选这么小的范围因为只有范围小你才能在有限的时间里把每个环节都做扎实。管线拉得太长最后一个环节还没来得及跑通热情已经消耗光了。目标拆下来大概是这样的环节交付物验收标准数据工程清洗后的数据集 标注规范标注一致性达到 85% 以上无数据泄漏模型训练微调后的分类模型验证集 F1 达到 85 分以上评测分析分类报告 错误样本分析每个错误类别有明确的改进建议上线部署封装好的推理服务单次预测延迟 100ms接口可压测迭代机制数据回流与模型重训脚本新数据可一键重训并生成新版本这套拆解方法可以复用到任何 AI 项目里。不要一开始就盯“最新最好的模型”先盯“交付链路里最弱的一环”把它补齐再往前走。2. 核心细节解析数据、模型、训练的三厢马车2.1 数据工程——AI 项目里最容易被低估的部分业内有个共识数据和特征决定了效果的上限模型和算法只是去逼近这个上限。这话放在 AI 工程里尤其准确。我见到很多项目训练阶段报错到最后发现原因很简单数据格式不对、标签搞错了、时间切分有泄漏。你要是把这些问题排查清楚往往能省下两倍于调模型的时间。做数据工程第一步是采集。评论数据从哪来内部业务库、爬虫、第三方购买都可以。采集的关键不是“越多越好”而是“来源是否覆盖你预期的场景”。比如你做电商评论必须覆盖多个类目、多个评分段、多个时间区间否则模型很容易做成“罔顾事实的偏科生”。第二步是清洗这一步最容易被跳过去。评论数据里通常有大量 HTML 标签、表情符号、重复字符这类噪声会直接影响模型效果。清洗的原则是能用一个确定性规则解决的就不要留给模型去学。比如把“”还原成“”这部分用正则一行就能解决没必要耗费模型容量。标注环节是最考验工程管理能力的地方。你以为标注就是“找人点点鼠标”大错特错。同一个评论不同标注员对“是否吐槽”可能有完全不同的判断。比如“价格比隔壁贵但是质量好总体来说还是满意的”这句话到底算不算吐槽价格所以必须提前写一份标注规范把分类的边界定义清楚把有分歧的 case 一次性讨论清楚。我常用的一个技巧是先找 50 条典型样本让标注员试标然后统计一致性有分歧的部分直接开会讨论形成制度再进行大规模标注。这么做能显著提高后续模型的稳定性和可解释性。2.2 模型选型——从基线到迭代的演进路径模型部分最容易犯的错误是一上来就用大模型。不是大模型不行而是你不理解它为什么行。AI 工程有一个非常务实的建议先做一个最简单的规则基线再做一个统计基线最后才上深度模型。规则基线最简单比如“包含‘电池’和‘差’两个词就判为电池吐槽”。它的意义不是效果好而是验证数据标签的合理性——如果规则能到 60 分说明数据本身有规律如果规则连 40 分都不到你得想想是不是标签体系出了问题。统计基线可以用 TF-IDF 逻辑回归这套组合训练速度快、可解释性强而且能非常直观地告诉你哪些词对分类最有贡献。我记得自己跑 TF-IDF 逻辑回归的时候发现模型特别看重“建议”“希望”这类词去看了数据才发现很多负向评论会写“建议加强品控”“希望电池能耐用一些”这类表达其实非常值得业务方关注。这一步的发现在后面做模型误差分析时给了我很大帮助。深度模型是最后上场的。我的建议是用预训练模型做微调比如中文场景下的中小规模预训练模型。预训练模型的优势在于它已经学到了大量语言知识微调只需要适配你的任务。这里有个关键点模型结构可以复用但数据处理必须自己来。数据和模型是分开的你不能指望预训练模型的 tokenizer 帮你完成业务级的清洗。我的实操顺序是先用自己的清洗规则跑数据再交给 tokenizer 编码最后才进模型。每一步都单独测试确保没有隐性坑。2.3 训练管线——让模型真正学起来的关键细节训练管线是 AI 工程里最“玄学”的部分很多问题看似是代码问题其实是训练配置问题。我拆开说。损失函数的选择不要想当然。多分类任务默认用交叉熵这没毛病。但你要分清是“单标签多分类”还是“多标签多分类”前者用 Softmax CrossEntropyLoss后者要用 Sigmoid BCEWithLogitsLoss。这两个混淆是初学者最高频的错误之一。另外一个容易忽略的细节是类别不平衡。如果“电池吐槽”有 2 万条“包装吐槽”只有 300 条模型很容易把所有样本都预测成大类。解决办法有很多最简单的就是调整损失里的类别权重 class_weight让稀有小类被误判的时候付出更大的代价也可以用阈值调整在预测时对大类的判定阈值卡得更严一点。我建议先试 class_weight它改动最小、效果最直接。优化器和学习率是第二个高频踩坑区。Adam 确实是最稳妥的默认选择但不代表它不需要调参。学习率设太大loss 会直接爆掉设太小模型长时间不收敛。我习惯的做法是先用一个较小的 batch比如 16或32跑 3-5 个 step 之后看 loss 曲线的下降速率如果 loss 在第一个 step 就飞涨说明学习率至少大了 10 倍如果 100 个 step 之后 loss 几乎不变说明学习率可以上调 3-5 倍。这个“快速试探法”比盲目搜索高效得多。训练管线里还有个很不起眼但极其重要的细节数据顺序。不要以为 dataset shuffle 是随便写写的事。在训练阶段shuffle 能打乱样本顺序避免模型学到顺序相关的假模式在验证阶段千万不要 shuffle因为要保证评测的一致性。更要注意的是时间序列类任务如果评论按时间排序而训练集和验证集是从整体里随机切分的那就会发生典型的数据泄漏——模型看到了“未来的信息”。我见过不止一次验证集 F1 高得离谱线上效果惨不忍睹最后才发现是切分时没按时间切。3. 实操记录从零搭建一个能跑通的系统3.1 环境准备与项目结构设计环境这块我强烈建议用虚拟环境管理依赖不要直接把包装进全局 Python。具体用 venv、poetry 还是 uv 都行关键是依赖版本要锁定。AI 项目里最怕的就是“在我机器上是好的”而版本不一致是最常见的原因。一个可复现的项目必须把 pyproject.toml 或 requirements.txt 的版本锁定信息提交到代码仓库里。项目结构我建议按功能域划分而不是按脚本市集堆放。下面这个结构是我反复调整后的版本兼顾了开发速度与可维护性ai-engineering-from-scratch/ ├── data/ │ ├── raw/ # 原始数据只读不改动 │ ├── processed/ # 清洗后的数据 │ └── splits/ # 训练/验证/测试切分 ├── src/ │ ├── data_processing/ # 清洗、预处理、切分 │ ├── models/ # 模型定义 │ ├── training/ # 训练循环、评估逻辑 │ ├── serving/ # 上线推理服务 │ └── utils/ # 公共工具 ├── configs/ # 超参数配置文件 ├── scripts/ # 可执行脚本入口 ├── experiments/ # 实验记录 ├── models/ # 产出的模型文件和报告 └── notebooks/ # 探索性分析一律不准进生产代码再强调一句raw 目录里的原始数据永远不要改。清洗逻辑只允许在 processed 目录里生成新文件这样任何时候你不会因为“某次清洗改了原始数据”而丢掉原始信息。这是数据工程的一条铁律。3.2 数据加载与预处理实现数据处理这一层我的做法是把每一步都做成显式调用的函数同时配合阶段输出测试。下面给一个简化但完整的参考实现import re import json from pathlib import Path def clean_comment(raw_text: str) - str: 清洗一条评论数据 # 去掉HTML标签 text re.sub(r[^], , raw_text) # 还原常见HTML实体 text text.replace(amp;, ).replace(lt;, ).replace(gt;, ) # 去掉URL text re.sub(rhttp\S, , text) # 压缩连续空白 text re.sub(r\s, , text).strip() return text def process_raw_data(input_path: Path, output_path: Path) - None: data [] with open(input_path, r, encodingutf-8) as f: for line in f: row json.loads(line) cleaned clean_comment(row[content]) if not cleaned: # 清洗后为空的直接丢弃 continue data.append({id: row[id], content: cleaned, label: row[label]}) with open(output_path, w, encodingutf-8) as f: for item in data: f.write(json.dumps(item, ensure_asciiFalse) \n)这段代码不复杂但体现了一个核心原则清洗逻辑显式化、可复用、可测试。你可以把这套函数在训练前跑一遍然后在 serving 时也复用同一个函数这样就避免了训练和线上数据预处理不一致的问题。实际上无数“线上效果崩盘”的案例根源都是训练时的预处理方式和上线时的不一致。3.3 手工实现一个训练循环很多人用惯了框架自带的高层 Trainer导致自己不会写训练循环遇到诡异报错根本看不懂。我建议每个做 AI 工程的人至少要手写一次训练循环把底层的五个步骤刻进 DNA。下面是用 PyTorch 风格手写一个训练 epoch 的骨架import torch from torch.utils.data import DataLoader from tqdm import tqdm def train_one_epoch(model, dataloader, optimizer, criterion, device, grad_clip1.0): model.train() total_loss 0.0 pbar tqdm(dataloader, desctraining) for batch in pbar: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) optimizer.zero_grad() # 1. 清零梯度 logits model( # 2. 前向传播 input_idsinput_ids, attention_maskattention_mask, ) loss criterion(logits, labels) # 3. 计算损失 loss.backward() # 4. 反向传播 torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) # 梯度裁剪 optimizer.step() # 5. 更新参数 total_loss loss.item() pbar.set_postfix({loss: loss.item()}) return total_loss / len(dataloader)你可能觉得这段代码平平无奇但它就是整个训练过程的内核。很多框架层 Trainer 帮你做的事情其实就是把这五步封装起来。你自己写一遍之后再去理解“梯度累积”当一个 batch 太大时拆成多个小 batch 累计梯度、“梯度裁剪”防止梯度爆炸这些操作就会非常直观。看懂底层五步之后所有高级技巧都只是在这个序列上做加法。训练时还要配合一个评估循环注意评估阶段要切到 model.eval() 模式并包裹 torch.no_grad()否则模型不会关闭 Dropout、BatchNorm 也会继续算训练状态的统计量结果会失真def evaluate(model, dataloader, criterion, device): model.eval() total_loss 0.0 all_preds [] all_labels [] with torch.no_grad(): for batch in dataloader: input_ids batch[input_ids].to(device) attention_mask batch[attention_mask].to(device) labels batch[label].to(device) logits model(input_idsinput_ids, attention_maskattention_mask) loss criterion(logits, labels) total_loss loss.item() preds logits.argmax(dim-1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.cpu().tolist()) avg_loss total_loss / len(dataloader) return avg_loss, all_preds, all_labels这段代码不长但直接决定了你能不能相信自己的指标所以在项目中我一直把它当成刚需模块来维护。3.4 模型评测与推理服务化评测阶段不能只看一个准确率。对于电商评论的场景我更关注每个类别独立的精确率、召回率、F1并生成一个分类报告。业界有一个常用函数直接可以输出这些指标但建议你自己动手写一遍理解每一列的含义因为你会需要用这些表格去跟业务方沟通你得讲清楚“为什么这个类别的召回率下降会带来什么影响”。服务化部分我推荐用 FastAPI 这类轻量级框架。核心逻辑是加载一次模型然后对外提供 predict 接口。下面是一个极简但完整的推理服务from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str score: float # 这里加载模型和 tokenizer全局只需要加载一次 MODEL, TOKENIZER load_model_with_tokenizer(./models/best_model/) app.post(/predict) def predict(req: PredictRequest) - PredictResponse: # 训练时的清洗逻辑在这里一定要复用 text clean_comment(req.text) inputs TOKENIZER( text, max_length128, truncationTrue, paddingTrue, return_tensorspt, ) with torch.no_grad(): logits MODEL(**inputs).logits probs torch.softmax(logits, dim-1)[0] label_id int(probs.argmax()) score float(probs.max()) return PredictResponse(labelID2LABEL[label_id], scorescore)这里有一个非常重要的工程细节服务里的 clean_comment 必须和训练时用的是同一个函数。很多团队线上效果不如离线一查就是服务端预处理写“简洁版”漏掉了几条规则。这个问题的唯一解法就是把清洗逻辑打包成一个统一模块训练和推理都从同一个入口引用。4. 常见问题与排查技巧实录4.1 训练不收敛的经典排查路线训练不收敛几乎是每个新手都要经历一遍的“成人礼”。我踩过很多坑之后总结出一个固定排查顺序。第一看 loss 是不是一开始就 NaN 或非常大。如果是大概率是学习率太大或者数据里有 NaN 值优先检查数据和标签再降学习率。第二看训练 loss 和验证 loss 的差距。如果两者都高说明模型欠拟合先增加模型容量或训练时间如果训练 loss 很低但验证 loss 很高说明过拟合先加正则、加 Dropout 或者减小模型容量。第三看验证 loss 曲线是否剧烈震荡如果剧烈震荡可能是 batch size 太小或者学习率需要加 warmup。排查这类问题我建议做一个“错误清单”文档每次遇到新的问题都往里面加一条原因下次遇到直接看表查。很土但很有效。AI 工程的很多知识其实就是这样一点一滴沉淀下来的。4.2 数据泄漏的几种隐蔽形态数据泄漏是“验证集指标好看、线上效果翻车”的头号元凶。最容易出现的形式有三种。第一种是切分泄漏时间序列数据随机切分导致模型在训练时见过“未来”的样本。解决方案是按时间切分保证训练集时间早于验证集。第二种是预处理泄漏你做 TF-IDF 向量化时如果是在全量数据上拟合之后再切分验证集的信息就已经混进了词表统计里。正确的做法是先切分再在训练集上拟合词表和统计量。第三种是特征泄漏你构建了一个特征但这个特征本身携带了标签的信息。举例来说如果评论里包含“退款成功”这个关键词几乎可以确定是正向而这个词在训练集里大量出现那模型很可能就学会了“看到‘退款成功’就判正类”但它实际上并没有真正理解语义。化解数据泄漏没有银弹之路唯一的路径是严格遵守数据切分规则以及每次做特征工程时都问自己一句“这个特征在预测时我真的能拿到吗”4.3 离线评测与线上预测不一致怎么查离线 F1 有 90%线上怎么连 70% 都不到这是常见现象但一定是哪里出了问题。我把排查点总结成一个固定的检查顺序。检查项做法优先级预处理一致性训练和推理是否使用同一个预处理函数高数据分布差异线上真实数据是否有新的写法如新品牌名高模型版本不一致线上服务加载的模型文件是否是评测版本中请求参数不同在线预测时 max_length、padding 策略是否与训练一致中我试过一次很典型的排查过程离线指标很好上线后明显变差。检查后发现训练时我对文本做了截断到 128 个 token但线上服务为了“尽量保留信息”把 max_length 设成了 256。看起来是“更宽容”反而打破了模型训练时的分布。问题虽然不是特别严重但足够让你明白推理阶段所有超参数都必须无条件对齐训练阶段。不要自作聪明。4.4 模型迭代时的版本管理模型不是一次性产物它需要持续迭代。没有版本管理的机器学习项目三个月后就会变成一团乱麻。我自己的做法是每次训练产出一个独立目录里面至少包含四样东西模型权重、配置文件超参、数据路径、模型结构说明、评测报告含指标和错误样例分析、训练日志。目录命名用时间加 Tag比如 best_model_20250615_v2_f1_86.5一看就知道这个模型是怎么来的。更正规一点的团队会引入 MLflow 或 DVC 这类工具但我个人经验是工具能帮你但工具替代不了纪律。哪怕只是一个小团队只要坚持“每次实验都有记录、每次产出都可复现”项目的健康度就会有质的提升。5. 工具链清单与技术选型思考5.1 各环节工具的实用推荐工具选型这件事特别容易走两个极端要么图省事一把梭要么图新鲜持续开箱即用。我的原则很简单优先选生态成熟、团队熟悉、出问题有资料查的。下面是个参考清单环节可选工具我的建议依赖管理venv、poetry、uv新人用 venv追求速度和现代用 uv数据处理pandas、polars数据量在几百万行内 pandas 足够模型训练PyTorch生态好调试灵活优先选超参配置YAML、OmegaConf把超参跟代码分离不要写死在脚本里实验记录MLflow、WandB小团队用 MLflow 自托管即可推理服务FastAPI、FlaskFastAPI 异步支持更好推荐容器化Docker统一环境隔离依赖值得投入时间选工具不是越复杂越好关键是你能不能控制住它。一个工具如果连调试起来都要查半天文档那它对项目的贡献可能就是负的。5.2 提升开发效率的几个小细节说几个花小力气但很有效的开发习惯。第一日志要设置好级别。开发时用 DEBUG 级别看细节训练时用 INFO 级别记录 loss 曲线上线时用 WARNING 级别屏蔽干扰。别把所有信息都混在一起不然等你查问题的时候会被海量日志淹没。第二做一个可复现的随机种子管理器。在项目入口统一设置 seed保证每次实验初始化一致这样你调参时才能确信效果差异来自超参数而不是随机初始化。第三数据样本的可视化检查。训练前随机抽 20 条样本打印出“预处理前的原文”和“预处理后的文本”放在一起对比这一步能帮你快速发现清洗逻辑的 Bug。这些细节单独拿出来都不值一提但组合在一起能让你的开发体验顺畅很多。写在最后的一点个人体会这个项目走完一遍我最深的一个感受是AI 工程里最贵的不是显卡而是你的调试时间。很多人觉得模型效果不好是模型结构不够新于是不停换更强的模型可实际上大部分项目的瓶颈都在数据质量、标注一致性和工程链路的一致性上。如果你能把数据管线做到干净、可复现把训练循环理解到骨子里把评测和部署做成闭环那不管换了什么模型、什么任务你都能稳稳地把它做上线。最后再分享一个我自己一直在用的小技巧每次实验只改一个变量。要么只改学习率要么只改数据清洗规则不要一次性换模型、改数据、调超参否则你永远不知道是哪个改动带来了提升。听起来很基础但这是我从无数次失败里爬出来的经验希望它能帮你少走几条弯路。