☰
从零开始AI工程:构建、训练与部署的完整实战
2026/10/4 11:54:49 网站建设 项目流程

从零开始做 AI 工程,我到底经历了什么

我一直觉得,AI 圈最缺的不是会调库的人,而是敢从零开始把一件事真正想明白的人。所以当有人问我"ai-engineering-from-scratch"到底是什么意思,我都会直接告诉他:这不是一个项目,而是一种态度,是你愿意从数学公式、数据清洗、权重更新这些最底层的东西一路堆起来,最终亲手造出一个能跑、能看、能用的 AI 系统。这篇博文,我就想把自己从头做 AI 工程的完整思路、实操步骤和踩坑记录分享出来,给那些准备动手却不知道从哪里开始的人,也给那些已经在路上但总觉得根基不稳的人。我不会讲太玄乎的理论,也不会只丢给你一堆框架调用,我会告诉你我是怎么选方向的、怎么拆任务的、怎么一步步验证自己想法的。

适合看这篇内容的人有三类:第一类是刚入门 AI 但不想只会用现成 API 的人,第二类是已经在做算法但缺乏工程化思维的人,第三类是纯粹对"从零构建一个 AI 系统"感兴趣、想通过完整项目理解全链路的人。无论你是哪一类,我希望你看完之后能获得一种感觉:原来从零开始搞 AI 工程,真的可以按图索骥,一步一步落地。

1. 内容整体设计与思路拆解

1.1 为什么我选择从零开始,而不是直接调库

刚开始接触 AI 工程的时候,主流的声音其实是"拿来主义":数据不够用现成的、模型不够用预训练的、部署不够用平台的。我也确实走过这条路,但时间久了发现一个问题——当项目出现奇怪的 bug,或者模型效果怎么调都不对的时候,我连排查的方向都没有,因为我只知道"怎么用",不知道"为什么能跑"。这就是典型的"框架依赖症"。我下定决心从零做一个完整的小型语言模型项目,不是为了重复造轮子,而是为了把黑盒子打开,看看里面每一个齿轮是怎么咬合的。

从零开始听起来像是在做重复劳动,但它的价值恰恰在于把"表面上会"变成"骨子里懂"。比如大家都知道 Transformer 有自注意力机制,但只有当你亲手实现 Q、K、V 三个矩阵的乘法反向传播时,你才会明白为什么维度要这样设、为什么缩放因子这么重要。我做的项目不是拿一个现成的大模型来微调,而是从数据构造、词表构建、模型结构定义、训练循环到推理部署,全部自己搭建一遍,每一步都记录清楚。这种做法的优点非常直接:核心逻辑完全掌控,出问题能快速定位;缺点是前期投入很大,开发周期比直接调库多出三到五倍。

但我想说的是,这个"多出来的三到五倍"恰恰是最有价值的部分。你在调库的时候遇到奇怪现象,很多时候只能搜索别人的经验,而从零开始你会发现自己能够直接推导出问题出在哪一层。就比如我项目里遇到过一个 loss 不下降的问题,后来定位到是学习率预热策略写错了,如果你只是调用别人封装好的 Trainer,可能根本不会去想预热策略对收敛的影响,只会一味调大 batch size。

1.2 核心需求解析:从标题到技术选型的推演

"ai-engineering-from-scratch"这个标题给我的第一个暗示是:必须有一个完整的工程闭环,不能只做模型训练一个点。所以我拆解出的核心需求包括四块:数据链路、模型构建、训练闭环、推理与评估。每一块都不是孤立的,数据链路决定模型能学到什么,模型构建决定学习能力的上限,训练闭环决定模型能否收敛到预期效果,推理与评估决定这个模型到底能不能用。这四个环节串起来,才算是一个完整的 AI 工程。

基于这四个需求,我对技术选型做了几个关键决策。数据方面我选择从公开语料中抽取并做一个小的清洗管道,而不是直接下载打包好的数据集,因为我想把"真实世界的数据有多脏"这个体验完整走一遍。模型方面以小型 GPT 风格架构为原型,参数量控制在 1500 万左右,这样单张消费级显卡就能训练,还能保证在合理时间内看到结果,镜像我搭的是 tokenizer 训练、模型初始化、数据加载三部分都从零写入的代码库。训练方面我选了 AdamW 优化器配余弦退火和线性预热,而不是直接用默认参数,因为在从头搭建的场景下,每一项配置影响都能被清晰观察,这本身就是学习的一部分。

