先说个结论:小模型指令微调翻车,十有八九不是因为模型太小,而是因为数据混合比例、模板格式、训练轮数这三个变量在互相打架。我最近在做一个资源受限场景下的指令跟随任务,手里只有一张 3060,API 预算为零,只能在 0.5B 级别的参数模型上做 SFT。前前后后卡了快两周,最后逼着自己把所有变量拆开,用一套最小复现方案才彻底看清问题出在哪。
在最小复现这条路上,我把变量压缩到三个:数据混合、模板、过拟合相关的训练轮数。这三个是最容易忽略、也最容易让实验结论失效的地方。这篇文章会把模型选型、数据配比、模板设计、过拟合定位、训练配置和踩坑排查全部摊开来写。里面的结论是基于我个人多次跑出来的趋势,不保证数字在每个数据集上一模一样,但相对规律是稳定的,可以直接作为你的实验参考。
1. 最小复现的三要素拆解:为什么先把变量锁死在这三个上
1.1 复现不是追求效果,而是追求"能归因"
指令微调(SFT)在小参数模型上的有效性已经被大量验证了,但实际操作中你会发现一个问题:照着别人的教程跑完,结果模型变笨了,或者只会说套话。你想排查,却发现哪里都像有问题——数据不够?模板不对?轮数太多?学习率太大?LoRA rank 不合适?模型底座本身就不行?变量太多,根本无法归因。
所谓最小复现,本质是做一个受控实验。我建议你和我一样,把变量固定到少数几个。数据混合、模板格式、训练轮数这三个是我认为小模型 SFT 里最核心、也最容易被误解的变量。其他维度,比如学习率、LoRA 配置、优化器、batch size,全部固定成一组常用值。这样一旦结果崩了,你能明确知道是哪个旋钮出了问题,而不是陷入"哪里都要调"的泥潭。
1.2 模型选型:为什么 0.5B 是绝佳的实验载体
我选了 Qwen2.5-0.5B-Instruct 作为主实验模型,又用 Qwen2.5-1.5B 做了一轮交叉验证。选它的理由非常朴素:
- 0.5B 参数量在消费级显卡上用 LoRA 训练很舒服,不需要多卡并行,也不用为了显存把 batch size 压到没法看的程度。
- Qwen2.5 的 tokenizer 对中文很友善,指令微调任务里中文占比高,不会因为分词低效浪费序列长度。
- 小模型容量小,任何数据配比或者模板设计的细微变化,都会非常灵敏地反映在生成结果上。这种灵敏度对做实验反而是优点——你能更快观察到因变量随自变量的变化趋势。
很多人担心的"0.5B 太小了,效果肯定不行"其实不是关键。如果连最小模型上的规律都没摸清,直接换 7B、14B 只会让实验变量更多、成本更高。先把小模型上的因果链打通,再放大也不迟。
1.3 复现前的验收标准
在训练之前,我建议你先把"什么叫效果好"定义清楚,否则实验做完了只能用"感觉还行"来评判,毫无意义。我给自己定了一套很简单但一致的验收指标:
- 验证集 Loss 与训练 Loss 之间是否存在显著差距。如果两者差得很大,说明过拟合已经发生。
- 留出指令上的首次回答成功率。注意是第一次生成的结果,不是 beam search 多次采样挑一个最好的,那样会掩盖问题。
- 生成文本的"复制率"。用 n-gram 重叠度判断模型是否在复读训练集中的句子,而不是真的在回答问题。
这套指标在后续所有实验里保持不变。它是我的"标尺",也是最小复现方法的核心理念:用固定度量去比较不同变量组合,而不是靠肉眼和感觉。
2. 数据混合比例实验:我的数据配方与"缓冲垫"理论
2.1 实验数据的三元构成
我在实验里构造了一个很典型但不复杂的场景,用来模拟真实业务需求中的小模型微调。一共三类数据:
- 指令数据 A:500 条通用指令,覆盖问答、摘要、改写、情绪分析等基础任务。人工整理,质量高,但数量少。
- 领域数据 B:300 条垂直领域问答,模拟一个具体的业务场景,比如某产品的常见问题解答。
- 通用语料 C:从开源中文语料里随机采样 8000 条,清洗后保留短文档。数据质量参差不齐,但胜在多样性。
A 和 B 负责教模型"怎么应对指令",C 负责让模型"别忘记怎么说话"。关键问题在于:这三者按什么比例混合,才能在 0.5B 的容量里同时保住语言能力和指令跟随能力?
2.2 三组配方的对比趋势
我在同样超参数下跑完了三组配方,结果趋势如下:
| 配方 | A:指令数据 | B:领域数据 | C:通用语料 | 验证Loss | 留出指令成功率 | 生成质量观察 |
|---|---|---|---|---|---|---|
| 配方一 | 50% | 30% | 20% | 0.71 | 58% | 语言空洞、套话比例高 |
| 配方二 | 20% | 20% | 60% | 0.55 | 74% | 指令跟随正常、语言自然 |
| 配方三 | 10% | 10% | 80% | 0.61 | 49% | 语言自然但不太遵循指令 |
表格里的数字是典型的相对量级。不同数据集的绝对值会有差异,但相对趋势稳定:指令数据占比过高时,验证集 Loss 会提前上升,生成内容开始出现"机械感";通用语料占比过高时,模型语言流畅度没问题,但指令跟随能力被稀释,回答常常跑题或者过于泛泛。
从信息论的角度看,这个现象很合理。小模型的参数容量有限,如果可学的语义全部集中在少部分重复模式上,它很快就会陷入对训练数据的局部拟合。通用语料保持了语言分布的多样性,弥补了指令数据覆盖面不足的缺陷。
2.3 通用语料为什么是必要的"缓冲垫"
很多人对小模型 SFT 有个误解:既然是微调,那就把数据全部换成任务数据。大模型基础能力足够强,这么做问题不大。但小模型不一样,它的先验知识本来就弱,如果全拿指令数据去训练,模型会快速背下这些少样本模式,却没有足够的容量构建泛化的指令映射。结果就是训练集上表现不错,一遇到新的指令表述就崩。
通用语料在这里承担的正是"缓冲垫"功能。它让模型在持续接触指令格式的同时,还有足够多的真实语言分布来校准生成质量。你可以这样理解:指令数据是教模型"如何表现",通用语料是让模型"保持正常说话能力"。小模型两者都缺一不可,缺了任何一边,另一端都会出问题。
2.4 去重是混合之前必须做的一步
混合前我做了一件很容易被忽略但非常重要的事:去重。很多"看起来不同"的指令样本其实存在较高的语义重叠,通用语料里也可能有片段重复。如果不去重,模型在训练时会过度放大这些重复模式的拟合强度,等于主动给过拟合递刀。
我的轻量方案是:
- 将所有样本做归一化处理:去空白、统一小写。
- 用 MinHash 计算样本间的 Jaccard 相似度,相似度超过 0.8 标记为重复。
- 重复样本只保留一条,优先保留更长、更完整的版本。
操作下来,我的有效样本量减少了约 10%,但训练稳定性明显提升。在小规模数据里,这一步尤其重要。你可能觉得 500 条数据不多,但只要有 20 条高度相似的样本,它们对模型拟合方向的影响就远超你的直觉。
3. 模板格式的隐性控制:模板语言如何决定模型的上限
3.1 模板的本质:一种自带倾向的"模板语言"
模板是 SFT 里最容易被当成"包装"忽略的变量。表面上看,模板只是把 instruction、input、response 拼插起来,不改变语义。但实际上,模板是一种特殊的"模板语言",它直接决定了模型对输入输出边界的认知。
我在相同数据、相同超参数下做了两种模板的对比。冗长模板长这样:
你是一个乐于助人的中文助手。请阅读下面的用户请求,并给出专业、准确且详细的回答。 用户请求:{instruction} 回答:精简模板长这样:
问题:{instruction} 回答:实验现象非常明显:使用冗长模板时,训练 Loss 下降更快。原因并不神秘——模板里的固定文字("你是一个乐于助人的中文助手"这类)在每条样本里都出现,模型很快就能背下来,这些位置变成确定性极高的输出,从而把整个序列的平均 Loss 拉低。表面上看起来学得更好了,实际上模型只是在快速拟合模板套话而已。
3.2 固定 token 对语义损失的稀释效应
更隐蔽的问题是,模板长度会稀释真正语义部分对 Loss 的贡献。在 max_length 受限的场景里,模板固定 token 占了序列长度的相当一部分。当我用 512 的最大序列长度训练时,冗长模板大约消耗 40 个 token 的固定头部,精简模板只消耗 10 个左右。
Loss 是序列级别的平均交叉熵。模板固定 token 对应的 Loss 在训练中会快速下降,而真正承载语义的内容——指令和回答——对应的 Loss 占比被相对压缩。你可以理解为:模型花了很多精力在"背套话"上,留给"理解指令"的梯度信号变少了。这导致一个反直觉的结论:模板越啰嗦,模型在指令跟随上的实际表现越差。
在我这组实验里,精简模板比冗长模板的留出指令成功率高出约 7 个百分点。这不是一个微小的差异,说明模板设计本身就是一个不可忽视的性能旋钮。
3.3 训练模板与推理模板必须完全一致
我在这上面踩过一个印象很深的坑。有一次训练用了精简模板,推理的时候觉得"为了效果更好",加了一段更详细的 system prompt,比如"你是专业的领域助手,请基于以下知识回答问题"。结果生成质量肉眼可见地下降,甚至开始重复用户输入。
排查了很久才发现,问题出在输入分布偏移上。模型在训练阶段从未见过这些额外的前缀文字,也没有见过被改变的分隔符结构。小模型对输入结构的泛化能力远弱于大模型,哪怕只是把"回答:"改成"答案:",都可能把模型推到分布外。
所以我现在有一个非常固执的原则:训练模板和推理模板必须完全一致。可以更换模板内的变量内容,但模板固定字面量一个字符都不动。如果你确实需要系统提示词,那就把它写进训练样本的模板里,让模型在训练时见过它。
3.4 我最后选用的精简模板
经过多轮对比,我的最终模板是这一套:
问题:{instruction} 回答:{response}训练和推理都保持完全一致。这套模板的优点是固定 token 占比极低,token 预算几乎全部留给指令和回答本体。如果你需要加入 system prompt 性质的约束,我建议直接把它放在"问题:"前,并保持训练和推理完全一致,而不是临时在推理侧添加。
在实际项目中,模板设计还用考虑输入里是否包含额外上下文(比如参考资料)。如果包含,可以改为:
背景:{context} 问题:{instruction} 回答:{response}同样遵循"训练推理一致、固定词少、语义词多"的原则。
4. 过拟合的三张脸:从 Loss 曲线到复读机的定位过程
4.1 第一张脸:Loss 曲线的早期拐点
小模型过拟合最典型的信号是:训练 Loss 持续下降,但验证 Loss 在某一步开始拐头向上,形成明显的 V 型或 U 型曲线。我在全参数微调 5 轮的实验里,验证 Loss 在 2 轮左右就开始回升,而训练 Loss 还在继续降低,两者差距越拉越大。
要高效识别这个问题,你需要一个独立于训练集的验证集,并且保证验证集和训练集不存在重叠。哪怕只有二三十条手工保留的样本也够用,关键是不能让模型在训练期间见过它们。每个训练步骤或者每隔固定步数记录一次验证 Loss,就能画出有效的曲线。
如果你发现验证 Loss 一直下降但实际生成效果很差,那可能是另一种情况:验证集和训练集太相似,验证 Loss 无法反映泛化能力。这时需要重新检查验证集的构造,否则它不过是一个自欺欺人的指标。
4.2 第二张脸:模板复读机与套话增生
过拟合并不只体现在 Loss 数字上,更直观的表现是生成内容的"复读"现象。当模型开始逐字复用训练集中某些句子的片段,或者回答里堆砌大量模板化的套话,就说明它在"背答案"而不是"学能力"。
我一般用一个简单的复制率指标来量化:把生成结果和训练集中所有样本做 n-gram 重叠计算,如果生成文本里有不少片段是训练样本中出现过的连续 n-gram,就得警惕了。正常泛化的模型应该能够在语义上参考训练数据,但不会大段复现;而过拟合模型恰恰相反。
举个例子,在一组全参数微调 5 轮后的模型里,我输入一个全新的领域问题时,它的回答里出现了大约 15% 的 4-gram 片段与训练集中的某条样本高度重合。改用 LoRA 训练 1 轮后,同样输入下这个比例降到了 3% 以下。这说明大部分"复读"不是模型底座的问题,而是微调方式直接把记忆刻进了参数。
4.3 第三张脸:新指令上的能力塌方
过拟合最坑人的表现是:在训练集上随便挑一条输入,模型回答得很漂亮;但稍微换一个表述角度、换一个没见过的场景,模型立刻表现得像换了个人。这种"能力塌方"在小模型上比大模型更常见。
我做的测试方法是:保留 20 条"风格完全不同"的指令,不放进训练集。这些指令覆盖训练集未出现的任务描述方式,比如把"总结这段话"改写为"用三句话告诉我这段话在讲什么"。过拟合模型在这种测试上会频繁出现以下几种失败模式:
- 答非所问:生成内容看起来通顺,但和指令无关。
- 机械复述:把输入文本复述一遍,没有任何实际处理。
- 套话堆砌:生成大段正确的废话,比如"这个问题很重要,我们需要认真考虑"。
如果模型在这类测试上的表现明显差于训练集,说明偏置过于严重。缓解手段不是继续加训练轮数,而是调整数据混合、降低训练轮数,或者引入更多样化的指令表述。
4.4 缓解手段的优先级:先调数据,再调参数
基于几次实验对比,我总结出一个优先级排序:
| 优先级 | 手段 | 说明 |
|---|---|---|
| 最高 | 减少 epoch | 小模型 SFT 通常 1 个 epoch 就够,0.5B 模型建议先从 0.5-1 epoch 试起 |
| 高 | 增加数据多样性 | 比调参有效得多,扩充指令表述的覆盖面 |
| 高 | 使用 LoRA 而非全参数微调 | 限制可学习参数数量,天然抑制过拟合 |
| 中 | 增加 dropout | LoRA 默认 0.05,过拟合严重时可提高到 0.1 |
| 中 | 增强权重衰减 | 从 0.01 起步,最高不超过 0.1 |
| 低 | 降低学习率 | 降低学习率会放缓过拟合,但也会降低收敛速度,需权衡 |
顺带回答一个常见问题:小参数模型该用 SFT 还是 RL?我的看法是,在做最小复现和问题排查阶段,SFT 是唯一合适的起点。RL 需要额外的奖励模型或偏好数据,引入的变量比 SFT 多一个数量级,不适合用来定位"数据混合、模板、过拟合"这类基础问题。先把 SFT 下的规律摸透了,再谈 RL 的事。
5. 最小复现环境:直接可跑的代码与超参配置
5.1 运行环境与依赖版本
我用的是单张 12GB 显存的 3060,PyTorch 2.1,Transformers 4.40,PEFT 0.10,TRL 0.8,Datasets 2.17。这套组合在 2025 年依然主流。如果你用的版本更新,接口可能有细微变化,但核心逻辑不变。
依赖安装:
pip install torch transformers datasets peft trl accelerate bitsandbytes如果你只有 8GB 显存,也可以把 max_length 缩到 384、batch size 缩到 2,依然能跑起来。
5.2 数据模板化与标签屏蔽
训练 SFT 模型时,一个非常重要的细节是:不要对 prompt 部分计算 Loss,只对 response 部分计算。如果训练时把整段文本都当作预测目标,模型会把"背 prompt 套话"也当成学习目标,浪费参数容量。这也是我上文中提到的模板稀释问题的工程化解法。
下面是一段可以直接改进到你项目里的数据处理函数:
from transformers import AutoTokenizer model_name = "Qwen/Qwen2.5-0.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token MAX_LENGTH = 512 def tokenize_with_labels(example): # 模板必须与推理阶段完全一致 prompt = f"问题:{example['instruction']}\n\n回答:" response = example["response"] + tokenizer.eos_token prompt_ids = tokenizer(prompt, add_special_tokens=False).input_ids response_ids = tokenizer(response, add_special_tokens=False).input_ids max_response_len = MAX_LENGTH - len(prompt_ids) if max_response_len < 1: prompt_ids = prompt_ids[: MAX_LENGTH - 1] max_response_len = 1 response_ids = response_ids[:max_response_len] input_ids = prompt_ids + response_ids labels = [-100] * len(prompt_ids) + response_ids return { "input_ids": input_ids, "labels": labels, "attention_mask": [1] * len(input_ids), }这里的关键是把 prompt_ids 对应的 labels 全部设成 -100。PyTorch 的交叉熵损失会自动忽略 -100 位置,所以模型只会从 response 部分学习。这个细节直接影响最终效果。
5.3 DataCollator 与 LoRA 配置
有了 tokenize 函数后,还需要一个自定义 DataCollator 来做动态 padding,并保证每个 batch 内长度对齐:
import torch from transformers import DataCollatorForSeq2Seq class SFTDataCollator: def __call__(self, features): max_len = max(len(f["input_ids"]) for f in features) batch_input_ids = [] batch_labels = [] batch_attention_mask = [] for f in features: pad_len = max_len - len(f["input_ids"]) batch_input_ids.append(f["input_ids"] + [tokenizer.pad_token_id] * pad_len) batch_labels.append(f["labels"] + [-100] * pad_len) batch_attention_mask.append(f["attention_mask"] + [0] * pad_len) return { "input_ids": torch.tensor(batch_input_ids), "labels": torch.tensor(batch_labels), "attention_mask": torch.tensor(batch_attention_mask), }LoRA 的核心配置我建议从这一组起步:
from peft import LoraConfig lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=[ "q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj", ], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", )r=8 是足够小的值,如果过拟合仍然明显,可以降到 4。lora_alpha 保持 16 的时候,有效缩放比是 2 倍,这是一个社区常见的稳妥比例。
5.4 训练超参骨架
下面这组超参是我在 0.5B 模型、约 8000 条混合数据上反复试出来的起点:
from transformers import TrainingArguments, Trainer training_args = TrainingArguments( output_dir="./sft_output", num_train_epochs=1, per_device_train_batch_size=4, gradient_accumulation_steps=4, learning_rate=2e-4, warmup_steps=50, weight_decay=0.01, logging_steps=20, save_steps=200, eval_strategy="steps", eval_steps=100, save_total_limit=2, fp16=True, remove_unused_columns=False, ) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, data_collator=SFTDataCollator(), ) trainer.train()几个值得解释的地方:
num_train_epochs=1:在数据量接近 1 万条时,1 个 epoch 通常已经足够。如果你只有两三千条数据,甚至可以设成 0.5,让模型只跑一部分数据。learning_rate=2e-4:这是 LoRA 微调比较常用的起点。全参数微调要用低一个量级的 2e-5。gradient_accumulation_steps=4配合batch_size=4,实际 batch size 是 16。小模型对 batch size 不太敏感,16 是一个稳妥的选择。- 用
fp16=True可以显著降低显存占用并加速训练,0.5B 模型上精度损失几乎无感。
训练时间方面,这张 3060 上跑 8000 条数据、max_length=512,大约需要 1 到 2 小时。如果你只有几百条数据,半小时内就能完成一轮实验。
5.5 评估阶段的三个指标脚本
训练结束后,我建议你写一个简单的评估脚本,一次性加载上述三个指标。核心逻辑可以复用:
import numpy as np from difflib import SequenceMatcher def evaluate_generation(model, tokenizer, eval_samples): total = 0 correct = 0 copy_scores = [] for sample in eval_samples: prompt = f"问题:{sample['instruction']}\n\n回答:" inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=128, do_sample=False, pad_token_id=tokenizer.pad_token_id, ) generated = tokenizer.decode(outputs[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) # 简单成功率:认为包含参考答案关键词即成功 if any(kw in generated for kw in sample.get("keywords", [])): correct += 1 # 复制率:和训练集最相似样本的相似度 sim = 0.0 for train_text in sample["train_candidates"]: sim = max(sim, SequenceMatcher(None, generated, train_text).ratio()) copy_scores.append(sim) total += 1 accuracy = correct / total avg_copy = np.mean(copy_scores) print(f"首次回答成功率: {accuracy:.2f}") print(f"平均复制率: {avg_copy:.3f}") return accuracy, avg_copy这只是骨架。真正的关键词判断要根据你的实际任务来定义。但重点是用同一套函数去评估不同实验组的模型,保持比较的公平性。
6. 踩透了这些坑才能算复现成功:典型问题的排查链路
6.1 问题一:训练 Loss 不下降,模型全程摆烂
如果你发现训练 Loss 几乎没有变化,甚至在小模型上完全不变,先别急着换超参数。我遇到的根源往往是数据格式问题。最常见的一种是:模板中的字段拼接错误,导致模型预测目标和实际输入完全错位。比如 prompt 部分也被计算了 Loss,或者 response 和 eos_token 之间缺失了正确分隔,模型无法建立上下文关联。
排查顺序建议如下:
- 打印一条 tokenize 之后的样本,人工检查 input_ids 和 labels 的对齐关系。
- 确认 loss 只计算了 response 部分,避免 -100 掩盖了真实的学习信号。
- 确认不是 tokenizer 的 padding 导致 label 错位。
- 最后才考虑学习率问题。LoRA 场景下 2e-4 级别一开始不下降的情况很少见。
我在早期实验里就栽在 DataCollator 的 padding 上。原来的 collator 把 padding 加在了右侧,但没有同步对齐 labels,导致模型学到一堆 padding 位置的错误映射。用自定义 collator 之后,问题立刻消失。
6.2 问题二:Loss 下降很快,但生成内容全是乱码
Loss 下降得快并不代表学得好。如果训练进展顺利,但生成内容读起来混乱,先检查模板一致性。我之前遇到过一种情况:训练时模板是"回答:"结尾,推理时为了美观改成"答:",差别只有一个字,但生成质量明显劣化。这种情况的本质是输入分布偏移,小模型对字面量的敏感度远超预期。
另一个高频原因是生成参数设置不合适。小模型在 do_sample=False 时容易产生短回答或重复循环,因为它的概率分布比大模型更尖锐。我的建议是:
model.generate( ..., do_sample=True, temperature=0.7, top_p=0.9, repetition_penalty=1.05, )温度 0.7 结合 top_p 0.9 是小模型比较稳妥的生成组合。repetition_penalty 设置到 1.05,可以减轻复读现象。
6.3 问题三:验证 Loss 正常,但实际效果仍然差
有时验证 Loss 看起来很正常,和训练 Loss 差距也不大,但生成出来的东西就是不理想。这往往是因为验证集构造过于简单。比如验证集里的指令表述高度模板化,模型只需要学会把指令文本原样重写到特定位置规则,就能通过验证 Loss。
一个更深层的原因在于:Loss 是一个 token 级别的平均指标,回答中大部分 token 可能是废话和连接词,即便模型的实质内容错误,Loss 值也不会特别差。所以只看 Loss 不够,必须辅以生成质量评估。
我自己的经验是:小模型的 SFT 评估至少需要两类信号——客观的 Loss 曲线,加上主观的生成样例检查。每轮实验固定抽出 10 条新指令看生成结果,比盯着验证 Loss 猜要有效得多。如果生成样例质量明显优于 Loss 数字给你的预期,也不要太惊喜,大概率是验证集太简单了。
6.4 最终排查清单
| 现象 | 第一优先排查 | 第二优先排查 | 第三优先排查 |
|---|---|---|---|
| Loss 不下降 | 数据 tokenize 对齐 | 模板拼接格式 | 学习率过低 |
| Loss 下降快但生成乱 | 训练/推理模板一致性 | 生成参数 | tokenizer 特殊 token |
| 训练效果好、验证差 | epoch 过多 | 数据多样性不足 | 验证集与训练集重叠 |
| 生成全是套话 | 指令数据比例过高 | 模板固定 token 过多 | 通用语料不足 |
| 新指令能力塌方 | 训练数据覆盖度低 | 数据去重不彻底 | 模型容量确实不够 |
排查的顺序非常重要。我见过很多人在"训练效果好、验证差"的现象里,一上来就调 dropout 和 weight decay,结果收效甚微。先检查 epoch 和数据多样性,往往一步就能解决。
最后分享一个我自己的习惯:每次实验只改一个变量,并且在训练脚本里用注释记录当前实验的目的和假设。比如"这一轮想把通用语料比例从 60% 降到 40%,验证是否会影响指令成功率"。这个习惯看起来很笨,但在排坑时有奇效——你不会在三天后忘了当初为什么要跑这组实验。最小复现的本质不是省事,而是让每一次改动都变得可以解释。这个思路不需要多强的硬件,也不需要多复杂的代码,只要坚持变量分离、指标固定,慢慢就能在看似杂乱的小模型微调里摸出规律。