2.1 指令数据的形态:单轮、多轮与系统提示


2.1 指令数据的形态:单轮、多轮与系统提示

本节摘要:本节给全书定一个数据契约:SFT 数据统一存成 JSONL,每行一个 messages 列表,消息按 role(system/user/assistant)与 content 组织。先看单轮问答的最小样例,再展开多轮对话(模型要学"结合上下文回答",且每一轮 assistant 回答都算 loss),最后是 system 提示(定义人设与规则,通常不算 loss)。本节同时清点四种最常见的脏数据——它们是"SFT 之后模型突然不会说话"的第一嫌疑人。本章所有后续管线(2.2 清洗、2.3 蒸馏)都产出这一格式。

学习目标

阅读完本节,你应当能够:

  1. 手写三种形态的标准 JSONL 样例,解释每个字段的语义。
  2. 说明多轮对话里哪些 token 参与训练、哪些不参与(为 3.1 预热)。
  3. 用一段脚本校验自己的数据是否符合契约。

一、数据契约:messages 列表

SFT 数据的行业通用形态(OpenAI 的聊天 API 格式,也是 HuggingFace 对话数据集的事实标准):每行一个 JSON 对象,核心是 messages 数组:

{"messages": [{"role": "user", "content": "中国最长的河流是哪一条?"}, {"role": "assistant", "content": "中国最长的河流是长江,全长约 6300 公里。"}]}

字段语义:

字段 取值 语义
role system / user / assistant 说话者身份;assistant 是模型要学着扮演的角色
content 字符串 消息正文
messages 数组 按时间排序的完整对话

为什么统一到这个格式?因为模板是数据的服务对象:第 4 章的 chat template 负责把这份结构序列化成 token 序列(加 special tokens、标边界),数据侧只管把语义装进统一容器,格式问题不和数据问题搅在一起。

二、三种形态

形态一:单轮(最小单元,上面即是)。适合问答、知识点解释类任务。模型学到的行为:见到一个 user 问题 → 给出一条回答 → 停。

形态二:多轮

{"messages": [ {"role": "user", "content": "帮我写一首关于秋天的四行小诗。"}, {"role": "assistant", "content": "梧桐叶落满阶前,雁字南飞九月天。一盏清茶温旧事,半窗斜照枕书眠。"}, {"role": "user", "content": "第二句里的\"雁字\"是什么意思?"}, {"role": "assistant", "content": "\"雁字\"指大雁飞行时排成的\"一\"字或\"人\"字形队列,古诗中常借它点出秋日南飞的时令感。"} ]}

多轮数据教两件事:指代消解("第二句"指的是上面那首诗)与角色恒定(assistant 不会中途变成提问的人)。注意训练时每一轮 assistant 回答都要算 loss(3.1 的掩码按每条消息的 role 分界,而不是只看最后一轮)。

形态三:带 system 提示

{"messages": [ {"role": "system", "content": "你是一个简洁的中文科普助手,回答不超过三句话。"}, {"role": "user", "content": "黑洞是什么?"}, {"role": "assistant", "content": "黑洞是引力强到光也无法逃逸的致密天体。它由大质量恒星坍缩形成。边界称为事件视界。"} ]}

system 消息定义人设、规则与禁区,通常放在数组首位。训练时 system 与 user 一样只算"条件"、不算 loss——模型不需要学"怎么写系统提示",只需要学"在给定系统提示下怎么回答"。nanochat 官方做法(发布帖口径)甚至更简:实验规模下直接用 user/assistant 两种角色起步,system 是数据规模上去之后的自然扩展。

💡 实践建议:124M 的小模型对 system 提示的遵循能力有限——与其指望它"扮演"复杂人设,不如把人设要点直接写进部分 user 消息。数据形态要匹配模型容量。

三、契约校验脚本

脏数据是 SFT 的头号隐患。四种最常见的坏法:role 拼写错误、assistant 消息缺失(user 连着 user)、空 content、首条不是 system/user。一段校验脚本全拦下:

# check_messages.py —— 校验 messages 契约(写法示意) import json ALLOWED_ROLES = {"system", "user", "assistant"} def validate(obj: dict, idx: int) -> list[str]: errs, msgs = [], obj.get("messages", []) if not msgs: return [f"line {idx}: messages 为空"] if msgs[0]["role"] not in ("system", "user"): errs.append(f"line {idx}: 首条消息角色应为 system/user") for i, m in enumerate(msgs): if m.get("role") not in ALLOWED_ROLES: errs.append(f"line {idx}: 第 {i} 条 role 非法 {m.get('role')!r}") if not m.get("content", "").strip(): errs.append(f"line {idx}: 第 {i} 条 content 为空") if msgs[-1]["role"] != "assistant": errs.append(f"line {idx}: 最后一条不是 assistant 回答") return errs if __name__ == "__main__": bad = 0 with open("data_sft/train.jsonl", encoding="utf-8") as f: for i, line in enumerate(f): errs = validate(json.loads(line), i) bad += len(errs) for e in errs[:3]: print(e) print(f"校验完成:{bad} 处问题")

最后一条必须是 assistant 的理由:序列的"学习段"以 assistant 消息收尾,末尾挂一条 user 意味着模型被要求预测"用户接下来说什么"——除非你刻意训练用户模拟器,否则这是脏数据。

四、从 JSONL 到 token 数:长度预算

数据定形后立刻量长度(用 1.2 验收过的分词器):

# length_stats.py —— 统计 token 长度分布(写法示意) import json from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained("base/tokenizer") lens = [ len(tok.apply_chat_template(json.loads(l)["messages"], tokenize=True)) for l in open("data_sft/train.jsonl", encoding="utf-8") ] lens.sort() n = len(lens) print(f"p50={lens[n//2]} p95={lens[int(n*0.95)]} max={lens[-1]}")

apply_chat_template 在第 4 章展开,这里先用它拿到"模板渲染后的真实长度"。记下 p95:它是第 3 章 max_seq_len 的定价依据——截在 p95 意味着只牺牲 5% 的长样本。注意 nanochat 的小词表(约 2000)会把同样文本切成比主流分词器更长的序列,长度预算要按小词表口径重新量。

本节要点回顾

  1. 契约:JSONL + messages 数组(role/content),assistant 消息是学习目标,system/user 是条件。
  2. 多轮教指代与角色恒定,每轮 assistant 都算 loss;system 消息不算 loss。
  3. 四种脏数据(role 错、缺 assistant、空 content、首条非法)用校验脚本前置拦截;p95 长度决定第 3 章的 max_seq_len

容器定形了,往里装什么?2.2 节先走最经济的路线——把现成的开源指令集挑干净。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U