选型逻辑中最重要的一个原则是"限制产生创造力"。当我把模型的规模限制在一个很小的范围内时,我就必须认真思考:模型容量够不够?数据质量是不是比数据数量更关键?哪些技巧是真正能带来提升的,哪些只是锦上添花?这种思考方式,远比无脑堆积资源要深刻得多。

2. 核心细节解析与实操要点

2.1 从零构建数据管道的三个关键环节

很多人一上来就急着建模,这是最要命的错误。我在这个项目里把数据环节拆成了三步,每一步都有明确产出。第一步是采集与清洗,我写了一个从原始网页文本中提取段落的脚本,去重、去广告、去乱码,把原始文本切成可以用于训练的干净片段。这一步的难点不是写正则表达式,而是要知道什么样的文本对模型训练是有害的:重复度过高的文本会让模型产生复读机倾向,包含大量乱码的文本会干扰 tokenizer 的学习,还有那些特殊符号很多的对话记录会拉偏分布。

第二步是构建词表,这一步的选择决定了模型的表达能力。我自己实现了一个 BPE(Byte Pair Encoding)训练器,从零开始训练一个词表,而不是加载现成的词表文件。BPE 的核心思想是不断合并高频共现的字符对和子词对,最终形成一层从字节到语义单元的映射。我在 5000 万字符的语料上训练了一个词表大小约 8000 的 BPE,并通过观察词表质量来反向调整语料的清洗策略。比如我发现语料里如果有很多重复的日期格式,tokenizer 就会把一整个日期学成一个 token,这其实浪费了词表容量。

第三步是构建采样器与批次生成器。很多教学代码这一步只做一个随机抽样打乱顺序,但我在实操中发现,如果直接把所有样本拼成长序列再切块,会出现"截断导致语义不完整"的问题。我后来采用的做法是:先把文档拆成较短的句子片段,再把相邻片段拼接到接近目标序列长度,最后用特殊的分隔符隔开不同文档。这个小细节在训练时能让 loss 下降更平滑,也能减少模型学到"跨文档无意义拼接"的概率。

这里我给你一个参数配置参考:如果目标是训练一个序列长度为 512 的小模型,那么批次大小可以设为 16,每批大约包含 8192 个 token。针对单张消费级显卡,这个体量是相对安全的。如果你显存更大,可以适当增大 batch size,但不要忽略梯度累加策略——每步实际更新时用的梯度是多个小批次累加的结果,这样可以稳定训练过程。

2.2 模型代码实现的四个核心模块

从零搭建模型代码,我不会一上来就写一个 giant transformer,而是先拆成四个模块:嵌入层、多头注意力、前馈网络和残差连接结构。嵌入层相对简单,就是把 token 索引映射为向量,再叠加可学习的位置编码。我推荐使用可学习位置编码,因为对小模型来说,它比三角函数式的位置编码更容易优化,也更好理解。我项目里用的嵌入维度是 256,序列长度是 512,因此位置编码的参数是 512 乘 256。

多头注意力是实现时最容易出错也最关键的部分。我建议把 Q、K、V 的线性变换合并成一个大的线性层,然后拆分到多个头,这样计算效率更高,也更方便调试。每个头的维度我设为 64,一共 4 个头,注意力头数乘每头维度等于总嵌入维度。具体实现里有个小细节很考验功底:注意力权重要除以根号下 head_dim,这个缩放因子在标准实现中是必须的,否则点积结果过大会导致 softmax 进入饱和区,梯度变得非常小。

前馈网络部分我用了两层线性变换加 GELU 激活函数。相比传统的 ReLU,GELU 在 Transformer 里的表现通常更平滑,但要注意初始化标准差要合适。如果初始化太大,很早就出现梯度爆炸;如果太小,整个模型在前几十步几乎不动。我这里给的初始化经验是标准差设为 0.02 乘模型维度的平方根加权调整,实际效果比较稳定。

