# 微调数据集：建好、检查并拆分你的训练数据

> 微调数据集是一个由示例对话组成的文件，每行一条，最后一条消息就是你想让模型给出的回答。先写大约 50 条贴近真实的样本，用脚本检查文件，再留一部分作测试集。质量比数量重要：LIMA 论文只用 1,000 条精心挑选的样本做微调。

Mindket 编辑部 · 发布于 2026-11-06 · 更新于 2026-11-06 · https://mindket.com/cn/learn/fine-tuning/fine-tuning-dataset

## 要点

- 第一批样本从真实输入写起，每条配上你愿意直接上线的回答。
- 第一步训练之前，先做格式检查和 token 计数。
- 先把测试文件拆出来，到最后打分时才动它。
- 训练时的提示词形状，要和应用实际发送的一致。

## 开始之前

你需要 Python、`datasets` 库（`pip install datasets`），以及第 1 课得到的那些行为行。下面的步骤需要一批真实的样本。现在就动笔。这个任务好不好学，要等样本写出来才看得出。

## 一行一条样本

微调数据是一个 JSONL 文件：每行一个完整的 JSON 对象，行与行之间不加逗号，外面也没有列表。文件里不放别的东西。OpenAI 的[微调指南](https://developers.openai.com/api/docs/guides/supervised-fine-tuning)要求用 chat completions 格式，也就是一个 `messages` 列表，列表里每个对象都有 `role` 和 `content`。第 3 课用来训练的 TRL 库，会把同样的布局当作对话式数据集来读。

第一行取自 OpenAI [数据准备食谱](https://developers.openai.com/cookbook/examples/chat_finetuning_data_prep)，第二行是本课自己写的，对应第 1 课的工单标题任务。Hugging Face Datasets 用 `load_dataset("json", data_files="all.jsonl")` 读取这样的文件，它的[文档](https://huggingface.co/docs/datasets/loading)称 JSON Lines 是最高效的 JSON 布局。

````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"}]}
````

1. 把每条样本存成一行，放进名为 `all.jsonl` 的文件。每行是一个 `messages` 列表，以你想要的助手消息结尾。
2. 系统消息和用户消息的措辞，要沿用应用在线上实际发送的写法。[准确率指南](https://developers.openai.com/api/docs/guides/optimizing-llm-accuracy)把格式上和线上输入略有出入的训练数据，列为最常见的坑之一。
3. 第一批自己写，手头有日志和专家答案的话，也可以从中取。OpenAI 把历史数据、专家数据和日志数据列为好来源，并要求问题和回答具体、清楚。

## TRL 怎样读这个文件

TRL 的 [SFT 训练器](https://huggingface.co/docs/trl/sft_trainer)接受两种数据集类型，语言建模和提示词加补全，每种都分标准（纯文本）和对话两种形式。拿到对话式数据集时，它会替你套上模型的聊天模板。哪些 token 计入损失，取决于数据集类型。

系统消息和用户消息只是你不想让模型照抄的固定套话时，设置 `assistant_only_loss=True`。它要求聊天模板带有 `{% generation %}` 标记。TRL 会为 Qwen3 这类已知模型家族修补模板，其他模型要自己检查，文档里是这么说的。

| 布局 | 一行长什么样 | 损失计在哪里 |
| --- | --- | --- |
| 对话式语言建模 | `{"messages": [{"role": "user", ...}, {"role": "assistant", ...}]}` | 默认计整个序列，设置 `assistant_only_loss=True` 后只计助手消息 |
| 对话式提示词加补全 | `{"prompt": [{"role": "user", ...}], "completion": [{"role": "assistant", ...}]}` | 默认只计补全部分 |

## 起步用多少条样本

OpenAI 的指南把下限定为 10 条，并报告说从 50 到 100 条开始见到改进。它的建议是从这个区间的低端起步，用精心写好的示范，评估之后，分数上升了再加。第一批样本毫无变化，说明该重新想任务或提示词了。要加训练数据，等那之后再说。

[LIMA](https://huggingface.co/papers/2305.11206) 的作者在 2023 年走了另一条路，为 65B 的 LLaMA 模型精选了 1,000 条样本，这说明一小批精心挑选的数据就能教会一种格式。你自己要多少条，由第一次评估的结果来定，而不是照搬这两个数字，所以先把测试文件写好，再决定要不要多加样本。

**资料里提到的样本数量** 训练样本条数，出自 OpenAI 的微调指南和 LIMA 论文。金色那一条是 OpenAI 建议的起点。

- OpenAI：下限: 10 条
- OpenAI：建议的起点: **50 条**
- OpenAI：见到收益的区间上端: 100 条
- LIMA：它精选的数据集: 1,000 条

来源：[OpenAI: Supervised fine-tuning](https://developers.openai.com/api/docs/guides/supervised-fine-tuning) (2026-11-06 查阅)；[Hugging Face Papers: LIMA: Less Is More for Alignment](https://huggingface.co/papers/2305.11206) (2026-11-06 查阅)

## 训练之前先检查文件

格式错误，现在找出来只要几秒，等训练开始一个小时后才发现就亏大了。OpenAI 的食谱已被标为存档，是按当年的聊天格式写的，不过它的检查仍然覆盖了聊天文件的基本要求。下面的循环数七类错误：条目不是对象、缺少 `messages` 列表、消息缺少 `role` 或 `content`、出现不认识的键、角色不认识、内容为空，以及一条样本里没有助手消息。

文件没问题时，脚本会打印 "No errors found"。把它点名的每类错误都改掉，再跑一遍。

````python
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 的[显存页面](https://huggingface.co/docs/trl/reducing_memory_usage)还说，这个值太小，会丢掉相当一部分 token，太大则可能让显存暴涨。

下面的代码是本课自己写的。它用模型的聊天模板把每条样本转成文本，数出 token，告诉你有多少条超出限制。

````python
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](https://huggingface.co/docs/datasets/process) 里默认会打乱数据，返回训练和测试两份。小文件的十分之一只剩寥寥几行测试数据，所以脚本留出五分之一。这个比例是本课自己选的。

调参期间不要碰 `test.jsonl`。第 4 课会在它上面给基础模型和微调后的模型打分。

````python
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 等人](https://huggingface.co/papers/2405.05904)在 2024 年用 PaLM 2-S 发现，这类样本学得慢，还会让幻觉变多。
- 最长的几条样本超过 `max_length`，训练器把它们的回答截掉了。
- 拆分之前先改写样本来扩充文件，结果测试行的近似副本混进了训练集。
- 一直对着测试文件改数据集，直到它不再是测试。

## 自己试一试

- 在 `all.jsonl` 里先写几条样本，运行格式检查，直到它打印 "No errors found"。
- 加上长度检查，记下最长的那条样本。
- 把文件补到上面说的起步规模，运行拆分脚本，读一遍 `test.jsonl` 里的行。
- 训练之前，先写下你在 `test.jsonl` 上愿意接受的分数。

## 接下来

[第 3 课](https://mindket.com/cn/learn/fine-tuning/lora-fine-tuning)用 TRL 在 `train.jsonl` 上训练一个 LoRA 适配器，并说明每项设置要占多少 GPU 显存。

## 自测一下

1. 下面哪种布局是合格的 JSONL 训练文件？

   A. 每行一个 JSON 对象，每个对象带 messages 列表
   B. 一个装着所有样本的 JSON 数组
   C. 带提示词和回答两列的 CSV
   D. 每条样本一个标题的 Markdown 文件

   答案：A。 OpenAI 和 TRL 都是每行读一个完整的 JSON 对象，用聊天格式的 messages 列表。

2. OpenAI 的微调指南建议，样本数量从多少起步？

   A. 10 条，也就是下限
   B. 1,000 条，像 LIMA 那样
   C. 50 条精心写好的示范，然后评估
   D. 10,000 条，图个保险

   答案：C。 指南建议从 50 条精心写好的样本起步，结果改善了才再加。

3. 一条样本比 SFTConfig 的 max_length 1024 还长，TRL 默认怎么处理？

   A. 把这条样本从数据集里去掉
   B. 报错停止
   C. 把样本拆成两条
   D. 切掉样本的结尾，这可能把回答也切掉

   答案：D。 截断保留序列的开头，所以被切掉的是结尾，也就是回答所在的位置。

4. 为什么要在训练之前拆出测试文件？

   A. 这样训练跑得更快
   B. 这样分数是在模型从没见过的样本上量出来的
   C. 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) (2026-11-06 查阅)
- [OpenAI: Optimizing LLM Accuracy](https://developers.openai.com/api/docs/guides/optimizing-llm-accuracy) (2026-11-06 查阅)
- [OpenAI Cookbook: Data preparation and analysis for chat model fine-tuning](https://developers.openai.com/cookbook/examples/chat_finetuning_data_prep) (2026-11-06 查阅)
- [Hugging Face TRL: Dataset formats and types](https://huggingface.co/docs/trl/dataset_formats) (2026-11-06 查阅)
- [Hugging Face TRL: SFT Trainer](https://huggingface.co/docs/trl/sft_trainer) (2026-11-06 查阅)
- [Hugging Face TRL: Reducing memory usage](https://huggingface.co/docs/trl/reducing_memory_usage) (2026-11-06 查阅)
- [Hugging Face Datasets: Load](https://huggingface.co/docs/datasets/loading) (2026-11-06 查阅)
- [Hugging Face Datasets: Process](https://huggingface.co/docs/datasets/process) (2026-11-06 查阅)
- [Hugging Face Papers: LIMA: Less Is More for Alignment](https://huggingface.co/papers/2305.11206) (2026-11-06 查阅)
- [Hugging Face Papers: Does Fine-Tuning LLMs on New Knowledge Encourage Hallucinations?](https://huggingface.co/papers/2405.05904) (2026-11-06 查阅)

[浏览 MCP 服务器](https://mindket.com/cn/mcp-servers)
