`history` 里的历史回复也会被模型学习
本文所有事实以
hiyouga/LlamaFactory官方仓库 2026-08-09 的内容为准,来源是data/README.md(共 475 行)与src/llamafactory/hparams/data_args.py。我们没有安装、训练或部署过任何模型,文中所有默认值均为仓库里写着的值。
准备 Alpaca 格式的监督微调数据时,history 这一列最容易被想当然。多数人的默认理解是:它像推理时传给模型的历史对话,只是给当前这一轮提供上下文。data/README.md 的原文不是这么写的——它在监督微调数据集这一节明确写了历史里的 response 也会被模型学习(原文:the responses in the history will also be learned by the model)。
这句话藏在 Alpaca 格式 Supervised Fine-Tuning Dataset 小节的字段说明里,位置不显眼,但它直接改变你怎么组织数据。这篇就把这一句拆开讲:它的准确范围是什么、ShareGPT 格式那边的对应说法是什么、还有哪些开关和它落在同一条轴上、以及你手上的数据该怎么自查。
先把这句话所在的上下文摆完整
Alpaca 格式的监督微调数据集,data/README.md 给的字段规格是这样的:
[
{
"instruction": "user instruction (required)",
"input": "user input (optional)",
"output": "model response (required)",
"system": "system prompt (optional)",
"history": [
["user instruction in the first round (optional)", "model response in the first round (optional)"],
["user instruction in the second round (optional)", "model response in the second round (optional)"]
]
}
]
配套的规则原文里,与这几列直接相关的有四条:instruction 列会与 input 列拼接作为用户提示词,即用户提示词是 instruction\ninput;output 列代表模型响应;system 列若指定会用作 system prompt;history 列是一个由字符串二元组组成的列表,表示历史消息里的 prompt-response 对。第四条后面紧跟的那句注意事项,就是本文的题眼。
也就是说,history 里每个二元组的第二个元素——上一轮的模型回复——在监督微调时的身份和 output 是同一类的,都是被学习的目标,而不是只读的上下文。
这条规则实际牵动什么
它牵动的是你数据的来源与清洗口径,而不是某个参数该设多少。
最常见的一种数据来源是导出既有的多轮对话日志:把最后一轮拆成 instruction / output,前面几轮原封不动塞进 history。如果历史里那些旧回复来自一个你并不打算模仿的模型、或者是被用户否掉过的答案、或者只是随手糊过去的应付式回复,那么按 data/README.md 的说法,这些内容也会进入学习范围。你以为自己只在教最后一轮,实际上前几轮也一起在教。
反过来,如果你的历史回复本身就是你想要的风格样本,那这条规则就不是坑,而是省事——多轮里的每一段好回复都不用再拆成独立样本。
**该不该往 history 里放东西、放多少轮,取决于你的数据和目标,官方没有给通用值,我们也不替你定。**这篇能给你的只是一条判断依据:先确认这一列会被学习,再决定往里放什么。
另外一个容易连带出问题的点是长度。hparams/data_args.py 里 cutoff_len 的默认值是 2048,历史轮次是要占这个长度预算的。至于你的数据该把 cutoff_len 设成多少,同样没有通用答案,取决于你的样本分布和硬件。
ShareGPT 格式那边是另一套说法
如果你用的是 ShareGPT 格式,那就根本没有 history 这一列——多轮全部放在 conversations 列里,以对象列表的形式表达。data/README.md 对这一侧写了两条硬规则:
- 位置约束:human 与 observation 必须出现在奇数位置,gpt 与 function 必须出现在偶数位置。
- 学习范围:gpt 和 function 会被模型学习。
[
{
"conversations": [
{ "from": "human", "value": "user instruction" },
{ "from": "function_call", "value": "tool arguments" },
{ "from": "observation", "value": "tool result" },
{ "from": "gpt", "value": "model response" }
],
"system": "system prompt (optional)",
"tools": "tool description (optional)"
}
]
把两边并排看,结论是一致的:不管你用 Alpaca 的 history 还是 ShareGPT 的 conversations,多轮里的助手侧内容都在学习范围内,区别只是这个事实被写在文档的哪一句里。ShareGPT 多出来的一层是 function——工具调用的参数同样被明确列为会被学习的角色,这一点在做 function calling 数据时值得单独盯一眼。
顺带说一个结构性的差异,两种格式的小节几乎一一对应(各 7 个),差别是 ShareGPT 多了 OpenAI Format 一节,而且 ShareGPT 的 Pre-training 一节原文只有一句 Not yet supported, please use the alpaca format. 这是一处明确写出来的能力边界。
同一条轴上还有几个开关
“哪部分内容参与损失计算”这条轴上,hparams/data_args.py 里还有几个字段,这里只列名字和默认值:
| 参数 | 默认值 |
|---|---|
train_on_prompt | False |
mask_history | False |
ignore_pad_token_for_loss | True |
enable_thinking | True |
preserve_thinking | False |
cutoff_len | 2048 |
这张表放在这里不是让你照着改的。它的用处是:当你发现训练出来的行为和预期对不上、怀疑”是不是学错了东西”时,这几个名字构成了一份检查清单——先确认它们当前各是什么值,再去翻文档确认语义,而不是凭猜测调一遍。data_args.py 里数据侧的参数远不止这六个,完整清单我们另有一篇专门讲。
其中和”被学习的内容”关系最直接、也最容易踩的是 enable_thinking。data/README.md 有一段 TIP 写得很细,逐条转述如下:
- 若模型有推理能力(例如 Qwen3)但数据集不含 CoT,会自动补一个空 CoT。(这段原文里用的是带连字符的旧写法 LLaMA-Factory,那是仓库内新旧名并存的残留,指的就是同一个项目。)
enable_thinking为True(慢思考,默认值)时,空 CoT 会被加到模型响应里,并计入损失计算。- 否则(快思考)空 CoT 会被加到用户提示词里,损失计算会忽略它。
- 训练与推理时
enable_thinking必须保持一致。 - 若想让含 CoT 的数据走慢思考、不含 CoT 的数据走快思考,可以把
enable_thinking设为None;但原文明确提醒这个特性相对复杂,使用需谨慎(should be used with caution)。
同一个空 CoT,放在响应里就计入损失,放在提示词里就被忽略——这和 history 那条注意事项是同一类问题:**内容放在样本的哪个位置,决定了它是不是训练目标。**顺便一提,如果你的数据本身带思维链,原文要求 CoT 放在模型响应里,形如 <think>cot</think>output。
怎么自查:四个可执行动作
第一步,确认这列到底有没有映射进去。 dataset_info.json 的 columns 规格里,history 字段的默认值是 None——按字段规格,默认状态下没有任何列被指定为 history,要用这一列就得在 dataset description 里显式写出映射。Alpaca 侧的完整映射写法原文是:
"dataset_name": {
"file_name": "data.json",
"columns": {
"prompt": "instruction",
"query": "input",
"response": "output",
"system": "system",
"history": "history"
}
}
打开你自己的 dataset_info.json,看这段里有没有 "history" 那一行。有,才轮到担心本文这件事。
第二步,数一遍历史里的助手侧内容。 下面是一段通用自查片段,不是官方脚本,只做统计不改数据:
import json
d = json.load(open("data.json", encoding="utf-8"))
hist_resp = [h[1] for s in d for h in s.get("history", [])]
print(len(d), "条样本,历史回复", len(hist_resp), "段")
Linux / macOS 上一般是 python3 self_check.py,Windows 上通常是 python self_check.py;Windows 写路径时如果直接用反斜杠,记得在 JSON 与 Python 字符串里做转义或改用正斜杠——这是通用做法,不是该项目文档的内容。数出来的这个”历史回复段数”,就是你在最后一轮之外额外教给模型的内容量。
第三步,确认文件放对了地方。 dataset_info.json 必须放在 dataset_dir 目录下。这里有一处口径差异可以顺手记下:data/README.md 写的默认值是 ./data,而 hparams/data_args.py 里 dataset_dir 的默认值字符串是 "data"。两处写法不同,以仓库当前状态为准。另外,允许的数据文件类型是 json、jsonl、csv、parquet、arrow 这五种。
第四步,什么情况说明问题不在这儿。 三种情形可以直接排除本文这条规则:
- 你用的是 ShareGPT 或 OpenAI 格式——那没有
history这个概念,该去核对的是奇偶位置约束,以及 gpt / function 的学习范围; - 你的 dataset description 里根本没写
history那一行——按字段规格它的默认值就是None,也就是没有任何列被指定为历史列; - 你做的是预训练而不是监督微调——Alpaca 的预训练数据集只用
text列参与学习,history不在这条链路上;顺带,val_size默认是0.0,即默认不切验证集,所以”训练看起来正常但没有验证信号”是另一回事,别混在一起排查。
排除完这三条,还是觉得模型学到了不该学的东西,那就得往别的方向查了——本文只负责把 history 这一格钉死。
一句话收束
history 不是上下文摆设,是训练目标的一部分;ShareGPT 那边的 gpt 与 function 同理;空 CoT 放响应还是放提示词,决定它计不计入损失。这三件事都写在 data/README.md 里,只是分散在三处,没连起来读就容易漏。关于 dataset_info.json 的完整字段规格、ShareGPT 的 tags 机制怎么适配任意字段命名,我们各有专门篇目讲。
本文依据 LlamaFactory 官方仓库(github.com/hiyouga/LlamaFactory)的 README、data/README.md、examples/ 下的配置与 src/llamafactory/hparams/ 的参数定义整理,核对日 2026-08-09。本文内容为仓库源码与文档口径,我们没有安装、训练或部署过任何模型,文中显存数字均为官方标注的估算值(README 原文标 * estimated)而非实测占用。参数与默认值随版本变动,请以 llamafactory-cli train -h 的实际输出为准。