残差连接和层归一化要特别注意"先归一化还是先残差"的问题。我选择的是 Pre-Norm 结构,也就是先做层归一化,再做注意力或前馈计算,最后加残差。这种结构在深层模型中更容易稳定训练,而且在小模型上也能看到明显的收敛优势。如果你用的是 Post-Norm,就必须配合更精细的学习率调节和 warmup,否则训练后期容易发散。

每次训练结束,我会打印每一层参数的梯度范数。如果梯度范数突然变成 NaN 或者超过 10,那百分之百是中间计算出现了数值溢出。把梯度过大的层单独拉出来检查,是定位模型代码 bug 的最快路径。

3. 实操过程与核心环节实现

3.1 用 5 分钟搭出最小可运行训练循环

我建议你搭建训练循环时从最简单的最小闭环开始,也就是"读取一个 batch,算一次前向,算一次反向,更新一次参数"。不要一开始就加入复杂的学习率调度、梯度裁剪、EMA(指数移动平均)等机制,先把主线跑通,再逐步加配置。

我项目里的最小训练循环包括四个函数:加载数据、前向计算、损失计算、优化器更新。损失函数我用的是交叉熵,其中要注意把所有非填充位置的 token 都计入损失,忽略填充部分,否则模型会不断学习预测"如何输出填充符",浪费学习能力。同时我设置了一个梯度累积步数,每 8 个微批次做一次参数更新,这样 batch size 的表观值可以做得较大,精度也能得到保证。

第一轮迭代我建议只训练大约 100 步,目标不是为了看到完美的模型效果,而是为了确认 loss 在下降、显存没有溢出、tokenizer 生成的样本可以被模型正常消费。我第一次跑的时候就踩了坑:数据的序列长度明明是 512,但数据加载器在最后一个 batch 没有做 padding 对齐,导致模型输入维度不一致,报错信息还很隐蔽。后来我在数据加载器里加了一个统一的 padding 函数,让所有序列动态对齐到最长长度,这个问题才解决。

一个值得记录的具体配置是我设置的最大学习率是 3e-4,权重衰减设为 0.1,线性预热步数为 500,之后按余弦退火衰减到 1e-5。选择这些参数不是拍脑袋,而是参考了小规模语言模型训练的常见组合,并在我的数据量和模型规模下做了少量调整。要注意的一点是,小模型通常不怕较高的学习率,但大词表和稀疏数据场景下调大学习率极易导致 loss 震荡,所以如果你复现时发现曲线不稳定,优先降低学习率至 1e-4 左右,而不是调大 batch。

3.2 从零训练一个 1500 万参数模型的完整记录

在当前软硬件环境下,我用一张显存 12GB 的消费级显卡训练这个 1500 万参数模型,总训练 token 数约为 1 亿,训练时间大约一个晚上。为了让训练过程更可追踪,我记录下每 500 步的 loss 值和学习率,并保存 checkpoint。整个训练过程的总 loss 曲线呈现快速下降后缓慢收敛的形态,但在大约第 3000 步附近出现过一次小的 loss 回升跳变。

那次跳变的根因是数据加载器在某个 epoch 边界没有重新洗牌,导致连续多个 batch 的数据分布非常接近,模型过拟合了局部模式,等在边界处切换到新数据分布时 loss 直接上升。理解这类现象非常重要,因为在小规模训练中,数据顺序的影响比大模型训练更明显。此后我把每个 epoch 结束后的数据文件随机打乱顺序,并在拼接时重新生成随机片段,问题就再也没有出现过。

在模型结构上,我使用了两层 Transformer block,每层 4 个头,隐藏维度 256,词表大小 8000。我计算的参数量大约是 1500 万,其中嵌入层占了很大比例,因为有 8000 乘 256 的参数。如果想在这个规模下继续提升效果,最有效的做法不是增加层数,而是扩大词表或嵌入维度,因为当前瓶颈主要在于嵌入表征的容量。

