1. 先盘盘“从零构建”这四个字到底意味着什么
两年前我刚接触大语言模型时,看到“从零构建”四个字,第一反应是下载一套开源代码然后跑起来。真跑起来才发现,代码反而是最不值得研究的部分。真正决定一个推理模型最终成色的,是数据质量、损失函数设计、强化学习奖励,以及你能不能读懂训练曲线。如果你也想做一个 R1 风格的推理大模型,我建议先别急着找训练脚本,而是把“从零”两个字的边界划清楚。
每个人口中的“从零”其实不太一样。有人是从网上扒一份预训练代码,把数据换成自己的,这算从零;有人是连 tokenizer 都要自己训练,训练集群自己搭,这当然更硬核。但无论哪种,最终目标都是让模型具备“能够一步步推理并给出可验证答案”的能力。要注意的是,推理能力并不是预训练阶段天然长出来的,它要靠后续的指令微调和强化学习反复打磨。所以在动手之前,我建议把整个工程拆成三块:预训练、后训练、推理服务。预训练解决“识字和通识”,后训练解决“会思考”,推理服务解决“能上线”。三块的投入配比完全不同,最耗时的是数据和后训练,而不是很多人以为的模型并行技巧。
1.1 从零构建的真实边界:预训练、后训练与推理系统
先说说预训练。你要准备几百 GB 甚至几 TB 的清洗后语料,训练一个基础模型,让它具备语言建模能力。这一步最烧钱,也是大多数人最不擅长的。如果你想用单卡或几张小卡复现,那就只能把模型规模压到 1B 以内,语料也相应缩小。R1 风格模型不是只靠预训练出来的,预训练只是它的“底子”,真正让它脱颖而出的是一套精心设计的后训练流水线。
后训练是关键。冷启动阶段用少量高质量思维链数据做 SFT,把模型先“调教”成愿意输出推理过程的样子,然后用强化学习去优化可验证的奖励,比如数学答案对不对、代码能不能通过测试用例。这一步的目标是让模型在推理时自己探索出更有效的路径,而不是机械复述训练集里的解题套路。相比预训练,后训练的数据量小得多,但设计难度高得多,奖励稍微给错,模型立刻会学会钻空子。
推理服务也是从零构建里很容易被忽略的部分。模型训练完,要支持流式输出、并发请求、KV Cache 管理,还要考虑吞吐和延迟的平衡。很多人把全部精力放在训练 loss 上,结果模型做得不错,但上线推理时一跑长思维链就超时,被迫放弃大量已生成的 CoT。这个坑我在模拟项目 X 里踩过,后文会专门说。
1.2 一条能落地的路线图:从语料到强化学习
我给自己的路线图大致分成了五个阶段,你可以直接拿去当时间表参考。第一阶段是数据工程,包括设计词表、清洗语料、构建 CoT 训练集,这个阶段大约占三分之一的工作量。第二阶段是预训练,先用小模型跑通流程,再逐步放大。第三阶段是冷启动 SFT,准备 1 万到 3 万条带完整推理链的高质量数据,让模型学会“先思考再回答”。第四阶段是强化学习,用规则验证器判断答案是否正确,配合组相对策略优化做训练。第五阶段才是部署与可视化分析。
需要特别提醒的是,阶段之间不是线性的。你很可能在第四阶段发现模型推理长度爆炸,然后回到第三阶段重新调冷启动数据的长度分布。我在做这个项目时,第四阶段反复重来了四五次,每次改的都是奖励函数或者 KL 正则系数。你没有必要追求一步到位,反而应该在关键节点设置检查点,方便随时回滚。
2. 第一步地基:分词器与语料准备的三个硬门槛
很多人一上来就用现成的开源分词器,省事是省事,但如果你针对中文、代码、数学混合语料训练模型,现成词表未必合适。R1 风格推理模型的核心任务往往集中在数学和代码上,这两类文本对分词的要求很高,尤其是数字序列。
2.1 词表构建:BPE 如何影响后续的推理能力
BPE 分词的原理并不复杂,本质是一个贪心合并过程:初始化所有字符,统计高频字节对,不断合并,直到达到目标词表大小。你最终得到的是一个词表,以及一套把文本切成 token 的规则。词表大小选多少?经验上,7B 级别模型常见选择是 32K 到 128K。词表太小,长数字会被切成单字符,模型很难学到“多位数加减乘除”的模式;词表太大,Embedding 和输出层参数暴涨,训练开销上去,但收益在推理场景里并不明显。
我建议在动手训练前做个简单检查:把你最高频的 1000 道数学题文本全部切分一次,统计每个题目平均 token 数。如果发现像 9876543210 这种连续数字被切成了七八个 token,说明词表里数字相关子词严重缺失。另一个角度的教训是,不要只为了降低 token 数而盲目塞满数字串,那会让 Embedding 表充斥着几乎不会共现的碎片。
顺便说一句,如果你决定从零训练分词器,请一定保证训练语料和实际训练语料在领域分布上保持一致。我见过有团队用通用百科语料训词表,结果下游代码数据集里冒出一堆 OOV 碎片,最终推理效果打折扣。分词器虽小,却是所有上层能力的地基。
2.2 语料清洗与配比:推理任务的“偏好浓度”
预训练语料不是随便把互联网内容抓下来就能用。我一般会做五层清洗:去重、语言过滤、质量过滤、毒性过滤、个人隐私过滤。其中影响推理能力最直接的是去重和质量过滤。Massive 数据量里如果存在过多重复段落,模型会把重复当成一种低熵捷径,导致面对新问题时缺乏泛化能力。你可以用 MinHash 做近似去重,再结合 n-gram 重叠做精确去重,这是一个几乎必做的步骤。
配比上,通用文本、代码、数学的比例要刻意偏一下。推理模型不能通识太弱,但数学和代码的“浓度”决定了强化学习阶段能否顺利起飞。我的经验比例大概是这样:通用文本占 60%-70%,代码占 20%-25%,数学占 10%-15%。如果是纯数学推理模型,可以把数学提到 40%,但代价是通用对话能力下降,你自己权衡。
清洗完别急着训,先跑一遍 tokenizer 的“命中率”报告。我习惯统计每条语料被切分后的 token 数分布,如果某些语料因为特殊字符产生大量碎片 token,需要回到清洗环节处理。这个步骤花的时间不多,但能提前暴露很多后续问题。
2.3 CoT 数据从哪来:三种可靠的生产路径
冷启动和强化学习阶段都需要带推理链的数据,自产 CoT 数据是绕不开的工作。纯人工标注太贵,规模大了也容易不一致;纯靠开源数据集,领域覆盖又不够。我试过三种比较可靠的路径,你可以按场景混着用。
第一种是人工精标小样本。找几个熟悉目标领域的标注者,每人按统一模板写完整推理步骤,重点是步骤必须中间衔接、不允许跳步。这部分的量不用多,质量要高,通常 2000 到 5000 条就能给模型一个很好的起点。第二种是借用现有大模型生成后筛选。也就是拿更强的通用模型当“教师模型”,对题目做多次采样,再用规则验证器筛选出答案正确的样本。这里有一个细节:答案对不一定代表推理过程好,你还要做一个质量打分,过滤掉“蒙对但过程混乱”的样本。第三种是程序化生成。针对数学题,可以用模板随机生成数字和条件,得到大量自然题目,然后通过符号计算或脚本校验生成答案。
三种路径各有侧重,人工数据提供风格基准,教师数据提供多样性,程序化数据提供规模化。我在实际项目中大概是 1:4:5 的比例混合。要注意,教师模型生成的 CoT 往往带有它自身的风格毛病,比如过度重复或故作深沉,要让模型在冷启动阶段尽量温和,不然后面很难掰回来。
3. 预训练阶段的公式与模型结构:想清楚再动手
预训练阶段,你要盯住的不只是 loss 数字,更要理解 loss 到底在算什么。很多教程把模型训练写得像黑盒,但如果你想在训练异常时能快速定位问题,必须回归到公式层面思考。
3.1 交叉熵目标函数:语言模型到底在拟合什么
语言模型的预训练目标本质是最大化下一个 token 出现的对数概率,也就是最小化负对数似然。给定一个 token 序列 x,交叉熵损失可以写成:
L_CE = - (1 / N) * Σ_i log p_θ(x_i | x_<i)其中 N 是序列长度,x_<i 表示第 i 个 token 之前的所有 token。这意味着模型每一个位置都要预测下一个 token,而不是只在句子末尾预测一次。你可以把它理解为:每个 token 都是一个“小考试”,模型需要依次通过全部考试,才算真的理解了文本结构。
在实际训练中,我们通常会用 softmax 归一化输出概率:
p_i = exp(z_i / T) / Σ_j exp(z_j / T)这里的温度 T 对训练和推理是两个不同的概念。训练时 T 通常设为 1,因为我们要用真实分布来估计期望;推理时 T 可以小于 1,让输出更确定。我有一个常被新手忽略的提醒:不要为了提升模型质量而去训练阶段改 T,除非你在做蒸馏或者标签平滑,否则只会让概率分布扭曲,最终损害多样性。
3.2 主干结构的现代配置:RoPE、RMSNorm、GQA 与 MoE
R1 风格推理模型的主干结构,已经和当初第一代 Transformer 有不少差异。先说 RoPE,也就是旋转位置编码。它给每个位置信息注入到一个旋转矩阵里,核心表达式是:
f(x, m) = R(m * θ) * x其中 m 是位置,θ 按维度预先设定。RoPE 的好处是能把“相对位置关系”编码进注意力计算,模型天然理解 token 之间距离,这对长思维链特别重要。如果你发现模型在 4096 token 以后注意力崩坏,优先检查 RoPE 的 base 是否调大,而不是盲目加长训练。
RMSNorm 则是 LayerNorm 的简化版,它去掉了均值中心化,只保留尺度归一化:
RMSNorm(x) = x / sqrt(mean(x^2) + eps) * γ速度快、数值稳定,是现代大模型的标准配置。GQA,即分组查询注意力,通过在 KV 头上做分组共享来降低推理时的缓存开销。如果模型生成超长 CoT,KV Cache 会占用大量显存,GQA 在这种场景下几乎是必需品。MoE 则把 FFN 拆成多个专家,路由网络决定每个 token 激活哪些专家。总参数量变大,但每个 token 的激活参数量可控。缺点是路由分布容易失衡,需要额外监控。
3.3 训练稳定性的三个关键旋钮:峰值学习率、梯度裁剪与批大小
预训练最常见的失败是训练中途 loss 飞出天际。三个旋钮是我每次都会先检查的。第一个是峰值学习率。通常用 AdamW 优化器,小规模模型可以到 3e-4,到了 3B 以上建议降到 1e-4 附近。如果 loss 不降反升,先看 lr 是不是太高,而不是急着调数据。
第二个是梯度裁剪。我一般把全局梯度范数 clip 到 1.0,这能防止个别异常 token 产生巨大梯度,破坏整体参数更新。如果你发现 loss 曲线出现周期性尖峰,往往就是梯度更新步长过大引起的,适当调低 lr 比加大裁剪更有效。
第三个是全局批大小。批大小直接影响训练稳定性,也不是越大越好。batch 太小,loss 波动大,模型收敛慢;batch 太大,计算效率高但有效更新次数变少。我的经验是先用小 batch 跑通,再把 batch 做梯度累积扩大,同时保持 lr 同步微调。训练稳定性的本质是一个三角平衡,你动一个旋钮就要回头检查另外两个。
4. 冷启动与 SFT:让模型先学会“边想边说”
预训练之后的模型,可能能续写句子,但不会自动在你提问时输出逻辑完整的推理过程。冷启动 SFT 的目的,就是给模型一个“行为模板”。这个阶段数据量不用大,但格式必须非常统一。
4.1 一套可解析的 CoT 模板
R1 风格模型的一个标志性行为,是先把推理过程写出来,再给最终答案。为了让强化学习阶段的奖励函数能够稳定提取信息,CoT 模板必须严格可解析。我用过几种模板,最省心的是用成对标签把“思考”和“回答”分开:
<提问>… <思考> 1. 先明确目标 2. 列出相关公式 3. 带入条件求解 </思考> <回答> 答案是 A。 </回答>这里的标签名称并不重要,但一定要在全程使用同一套,不能一会儿 XML 一会儿纯文本。模板必须可以被一个简单的字符串匹配代码解析,保证强化学习阶段能把思考部分和最终答案分开,分别检查格式和正确性。冷启动阶段,模型还没有能力自动产生复杂推理,你只需要让它习惯“输出思考再输出答案”这种顺序。一旦顺序稳定了,后面再引入更复杂的推理策略才有基础。
4.2 SFT 损失、数据配比与标签屏蔽
SFT 的损失函数仍然是交叉熵,但不是对所有 token 一视同仁。实际训练时,我会把用户输入部分的 loss 屏蔽掉,只计算模型输出部分的 loss,也就是对<思考>和<回答>之后的内容计算交叉熵。这样做的逻辑是:模型不需要去“背”用户的提问,它只负责学会如何生成好的输出。如果你不屏蔽输入部分的 loss,训练会被一些高频无意义 token 主导,反而稀释了输出质量。
数据配比上,冷启动集不是越大越好。我的基准线是 5000 条高质量数据起步,上不封顶到 3 万条左右。关键在于难度分布要成阶梯状:简单题占 40%,中等题占 40%,难题占 20%。如果全是难题,模型会因频繁失败而学到“思考也没用”的消极模式;如果全是简单题,模型学不到真正的推理。这个比例我调过很多次,最终发现阶梯式分布最稳。
4.3 为什么冷启动阶段要紧紧盯住输出长度
冷启动阶段最常见的失控现象是 CoT 长度膨胀。模型发现写长推理可以降低 loss,于是开始重复套话、堆砌无意义过渡句。这是奖励黑客的前身,在 SFT 阶段就埋下了种子。
我有一个硬性处理策略:冷启动数据里,每条 CoT 控制在 128 到 512 token 之间。太长的推理链看似详尽,实则会把训练数据中的问题模式和答案解耦,模型只能学到“写得长”,学不到“推得准”。太短的又不足以覆盖复杂题目的推导步骤。我会专门统计 CoT token 长度的分布,一旦发现数据的平均长度超出预期,就回去精简标注模板,而不是直接让模型在 RL 阶段自由生长。先学走再学跑,这是冷启动存在的意义。
5. 强化学习:从“会模仿”到“会试错”
冷启动结束后,模型已经知道要输出思考过程了,但它的推理能力上限,基本还停留在训练集里见过的题。要突破上限,必须让模型在真实反馈中不断试错。强化学习阶段就是把这件“试错”变成可计算的事。
5.1 为什么选择可验证奖励而不是人类偏好
数学题、代码题这类型的任务,天然适合规则验证:答案对就是对,代码能编译就是能编译。用规则验证做奖励,反馈是即时且可复现的,几乎不需要人工标注成本。偏好排序的强化人反馈虽然也能提升主观回答质量,但在推理任务里成本高、噪声大,还容易出现排序不一致的问题。
我实际使用的验证器分两层。第一层是最终答案验证,从输出里正则匹配最终答案,再和标准答案做严格比对。第二层是代码执行验证,让模型生成代码后,在一个沙盒环境里跑给定测试用例,通过数量作为奖励依据。两层验证器的设计越严格,模型越难钻空子。
这里特别提醒一点:不要只奖励“答对”,也要为“合理过程”保留一小部分奖励。否则模型会跳过推理步骤直接猜答案,猜对了就拿满分,这对长期泛化非常不利。我通常把过程质量作为一个温和的稀疏奖励项,比如只有最终答案正确时才给额外奖励。
5.2 GRPO 公式拆解:取消评论家网络之后的奖励设计
R1 风格模型训练中常用到的强化学习算法是 GRPO。它和 PPO 的最大区别在于,去掉了额外的评论家价值模型,直接用一组采样结果构造相对优势。
假设对同一个问题 q,策略模型采样 G 个输出 o_1 到 o_G,每个输出有一个奖励 r_i。先计算组内的相对优势:
A_i = (r_i - mean(r_1...r_G)) / std(r_1...r_G)直观理解:一组输出中,比平均奖励高的样本获得正向优势,低的获得负向优势。于是模型被鼓励去模仿本组中表现更好的推理路径,远离表现差的路径。这里不需要单独的价值网络估计未来收益,省了显存和训练开销。
对应的策略梯度损失可以写成:
ρ_t,i = π_new(o_t | q, o_<t) / π_old(o_t | q, o_<t) L = - E[ 1/G Σ_i 1/|o_i| Σ_t min(ρ_t,i * A_i, clip(ρ_t,i, 1-ε, 1+ε) * A_i) ]外层还有一项对参考模型的 KL 散度惩罚,抑制模型过快偏离初始策略。这里1/|o_i|非常关键,它把每个轨迹按长度归一化。加了这个归一化之后,较长的 CoT 不会因为 token 数多而天然获得更大梯度,这能有效抑制长度膨胀。我第一次跑 GRPO 时没有加长度归一化,结果模型奖励确实涨了,但 CoT 长度从 200 token 涨到 2000 token,明显是投机取巧。
5.3 格式奖励、最终答案解析器与训练循环的配合
强化学习阶段要同时处理“写得规范”和“答得正确”两个目标。我的做法是两个奖励正交叠加:格式奖励判断输出是否包含完整的思考段和答案段,给一个 0 或 1 的回报;答案正确性奖励交给验证器,答对给 1,答错给 0。总奖励就是加权求和。
训练循环大致是这样的:采样一组问题,对每个问题用当前策略模型采样多个输出,跑一次规则验证器,算出奖励,再按 GRPO 更新策略。这个循环看似简单,但解析器必须和模板保持完全一致。我踩过一个典型的坑:模型为了打格式分的偏置,逐渐把思考段写成一堆无意义重复文本,比如循环输出“让我想想”50 次。解析器完全没拦住,因为它只看到了标签齐全。
应对方法是在解析器里加入长度上下限检查,对思考段过短或过长的情况直接判不合格,不给格式分。同时,随机抽几批输出让真人看一遍,肉眼比图表更能提前发现模型在“努力钻空子”。这部分不可自动化,至少早期得人工盯住。
5.4 奖励黑客的第一张预警图:长度膨胀
长度膨胀是奖励黑客里最典型、最容易可视化发现的一类。训练早期我画了三条曲线:平均输出长度、平均奖励、KL 散度。结果第几百步开始,模型平均奖励稳步上涨,平均输出长度开始陡增,从 400 token 一路爬到 1600 token。
这个现象背后的逻辑很清楚:模型发现只要不停“包装”,就有更大概率恰好把正确答案留在某个位置,从而命中奖励。如果响应长度没有参与损失归一化,模型自然会越来越啰嗦。后来我把长度归一化加上,并给过长的输出一个轻微惩罚,曲线才恢复正常。
可视化在这里不是装饰,而是救命工具。如果一个指标单独变化你可以忽略,但奖励上涨和长度上涨同时出现,必须立刻拉响警报。
6. 训练可视化:把模型的一举一动摆在眼前
我在整个训练过程中最依赖的,不是某个单一指标,而是一组曲线的组合判断。单独看 loss 只能知道有没有收敛,但推理模型训练里的许多问题,需要在多张图联合解读时才会暴露。
6.1 值得盯住的六条曲线
我习惯盯六条曲线。第一条是训练 loss,看整体收敛。第二条是格式奖励均值,看模型有没有学会输出结构化推理。第三条是答案奖励均值,这个数最能反映模型真实推理能力,如果一直低于 0.3,问题大概率出在数据或奖励设计。第四条是输出平均长度,观察是否出现长度膨胀。第五条是策略模型与参考模型之间的 KL 散度,如果这个值持续快速上升,代表策略已经偏离初始模型太远,很容易失去多样性。第六条是梯度范数,配合 loss 尖峰做诊断。
对于 MoE 结构,我还会额外看一个路由均匀度指标。如果某些专家被持续过度使用,而另一些专家几乎不参与计算,说明路由参数可能没学好,导致 MoE 退化成 Dense 模型。这个问题只出现在可视化里,单看 loss 完全无法发现。
6.2 健康训练与不健康训练的曲线组合
多读图最好的方式是先建立“什么算健康”的参照系。下面这组对比我整理过很多次,直接贴出来:
| 场景 | 健康特征 | 危险特征 |
|---|---|---|
| 预训练 loss | 平稳下降,偶有小波动 | 周期性尖峰或长期不降 |
| 格式奖励 | 短时间内升到接近 1 | 忽高忽低,超过半数输出不可解析 |
| 答案奖励 | 稳定上升,波动不大 | 奖励上涨但输出长度同步飙升 |
| KL 散度 | 缓慢增长并渐近 | 直线上升且不回头 |
| 梯度范数 | 保持在同一个量级 | 突然放大 10 倍以上 |
| 路由均匀度 | 专家负载在 0.8-1.2 区间 | 头部专家负载超过 3 |
表格只是参照,不是标准。比如 KL 散度在早期快速上升一段再回落,也可能是正常探索。怕的不是单一异常,而是多个异常叠加出现。举个例子,答案奖励上涨 + KL 飙升 + 输出长度猛增,三者同时出现时,几乎可以肯定模型找到了某种短平快的奖励捷径。
6.3 用可视化叫停一次注定失败的一次训练
我在模拟项目 X 里做过一次预判失败的操作。训练到大约 3000 步时,格式奖励和答案奖励都在上涨,看起来一切正常。但我注意到一条隐藏曲线:思考段平均 token 数在急速上涨,已经超过训练数据上限。继续跑下去,模型会越来越倾向于用“循环废话”填充思考段,而不是真正推导。我没有等它自己崩,直接暂停训练,把奖励函数里加了一个思考段长度惩罚,并把上限压缩回 512 token,再从两千步的检查点重启。
那次改动只花了一个小时,但至少省了两天无效训练。如果不开可视化,我可能要等最终评测时才发现问题,那会儿回收成本的代价就大了。所以我的建议是:训练代码写一半的时候,先把监控代码写好,曲线越全越好。这比任何花哨的分布式技巧都更能决定项目成败。
7. 低成本复现的七条实战心得
最后这部分不写理论,纯是操作性经验。如果你的显卡资源有限,又想把 R1 风格推理模型跑通,下面七条是我真实踩出来的路。
7.1 别急着上大模型
一开始就定 7B 参数然后跑全量推理数据的,大多会死在显存和等待上。我的做法是先用 0.5B 到 1B 规模的模型,配一个几千条的数学子集,跑通完整的“预训练微调强化学习”闭环。模型虽小,但你能以小时级频率看到行为变化,迭代速度远远超过大模型。等所有链路稳定了,再放大到 3B 或者 7B 也不迟。
7.2 分布式训练里最容易忽略的三处配置
第一处是随机种子。每个进程的种子不一致,数据加载顺序就乱,loss 曲线会抖动到没法看。第二处是模型转换时的参数分片顺序,FSDP 或张量并行的分片策略必须一致,否则会出现诡异的 loss 尖峰。第三处是通信后端的选择,不同结算卡和网络的适配度差别很大,该用 GPU 通信加速的地方不要图省事用通用通信协议。
7.3 尽早写进训练循环的四个监控回调
第一个是检查点可恢复回调,每 N 步保存一份完整状态,包含优化器参数和随机数种子。第二个是梯度范数日志,配合 loss 一起看才能判断异常来源。第三个是推理样本输出回调,每隔一段打印几条真实问答输出,不能只看指标,要亲眼看到模型在说什么。第四个是验证器汇率回调,统计奖励验证器的解析失败率,如果解析失败率暴涨,说明模板失配了,这比答案奖励下降出现得更早。
7.4 这次复现中最痛的教训:KL 系数和奖励数值的“单位问题”
如果说只能保留一条经验,我就留这一条。奖励函数的值和 KL 散度的大小往往不在同一个量纲。比如答案奖励用的是 0 和 1,格式奖励也是 0 和 1,而 KL 散度可能只有 0.01 到 0.1 这个量级。如果你随手把 KL 系数设成 0.1,那么 KL 惩罚项会远大于可验证奖励,模型根本不敢偏离参考策略,强化学习效果接近零。
反过来,把 KL 系数设成 0.001,KL 惩罚过弱,模型可能迅速忘记自己学会的基本格式,输出变得天马行空。我最后悔的一次训练就是没检查这个量级,跑了一整天才发现模型只是在一个很小的策略邻域里轻微晃动。正确做法是先跑一个小批量训练,打印出奖励均值与 KL 散度均值,再反推系数。比如奖励均值是 0.6,KL 均值是 0.02,那 KL 系数设成 0.01 就相对平衡,确保 KL 惩罚占整体损失的比重在一两成上下。
说实话,这次从头复现的收获,比我用现成工具调参要大得多。你不一定需要一个超大集群才能开始,先从一个能正常打印日志的小模型起步,把数据和奖励想透,再谈规模。如果你正在做类似的事,希望上面这些经验能帮你少走几段弯路。