☰
从零构建大语言模型到推理模型:AI工程师实战路线图
2026/10/2 19:58:38 网站建设 项目流程

如果你刷到这篇博客,大概率和我一样,不太甘心只当一个“调 API 的工程师”。过去两年我陆续用业余时间把ai-engineering-from-scratch这条路走了一遍——不是从零实现一个 PyTorch,而是从原始数据、tokenizer、Transformer 训练,一直到能跑通一个小规模的推理模型(reasoning model)。网上关于“build a large language model from scratch”的热度一直很高,但真正动手的人少,能坚持到把损失函数压下去并看到模型说出连贯句子的人更少。这篇文章就是把我踩过的坑、验证过有效的路线和为什么这样做,一次说清楚。

它适合两类读者:一是对 LLM 原理有基本概念、想摆脱黑盒思维,亲手训练一个模型的工程师;二是想低成本复现“从零构建推理模型”这条技术路线的研究者。我不会只给结论,会把你带进每一个关键决策的现场。

1. 为什么“从零开始”不是炫技,而是 AI 工程的基本功

1.1 API 时代的能力错觉

先泼一盆冷水。用 GPT-4 或者 Claude 写出很漂亮的 demo,和真正具备 AI 工程能力,是两回事。API 调用就像开自动挡汽车,你能到达目的地,但对发动机、变速箱的配合逻辑完全没有感知。一旦遇到线上问题——为什么同样提示词今天输出质量骤降、为什么长上下文下模型忽然“失忆”、为什么量化后某些数学题开始乱答——你就只能靠玄学调参。

我亲眼见过一个团队,产品全都架在闭源 API 上,某天上游模型升级,整个业务的输出风格变了,他们连降级方案都没有,因为没人知道模型内部发生了什么。从零训练过模型的人,至少会懂得:输出的变化可能来自采样温度、系统提示词、上下文长度、甚至 tokenizer 对特殊字符的处理。这个“懂得”就是基本功。

1.2 从零复现的价值:调试、评估与性能优化

自己从零训练一个小模型,第一个收获是你会真正理解损失曲线的语言。什么时候该涨却跌了,什么时候明明在降但生成效果变差,这些都是 API 调用永远教不会你的。第二个收获是评估能力。网上大多数人对模型效果的评价停留在“能不能对”,但 AI 工程需要你定义什么是“对”:数学题要看最终答案,代码题要看能否编译运行,开放问答要看内容安全性。这些评估体系,只有在你亲手造模型、亲手标注数据时才能建立起来。

第三个收获是成本直觉。API 按 token 计费,你很难感知一个回答背后是多少算力。自己训练时,你会清楚 1B 参数模型跑一次 forward 需要多少显存、微调一个 epoch 要多少卡时。这种感知在做技术选型和成本控制时极其重要。

1.3 我走的完整链路:从数据到推理模型

我选择的技术路线是这样一条链路:

  1. 数据工程:爬取、清洗、去重、配比
  2. Tokenizer:从零训练一个 BPE 词表
  3. 预训练:训练一个 0.1B~1B 的 base 模型
  4. 指令微调:构造 SFT 数据,让模型学会对话
  5. 推理能力:用可验证奖励训练一个 reasoning model
  6. 部署:量化、推理加速、服务化

每一步都有大量细节。我先把话说清楚:所谓“from scratch”,并不是连 PyTorch 的算子都要自己写,而是不依赖任何闭源大模型为你生成数据和技术细节。计算框架该用就用,但模型架构、训练脚本、评估方法必须是你自己可控的。这一点想明白,后面才不会走偏。

2. 第一块基石:数据工程与 tokenizer,决定上限的往往不是网络结构

2.1 数据清洗:去重比你想的重要得多

很多人以为模型效果差是网络结构不够好,我实际跑下来发现,数据才是第一生产力。我在一次 0.3B 模型的预训练中,前期用未清洗的网页数据,loss 一直卡在 2.1 下不去;后来做了去重和过滤,同样算力下 loss 在 1.8 附近稳定。差距非常大。

清洗管线我按这个顺序做:

  • 编码检测与乱码过滤:用ftfy修复常见 mojibake,过滤控制字符。
  • 语言识别:用fasttext的 langid 模型区分中英文,按需配比。
  • MinHash 去重:对段落做 MinHash + LSH,重复内容直接剔除。公共爬虫语料里面重复率常常超过 30%,这个步骤不能省。
  • 质量过滤:用gzip压缩比作为启发式信号,压缩比异常低的文本通常是列表页、导航栏这类低质量内容。
  • PII 与安全过滤:把邮箱、手机号、身份证号模式匹配后替换成占位符。