训练结束后我测试了模型的生成效果:给定一句"人工智能的未来在于",模型能够给出语义基本连贯的续写,这说明它对语言分布已经有了一定的学习能力。对于一个只有 1500 万参数的模型来说,这个结果是合理的,毕竟它的容量不足以产生复杂的知识和推理能力,但它已经完全掌握了词语搭配、句法结构等基础模式。

3.3 推理部署与效果评估的关键细节

从零训练完模型只完成了一半工作,你还需要把模型变成可以被服务的东西。这一步我用的是一个简单的文本生成接口,输入 prompt 后经过 tokenizer 编码、模型前向计算、采样解码,最终返回生成的文本。采样时我用了 top-k 采样加温度控制,温度设为 0.8,top-k 设为 50。在这个配置下,生成结果既不太保守也不太发散。

评估阶段我除了看生成样例,还计算了困惑度(perplexity),在验证集上大约得到 80 左右。对一个小模型来说这个数字是可接受的,但如果想让它更低,就需要成倍增加训练数据量或者扩大模型。要特别注意的是,困惑度是衡量模型对数据分布拟合程度的指标,并不等同于下游任务的效果。如果你做的是分类、抽取等具体任务,还需要专门构造评估集。

推理性能也是一个不能忽略的环节。我这个模型在 CPU 上生成 100 个 token 大约需要 5 秒,在 GPU 上只要 0.5 秒。如果想把推理速度进一步优化,可以考虑 batch 推理、int8 量化或 KV Cache。我在项目里实现了简单的 KV Cache,使生成时不必重新计算之前的键值对,生成的显存占用下降约 30%,速度也有明显提升。这里有一个很容易犯的错误:如果实现 KV Cache 时没有正确管理缓存长度,长文本生成到后期会出现索引错误,建议每一步生成都显式更新缓存长度。

4. 常见问题与排查技巧实录

4.1 训练 loss 不下降,应该如何快速定位

我遇到的第一类典型问题是 loss 不下降,具体表现是训练了好几百步,loss 一直在初始值附近震荡,或者下降得非常缓慢。排查思路要分三条线走:第一,检查数据标签是否对齐,比如 token 移位导致模型预测目标错误,这是非常隐蔽的 bug;第二,检查学习率是否过低或过高,过低会出现"能下降但很难看",过高会出现 loss 震荡甚至发散;第三,检查初始化是否合理,如果 embedding 初始化太大,模型一开始就处于一个很不稳定的状态,优化器往往很难把它拉回来。

还有一种非常容易被忽视的情况:模型的输出层没有和词表大小匹配。我记得有一次我在改词表大小后,忘记同步修改模型输出维度,导致代码跑出的 loss 完全异常,最后是通过逐层打印张量形状才定位到的。我的建议是把模型前向过程中所有关键张量的形状打印出来,从输入 token 到最终 logits,每一步检查是否符合预期。这个习惯能帮你省下非常多调试时间。

如果 loss 曲线一直不降,还有一个可能是你使用了过大的梯度累积步数,导致参数更新频率太低,模型收敛极慢。我建议把梯度累积步数先设为 1,观察 baseline 表现,再逐步增大。否则你很容易陷入"看起来 loss 在动,但实际上是在累积多个 batch 的噪声信号"的错觉。

4.2 显存不足或训练崩溃的排查经验

显存不足在小规模项目中也会出现,特别是当你使用了较长的序列长度或较大的 batch size 时。我的建议是先按显存的一半规划 batch size,再逐步往上加,同时用 gradient checkpointing 技术减少中间激活的存储。在 12GB 显存上,我的 1500 万参数模型本来可以轻松运行,但我在第一次尝试时因为设置了过大的 batch size 和过长的序列长度,直接 OOM。后来我把 batch size 降到 16,序列长度保持 512,问题就解决了。

训练崩溃是另一个常见问题,最常见的表现是 loss 变成 NaN。我从自己的实践中总结了四个排查点:一是学习率过大,二是数据中存在异常值,三是某一层梯度爆炸,四是混合精度下出现数值下溢。我的做法是先在纯 float32 下训练几十步,确认没有 NaN 之后再尝试混合精度,这样定位问题会非常高效。如果一旦发现某一步 loss 变为 NaN,我会立即减小学习率并检查最后一个 batch 的数据是否有空值或极大值。

