1. 从零搭建AI工程体系,为什么我劝你别急着调包
"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。不是因为陌生,恰恰相反,是因为太熟悉了——过去两年里,我见过太多人抱着"从零开始学AI工程"的念头冲进来,结果三天之后就开始复制粘贴别人的notebook,两周之后连自己跑的是什么都说不清楚。这个项目标题背后藏着的,其实是一个很朴素但极少有人真正做到的问题:当你把所有的库、框架、预训练权重全部拿掉之后,你还剩下什么?
我自己是从传统后端转过来的,最早接触AI那会儿,上来就是pip install transformers,然后from_pretrained一把梭。模型能跑,demo能出,但一旦线上出了bad case,我连从哪儿查起都不知道。tokenizer到底做了什么?attention的显存为什么是平方级增长?batch size调大之后为什么loss反而炸了?这些问题在"调包"的路径下永远得不到答案。所以当我看到"ai-engineering-from-scratch"这个方向的时候,我是真心觉得它值得认真写一写——它不是教你造轮子,而是让你在拆开轮子之后,再装回去的时候手不抖。
这篇文章适合谁看?三类人。第一类是有一定编程基础、但AI工程经验为零的开发者,你想知道一个完整的AI系统从数据到上线到底经过哪些环节;第二类是用过框架但心里没底的中级工程师,你想把那些"黑盒"打开看看里面到底怎么回事;第三类是做技术管理或者面试官的朋友,你需要一套判断候选人是否真正理解AI工程的标准。全文我会围绕从零构建这个核心,把数据管线、模型训练、推理服务、评估迭代这几个环节拆开讲,每个环节都告诉你为什么这么设计、坑在哪里、怎么验证。
需要提前说明的是,"from scratch"不等于"不用任何工具"。真正的从零,是指你理解每一层的职责边界,知道什么时候该自己写、什么时候该用现成的。盲目造轮子是另一种形式的偷懒。下面我按实际项目推进的顺序来展开,你可以当成一份施工图来看。
2. 整体架构设计:先想清楚边界,再动手写代码
2.1 为什么"从零"的第一步是画数据流图而不是建模型
很多人一上来就想着搭网络结构,这是典型的本末倒置。我在实际项目里踩过最大的坑,就是模型训到一半发现数据格式不对,回头改数据管线,结果之前训的全废了。所以"ai-engineering-from-scratch"的第一课,应该是先把数据从产生到消费的完整路径画出来。
一个最小可用的AI工程系统,数据流大致是这样的:原始数据采集 → 清洗与去重 → 标注或弱标注 → 特征/分词处理 → 数据集切分 → 训练时动态加载 → 模型消费 → 输出后处理 → 评估回流。这条链上任何一环出问题,最终表现都是"模型效果不好",但根因可能跟模型一点关系都没有。我见过一个团队调了两个月超参,最后发现是训练集和验证集有重叠样本,这种问题不画数据流图根本发现不了。
画图的时候有个技巧:每个节点标注三样东西——数据量级、更新频率、责任人。数据量级决定了你后面选什么存储和加载方式,更新频率决定了你是批处理还是流处理,责任人决定了出问题找谁。这三样东西写清楚,架构就成型了一半。
2.2 技术选型的取舍逻辑:什么该自己写,什么该用库
"from scratch"最容易被误解的地方,就是有人真的从矩阵乘法开始手写。我的观点很明确:底层算子用成熟库,工程骨架自己搭。原因很简单,矩阵运算、卷积、attention这些底层实现,成熟库经过大量优化和验证,你自己写的版本在数值稳定性和性能上大概率更差,这是重复造轮子。但数据管线、训练循环、服务封装这些工程骨架,恰恰是体现你理解深度的地方,也是出问题最多的地方,必须自己掌控。
具体来说,我的选型原则是这样的:
| 层级 | 建议 | 理由 |
|---|---|---|
| 数值计算 | 用成熟张量库 | 性能与数值稳定性经过验证 |
| 自动求导 | 用成熟框架 | 手写反向传播极易出错 |
| 数据加载 | 自己封装 | 业务逻辑强,通用库难覆盖 |
| 训练循环 | 自己写 | 需要精细控制日志、断点、恢复 |
| 推理服务 | 自己封装 | 涉及并发、批处理、超时等业务需求 |
| 评估体系 | 自己建 | 指标定义与业务强相关 |
这张表的核心逻辑是:越靠近业务、越需要定制的部分,越应该自己写;越靠近数学、越通用的部分,越应该用库。很多人搞反了,用现成的训练框架却自己写数据加载,结果两头不讨好。
2.3 目录结构:一个能撑住半年迭代的工程骨架
工程骨架最直观的体现就是目录结构。我试过很多种组织方式,最后稳定下来的结构是这样的:
project/ data/ raw/ # 原始数据,只读 interim/ # 中间产物,可重建 processed/ # 最终训练数据 src/ data/ # 数据管线代码 model/ # 模型定义 train/ # 训练循环 eval/ # 评估逻辑 serve/ # 推理服务 configs/ # 配置文件 scripts/ # 入口脚本 tests/ # 测试 artifacts/ # 模型、日志等产物这个结构的关键在于data目录的三层划分。raw只读,保证原始数据永远可追溯;interim放中间产物,随时可以删掉重建;processed是训练直接消费的,必须可复现。我见过太多项目把处理后的数据覆盖原始数据,出了问题连回滚都做不到。另外configs单独抽出来,是为了让实验可复现——同一个代码配不同配置,跑出来的结果必须能对上。
提示:目录结构定下来之后,写一个
Makefile或者justfile把常用命令固化下来,比如make data、make train、make eval。新人接手的时候,看命令比看文档快得多。
3. 数据管线:AI工程里最脏最累但最值钱的活
3.1 数据清洗的四个层次,别想着一步到位
数据清洗这件事,新手容易走两个极端:要么完全不洗,要么想一次洗到完美。我的经验是分四个层次逐步推进,每一层解决一类问题。
第一层是格式清洗,处理编码错误、字段缺失、类型不一致。这一层用脚本批量跑就行,重点是记录清洗前后的数量变化,任何一步清洗导致数据量骤降超过10%,都要停下来查原因。第二层是内容清洗,去重、去噪、过滤明显无意义的样本。去重我推荐用minhash或者simhash做近似去重,精确去重会漏掉大量改写样本。第三层是质量清洗,这一步需要定义质量信号,比如文本长度、语言置信度、困惑度等,用规则或者小模型打分过滤。第四层是分布清洗,检查各类别、各来源的分布是否合理,避免某一类样本占比过高导致模型偏置。
这四层不要一次性全上,而是每加一层就重新训一次baseline,观察指标变化。我踩过的坑是:一次性加了所有清洗规则,结果指标掉了,根本不知道是哪条规则的问题。分层推进虽然慢,但每一步都可归因。
3.2 分词器:自己训一个还是用现成的
分词器这个环节特别能体现"from scratch"的价值。用现成的分词器当然省事,但你会遇到两个问题:一是词表跟你的领域不匹配,大量专业术语被切成碎片;二是你完全不知道分词器对特殊字符、数字、多语言的处理逻辑,出了问题无从下手。
我的建议是:通用场景用现成分词器,垂直领域自己训一个。自己训分词器其实不难,核心是准备好语料、选好算法(BPE、WordPiece、Unigram各有适用场景)、定好词表大小。词表大小这个参数很关键,太小会导致序列过长,太大则embedding矩阵浪费显存。经验值是:中文场景3万到5万,英文场景3万左右,多语言场景10万以上。
训完之后一定要做覆盖率检查:随机抽1000条真实样本,统计有多少token是词表外的(OOV),OOV比例超过1%就说明词表不合适。另外还要检查压缩率,也就是原始字符数除以token数,这个值太低说明分词太碎,会拖慢训练和推理。
3.3 数据集切分的三个陷阱
数据集切分看起来简单,实际上坑最多。第一个陷阱是随机切分导致数据泄漏。如果你的数据里有同一来源的多个样本,随机切分会让训练集和验证集出现高度相似的样本,验证指标虚高。正确做法是按来源、按时间、按用户等维度做分组切分。
第二个陷阱是验证集太小导致指标波动大。验证集至少要保证每个类别有足够的样本量,一般建议验证集占总数据的10%到20%,但类别不平衡的时候要按类别分层采样。第三个陷阱是测试集被反复使用。测试集只能用一次,用多了就变成了验证集,失去了评估意义。我见过一个团队把测试集当验证集用了半年,最后上线效果和测试指标差了十几个点。
注意:切分完之后,把三个集合的样本ID列表存下来,写进版本控制。任何一次实验都要能追溯到用的是哪个版本的切分。
3.4 数据加载的性能优化:别让GPU等数据
训练的时候GPU利用率上不去,十有八九是数据加载拖了后腿。优化数据加载有几个层次:最基础的是预取,用多进程或者多线程提前把下一批数据准备好;进阶的是缓存,把处理好的数据缓存到内存或者本地磁盘;再进一步是格式优化,把数据存成列式格式或者二进制格式,读取速度比文本快一个数量级。
我实测下来,一个中等规模的数据集(百万级样本),从文本格式换成二进制格式,加载速度能提升5到10倍。另外要注意shuffle的代价,全局shuffle需要把整个数据集读进内存,大数据集下不现实,通常用shuffle buffer做局部打乱,buffer大小设成batch size的100到1000倍比较合适。
4. 模型训练:把黑盒拆开,看清楚每一步在干什么
4.1 训练循环的骨架:五个必须自己控制的环节
用现成的训练框架,一个trainer.train()就完事了。但"from scratch"要求你自己写训练循环,因为只有自己写,你才能控制这五个环节:梯度累积、混合精度、梯度裁剪、学习率调度、检查点保存。
梯度累积是为了在小显存上模拟大batch,核心是每累积N步才更新一次参数,注意loss要除以N。混合精度能省显存加速训练,但要注意loss scaling,否则梯度会下溢。梯度裁剪防止梯度爆炸,通常按范数裁剪,阈值设1.0是常见起点。学习率调度里warmup特别重要,前几百步用线性warmup能显著稳定训练。检查点保存要同时存模型参数、优化器状态、学习率调度器状态和随机种子,少一样都没法精确恢复。
这五个环节每一个都有坑。比如梯度累积和batch norm一起用的时候,统计量是按micro-batch算的,跟真实大batch不一致,这时候要么改用layer norm,要么调整momentum。这些细节,不自己写一遍是体会不到的。
4.2 超参选择的经验法则
超参调优是玄学重灾区,但有一些经验法则能帮你少走弯路。学习率是最重要的超参,一般从1e-4到1e-3之间试,大模型用小学习率,小模型用大学习率。batch size和学习率要联动,batch size翻倍,学习率通常也要相应增大,但不是线性关系,平方根关系更常见。权重衰减一般设0.01到0.1,太小没效果,太大欠拟合。dropout在数据量大的时候可以关掉,数据量小的时候0.1到0.3比较合适。
我的实操流程是:先用一组保守的超参跑通全流程,确认没有bug;然后固定其他参数,单独调学习率,找到loss下降最快的值;再调batch size,观察显存和收敛速度的平衡;最后微调正则化参数。整个过程用配置文件管理,每次实验记录配置和结果,方便对比。
4.3 训练过程的监控:看什么指标,什么时候该停
训练监控不是看个loss曲线就完事了。我通常同时盯四个指标:训练loss、验证loss、学习率、梯度范数。训练loss下降但验证loss上升,是过拟合;两个都不降,是欠拟合或者学习率太小;loss突然飙升,多半是梯度爆炸;梯度范数长期接近0,说明梯度消失。
早停策略也很关键。我一般设两个条件:验证指标连续N个epoch不提升就停,或者验证指标提升幅度小于阈值就停。N通常设3到5,阈值看具体任务。早停能省大量算力,但要注意早停的耐心值跟学习率调度要配合,如果学习率还在下降阶段就早停,可能错过后面的提升。
4.4 显存不够怎么办:六个层次的优化手段
显存不够是训练时的常见问题,从易到难有六个层次的优化手段。第一层是减小batch size,最直接但影响训练稳定性。第二层是梯度累积,用小batch模拟大batch。第三层是混合精度,能省30%到50%显存。第四层是梯度检查点,用计算换显存,能省大量激活值显存但训练变慢。第五层是模型并行,把模型切到多卡上。第六层是offload,把部分参数或优化器状态放到CPU内存。
这六层不是越往后越好,而是按需选择。大部分场景前四层就够了,模型并行和offload会引入额外的通信开销和复杂度,非必要不用。我个人的经验是,先把前四层用满,实在不够再考虑后面两层。
5. 推理服务:从能跑到能扛住线上流量
5.1 推理服务的三个核心指标
模型训完只是开始,推理服务才是真正面对用户的环节。评估一个推理服务,核心看三个指标:延迟、吞吐、资源占用。延迟是单个请求的响应时间,通常看P50、P95、P99三个分位数,P99特别重要,因为它决定了最差体验。吞吐是单位时间能处理的请求数,跟批处理策略强相关。资源占用包括显存、内存、CPU,决定了你的部署成本。
这三个指标是相互制约的。批处理能提升吞吐,但会增加延迟;量化能降低资源占用,但可能损失精度。所以推理服务的核心工作是在这三者之间找平衡点,而这个平衡点取决于你的业务场景。实时交互场景优先延迟,离线批处理场景优先吞吐。
5.2 批处理策略:动态批处理怎么实现
动态批处理是提升吞吐的关键技术。核心思想是:不固定batch size,而是攒够一定数量或者等够一定时间就发一批。实现上有两个参数:最大batch size和最大等待时间。最大batch size受显存限制,最大等待时间受延迟要求限制。
我实现过一个简单的动态批处理调度器,逻辑是这样的:请求进来先进队列,调度器每隔一小段时间检查队列,如果队列长度达到阈值或者最早请求的等待时间超过阈值,就取出一批请求组成batch送进模型。这个逻辑用生产者-消费者模式实现,注意要处理好超时和异常,避免请求卡死。
5.3 模型量化与加速:精度和速度的权衡
量化是推理加速的常用手段,把浮点参数转成低精度整数,能显著降低显存占用和加速计算。常见的有动态量化、静态量化和量化感知训练三种。动态量化最简单,训练后直接转,但加速效果有限;静态量化需要校准数据,加速效果更好;量化感知训练在训练时就模拟量化误差,精度损失最小但需要重新训练。
我的建议是:先试动态量化,精度损失可接受就用;不行再试静态量化;还不行才考虑量化感知训练。量化后一定要做精度对比,在验证集上跑一遍,指标掉超过1%就要谨慎。另外要注意,不是所有层都适合量化,embedding层和最后的输出层通常保持高精度。
5.4 服务封装:接口设计、错误处理和限流
推理服务的接口设计要遵循几个原则:输入校验前置、错误信息明确、超时可控。输入校验前置是指在进入模型之前就把格式不对的请求拦掉,避免浪费算力。错误信息明确是指出错时要告诉调用方是参数问题还是服务问题,方便排查。超时可控是指每个请求都要有超时时间,避免慢请求拖垮整个服务。
限流是保护服务的最后一道防线。常见的限流算法有令牌桶和漏桶,令牌桶允许突发流量,漏桶则更平滑。我一般用令牌桶,配置上根据服务的实际承载能力设定速率和桶容量。限流触发时要返回明确的错误码,让调用方知道是限流而不是服务挂了。
6. 评估与迭代:让系统持续变好的闭环
6.1 离线评估和在线评估的分工
评估体系分离线评估和在线评估两部分。离线评估在测试集上跑,快速迭代,但跟真实效果有差距。在线评估用真实流量做A/B测试,结果可信但成本高、周期长。两者的分工是:离线评估做筛选,在线评估做决策。
离线评估的指标要跟业务目标对齐。分类任务看准确率、召回率、F1,生成任务看BLEU、ROUGE或者人工评估,排序任务看NDCG、MRR。但要注意,离线指标高不代表线上效果好,因为线上数据分布跟测试集可能不一样。所以离线评估通过之后,一定要小流量上线验证。
6.2 构建评估集的三个原则
评估集的质量直接决定评估结果的可信度。构建评估集有三个原则:代表性、独立性、稳定性。代表性是指评估集要覆盖真实场景的各种情况,包括长尾case。独立性是指评估集不能跟训练集有重叠,也不能被反复用来调参。稳定性是指评估集要固定下来,不能随意增删样本,否则指标没法横向对比。
我通常会把评估集分成两部分:公开评估集用于日常迭代,私有评估集用于最终验收。公开评估集可以频繁使用,私有评估集只在关键节点用,避免过拟合。
6.3 从bad case到改进:一个可复用的分析流程
线上出bad case是常态,关键是怎么从bad case里挖出改进点。我的分析流程是四步:收集、归类、归因、验证。收集是把bad case集中起来,归类是按问题类型分组,归因是分析每类问题的根因,验证是针对根因提出改进方案并验证效果。
归类这一步特别重要,因为bad case往往是长尾分布,不归类根本看不出规律。我一般会定义几个维度:输入类型、错误类型、影响程度。归因的时候要区分是数据问题、模型问题还是工程问题,不同问题解法完全不同。验证的时候要控制变量,一次只改一个地方,否则不知道是哪个改动起了作用。
6.4 版本管理与实验追踪:别让实验变成一团乱麻
做AI工程,实验数量会快速增长,没有好的版本管理和实验追踪,很快就会乱套。我的做法是:代码用git管理,数据用版本号管理,实验用配置文件管理。每次实验记录配置、代码commit、数据版本、评估结果,存到一个统一的实验记录系统里。
实验追踪工具市面上有不少,但我建议至少自己实现一个最小版本:一个表格,记录实验ID、配置、指标、备注。这个表格用csv或者数据库存都行,关键是坚持记录。我见过太多团队实验做了一堆,最后要复现某个结果的时候找不到配置,只能重跑,浪费大量时间。
7. 我踩过的坑和给你的实操建议
7.1 那些文档里不会写的教训
第一个教训:永远不要相信"数据已经处理好了"这句话。我接手过一个项目,前任说数据已经清洗过了,结果我抽查了100条,发现里面有20条是重复的。从那以后,我接手任何项目,第一件事就是自己抽查数据,不看不放心。
第二个教训:训练日志要记全,尤其是随机种子。有一次我训出一个效果特别好的模型,想复现的时候发现没记种子,怎么跑都跑不出那个结果。后来我强制要求所有训练脚本必须记录种子,并且支持从种子恢复。
第三个教训:推理服务的性能测试要在真实数据上做。我用合成数据测出来延迟很低,上线之后发现真实请求的输入长度分布跟合成数据完全不同,延迟翻了三倍。性能测试一定要用真实流量采样。
7.2 新手最容易犯的五个错误
第一个错误是跳过baseline直接上复杂模型。没有baseline,你根本不知道复杂模型带来的提升是真的还是运气。第二个错误是在验证集上反复调参。调多了验证集就失效了,必须留一个从没碰过的测试集。第三个错误是忽略数据质量只关注模型。数据是地基,地基不稳,模型再好也白搭。第四个错误是不做错误分析只看总体指标。总体指标掩盖了大量细节问题,必须做分类分析。第五个错误是过早优化性能。先跑通再优化,过早优化会让你陷入细节出不来。
7.3 工具链推荐:少而精的选择
工具不在多,在于用熟。我的核心工具链是这样的:版本控制用git,环境管理用conda或者venv,实验追踪用自己搭的表格加tensorboard,配置管理用yaml,测试用pytest。这些工具都足够成熟,学习成本低,组合起来能覆盖大部分需求。
不要盲目追新工具。我见过团队每出一个新框架就换一次,结果每个都只懂皮毛,出了问题都不知道怎么查。工具链稳定下来之后,把精力放在业务和算法上,这才是正道。
7.4 从零到一的路线图:给不同阶段的你
如果你是完全的新手,我的建议是先跑通一个最小闭环:用一个小数据集,训一个小模型,做一个简单的推理接口,走完整个流程。这个闭环可能很粗糙,但能让你理解各个环节的衔接。
如果你已经能跑通闭环,下一步是优化每个环节:数据管线加缓存,训练加混合精度,推理加批处理,评估加错误分析。每优化一个环节,记录前后的指标变化,积累经验。
如果你已经能优化各个环节,再下一步是建立体系:把流程标准化,把经验文档化,把监控自动化。这时候你的重点从技术转向工程管理,怎么让团队高效协作,怎么让系统稳定运行。
这个路线图不是线性的,很多环节会反复迭代。但大方向是这样,从点到线到面,从能跑到跑好到跑稳。AI工程这件事,急不得,但也停不得,每天进步一点,半年之后回头看,你会发现自己已经走了很远。