这里有一个重要经验:不要在主流程里直接改原始数据。每次清洗都生成一个新版本数据集,并记录清洗前后的 token 数量变化。否则你根本不知道哪个环节提升了模型效果,后续想做消融实验都无从下手。

2.2 从零训练 BPE tokenizer 的实操要点

我使用 HuggingFacetokenizers库训练 byte-level BPE,核心配置如下:

from tokenizers import Tokenizer, models, trainers tokenizer = Tokenizer(models.BPE()) trainer = trainers.BpeTrainer( vocab_size=32000, min_frequency=2, special_tokens=["<pad>", "<unk>", "<s>", "</s>"], initial_alphabet=pre_tokenizer.get_alphabet(), ) tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel() tokenizer.train(files, trainer)

我在这个环节踩过三个坑,逐一说明。

第一个坑:训练语料太少。BPE 词表需要足够的语料统计频率,我只扔了 500MB 中文语料进去,结果很多常用词被切得稀碎,序列长度暴涨,训练速度明显变慢。后来我把语料扩到 2GB,情况才好转。经验是:词表 16K~32K 需要至少 1~2GB 清洗后的文本。

第二个坑:忽略了 normalizer。中文还好,英文的 Unicode 全角字符、大小写归一化如果没做,同一个词会出现多种形态,词表被无效占位。务必在 BPE 之前加normalizers.Sequence。

第三个坑:特殊 token 的位置。<unk>不是越多越好,base 模型如果用一堆特殊 token,预训练时语料里几乎见不到它们,微调阶段才开始接触,模型容易表现怪异。我的做法是只在必要场景使用<s>、</s>这类结构 token。

2.3 数据配比的经验值

多语言多领域数据怎么配比?我推荐一个起点,再基于验证集 loss 做调整:

数据域配比说明
通用网页60%多样性来源,覆盖常识
书籍/长文15%长依赖能力,降低困惑度
代码15%结构化逻辑,近年重要性上升
数学/科学5%推理能力的基础
对话/问答5%指令遵循的种子

注意:这里说的是预训练阶段的配比。如果你直接复用社区微调数据,可能因为配比失衡导致模型只会聊天、不会做正经推理。我用一个简单办法验证:预训练结束后,分别跑一个语言建模测试集和一个小规模下游任务集,看哪个方向损失偏高,再回头调数据。

3. 手写 Transformer 时最容易翻车的三个地方

3.1 位置编码到底加在哪里、用哪种

Transformer 本身没有顺序概念,位置编码是必须的。现在主流已经不是绝对正弦编码,而是旋转位置编码(RoPE)。我第一次实现时天真地以为“加在 embedding 上就行”,结果训练不稳定。原因是 RoPE 是乘性的,它的正确做法是在 attention 的 q 和 k 计算之后、点积之前,对向量做旋转变换。

用 PyTorch 实现 RoPE 的核心逻辑如下:

def apply_rope(x, cos, sin): # x: [batch, seq, heads, head_dim] d = x.shape[-1] x1 = x[..., : d // 2] x2 = x[..., d // 2 :] return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1)

RoPE 的优势是相对位置信息天然蕴含在内,外推性也比绝对编码好。我的建议是:新项目直接用 RoPE,别在绝对编码上浪费时间。实现时注意cos和sin的维度是[seq, head_dim],要分别对应 q 和 k 的序列长度。

3.2 因果掩码的细节陷阱

训练 decoder-only 模型时,因果掩码(causal mask)是核心。问题出在很多人把 mask 加错了位置。它必须加在 softmax 之前的 attention score 上,把未来位置的分数置为-inf,而不是在输出层做截断。

我第一次写的时候把 mask 用在了 attention 输出之后,模型训练 loss 确实下降了,但生成阶段暴露了问题——因为生成时是逐步解码,根本没有“未来 token”可以看,训练和推理行为不一致,生成文本东一句西一句。排查半天才发现是掩码位置错误。

正确代码如下:

scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(d_k) scores = scores.masked_fill(mask == 0, float("-inf")) attn = torch.softmax(scores, dim=-1)

注意mask == 0的位置会把整个未来行变成-inf,softmax 之后对应概率为 0,这样就保证第 i 个 token 只看得到前 i 个 token(含自身)。

