大模型训练像做面包?一文看懂预训练、微调与对齐全流程 你是不是也遇到过这种情况刚接触大模型时看论文、看源码、看视频到处都在说预训练、微调、SFT、RLHF感觉每个词都认识但连在一起就不知道整个流程到底在干什么。然后你去看训练框架的文档里面全是数据预处理、优化器、学习率调度、检查点保存……越看越乱最后只能在别人的代码里改改参数跑起来也不知所以然。这篇文章想换一个思路用“烘焙”来做比喻把 LLM 训练的完整链路重新讲一遍。“Baking a Model” 这个说法在英文技术社区里并不少见它不是玩梗而是因为大模型训练和烘焙在工程逻辑上有很多高度对应的地方都是非线性过程、都极度依赖经验、最终质量都取决于原料和过程控制而不是单点“魔法”。读完这篇文章你应该能收获三样东西一张 LLM 训练全流程的“心智地图”预训练、微调、对齐分别对应烘焙的哪个阶段它们为什么不能跳步。一份可以照着跑的完整代码示例用 Hugging Face 生态训练一个真正的语言模型从数据处理到推理验证。一套判断训练过程是否健康的经验指标什么时候 loss 下降是正常的、什么时候该停、什么时候模型“坏了”。文章会从比喻入手但不会停留在比喻。每个环节都会给出技术定义、关键代码和实际工程建议毕竟比喻只是脚手架真正要建立的还是技术直觉。1. 为什么“烘焙”是理解 LLM 训练的最佳比喻先给结论用烘焙比喻大模型训练最大的价值不是让概念变得“可爱”而是让初学者看清单个操作在整个流程中的位置。很多人学 LLM 训练的难点在于每个单独的技术点都有大量资料但很少有人讲清楚它们之间的前后依赖。比如你知道数据清洗很重要但不知道为什么预训练阶段的数据清洗和微调阶段完全不同你知道学习率很关键但不理解为什么训练后期要下降为什么同样一个超参数在预训练和微调里作用差别那么大。这些问题用烘焙类比会立刻清晰起来。想象你在做一款面包整个流程是挑选小麦、面粉、水、酵母、盐并称量。把原料混合揉成面团。第一次发酵让面团膨胀。排气、整形决定最终形状。第二次发酵。入炉烘烤控制温度和湿度。出炉冷却、切片、包装。现在把它映射到 LLM 训练上烘焙步骤LLM 训练对应阶段关键技术挑选原料、称量数据收集与清洗数据质量、去重、过滤、tokenization揉面数据组织与批次构建采样策略、batch size、序列长度第一次发酵预训练pretraining自监督学习、语言建模损失、学习率调度排气、整形模型结构设计与初始化transformer 架构、参数量、初始化方式第二次发酵继续预训练 / domain adaptation领域数据继续训练、增量预训练入炉烘烤监督微调SFT指令数据、对话数据、交叉熵损失出炉冷却、切片对齐 / RLHF / 部署前评估奖励模型、PPO、评测集验证试吃、调整配方迭代实验实验管理、超参数搜索、回滚这张对照表有两点值得展开。第一烘焙的每个阶段都不能跳步。你不可能不发酵就直接烤也不可能在面团还没成型时就裱花。同样LLM 训练中跳过预训练直接微调会效果很差因为模型根本不具备语言基础能力反过来你也不能指望预训练直接产出能聊天的助手因为预训练只是“学会了字词规律”还没有“学会听从指令”。第二原料质量决定了上限工艺只决定你能多接近这个上限。面包好不好吃最关键的其实是面粉和发酵状态而不是最后那几分钟烤箱温度模型最终效果的上限同样由数据质量和数据分布决定超参数调优只是让你逼近这个上限。这个认知非常重要因为很多新手把大量时间花在调学习率上却不愿意花时间检查数据质量方向就反了。所以要理解 LLM 训练不要从“训练”本身开始而要从“原料”开始。接下来各节就按这个顺序展开。2. 原料准备数据收集、清洗与 Tokenization在烘焙里面粉的蛋白质含量、新鲜度、吸水性直接决定了面团能不能揉出筋、面包能不能膨胀。在模型训练里数据就是面粉。但数据不能直接扔给模型。原始文本要先经过一条流水线收集从网页、书籍、代码仓库、学术论文等来源采集文本。清洗去 HTML 标签、去重复段落、去低质量内容、过滤有害信息。去重计算文本相似度去除近似重复的数据。重复数据会严重干扰训练。分词Tokenization把文本切分成 token 序列这一步相当于“把面粉过筛”。构建训练样本将 token 序列截断或拼接成固定长度组装成 batch。其中Tokenization 是初学者最容易忽略、但影响极其深远的一步。模型看到的世界不是字符不是单词而是 token 的序列。同一个词在中文和英文里被切分的方式完全不同tokenizer 词表大小、切分粒度直接影响训练效率和最终效果。在实际代码里用 Hugging Face 生态处理数据通常长这样# 文件路径prepare_data.py from datasets import load_dataset, concatenate_datasets from transformers import AutoTokenizer import numpy as np # 1. 加载原始数据集 # 这里用中文维基作为演示数据实际项目请根据任务替换 dataset load_dataset(wikipedia, languagezh, date20240101, trust_remote_codeTrue) # 2. 清洗去掉空文本和过短的内容 def clean_text(example): text example[text] if text is None: return {text: } text text.replace(\n, ).strip() return {text: text} dataset dataset.map(clean_text, num_proc8) dataset dataset.filter(lambda x: len(x[text]) 50) # 3. 加载 tokenizer tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) # 4. 切分文本并截断/拼接为固定长度 block_size 512 def tokenize_function(examples): return tokenizer(examples[text], truncationFalse) tokenized_dataset dataset.map(tokenize_function, batchedTrue, num_proc8, remove_columns[text]) def group_texts(examples): concatenated sum(examples[input_ids], []) total_length (len(concatenated) // block_size) * block_size result { input_ids: [concatenated[i: i block_size] for i in range(0, total_length, block_size)], attention_mask: [ [1] * block_size for _ in range(0, total_length, block_size) ], } return result lm_dataset tokenized_dataset.map(group_texts, batchedTrue, num_proc8) print(lm_dataset[train][0].keys())这段代码做了什么加载原始语料。过滤掉短文本因为短文本往往信息量低或属于没实际内容的模板页面。使用中文 BERT 的 tokenizer 把文本切成子词片段。把所有 token 首尾拼接后按固定长度切成块每一块就是一条训练样本。最后一步group_texts是实践中一个经典技巧。直接按段落切分会导致大多数样本长度不足造成计算浪费拼接后再切可以最大化每个训练样本的利用率。2.1 数据质量检查清单很多新手训练完模型发现效果差第一反应是模型结构有问题实际原因却在数据。这里给一份经验清单重复率用 MinHash 等算法做近似去重。重复数据会让模型记忆而不是泛化。语言分布混合多语言数据时各语言占比要和目标使用场景匹配而不是随意混合。污染检测确保评测集没有出现在训练集里否则评测结果虚高。质量过滤参考 C4、Gopher 论文的过滤规则用启发式规则长度、符号密度、语言识别筛掉低质量页面。3. 揉面与发酵预训练阶段到底在做什么有了原料下一步是揉面。在 LLM 训练里预训练就是“把数据中的语言规律揉进模型参数里”。这一步的技术定义是在超大规模无标注文本上以自监督方式训练 Transformer 模型最常用的目标函数是自回归语言建模损失Causal Language Modeling Loss也就是让模型根据前面的 token 预测下一个 token。用公式表达就是L - (1/N) * Σ log P( token_i | token_1, token_2, ..., token_{i-1} )模型看到一句话前 511 个 token预测第 512 个 token 是什么。训练数据里有正确答案所以这是标准的监督信号——只是“标签”来自文本本身因此叫自监督。很多人问为什么预测下一个词就能学到知识这个问题其实和“为什么揉面能形成面筋”一样。面筋不是加进去的是揉出来的——反复折叠、拉伸让蛋白质分子重新排列。语言模型也一样预测下一个词看起来是一个非常简单的任务但要在海量文本上反复做这件事模型就不得不学会词法、句法、事实关联、逻辑推理等大量能力因为这些能力都能帮助降低预测损失。这就是“涌现”的朴素解释能力不是被显式编程进去的而是在足够大的模型上、足够多的文本上通过预测下一个词这个简单任务被迫发展出来的。3.1 分布式训练单卡烤不动预训练阶段的数据量通常是 TB 级单张 GPU 根本装不下。工程上需要分布式训练常见策略有数据并行Data Parallelism每张卡持有完整模型副本处理不同 batch梯度同步。张量并行Tensor Parallelism把单个 Transformer 层的矩阵拆分到多张卡上。流水线并行Pipeline Parallelism把不同层放到不同卡上数据像流水线一样流过。ZeRO / DeepSpeed 优化把优化器状态、梯度、参数分片存储降低显存占用。这已经是训练框架层面的问题了。对初学者来说不需要立刻掌握全部细节但要明白预训练依赖大规模分布式并行和极高的数据工程能力这正是个人开发者几乎不可能复现的环节。这也是为什么现在业界的标准路径不是从零预训练而是“加载开源基座模型 领域微调”。代码层面如果用 Transformers 搭配 Accelerate可以很直观地看到数据并行的抽象# 文件路径train_pretrain.py # 一个简化的预训练训练循环示例 import torch from torch.utils.data import DataLoader from transformers import AutoModelForCausalLM, get_cosine_schedule_with_warmup from accelerate import Accelerator accelerator Accelerator() model_path your-base-model-path # 实际项目替换 model AutoModelForCausalLM.from_pretrained(model_path) train_dataloader DataLoader(lm_dataset[train], batch_size32, shuffleTrue) optimizer torch.optim.AdamW(model.parameters(), lr5e-5) scheduler get_cosine_schedule_with_warmup( optimizer, num_warmup_steps100, num_training_stepslen(train_dataloader) * 3, ) model, optimizer, train_dataloader, scheduler accelerator.prepare( model, optimizer, train_dataloader, scheduler ) model.train() global_step 0 for epoch in range(3): for batch in train_dataloader: batch {k: v.to(accelerator.device) for k, v in batch.items()} outputs model(input_idsbatch[input_ids], labelsbatch[input_ids]) loss outputs.loss accelerator.backward(loss) optimizer.step() scheduler.step() optimizer.zero_grad() global_step 1 if global_step % 100 0: print(fstep {global_step} loss {loss.item():.4f} lr {scheduler.get_last_lr()[0]:.2e}) # 每个 epoch 结束保存一次检查点 accelerator.save_state(f./checkpoints/epoch_{epoch})这里需要强调labelsbatch[input_ids]这一行的含义是让模型把「预测下一个 token」作为目标模型内部会自动把标签左移一位。新手经常在这里写错导致 loss 不为负却学不到东西。3.2 预训练时期望看到的 loss 变化预训练过程中loss 的典型变化模式是前期快速下降中期缓慢下降后期进入平台期。如果 loss 出现以下异常要立刻停止排查loss 为 NaN学习率过高或数据里有异常值立即降低学习率或检查数据。loss 不下降数据 pipeline 有问题比如 labels 与 input_ids 错位。loss 下降后反弹学习率调度异常或 batch size 太小导致梯度不稳定。4. 整形与二次发酵继续预训练与领域适配面包第一次发酵后要排气、整形有时还会加入不同配料。LLM 训练里对应的环节是“二次训练”在通用基座模型的基础上用特定领域数据继续训练。这有几种常见叫法继续预训练Continued Pretraining / Domain-Adaptive Pretraining增量预训练Incremental Pretraining它的目的是让模型更熟悉某个领域的语言风格和知识分布比如代码、医学、法律、金融。实操上和预训练几乎一样只是数据从通用语料换成了领域语料学习率通常调小。为什么需要这一步因为通用模型虽然在大量文本上训练过但领域文本的分布和通用文本差别很大。比如代码数据的特点是结构化强、括号和缩进重要、错误信息频繁出现法律数据则有大量固定句式这些分布特征需要在领域数据上继续训练才能被强化。继续预训练的关键经验学习率要比预训练更小通常用预训练的 1/10 甚至更低。混合通用数据防止“灾难性遗忘”后面会讲。这个阶段的评估不是看对话能力而是看困惑度Perplexity和下游任务表现。5. 入炉烘烤监督微调SFT让模型学会“说话”面包经过整形后终于可以入炉了。在 LLM 训练中监督微调是让模型从“会预测下一个词”变成“会回答用户问题”的关键烘烤步骤。微调和预训练的区别用一个表格就能说清楚维度预训练监督微调SFT数据来源海量无标注文本人工标注的指令-回答对数据量TB 级数千到百万条目标学习语言规律和世界知识学会遵循指令、对齐人类偏好训练时间数周到数月数小时到数天模型状态基座模型指令微调后的助手模型SFT 的数据格式非常重要。现在开源生态里通用的格式是对话模板例如 ChatML|im_start|system 你是一个有帮助的助手。|im_end| |im_start|user 介绍一下大模型训练的基本流程。|im_end| |im_start|assistant 大模型训练通常包含预训练、微调和对齐三个阶段……|im_end|训练时只有 assistant 回答部分的 token 才参与 loss 计算system 和 user 部分应该被 mask 掉。这个细节直接决定模型能不能学会“对话”而不是重复整个模板。用代码实现时可以用 Transformers 的DataCollatorForCompletionOnlyLM它专门处理这种“只对回答部分计算损失”的场景# 文件路径train_sft.py from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForCompletionOnlyLM, ) from datasets import Dataset tokenizer AutoTokenizer.from_pretrained(your-base-model-path) tokenizer.pad_token tokenizer.eos_token train_data [ { conversation: [ {role: user, content: 什么是 Transformer}, {role: assistant, content: Transformer 是一种基于自注意力机制的神经网络架构最早由 Vaswani 等人于 2017 年提出。}, ] }, # ... 实际项目请准备更多数据 ] def format_conversation(example): prompt conv example[conversation] for i, turn in enumerate(conv): if turn[role] user: prompt f|im_start|user\n{turn[content]}|im_end|\n else: prompt f|im_start|assistant\n{turn[content]}|im_end|\n prompt |im_start|assistant\n # 这里需要把目标文本也拼接出来供 DataCollator 计算 completion 位置 response conv[-1][content] return {prompt: prompt, completion: response} formatted_data [format_conversation(item) for item in train_data] dataset Dataset.from_list(formatted_data) def tokenize_with_response(example): prompt_ids tokenizer.encode(example[prompt], truncationTrue, max_length1024) completion_ids tokenizer.encode(example[completion], add_special_tokensFalse) input_ids prompt_ids completion_ids [tokenizer.eos_token_id] labels [-100] * len(prompt_ids) completion_ids [tokenizer.eos_token_id] return {input_ids: input_ids, labels: labels} tokenized_dataset dataset.map(tokenize_with_response, remove_columns[prompt, completion]) data_collator DataCollatorForCompletionOnlyLM( response_template|im_start|assistant\n, tokenizertokenizer, ) training_args TrainingArguments( output_dir./sft_model, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate2e-5, num_train_epochs3, logging_steps10, save_strategyepoch, report_tonone, ) trainer Trainer( modelAutoModelForCausalLM.from_pretrained(your-base-model-path), argstraining_args, train_datasettokenized_dataset, data_collatordata_collator, ) trainer.train() trainer.save_model(./sft_model_final)这段代码的核心在于labels列表中的-100。PyTorch 的交叉熵损失会把-100位置的 token 忽略掉这样模型在训练时只需要预测 assistant 的回答部分而不是去预测用户的提问。这里的response_template|im_start|assistant\n是告诉 DataCollator 从哪个标记开始才算 completion这样它就能自动构造出正确的labels。很多新手在微调时遇到同一个问题模型训练完 loss 很低但对话时只会把用户的问题和回答一起复述出来。这个现象的根源通常就是 label 没有 mask模型把“预测用户问题”也当成了学习目标。如果你用的是 LLaMA 系模型官方没有 ChatML 模板需要把response_template换成模型对应的 assistant 起始标记同时把tokenizer.add_special_tokens处理到位。5.1 微调数据的关键认知SFT 的数据质量比数据量更重要。业内不少团队的实践经验是几千条高质量、覆盖多任务的指令数据效果往往好于几万条低质量、模板雷同的数据。高质量 SFT 数据应满足指令多样覆盖真实用户使用场景。回答准确、信息密度高、逻辑清晰。避免大量“复读机式”的问答对。注意安全性和价值观对齐不包含违规内容。6. 出炉与试吃评估、困惑度与人工评测面包出炉后要先冷却、切开试吃才知道火候对不对。LLM 训练也是如此训练结束时必须做系统评估而且要在训练过程中就建立评估机制。6.1 客观指标Perplexity困惑度语言模型对测试数据的负对数似然的指数形式。困惑度越低说明模型对文本的预测能力越强。但要注意困惑度低不代表对话能力强它只是基础能力的指标。Loss训练损失监控训练过程用。下游任务指标比如代码生成的 passk、数学推理的准确率、阅读理解 EM/F1 等。6.2 人工评测真正的对话质量必须靠人工评测。实践中通常会设计一个评测集包含几百到几千条覆盖不同能力的 prompt然后让多个评测者给模型回答打分对比不同训练版本的优劣。现在也有很多自动评测框架可以辅助但最终人工抽检不可替代尤其是对安全性和语气这类难以量化的维度。6.3 一个简单的评估脚本# 文件路径evaluate_model.py from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_path ./sft_model_final tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForCausalLM.from_pretrained(model_path, device_mapauto) model.eval() def generate(prompt, max_new_tokens200): messages [{role: user, content: prompt}] input_text tokenizer.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue) inputs tokenizer(input_text, return_tensorspt).to(model.device) with torch.no_grad(): outputs model.generate( **inputs, max_new_tokensmax_new_tokens, do_sampleTrue, temperature0.7, top_p0.9, ) return tokenizer.decode(outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) test_prompts [ 用一句话解释什么是注意力机制。, 写一个 Python 函数计算斐波那契数列。, 大模型训练分为哪几个阶段, ] for prompt in test_prompts: print(fPrompt: {prompt}) print(fAnswer: {generate(prompt)}) print(- * 50)运行后逐个检查回答是否流畅、准确、符合指令。这里要特别提醒评估时必须设置temperature和top_p等采样参数固定评估方式和参数否则同一 prompt 每次生成结果可能不同不利于对比版本效果。7. 常见问题与排查方法问题现象可能原因排查方式解决方案训练 loss 不下降labels 与 input_ids 错位模型在预测自己的输入而非下一 token检查数据预处理打印一个 batch 的 input_ids 和 labels 对比修正 labels 构造逻辑或使用 DataCollatorForCompletionOnlyLMloss 变成 NaN学习率过高、数据中存在异常值、混合精度训练精度溢出查看 loss 变 NaN 前的日志和当时的输入数据降低学习率检查数据关闭 fp16 或改用 bf16显存不足OOMbatch size 过大、序列过长、优化器状态占用太大查看报错栈确认是模型参数还是激活值占满显存减小 batch size、开启梯度累积、使用 LoRA/QLoRA、使用 DeepSpeed ZeRO微调后“复读机”现象没有对 prompt 部分做 label mask打印 labels 中的 -100 位置是否正确覆盖 user 部分使用 response_template 自动 mask 回答部分灾难性遗忘微调后通用能力下降微调数据分布和预训练分布差异过大或学习率过高、训练轮数过多在通用评测集上对比基座模型和微调模型混合 5%-20% 通用数据降低学习率控制 epoch使用参数高效微调评测集和训练集重叠数据泄漏评测结果虚高计算训练集和评测集的 n-gram 重叠率构建评测集时做相似度去重排除训练数据生成结果重复单调温度过低、top_p 过小、模型未收敛尝试不同采样参数组合调整 temperature / top_p / repetition_penalty这里单说两个高频问题。灾难性遗忘是微调中最常见的隐性坑。模型在领域数据上训练太久可能会遗忘预训练阶段学到的通用能力。解决思路不是“尽量多训”而是“控制训练强度”降低学习率、减少 epochs、混合通用数据、优先选择 LoRA 这类参数高效微调方法。OOM 的排查逻辑也要说清楚。很多新手第一反应是把 batch size 调小但有时真正的问题是优化器状态太大。AdamW 优化器每个参数要维护两份额外状态单个 float32 参数实际占用显存可能是参数本身的 3 倍以上。这时用 AdamW 的 8-bit 版本或换用 Adafactor效果立竿见影。8. 工程最佳实践从“能跑”到“可维护”训练模型和做面包一样最大的坑往往不在配方本身而在流程管理。下面这几条实践经验是比任何单一技巧都重要的。8.1 用 LoRA 降门槛如果你在单个消费级显卡上做微调不要直接训练全部参数优先选择 LoRA。LoRA 的核心思路是冻结原始权重只训练一小部分低秩增量矩阵。这能把可训练参数量降低到原来的 1% 甚至更低显存占用大幅下降。用 PEFT 库实现非常方便# 文件路径train_lora.py from peft import LoraConfig, get_peft_model, TaskType lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, r8, # 低秩矩阵的秩 lora_alpha32, # 缩放系数 target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.1, ) model AutoModelForCausalLM.from_pretrained(your-base-model-path) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 输出示例trainable params: 4,194,304 || all params: 1,234,567,890 || trainable%: 0.34LoRA 的额外好处是可插拔。训练完的增量矩阵只有几十到几百 MB可以单独保存、加载、切换多个 LoRA 模块可以复用到同一个基座模型上很适合团队内部做多业务场景的模型复用。8.2 实验管理每次训练都要“可复现”面包师傅会记录每次的配方、温度、时间模型训练更应该如此。实践建议每次实验记录数据版本hash 或 commit id、代码 commit、超参数、训练日志、评测结果。使用实验管理工具如 Weights Biases、MLflow、TensorBoard或至少用一个统一的实验记录表格。保存检查点时同时保存 tokenizer 和配置文件确保模型可以独立加载。8.3 评估先行还没训练就要想好怎么评测这是最有价值的一条建议。很多人训练完模型才开始设计评测结果发现评测集不完善只能重新训练。正确做法是训练开始前先构建评测集。用基座模型跑一遍生成结果作为 baseline。每次微调后在同一批 prompt 上对比。如果没有 baseline你无法判断模型是变好了还是变差了。很多“微调后效果变差”的结论其实是因为根本没有对比对象。8.4 节省资源的小技巧用梯度累积替代直接放大 batch size。优先用 bf16 而不是 fp16bf16 在训练稳定性上更优。检查点保存不要每步都存用save_strategyepoch或按步数间隔保存。训练结束后用model model.merge_and_unload()合并 LoRA 权重部署时零额外开销。9. 总结烤箱还是那台烤箱配方和手艺才是核心用烘焙来理解 LLM 训练学到的不是比喻本身而是流程思维。数据是面粉决定模型能力的上限。预训练是揉面和发酵把语言规律揉进参数。继续预训练是整形和二次发酵让模型适配领域。微调是入炉烘烤让模型学会对话和指令遵循。评估是试吃没有评估的训练等于闭着眼睛烤面包。灾难性遗忘是烤过头OOM 是烤箱塞太满loss 不降是配方抄错了。对于正在入门 LLM 训练的同学我的实战建议是不要急着从零预训练大模型也不要上来就追最新的训练框架。先用中文小规模数据跑通一个最小的预训练 微调流程把数据 pipeline、loss 变化、评估方法、领域适配这几个环节全部走一遍然后再去学习分布式训练和 RLHF。有了完整的流程视角你再去看任何一篇训练相关的论文或代码都会清楚它在整个流程里处于哪个位置。下一步值得深入的方向有三个一是继续预训练时如何解决灾难性遗忘二是 SFT 数据配比到底怎么设计才更优三是 RLHF 中的奖励模型如何评估它和 SFT 优化目标的差异在哪里。这三块都是通往更高质量助手模型的必经之路也可以作为你接下来的动手实践主题。烘焙的关键不是记住一个面包配方而是理解每一种原料和每一步操作对成品的影响训练模型也是如此。把这份“配方思维”建立起来你就已经从“调参侠”迈向了真正理解大模型引擎的开发者。