开始之前
你需要 Python、datasets 库(pip install datasets),以及第 1 课得到的那些行为行。下面的步骤需要一批真实的样本。现在就动笔。这个任务好不好学,要等样本写出来才看得出。
一行一条样本
微调数据是一个 JSONL 文件:每行一个完整的 JSON 对象,行与行之间不加逗号,外面也没有列表。文件里不放别的东西。OpenAI 的微调指南要求用 chat completions 格式,也就是一个 messages 列表,列表里每个对象都有 role 和 content。第 3 课用来训练的 TRL 库,会把同样的布局当作对话式数据集来读。
第一行取自 OpenAI 数据准备食谱,第二行是本课自己写的,对应第 1 课的工单标题任务。Hugging Face Datasets 用 load_dataset("json", data_files="all.jsonl") 读取这样的文件,它的文档称 JSON Lines 是最高效的 JSON 布局。
{"messages": [{"role": "system", "content": "You are a happy assistant that puts a positive spin on everything."}, {"role": "user", "content": "I fell off my bike today."}, {"role": "assistant", "content": "It's great that you're getting exercise outdoors!"}]}
{"messages": [{"role": "system", "content": "Write a ticket title of at most 12 words."}, {"role": "user", "content": "Hi, the invoice PDF in my account shows last year's logo and I need the new one for my accountant."}, {"role": "assistant", "content": "Invoice PDF shows the old logo instead of the new one"}]}- 把每条样本存成一行,放进名为
all.jsonl的文件。每行是一个messages列表,以你想要的助手消息结尾。 - 系统消息和用户消息的措辞,要沿用应用在线上实际发送的写法。准确率指南把格式上和线上输入略有出入的训练数据,列为最常见的坑之一。
- 第一批自己写,手头有日志和专家答案的话,也可以从中取。OpenAI 把历史数据、专家数据和日志数据列为好来源,并要求问题和回答具体、清楚。
TRL 怎样读这个文件
TRL 的 SFT 训练器接受两种数据集类型,语言建模和提示词加补全,每种都分标准(纯文本)和对话两种形式。拿到对话式数据集时,它会替你套上模型的聊天模板。哪些 token 计入损失,取决于数据集类型。
系统消息和用户消息只是你不想让模型照抄的固定套话时,设置 assistant_only_loss=True。它要求聊天模板带有 {% generation %} 标记。TRL 会为 Qwen3 这类已知模型家族修补模板,其他模型要自己检查,文档里是这么说的。
起步用多少条样本
OpenAI 的指南把下限定为 10 条,并报告说从 50 到 100 条开始见到改进。它的建议是从这个区间的低端起步,用精心写好的示范,评估之后,分数上升了再加。第一批样本毫无变化,说明该重新想任务或提示词了。要加训练数据,等那之后再说。
LIMA 的作者在 2023 年走了另一条路,为 65B 的 LLaMA 模型精选了 1,000 条样本,这说明一小批精心挑选的数据就能教会一种格式。你自己要多少条,由第一次评估的结果来定,而不是照搬这两个数字,所以先把测试文件写好,再决定要不要多加样本。
来源:OpenAI: Supervised fine-tuning (2026年11月6日 查阅);Hugging Face Papers: LIMA: Less Is More for Alignment (2026年11月6日 查阅)
训练之前先检查文件
格式错误,现在找出来只要几秒,等训练开始一个小时后才发现就亏大了。OpenAI 的食谱已被标为存档,是按当年的聊天格式写的,不过它的检查仍然覆盖了聊天文件的基本要求。下面的循环数七类错误:条目不是对象、缺少 messages 列表、消息缺少 role 或 content、出现不认识的键、角色不认识、内容为空,以及一条样本里没有助手消息。
文件没问题时,脚本会打印 "No errors found"。把它点名的每类错误都改掉,再跑一遍。
import json
from collections import defaultdict
with open("all.jsonl", "r", encoding="utf-8") as f:
dataset = [json.loads(line) for line in f]
# Format error checks
format_errors = defaultdict(int)
for ex in dataset:
if not isinstance(ex, dict):
format_errors["data_type"] += 1
continue
messages = ex.get("messages", None)
if not messages:
format_errors["missing_messages_list"] += 1
continue
for message in messages:
if "role" not in message or "content" not in message:
format_errors["message_missing_key"] += 1
if any(k not in ("role", "content", "name", "function_call", "weight") for k in message):
format_errors["message_unrecognized_key"] += 1
if message.get("role", None) not in ("system", "user", "assistant", "function"):
format_errors["unrecognized_role"] += 1
content = message.get("content", None)
function_call = message.get("function_call", None)
if (not content and not function_call) or not isinstance(content, str):
format_errors["missing_content"] += 1
if not any(message.get("role", None) == "assistant" for message in messages):
format_errors["example_missing_assistant_message"] += 1
if format_errors:
print("Found errors:")
for k, v in format_errors.items():
print(f"{k}: {v}")
else:
print("No errors found")对照 max_length 数 token
模型读的是 token,训练器会在 max_length 处截断每条样本。TRL 的 SFTConfig 里默认是 1024,截断保留序列开头,所以被切掉的是结尾,也就是回答所在的位置。TRL 的显存页面还说,这个值太小,会丢掉相当一部分 token,太大则可能让显存暴涨。
下面的代码是本课自己写的。它用模型的聊天模板把每条样本转成文本,数出 token,告诉你有多少条超出限制。
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
limit = 1024 # SFTConfig's default max_length
too_long = 0
for ex in dataset:
text = tokenizer.apply_chat_template(ex["messages"], tokenize=False)
if len(tokenizer(text, add_special_tokens=False)["input_ids"]) > limit:
too_long += 1
print(too_long, "of", len(dataset), "examples are over", limit, "tokens")先留出测试集
OpenAI 的准确率指南建议留出一份数据来发现过拟合,它的微调指南则说,要在开跑训练任务之前,从收集到的数据里把它划出来。下面的脚本同样是本课自己写的,它先去掉重复的提示词,免得同一条输入两边都有,然后再拆分。
train_test_split 在 Hugging Face Datasets 里默认会打乱数据,返回训练和测试两份。小文件的十分之一只剩寥寥几行测试数据,所以脚本留出五分之一。这个比例是本课自己选的。
调参期间不要碰 test.jsonl。第 4 课会在它上面给基础模型和微调后的模型打分。
import json
from datasets import Dataset
seen, rows = set(), []
for ex in dataset:
key = json.dumps(ex["messages"][:-1], sort_keys=True) # everything except the answer
if key not in seen:
seen.add(key)
rows.append(ex)
splits = Dataset.from_list(rows).train_test_split(test_size=0.2, seed=42)
splits["train"].to_json("train.jsonl")
splits["test"].to_json("test.jsonl")常见错误
- 用整齐的句子训练,用户发来的却是杂乱的邮件。
- 写出的回答需要基础模型没有的事实。Gekhman 等人在 2024 年用 PaLM 2-S 发现,这类样本学得慢,还会让幻觉变多。
- 最长的几条样本超过
max_length,训练器把它们的回答截掉了。 - 拆分之前先改写样本来扩充文件,结果测试行的近似副本混进了训练集。
- 一直对着测试文件改数据集,直到它不再是测试。
自己试一试
- 在
all.jsonl里先写几条样本,运行格式检查,直到它打印 "No errors found"。 - 加上长度检查,记下最长的那条样本。
- 把文件补到上面说的起步规模,运行拆分脚本,读一遍
test.jsonl里的行。 - 训练之前,先写下你在
test.jsonl上愿意接受的分数。
接下来
第 3 课用 TRL 在 train.jsonl 上训练一个 LoRA 适配器,并说明每项设置要占多少 GPU 显存。
自测一下
下面哪种布局是合格的 JSONL 训练文件?
查看答案
A. 每行一个 JSON 对象,每个对象带 messages 列表 OpenAI 和 TRL 都是每行读一个完整的 JSON 对象,用聊天格式的 messages 列表。
OpenAI 的微调指南建议,样本数量从多少起步?
查看答案
C. 50 条精心写好的示范,然后评估 指南建议从 50 条精心写好的样本起步,结果改善了才再加。
一条样本比 SFTConfig 的 max_length 1024 还长,TRL 默认怎么处理?
查看答案
D. 切掉样本的结尾,这可能把回答也切掉 截断保留序列的开头,所以被切掉的是结尾,也就是回答所在的位置。
为什么要在训练之前拆出测试文件?
查看答案
B. 这样分数是在模型从没见过的样本上量出来的 OpenAI 建议留出一份数据来发现过拟合,这只有在模型没训练过的行上才成立。
常见问题
微调大模型需要多少条样本?
够让你的测试分数动起来就行。OpenAI 的指南说,先用一小批精心写好的样本,评估之后,分数上升了再加。一小批样本毫无变化,就先重新想任务。
微调数据需要什么格式?
对话模型用 JSONL,每行一个 messages 列表,每条消息带 role 和 content。TRL 也接受提示词加补全的配对,语言建模还可以用纯 text。
能用 CSV 文件代替 JSONL 吗?
Datasets 也能读 CSV,带提示词和补全两列的 CSV,符合 TRL 的标准提示词加补全格式。OpenAI 的微调指南要求 JSONL,嵌套的消息列表放在 JSONL 里更合适。
每条样本都要带系统提示词吗?
线上发了才带。措辞也要和线上一致。按 OpenAI 的说法,样本和线上模型看到的内容不一样,是常见的坑。
测试集该有多大?
资料里没有给出具体大小。OpenAI 建议留出一份数据来发现过拟合。数据集还小的时候,本课留出五分之一,这样分数建立在不止寥寥几行之上。
可以用我的聊天记录来训练吗?
可以,清理之后。OpenAI 把日志数据列为好来源,并介绍了 prompt baking:试运行期间记录提示词的输入和输出,再筛成训练集。先去掉个人信息和不好的回复。
资料来源
- OpenAI: Supervised fine-tuning https://developers.openai.com/api/docs/guides/supervised-fine-tuning
- OpenAI: Optimizing LLM Accuracy https://developers.openai.com/api/docs/guides/optimizing-llm-accuracy
- OpenAI Cookbook: Data preparation and analysis for chat model fine-tuning https://developers.openai.com/cookbook/examples/chat_finetuning_data_prep
- Hugging Face TRL: Dataset formats and types https://huggingface.co/docs/trl/dataset_formats
- Hugging Face TRL: SFT Trainer https://huggingface.co/docs/trl/sft_trainer
- Hugging Face TRL: Reducing memory usage https://huggingface.co/docs/trl/reducing_memory_usage
- Hugging Face Datasets: Load https://huggingface.co/docs/datasets/loading
- Hugging Face Datasets: Process https://huggingface.co/docs/datasets/process
- Hugging Face Papers: LIMA: Less Is More for Alignment https://huggingface.co/papers/2305.11206
- Hugging Face Papers: Does Fine-Tuning LLMs on New Knowledge Encourage Hallucinations? https://huggingface.co/papers/2405.05904
