1. 为什么选择自底向上:AI工程最值得走的一段弯路
这两年“AI工程师”变成了一个相当抢手的岗位名,LinkedIn和招聘网站上到处挂着相关职位,市面上也出现了大量“三天上手LangChain”“一周搞定RAG应用”的速成课。我的建议是:如果你想长期吃这碗饭,尤其想理解最近那些推理模型(reasoning model)到底是怎么工作的,不妨走一条看起来更慢的路——从零开始构建一个迷你大语言模型。
我说的“ai-engineering-from-scratch”不是一个营销概念,而是一条真实的学习路线:亲手准备数据、实现分词器、搭建Transformer骨干网络、跑通预训练、观察损失曲线、再往后训练阶段补上思维链和偏好优化。这个过程看起来像是在“重复造轮子”,但实际上它解决的是当前AI工程里最普遍的问题——大部分人对模型的黑盒认知。
我自己带过的不少新人,简历里写着“熟悉Transformer”,但你问他“为什么GPT类的模型要分多头注意力,head数量改大会发生什么”,他答不上来。这不能全怪他们,因为市面上主流框架把一切都封装得太好了:调用API三行代码出结果,微调脚本clone下来填几个路径就能跑。这些工具让“工程”变得太容易,反而让真正重要的判断力没有地方生长。判断力恰好是AI工程和普通后端开发最不一样的地方:你需要知道一个训练任务失败是因为数据太脏、学习率过高、还是模型容量不够;你需要知道某个开源模型能不能在你的业务场景里继续训练;你需要知道评测指标明明漂亮,上线之后为什么表现稀烂。这些判断,只能来自对底层机制的实感。
我自己入门时的转折点,是跟着一本讲“从零构建大语言模型”的书把代码逐行敲了一遍。当我看到自己用几十行代码实现的“下一个词预测”真的在本地跑出一段连贯文本时,那种感觉跟直接调用GPT-4 API完全不同。你能意识到所谓“智能”不过是一套经过精心调教的统计系统,你会开始尊重数据、尊重每一处工程细节,同时也再也不会被各种营销话术忽悠。
所以这篇东西,我想完整记录一下这条“从零开始”路线上的关键节点:环境怎么配、数据怎么处理、模型怎么搭、训练怎么调、以及从普通大模型走向推理模型的那几步。同时我也会把踩过的坑、花过的冤枉钱、反复试错才明白的道理一并写出来。如果你正打算系统学习AI工程,或者已经在做相关应用但总觉得缺一块底层拼图,这篇文章应该能给你一张比较真实的地图。
2. 一台够用的机器:本地环境的最简配置与取舍
2.1 别迷信显卡堆料,关键在于任务量级
很多人在“从零开始训练模型”这件事上的第一道坎是心理门槛——觉得必须有好几张A100才能动手。实际上,如果你目标只是把原理跑通、把训练流程走完,一张消费级显卡就能做很多事。我用过的两套配置可以给你参考:
| 使用场景 | 我用的环境 | 大概能做的事 |
|---|---|---|
| 纯学习、跑通代码 | MacBook M1 Pro(16GB) | 训练参数量千万级的小模型,跑通前向/反向流程,做数据实验 |
| 认真做小型预训练 | 一张RTX 3060 12GB或4060Ti 16GB | 训练参数量亿级左右的模型(约1-2亿参数),上下文长度512,跑完整流程 |
| 想玩更大一点的 | 云GPU按需租用(如A10、L40S) | 做几亿到十几亿参数的小规模预训练 |
如果你的机器只有一张消费级显卡,就老老实实把训练任务控制在“能吃饱”的范围。比如1.24亿参数(大约是GPT-2 Medium级别的1/3)的模型,在1080p输入长度、批量大小8~16的情况下,12GB显存是完全能跑起来的。训练步数可以缩减,数据规模控制在几亿token以内。这样既能看到完整的训练过程,又不会被漫长的等待劝退。
我见过一些人上来就想复现13B模型,结果训练到第10天发现数据里有大量重复和错乱内容,不得不全部重来。这是最典型的“配置幻觉”——卡越多,越容易忽视数据质量,反而不利于养成严谨的工程习惯。在自己资源受限的环境里先把问题做小、做透,收益远大于直接上大集群跑“轰隆”实验。
2.2 软件环境:Python、框架与依赖的实用版本
软件部分我建议用最主流、最不折腾的组合:
- Python:3.10或3.11。太老的版本在新库支持上有问题,太新的版本偶尔会遇到部分底层库还没有预编译wheel的尴尬。
- PyTorch:2.x以上,安装时直接去官方管网选对应CUDA版本。Windows上特别注意CUDA版本的对应关系,最容易翻车的就是装了最新CUDA却用了旧版PyTorch。
- Tokenizers / Datasets:Hugging Face旗下的这两个库是工业标配。
- 实验管理:如果你不追求复杂体系,直接用一个包含训练日志、损失记录和模型检查点的字典目录就够了。
这里要强调一点:你在很多教程里能看到用transformers库几行代码就加载好模型,但“从零构建”的意义在于你要自己写模型定义。所以即便安装了transformers,也只建议把它当作参照物(用来对照自己的实现是否正确),所有nn.Module都应该亲手编写。我的习惯是先把GPTModel类写成纯PyTorch代码,再在单元测试里用transformers的GPT2Config做权重加载比对。这一步通过之后,你对模型结构的理解才算是真过关了。
2.3 云GPU真香,但要提前算清成本
本地卡不够用的时候,租云GPU是常见方案。我的建议是:准备阶段用本地CPU/小卡调试代码,确认逻辑没问题之后再上云,千万别在排错阶段烧云GPU的钱。我吃过一次亏,图省事直接在云实例上调代码,试错跑了好几轮,一天下来账单直接让人“肉疼”。后面我养成一个习惯:所有训练代码先在本地GPU上一轮小batch(甚至直接用CPU跑几step)验证通过,再转云上跑正式实验。
租到的机器也别急着“一键开始训练”。先跑nvidia-smi确认驱动正常,再看一下torch.cuda.get_device_name()是不是你租的那张卡,最后用一个小数据跑100步,把基线时间测出来。云GPU厂商提供的镜像有时预装一堆用不上的库,环境“干净度”直接影响你排障的效率,这一点别偷懒。
3. 数据与分词器:比模型更影响结果的基础设施
3.1 先下结论:数据工程的时间占比应该到一半以上
如果你让我估算一个从零开始的LLM项目的时间分配,我会说:数据清洗和构建占50%,模型设计和调参占20%,训练调试占30%。这跟很多新手直觉相反——他们以为模型架构才是重头戏。
原因很简单:Transformer架构本身已经高度成熟,说白了就是一套通用函数逼近器,你很难在架构层面获得颠覆性超越。但你喂给它的数据,直接决定了它能学到什么、学得干净不干净。垃圾进垃圾出,在下游评测里会体现得非常具体。
我第一次做小型预训练时用的数据集是从一个开源语料里抽出来的几千万条文本,本想着直接丢进去训练就行。结果训练到中后期,我发现损失值降不下去,抽样生成的文本里出现大量重复的“××××欢迎阅读×××”式内容。排查了很久才定位到问题是语料里有大量嵌套了模板噪音的网页正文——同一个新闻在不同URL下被采集了几十次,而且乱码与广告残留严重。说是做模型,实际上是在给搜索引擎的爬虫擦屁股。
3.2 到底哪来的数据:公开语料的获取与清洗
以中文场景为例,可获取的数据源通常分为几类:
- 通用爬虫语料:类似Common Crawl的中文子集,量大但噪音多。
- 高质量长文:维基百科、知乎高赞回答、GitHub代码库、论文摘要等,质量高但规模小。
- 领域语料:新闻稿、法律文书、医学知识库等,需要版权评估。
- 合成数据:用已有模型生成对话或推理链,再经过筛选作为训练数据。
清洗的步骤我基本会做这几轮:
- 格式去重:对长文本做MinHash指纹识别,去掉重复和近重复内容。
- 规则过滤器:用正则去除HTML标签、URL、连续无意义标点,过滤掉过短或过长的文本(例如低于50字符的文本通常没有上下文价值,超过一定长度的整页文档可能包含版权风险内容)。
- 质量打分器:用一些启发式规则(标点密度、词语重复率、语言识别置信度)给文档打分排序。更高端的做法是用一个小型分类器判断文档是否“像人写的”。
- 安全过滤:去除特定敏感内容、PII信息(证件号、手机号等),这一步不只是合规要求,也能避免模型学到无意义的乱码输出。
这个流程建议写成流水线脚本,每天跑一遍,每次输出一个带版本号的数据集目录。数据集的版本管理做不好,后续实验对比就是一团乱麻。我见过有人从旧数据集训练出的模型跟新基线比效果反而变差,查了半天才意识到是两个不同版本的数据混了。
3.3 分词器:最容易忽略,却最直接影响“模型视野”
分词器(Tokenizers)做的事情,是把原始文本切成模型能处理的离散符号序列。你可能会觉得“分词有什么难的,按空格切不就行了?”但中文和很多自然语言并没有清晰的空格边界,英文又有形态变化,同一单词的不同时态、复数形式要不要切开?标点怎么处理?数字怎么处理?这些决策直接影响模型的“词汇表”大小和序列长度利用率。
现代LLM使用得最多的方案是BPE(Byte Pair Encoding)或其变体WordPiece、Unigram。核心逻辑一句话:从字符集开始,不断合并出现频率最高的相邻符号对,直到达到预设的词表大小。举例来说,如果“中国”这两个字在语料里频繁相邻,分词器学习过程中就会把它合并成一个token;但如果“中”和“国”经常分开出现,它就保留为两个token。
用Hugging Face的tokenizers库训练一个自己的BPE分词器非常简单:
from tokenizers import Tokenizer from tokenizers.models import BPE from tokenizers.trainers import BpeTrainer from tokenizers.pre_tokenizers import ByteLevel tokenizer = Tokenizer(BPE(unk_token="[UNK]")) tokenizer.pre_tokenizer = ByteLevel() trainer = BpeTrainer(vocab_size=30000, min_frequency=2, special_tokens=["[PAD]", "[BOS]", "[EOS]", "[UNK]"]) files = ["data/corpus_1.txt", "data/corpus_2.txt"] # 按需传入语料文件 tokenizer.train(files, trainer) tokenizer.save("tokenizer/tokenizer.json")注意代码里vocab_size的选择。太小(比如几千)会导致信息密度低,一句话要用很多token,浪费序列长度;太大(比如50万)会让embedding矩阵变得巨大,小模型根本学不动,同时推理时的softmax计算也会变慢。以一个小型GPT模型(1亿参数级别)为例,词表选3万~5万是比较平衡的区间。英文场景可以更小(字节级BPE本身上限有限),中文场景建议稍大一些。
还有一个细节:训练分词器的语料需要和预训练语料尽量同分布。如果你用维基百科语料训练了分词器,转头又拿一推代码语料去预训练模型,代码里那些特殊符号会被生硬地拆成七零八落的片段,模型等于瞎着眼睛学。这属于“数据-分词器错配”,也是很多复现项目静默失败的原因之一。
3.4 预训练数据的最优配比:别只堆量,要讲构成
很多人以为数据越多越好,但实际上不同来源的文本价值不同。我现在做数据配比时会遵循一个简单原则:核心通用语料保证语言流畅度(40%~50%),开放造诣类知识语料保证信息密度(20%~30%),代码和数学类语料保证逻辑能力(15%~20%),剩余部分留给对话指令与特殊格式(5%~10%)。这个比例不是一个绝对真理,但至少能让模型的语言能力不至于太偏科。
值得特意提醒的是:代码和数学语料虽然比例不高,但对后续做推理模型极其重要。你后面想让模型具备“思维链”能力,前提是基础模型里已经有一定规模的逻辑推导数据作为“种子”。如果基础模型纯粹练的是“分享生活日常”,那后续再怎么用RLHF训练,也很难指望它学会严谨的数学推理。
4. 亲手重建GPT架构:注意力、层归一化与残差连接的落地顺序
4.1 从一张结构图到可运行的代码之间,隔着一大堆细节
很多人看Transformer论文和架构图时觉得自己懂了:多头注意力、前馈网络、残差连接、层归一化,听起来都有概念。真到自己写代码才会发现,位置编码怎么加、为什么GPT用绝对位置嵌入而不用三角函数、多头注意力各个头的维度怎么切、最后一个LayerNorm放在哪里……每一点都影响着最终效果甚至代码能不能跑通。
我强烈建议你按顺序实现以下模块,每实现一个都做一次单元级验证:
- 嵌入层与位置嵌入:一个
nn.Embedding做token映射,另一个nn.Embedding做位置映射,两者相加得到输入表示。 - 多头自注意力:这一步最核心。要点是先用一个
Linear投影出Q、K、V,然后切成多个头分别计算缩放点积注意力,最后把多头结果拼接回去再过一层Linear。 - 前馈网络:经典配方是“升维再降维”——把隐藏维度放大4倍过ReLU或GELU激活,再降回来。
- 层归一化与残差连接:GPT类模型通常采用“先归一化再子层(Pre-LN)”的顺序,训练比原始Transformer更稳定。
- 输出头与损失函数:把最后一层的表示投影到词表维度,计算交叉熵损失。
4.2 多头注意力的一个简洁实现
这里给一个非常贴近图解结构的PyTorch实现片段,我建议你逐行读,并把维度注释列出来:
import torch import torch.nn as nn class MultiHeadAttention(nn.Module): def __init__(self, embed_dim, num_heads, dropout=0.1): super().__init__() assert embed_dim % num_heads == 0, "embed_dim必须能被num_heads整除" self.embed_dim = embed_dim self.num_heads = num_heads self.head_dim = embed_dim // num_heads self.q_proj = nn.Linear(embed_dim, embed_dim) self.k_proj = nn.Linear(embed_dim, embed_dim) self.v_proj = nn.Linear(embed_dim, embed_dim) self.out_proj = nn.Linear(embed_dim, embed_dim) self.dropout = nn.Dropout(dropout) def forward(self, x, mask=None): batch_size, seq_len, _ = x.shape # 投影并拆分为多头 q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) k = self.k_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) v = self.v_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2) # 注意力分数:Q @ K^T / sqrt(d_head) scores = torch.matmul(q, k.transpose(-2, -1)) / (self.head_dim ** 0.5) if mask is not None: scores = scores.masked_fill(mask == 0, float("-inf")) attn_weights = torch.softmax(scores, dim=-1) attn_weights = self.dropout(attn_weights) # 加权求和 context = torch.matmul(attn_weights, v) # [batch, heads, seq_len, head_dim] # 拼接多头结果 context = context.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim) return self.out_proj(context)很多人第一次写完这个类会困惑:mask是什么?什么时候用?GPT类模型做自回归生成时,当前位置只能看到它之前的token,不能回头看到未来的token。所以需要生成一个右上三角为0的矩阵,让注意力在计算时将未来位置“屏蔽”掉。这点极其关键:如果你忘了加因果掩码,模型就在“偷看未来”,训练损失会非常低,但生成时完全不能用。这属于那种“训练时幸运、推断时翻车”的经典事故。
4.3 层归一化与残差连接:顺序错了,训练直接崩
我在第一次复现时犯过一个错:把LayerNorm放在了残差相加之后,也就是“Post-LN”的写法。在小模型上跑起来没有什么明显问题,但把模型层数加到12层以上后,训练变得异常不稳定:损失在前期骤降之后立刻发散,偶尔还出现NaN。查过资料才发现,现代GPT实现基本默认使用Pre-LN——
class TransformerBlock(nn.Module): def __init__(self, embed_dim, num_heads, ff_dim, dropout=0.1): super().__init__() self.ln1 = nn.LayerNorm(embed_dim) self.attn = MultiHeadAttention(embed_dim, num_heads, dropout) self.ln2 = nn.LayerNorm(embed_dim) self.ff = nn.Sequential( nn.Linear(embed_dim, ff_dim), nn.GELU(), nn.Linear(ff_dim, embed_dim), nn.Dropout(dropout) ) def forward(self, x, mask=None): # Pre-LN:先归一化,再计算子层,最后残差连接 x = x + self.attn(self.ln1(x), mask) x = x + self.ff(self.ln2(x)) return x这一小段里面包含了残差连接的灵魂:每一层的输入都被“加”回输出。如果没有残差连接,几十层的梯度根本无法稳定传播;有了它,深层网络哪怕某些子层暂时失效也不至于让训练中断。为什么要先归一化再进子层?因为归一化会把输入拉回到均值为0、方差为1的标准分布,让后续变换更容易收敛。这个设计在深层网络中几乎成了标配。
4.4 小模型练手:应该复现多大的规模?
说到“从零构建”,很多教程喜欢一上来就让你复现1700亿参数的GPT-3,但那是没有工程意义的空谈。我建议按照“一个下午能跑完一轮小预训练”的规模来练手:
| 参数规模 | 层数 | 隐藏维度 | 注意力头数 | 参数量(约) | 单卡训练时间 |
|---|---|---|---|---|---|
| 迷你版 | 4 | 128 | 4 | 约500万 | 几十分钟CPU可跑 |
| 小号 | 6 | 256 | 8 | 约1500万 | 1~2小时(消费级GPU) |
| 中小号 | 12 | 384 | 12 | 约4500万 | 半天(消费级GPU) |
| 进阶级 | 12 | 768 | 12 | 约1.2亿 | 1~2天(消费级GPU) |
对大多数学习者来说,从“迷你版”起步是最理性的选择。先把代码正确性验证通过,再逐步增加层数和数据量。很多人犯的错误是:一开始就把模型配得很大,结果训练时间过长,一次实验就要跑一整天,根本无法快速迭代。把单次实验控制在30分钟以内,你才有条件做有效的对比实验,才谈得上“调参”。
5. 训练曲线不会说谎:损失、学习率与常见陷阱
5.1 从训练日志里读出的信息,比想象中多得多
训练过程绝不是“把数据丢进去,等着出结果”那么简单。每一步loss的变化都在向你反馈模型的健康状况。我习惯记录这样几项指标:
step_loss:当前step的loss,波动大是正常的。smooth_loss:滑动平均后的loss,这是观察趋势的主视角。lr:当前学习率,用于确认学习率策略是否按计划执行。tokens_per_sec:吞吐量,判断机器性能是否正常。grad_norm:梯度范数,异常值直接指向训练稳定性问题。
训练开始时,loss会从初始值(大概是词表大小的自然对数,比如词表3万,初始loss大约10.3附近)快速下降,然后进入缓慢下降的“长尾”阶段。看到loss在5000步左右还稳得像条直线,很多人会焦虑是不是模型学不动了。其实这种缓慢其实很正常——从能说通顺的话到能讲完整知识,需要的步数是指数级增长的。
5.2 学习率策略:Warmup和余弦退火背后的原因
大部分现代LLM训练采用“先线性预热,再余弦衰减”的学习率曲线。为什么需要热身?因为模型刚初始化时参数还很“脆弱”,如果直接上大学习率,很容易把参数推向数值不稳定区域,表现为loss瞬间发散或出现NaN。先用小学习率“慢跑”几千步,把参数的分布稳定下来,再逐步提高到目标学习率,这是主流做法。
我的一个简单配置示例:
import math def lr_schedule(step, warmup_steps, total_steps, peak_lr): if step < warmup_steps: # 线性预热 return peak_lr * (step + 1) / warmup_steps else: # 余弦退火 progress = (step - warmup_steps) / max(total_steps - warmup_steps, 1) return peak_lr * 0.5 * (1 + math.cos(math.pi * progress))经验值上,1亿参数左右的模型,peak_lr取3e-4到1e-3之间。如果你数据量不大、训练步数不长,峰值学习率可以适当降低。另外一个容易踩的坑是批大小与学习率需要联动:如果你把batch size从8增加到32,相当于每一步的梯度估计更稳了,那学习率也可以相应放大一些,否则收敛速度会变慢。反过来,如果你的显存只允许小batch,学习率还设得很大,loss大概率会在一个区间来回震荡,怎么都降不下去。
5.3 训练中的三种典型翻车场景
这里总结我亲身踩过的三类坑,每个都有具体的表象和解决办法:
翻车一:loss变成NaN表象:某一步开始loss变成nan,之后彻底救不回来。 排查顺序:先查数据里有没有NaN(动手写个脚本扫一遍文本嵌入向量的min/max)。再查学习率是不是太大,把学习率降到原本的1/10试试。如果数据和学习率都没问题,去查梯度范数——在反向传播后打印grad_norm,如果它在一两步内暴涨到几千,大概率是某个注意力分数溢出到极大值导致的。解决办法一般是加梯度裁剪:torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)。
翻车二:loss很低,但生成质量一塌糊涂表象:训练loss已经降到2.0以下,模型生成的文本仍然前言不搭后语。 这种情况通常是数据质量出问题了:可能是你有大量重复文本,模型记住了复制粘贴的模板但没有学习到语义建模。也可能是在分词阶段把标点符号全部过滤掉,导致模型失去了对句子边界的感知。回到第3节,把数据流水线的中间产物拿出来看几眼,往往能发现问题。
翻车三:训练loss一直在降,验证loss却涨了表象:训练集loss稳步下降,验证集loss不降反升。 这是过拟合的典型信号,但跟深度学习教科书里那种过拟合不同,LLM在小规模数据上过拟合非常快。解决思路很简单:要么增加数据量(最根本的办法),要么增大dropout比例,要么缩短训练步数。这里不要指望用早停去“硬撑”,回头去补数据才是正路。
5.4 评估:别只盯着loss一个数字
Loss下降并不代表模型“好用”。我见过不少项目训练loss降得很漂亮,但实际生成的文本让人哭笑不得。建议在小规模训练结束后,至少做几组“穷人版”评估:
- 让模型生成10段文本,人工检查流畅度、主题一致性、重复率。
- 给定几个完形填空式的句子(如“中国的首都是____”),检查模型是否能填出正确内容。
- 用一组固定prompt跑一批对比生成,把模型训练前后的输出并排摆在一起,直观感受变化。
对于追求严谨的人来说,可以跑公开benchmark,但用小模型跑perplexity和生成样例已经能筛掉大部分方向性错误,省时省力。
6. 从基础模型走向推理模型:后训练与思维链的演进路线
6.1 基础模型和经验直觉之间的鸿沟
预训练完毕的基础模型(Base Model)其实相当“叛逆”:它只会继续文本,不会遵循人类指令,更不会说“让我一步一步思考”。要从一个“文本预测机”变成真正可用的助手甚至推理模型,关键的差距在于后训练(Post-training)阶段。这也是去年以来AI工程领域最值得关注的变化之一——公开的推理模型之所以“能思考”,核心不是基础预训练时加了什么魔法,而是后训练阶段标定出来的能力。
先理解一下链条:预训练 → SFT(有监督微调) → 偏好对齐(RLHF/DPO等) → (可选)推理强化。每走一步,模型的行为模式会发生明显变化。特别是最后一步,也就是让模型学会在输出正式答案之前先输出一段“推理过程”(思维链),这通常需要专门的推理数据训练和强化学习优化。
6.2 数据形态的设计:为什么要让模型“吐出推理过程”
推理模型(reasoning model)最有辨识度的特点是:它会先输出一段类似草稿的思维链(CoT),再给最终答案。为什么这会有效?因为许多复杂的数学、逻辑和编程问题,人类一步到位写也容易错,但若强迫自己做中间推导,正确率会显著提升。对语言模型,推理过程实际上把“复杂问题拆解”变成了“多个简单token的连续预测”,降低了每一步的预测难度。
后训练阶段做推理数据时,最经典的一个操作是“让大模型为已有答案补一句解释”。比如你的训练集里有数学题“甲乙两地相距240公里,货车从甲地以60km/h开往乙地,多久到达?答案:4小时”。直接把“答案:4小时”喂给模型,它也能学,但没有推理能力的泛化效果。如果改成“路程=速度×时间,所以时间=240÷60=4(小时),因此答案是4小时”,模型就学会了一种可迁移的解题模式。到新的题目上,它能照着同样的思路去推演。
6.3 从SFT到RLHF再到GRPO:一小段通俗拆解
SFT阶段:拿一批高质量“问题-答案”(或“问题-推理链-答案”)对数据去微调模型。这个阶段解决的问题是让模型“学会回答”,代价是模型可能只会照搬格式,不会主动探索更优解。
RLHF阶段(或DPO等偏好优化):收集人类对多个模型回答的排序偏好,训练一个奖励模型,再通过强化学习优化策略模型,让模型更倾向于生成被人类偏好的输出。这个阶段解决的问题是“回答得好不好”。实际工程中,RLHF的经典实现PPO非常耗时,200行代码的内容涉及大量采样和更新循环,网上有无数坑。
GRPO/新式优化:与其训练独立奖励模型,不如让策略模型在一组候选回答上自己估算相对优势。它对显存和工程复杂度更友好,现在很多推理模型的后训练都往这个方向走。具体做法是:对同一个问题采样多个回答,用规则(比如答案是否正确)或一个通用奖励函数给每个回答打分,然后基于组内分数的相对大小做优化。这套逻辑如果配上数学/代码这类可规则校验的任务,效果立竿见影——因为不需要昂贵的人工偏好标注,奖励信号直接来自“答案对错”。
6.4 一个小型推理模型的“复刻”路线图
如果你手头已经有一个基础模型,想往推理方向做后训练,我建议按以下路线走:
- 构建推理数据集:从开源数据集(数学题、代码评测、逻辑谜题)中筛选几千到几万条,把每个问题附上人工或大模型生成的推理链与最终答案。
- SFT:用这批数据做2~3个epoch的有监督微调。你可以对比微调前后模型在
GSM8K这类数学测试上的表现,提升往往已经很明显。 - 做一组候选答案采样:写一个脚本对每个测试问题采样N个答案(例如N=8),用规则判定对错,记录正确率。这一步的主要目的是收集后训练所需的偏好信号。
- 偏好优化:用GRPO或DPO对模型进行一轮优化。如果你不太熟悉这两者,推荐先看
trl库里的DPO实现,代码量不大,能在小模型上直接跑通。
整个过程不复杂,但会明显让你更深地理解“为什么某些模型能在数学上强得离谱”。说白了,数据决定上限,优化算法只是更高效地去逼近上限罢了。
6.5 复用开源权重:你应该知道的法律与风险边界
“从零构建”作为一个学习项目很有价值,但真到了产品层面,大多数人还是会继承开源权重起步。这里必须说清楚:开源不等于无限制允许商用。不同许可证(如MIT、Apache 2.0对商用宽容,有些针对学术用途的模型则要求逐项确认)的差异直接影响你的产品能否合法上线。我见过有团队因为用了带限制条款的模型做商用产品,后期被发函要求下线的,这属于最伤筋动骨的工程事故。
同样的道理适用于你搜到的那类所谓“网盘资源”。如果在浏览器里搜“某书 百度云网盘”,首页大概率会跳出各种来路不明的分享链接。我的观点一直很明确:这类资源既侵犯原作者的权益,也极可能在文件里被加入恶意代码或数据投毒,拿来做学习项目等于给自己埋雷。现在正版渠道很成熟:出版社官网、电商平台、原作者的GitHub仓库都有公开支持信息,很多官方示例代码本来就是免费开放的。花几十块买正版书或者直接从GitHub读作者的开源笔记,既安全又体面。
7. 从学到用:我的几个实战建议与踩坑记录
7.1 代码优先级:先把训练脚本写“脏”一点
很多人在学习阶段就过度追求工程的“完美”:日志系统要上WandB、配置管理要搞一套复杂体系、数据集要做分布式采样。这本身没毛病,但问题是对于第一次从零构建的练手项目,越复杂的周边工程越会让你迷失主线。主线永远只有一个:用最小的代码量,把一次完整的预训练和采样生成跑通。
可以先怎么写简单怎么来:用Python字典存配置,用print打loss,用torch.save存权重。当你能在一台普通机器上很快地出结果时,再去想“如何工业化”。很多做产品项目的老手也喜欢保持这种“能跑就够”的心态,因为复杂系统里你很难分清一个性能差异来自算法还是来自框架。
7.2 记录实验的成本,比记录实验结果更紧急
这是我从无数次“事后复盘失败”里学到的教训:每次跑实验之前,先想清楚这次实验要回答什么问题,以及它大概要花多少钱、多少时间。如果这个问题已经能在10分钟的小实验里看到趋势,就绝对不要直接跑到2小时的大实验。我的做法是把“待验证假设”和“计划成本”写在实验笔记的第一行。跑完后再回看,哪些实验值得,哪些是在做无效的重复劳动,一目了然。
举个具体的例子:你想验证增加训练数据对loss收敛的帮助,那就不该一次性从1亿token直接跳到10亿token去全量训练,而是先用1亿、2亿、3亿token划出三个小梯队,各跑相同step看趋势。如果趋势已经能说明问题,就不用再花大钱跑更大的数据集了。
7.3 复现的幻觉:别人的代码跑通不等于你理解了
我见过最典型的复现翻车现场是:把GitHub上一个标注着“训练了ChatGPT-like模型”的repo克隆下来,改改数据路径,跑通了训练,觉得自己已经会了。但当我问他“位置编码为什么用可学习嵌入而不用Sinusoidal”,他回答不上来——哪怕跑通了一百遍也不代表理解了。从零构建的意义就是要在关键部位“自己做出选择”,并且能说出选择背后的权衡。下面列几个你应该能反过来讲清楚的问题:
- 为什么用GELU而不是ReLU?
- 为什么
LayerNorm要放在残差连接之前(Pre-LN)? - 为什么参数初始化要控制方差?太大了会怎样?
- 为什么用AdamW而不是SGD?
- 为什么训练时把序列长度固定为512,而推理时可以一步一步生成长文本?
如果你能不看代码、不看文档,在纸上画出完整的GPT训练-推理流程并解释每一步存在的必要性,那才算真正越过了“从零”的坎。
7.4 算力焦虑的解药:用“小规模完整流程”代替“大规模半途而废”
文章快结尾了,我想解决一个很多人心里一直在挠的问题:我没有很多显卡,是不是做不了AI工程?
我的答案是:做应用层的AI工程完全不需要大集群;做研究复现,也可以用小模型跑通机制实验。训练1.2亿参数的小模型,一张消费级显卡够了;理解推理模型是怎么训练的,用小模型跑几千条数据够了。真正不够的是“没有耐心跑完整流程、没有习惯做系统记录”的人,而不是GPU数量。
我认识一位做LLM应用开发的朋友,他从头到尾没有自己训练过大模型,但他把base model、SFT model和RLHF model三者的行为差异做了大量抽样对比测试,每一次都整理成表格。靠着这种“用手头资源做高信息密度实验”的能力,他在团队里承担了所有关于“要不要换模型”“基线该选谁”的关键决策。这说明一件事:AI工程的核心竞争力不是算力,而是建立在深度理解之上的判断力。
如果你现在准备开始走“ai-engineering-from-scratch”这条路,我最后再分享一个可操作的小目标:从今天起,用一个周末的时间,在一台普通电脑上复现一个迷你版GPT(参数量千万级),让它跑出功能正常的文本生成结果,并画出一条loose下降曲线。把这个闭环走通的瞬间,你会看到后面所有的路都清晰了起来。接下来无论是搭RAG、做Agent、还是深入后训练,你手里都已经有了一根实实在在的拐杖——你知道这些上层应用底下承托的是什么,也知道当它们失效时该去检查哪一层。