3.3 数值稳定性:LayerNorm、fp16 与梯度裁剪

训练 Transformer 的经典难题是数值不稳定。我遇到过几次 loss 突然变成 NaN,几乎都是这几个原因:

  • fp16 下 softmax 上溢出。解决:实现时用max减去最大值再做 exp(PyTorch 内部已做,但如果你手写 attention 要自己注意)。
  • 学习率过大导致梯度爆炸。解决:max_grad_norm=1.0的梯度裁剪必不可少。
  • 初始化尺度不当。GPT-2 风格的初始化会对残差层做1/sqrt(2 * num_layers)比例的缩小,这个细节很容易漏。

还有一个容易被忽略的点:pre-norm 和 post-norm 的选择。我的对比经验如下:

结构稳定性最终效果应用场景
Post-LN训练后期易波动收敛效果理论更优经典 Transformer
Pre-LN训练稳定,可加大学习率略低于 Post-LN,但更稳现代 LLM 主流

绝大多数开源大模型用的都是 Pre-LN(如 LLaMA 系列),因为它的梯度更容易流动,适合超大批次训练。我建议你也直接用 Pre-LN,省掉跟 NaN 搏斗的时间。

4. 从语言模型到推理模型:reasoning model 的从零构建路线

4.1 先纠正一个误解:不是所有推理能力都靠 RL

“build a reasoning model from scratch”最近很热,很多人以为推理模型就是把 RL 跑一遍。实际上推理能力有三个来源:预训练时塞进去的代码和数学数据、SFT 阶段用的长思维链数据、以及 RL 阶段的可验证奖励。三者缺一不可。我在没有充足计算资源时,仅靠前两步就能让一个 1B 模型在小规模数学题上显著提升,RL 是锦上添花但绝非唯一答案。

4.2 可验证奖励:最朴素的 RL 训练流程

如果你真的要训练一个能“想清楚再回答”的模型,核心思路是这样的:

  1. 构造带推理过程的数据:每道数学题包含逐步推导和最终答案。
  2. SFT 让模型先学会模仿这种长回答格式。
  3. RL 阶段用规则打分器做奖励:最终答案正确得 1,错误得 0。不训练额外奖励模型,避免奖励黑客问题。

我用过一个轻量实现,参考了 GRPO 的思路,流程如下:

  • 对每个 prompt 采样 8 个 response。
  • 计算每组 response 的奖励均值,作为 baseline。
  • 对每个 response 计算优势值(r_i - mean_r)。
  • 用 PPO 风格的 clipped loss 更新策略模型。

这个流程的代码量不大,但训练极其不稳定。我的经验是:先冻结参考模型,KL 惩罚系数从 0.01 开始调;采样温度保持 0.7~1.0;batch 不要太小,至少 512 条 prompt。另外一个关键:推理模型的 RL 数据只能选有确定答案的领域,比如数学、代码。开放问答根本没有可验证奖励,强行 RL 只会让模型学会钻空子——比如回答“答案是 A 吗?肯定是 A”来骗分。

4.3 算力受限时的替代方案

没有几十张 A100,能不能做推理模型?能,但要降低预期。我试过三条路:

  • 蒸馏:用一个大模型生成推理轨迹,然后用这些轨迹做 SFT。这是性价比最高的路线。
  • 自一致性:在推理时多次采样,投票选择出现频率最高的答案。不训练也能涨点。
  • 专门化小模型:把模型限制在数学或代码单领域,把模型规模压到 0.5B,再配一个领域 tokenizer,效果远好于一个啥都想干的通用小模型。

如果你想快速看到效果,我的建议排序是:蒸馏 SFT > 自一致性 > 轻量 GRPO。先让流程跑通,再逐步加大 RL 投入。

5. 从 0.1B 到 1B:训练过程踩过的坑和评估体系

5.1 训练不收敛的排查顺序

训练不收敛时,第一反应别是调网络架构。我的排查顺序固定是:

  1. 看 loss 是否下降。如果完全不动,先跑一个极小规模(单 batch)过拟合测试,确认模型能记住数据。
  2. 查数据问题:字段有没有对齐、tokenizer 有没有把 prompt 切废。
  3. 查学习率:通常 1e-4 附近起步,warmup 占总步数 1%~2%。warmup 没做,前期很容易震荡。
  4. 查 loss 曲线细节:如果是先降后涨,大概率是学习率过大或数据重复过拟合。
  5. 查梯度范数:每次打印grad_norm,如果超过 10,优先调梯度裁剪而不是降低学习率。