我试过在小模型上用 bfloat16 混合精度训练,相比 float32 损失了一点精度,但速度提升约 30%。如果你的显卡不支持 bfloat16,建议老老实实用 float32,因为 float16 在小模型上的动态范围太小,容易出现梯度消失。这个坑我在一次快速实验里踩得很好,让我彻底明白"硬件支持混合精度"和"模型适合混合精度"是两回事。

4.3 生成质量差、复读或者语义混乱怎么办

训练完成后生成质量不理想是非常普遍的现象,尤其是小模型。复读问题我看到最多的原因是训练数据中有大量重复片段。比如我从语料里清洗的时候没有去除连续重复的段落,模型就学到了"不断重复最近见过的内容"的模式。解决方法是提升数据清洗强度,把连续重复超过三次的 n-gram 筛掉,同时在生成的时候,把重复惩罚系数设高一些,像 repetition_penalty 设为 1.2 左右,效果立竿见影。

语义混乱则更可能和上下文长度不足有关。模型只有 512 的序列长度,它能看到的信息是有限,一旦输入太长,前面信息会被"忘掉"。缓解方案是训练时随机截取长文档的连续片段,并且在推理时只使用最近的一部分上下文。另外,如果模型生成的句子语法正确但逻辑不通,最根本原因还是模型容量和训练数据不足,这需要从更大的模型和更高质量的数据方向来解,而不是靠调参能解决的。

还有一个容易踩的细节:生成时温度设置太高或者太低都会导致问题。太高模型会逐渐漂移,说话毫无逻辑;太低模型会走向极端保守,变成一个复读机。我前文给出的 0.8 温度加 top-k 50 的组合,是在我这个小模型上经过几次生成对比之后选出来的,你可以用它作为起点,再根据你的场景微调。当你真正理解了温度、top-k、top-p 每个参数对生成分布的影响,你就能像一个调音师一样让模型在不同场景下表现出不同风格。

4.4 一套排查速查表,值得直接收藏

我把从零构建 AI 工程遇到的高频问题整理成一张速查表,方便你在卡住的时候快速对号入座。这张表不是网上能随便搜到的废话合集,而是我在实操中真实遇到并解决的场景,每一行都有针对性。

现象可能原因排查方向
训练 loss 恒为初始值数据标签错位 / 输出层维度不匹配检查 token 对齐、打印 logits 形状
loss 下降到一半突然升高数据顺序问题 / 学习率调度异常检查 epoch 边界数据洗牌、打印学习率
loss 变成 NaN学习率过大 / 数据异常 / 梯度爆炸降低学习率、纯 float32 验证、检查梯度范数
生成结果频繁复读训练数据重复严重 / 采样温度过低清洗 n-gram 重复、提高温度、增加重复惩罚
生成语义混乱上下文长度不足 / 模型容量不足缩短输入、增大模型或数据量
显存不足batch size 过大 / 序列过长降低 batch size、开启梯度检查点
推理速度慢没有缓存 KV / 实时计算重复实现 KV Cache、批量推理
参数更新无效梯度累积步数过大 / 优化器配置错误检查累积步数、打印参数更新前后的差值

5. 工程扩展:从玩具模型走向真实应用

5.1 数据规模与模型规模同时放大的策略

如果你已经成功复现了小型模型的全流程,下一步就是如何扩展到更大规模。我想提醒你的是,增加模型参数量和增加数据量不应该是同步进行的,而是遵循一个"阶梯式"原则:先固定数据规模,增加模型参数量,观察是否出现欠拟合;再固定模型规模,增加数据量,观察是否出现瓶颈。这样做的好处是你能明确知道当前系统性能的短板究竟在哪里。

我在 1500 万参数模型的基础上尝试过扩大到 6000 万参数,同样保持 1 亿 token 的数据量,结果提升非常有限。这说明 1 亿 token 的数据对 6000 万参数是不够的。反过来,我在保持 1500 万参数不变时把数据扩大到 5 亿 token,困惑度有了显著下降。这个实验告诉我,对小模型来说,数据量的优先级远高于参数量。

扩数据的同时要格外注意数据处理管道的吞吐能力。我在首次扩容时发现数据加载成了瓶颈,GPU 经常处于等待状态。解决办法是采用多进程数据加载预取,配合内存映射文件,把数据加载时间降到每步可以忽略不计。如果你不想折腾这些工程优化,也可以先用一个简单的数据管道跑通扩容实验,但你要有心理准备:数据管道不优化,GPU 利用率通常只有 20% 到 30%,非常浪费。

5.2 将模型接入实际业务场景的落地思考

从零训练一个模型,不代表你永远不用预训练模型,而是在需要用的时候你能做出更明智的决策。以我经验来看,如果你的业务场景很垂直、数据量很大、且有独特的语言模式,从零训练小型专用模型是值得考虑的,因为你可以完全控制数据分布,还能避免外部模型带来的数据合规风险;如果你的业务场景是通用型任务,那么基于开源大模型做微调会更高效。

我在项目里做了一个简单的文本分类头,把从零训练的模型接在一个客服工单自动打标场景下测试。由于任务比较单一,只用了模型的最后一层隐藏状态,接一个线性分类器,准确率就已经接近使用通用大模型 prompt 的水平,而且推理成本低了一个数量级。这个实验让我确信:从零构建模型在特定场景下确实可以成为一套性价比极高的方案。

当然,从零训练模型也意味着你要负责整个生命周期,包括数据更新、模型重训、效果监控、版本回滚。我建议即使是最小的项目也要把 checkpoint 管理器、实验记录、评估脚本这些工程基建搭好,否则等模型迭代到第五版第六版时,你会发现自己连"哪个参数组合产生了哪个效果"都说不清楚。

我个人的习惯是每跑完一次实验就更新一份 markdown 格式的实验日志,里面记录数据版本、模型配置、训练曲线、生成样例。这个习惯帮我避免了很多重复劳动,也让我在写项目文档的时候事半功倍。如果你打算走得更远,这套日志习惯一定要趁早养成。

6. 一些没人告诉你的经验与心得

从零开始做 AI 工程这件事,技术本身是公开的,论文是公开的,代码也有无数开源实现可以借鉴,真正拉开差距的是那些"没人明确教过你"的经验。我想用最后这一部分多说几句真心话。

第一,不要害怕写看起来"幼稚"的代码。我第一次写注意力机制的实现时,为了验证矩阵乘法结果是否正确,我用一个很小的随机张量手算了前向和反向,整个过程非常笨拙,但它让我彻底理解了张量形状的变化。这种理解在以后调试复杂模型时是巨大的财富。很多人觉得写复杂代码才算厉害,但我觉得把基础代码写到滴水不漏才是真正的工程能力。

第二,训练一个模型就像养一盆花,环境稳定比拼命施肥更重要。我踩过最大的坑就是想同时改进所有模块,结果每次改动一多,模型效果反而变差,连排查都不知道从哪开始。正确的节奏是每次只改变一个变量,比如只改学习率调度、只改数据清洗策略,并且记录它对 loss 和生成效果的影响。这样你的项目演进是有迹可循的,而不是一团乱麻。

第三,我强烈建议你在动手之前想清楚"评价标准"。很多人从零构建模型只顾着让 loss 下降,却没想清楚最终要解决什么问题。我在项目中途调整过评价标准,从"困惑度更低"改成"生成文本在特定领域的准确率更高",这个转变看似微小,却直接导致我重新设计了数据采样策略。如果你的目标是生成流畅对话,那评价标准就应该是对话质量;如果目标是分类准确,那评价标准就应该是标注数据上的指标。想清楚了,你会少走很多弯路。

最后说一个我在多次实操中得到的体会:从零起步,进度永远比想象中慢,但每一步都是扎实的。当你第一次看到自己亲手训练的模型生成出连贯的文本时,那种成就感绝不是调用 API 能比的。这个项目虽然只是万里长征的第一步,但它会让你建立起一种判断力,知道哪些技术决策是可靠的,哪些只是表面热闹。无论你接下来是继续深耕模型架构,还是转向分布式训练,又或者是做模型部署和产品化,这次的从零开始都会成为你技术判断力里最坚实的一块基石。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询