我记得有一次模型到第 800 步突然 loss 飙升到 12,查半天发现是清洗脚本把某个语料库的标签符号全部删成了空行,模型被迫预测大量空行。这类问题不看数据根本定位不到。

5.2 困惑度之外,你需要一套自定义评估任务

很多人习惯只看 perplexity,但它和真实产品体验的相关性没那么高。我在训练中同时维护三个评估维度:

维度指标说明
语言质量PPL、复现率关注句式连贯性
指令遵循自建 200 条指令集每条按规则打分
推理能力GSM8K 数学子集、代码编译客观可验证

这套评估每周跑一次,对比不同 checkpoint。它能帮你发现 PPL 很低但模型“变傻”的情况——比如只学会了高频套话,遇到具体问题就失忆。

5.3 我从 0.5B 模型上线得到的部署经验

模型训完不等于结束。我用vLLM做推理服务时遇到第一个问题是显存不够:0.5B 的模型,fp16 权重约 1GB,但 kv cache 在长上下文下会撑爆。解决方法是限制最大长度,并把gpu_memory_utilization调低预留缓存。

第二个问题是量化。我对模型做了 AWQ 4bit 量化,精度损失在可接受范围,显存降到 0.4GB。如果你的应用对延迟敏感,量化和 PagedAttention 几乎必备。

第三个经验很多人不知道:生成参数比模型权重更影响体感。同一个模型,temperature=0.3和temperature=1.0差别巨大。做产品时,先固定生成参数再优化模型,否则你永远不知道问题出在模型还是采样策略。

6. 想从零开始的人,这是我给你的一条可执行路线图

6.1 一份 12 周路线图

我根据自己的经验,整理了一份面向全职工程师、每天两小时的 12 周路线:

阶段内容产出
第 1-2 周准备数据清洗管线 + BPE tokenizer清洗后 1GB 数据集
第 3-4 周实现 Transformer 训练脚本,跑通单卡小模型loss 收敛的 0.05B 模型
第 5-6 周扩大数据到 3GB,训练 0.1B 模型能生成连贯文本的 base 模型
第 7-8 周构造 SFT 数据,做指令微调能够对话的模型
第 9-10 周构建推理评估集,跑通 GRPO 训练数学准确率有明显提升
第 11-12 周量化 + vLLM 部署 + 小产品 demo可用的低成本服务

这个路线最大的好处是每两周都有看得见的产出,不容易中途放弃。

6.2 算力问题:没有 A100 怎么活

我默认你只有一两张 3090/4090(24GB 显存)。完全可行。关键技巧:

  • 用梯度累积模拟大 batch,micro_batch_size=4,grad_accum_steps=8,等效 32 batch size。
  • 启用torch.compile和 bf16 混合精度,24GB 显存可以训 0.7B 模型。
  • 序列长度先从 512 开始,稳定后再逐步加长。这比一开始就上 2048 更容易成功。
  • 盘活现有资源:我在 AMD CPU 的机器上用纯 CPU 训练过一个小模型做验证,速度慢但能排查代码逻辑,节约 GPU 时间。

6.3 一个容易被忽略的团队协作问题

最后说一个非技术但很重要的事:从零训练模型的项目,一定要保存完整的训练日志。我见过太多人跑完一轮训练,说“效果还行”,但问他要训练超参、数据版本、评估结果,什么都拿不出来。没有日志,你无法复现自己的成果,更无法证明 pipeline 的可靠性。我在每一轮训练前都会建一个实验目录,包含:数据版本哈希、tokenizer 配置、训练超参、评估指标、wandb 曲线地址。写实验卡的习惯救了我无数次。

6.4 我最后想补充的

做ai-engineering-from-scratch这件事,我的真实体会是:它并不需要你是天才,但需要你有耐心。训练一次模型最短几个小时,迭代一次可能要一周。你会在凌晨三点盯着 loss 曲线等它下降,会为 tokenizer 的一个超参数折磨一整天。但当你亲手训练的小模型第一次流畅地回答出问题、第一次通过数学推理得到正确答案时,那种“底层原理尽在掌控”的感受,是调用任何 API 都无法替代的。

如果你准备动手,我的建议是先别想太大的目标。从 0.05B 的玩具模型开始,走通一条微小的闭环,再逐步放大。这条路上每一步的坑,都会变成你判断大模型产品问题时的直觉。祝训练顺利。

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

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

立即咨询