单文件科学对话数据¶
1 只维护一份 dataset.json¶
打开 data/dataset.json。所有资料、500 段训练对话和评估对话都在这个文件里。顶层有四个平级数组:facts 是参考事实卡,train、val、test 是三组完整对话。真正参与训练的只有 train;事实卡只作为上下文,不是学习目标。
| 集合 | 科学主题 | 完整对话 | 回答目标 | 用途 |
|---|---|---|---|---|
| train | 50 | 500 | 1150 | 更新 LoRA 参数 |
| val | 5 | 50 | 115 | 比较配置、选择候选 |
| test | 5 | 50 | 115 | 候选冻结后评估 |
训练学段分布:1–2 年级 167 段,3–4 年级 167 段,5–6 年级 166 段。每个主题 10 条交流路径,每段 2~3 轮;既有同一知识在不同学段的解释,也有不同反馈导致的后续变化。
data/dataset.json
├── schema_version / description 文件版本与说明
├── facts[60] 参考事实卡,只存一份,被对话按 id 引用
├── train[500] 训练对话:唯一参与参数更新的部分
├── val[50] 验证对话:比较配置、选候选、算 eval loss
└── test[50] 测试对话:候选冻结后才使用
train / val / test 里的每段对话结构相同:
├── id / group_id 对话编号 / 科学主题分组
├── grade_band 1-2 / 3-4 / 5-6
├── fact_ids 引用 facts 中的 id
└── turns[] user / assistant / behavior
对话属于哪个集合,只由它放在哪个数组决定,对话里不再写 split 字段,避免“放在 val 里却写着 train”这种自相矛盾。程序读入时(lab.data.read_dataset,Notebook 里是 tag_splits)会在内存里给每段对话补上 split,后面的筛选和跨集合检查都用它;源文件保持不变。要把一段对话挪到别的集合,就把整段剪切到另一个数组里,同一 group_id 的其他对话和它引用的事实卡要一起挪。
JSON 不支持 // 注释。这里解释字段,Notebook 用 D1~D7 标记实际使用位置。不要把整份 JSON 塞进提示词,也不要把评估对话一起传给训练器。
2 第一张事实卡¶
{
"id": "day-night",
"category": "space",
"topic": "昼夜",
"text": "自转使地球表面各处交替朝向和背向太阳,形成主要的昼夜变化。太阳没有按昼夜关闭发光。地球还绕太阳公转;假定取消自转但保留公转,光照位置仍会随公转缓慢变化。",
"source_url": "https://spaceplace.nasa.gov/seasons/en/",
"checked_at": "2026-09-29",
"source_review": "reference_read_claim_review_pending",
"scope": "昼夜的入门解释;模型和比喻不能替代真实测量。",
"kind": "fact",
"reuse": "科学事实摘要和教学对话为原创中文表达;来源仅供事实核对,不复制来源的教材段落、题目或图像,不把网页正文纳入训练。链接不表示来源机构认可本数据或授予网页内容的训练许可。"
}
| 字段 | 含义与用途 |
|---|---|
id |
主键,由对话 fact_ids 引用 |
category/topic |
分类及主题,供检索、统计和审核 |
text |
可作为回答依据的科学资料;进入上下文 |
kind/scope |
资料类型与适用范围;进入上下文 |
source_url |
人工回查来源的入口;程序不会自动上网阅读 |
checked_at/source_review |
pending 表示待核对;日期只代表已阅读参考入口,reference_read_claim_review_pending 表示逐项断言仍待审核 |
reuse |
来源署名和原创改写说明,不代表来源机构认可本数据 |
NASA、NOAA、USGS 等官方科普和 OpenStax 教材是参考入口。正文为原创中文整理;比喻、儿童反馈和观察设计为合成示范,不能宣称全部来自这些来源。URL 存在不等于每句话有来源支持,需要逐项核对。
为什么需要事实卡:三个常见疑问¶
微调教的是“怎么说”(教程第 0.1 节),初学者读到这里通常会接连冒出三个问题。
问 1:小学科学是常识,基座模型早就知道,为什么还要事实卡?
确实,这些常识基座模型大多知道。只论“知识从哪来”,事实卡不是必需的。仍然要它,原因不在补知识:
- 让训练只学“怎么说”。 loss 算在参考回答上,而参考回答里既有说法,也有事实内容。输入里没有资料时,模型只能靠权重把“转向太阳”“背光的一面”这些内容硬记下来,训练会把“说法”和“知识”混在一起学;资料就在输入里时,内容照着资料说就能预测对,LoRA 有限的容量主要花在“短句、先纠正误解、给一个安全的小观察”上。原理见问 3。
- 基座的知识检查不了、也改不了,事实卡可以。 你说不准一个 4B 模型会在哪道题上出错(比如把季节归因于离太阳远近),错了也没法打开权重去改。事实卡是一段看得见的文字,有来源、有审核状态,写错了改一张卡,下一次请求就生效。类比后端:业务规则放在配置或数据库里,而不是散落在编译好的二进制里。
- 卡上有基座不会主动遵守的边界。 例如
day-night的text写明“假定取消自转但保留公转,光照位置仍会随公转缓慢变化”,参考对话第二轮的追问就以它为依据;scope写明“模型和比喻不能替代真实测量”;kind区分 fact、legend、current,传说不能当事实讲。 - 这就是真实产品的形态,也是评估的依据。 业务里模型要讲的往往是它根本不知道的东西:产品规格、本校课本、当天的数据。常见做法是调用方查到资料放进提示词,模型负责讲清楚,也就是常说的 RAG(检索增强生成)。本项目用事实卡完整练一遍这个模式。盲评时,评分人也要对照事实卡判断每条断言有没有依据(教程第 6.6 节)。
问 2:事实卡也在训练样本里,那训练完模型就记住这些知识了吗?
不能这样理解。事实卡是训练样本的输入,不是学习目标。教程第 2.5 节那条 408 Token 的样本里,事实卡在 mask=0 的 386 个 Token 中:模型看得到它,但不会因为“没记住它”而被扣分。loss 只考一件事:看着这些资料,回答写得像不像参考回答。
| 训练后学到了吗 | 原因 | |
|---|---|---|
| 怎样读资料、怎样用资料组织回答 | 学到了 | 这是 loss 推着它学的东西 |
| 资料本身的内容和边界 | 不可靠 | 训练时资料一直摆在眼前,不需要记 |
需要说实话的一点:参考回答本身含有事实内容,这部分在 mask=1 的区域,模型会顺带记住一点常见说法。但只训 1 个 Epoch、每条只看一遍,记得很浅,也记不住卡上的完整内容和边界;你更没法知道它记住了哪些、记错了哪些。所以部署时每次都要传事实卡,教程第 5.5 节的调用示例先 read_dataset 读出 facts,就是这个原因。
想亲眼验证:训练完后用同一个问题问两次,一次带事实卡,一次去掉 CURRENT REFERENCE FACTS 那段,比较回答的准确性和边界。差别越明显,说明模型依赖的是当场给的资料,而不是背下来的知识。
问 3:既然不靠它背知识,训练时为什么还要放?部署时再传不行吗?
不行。训练不是要模型记住事实卡,而是要它学会使用事实卡;要学会使用,训练时就必须看见。用新员工培训打个比方:
| 练法 A:只给问答 | 练法 B:问答旁边附产品手册(本项目) | |
|---|---|---|
| 培训材料 | 客户问 X → 标准回复 Y | 客户问 X + 手册相关页 → 标准回复 Y |
| 练出来的习惯 | 听到问题,凭记忆背答案 | 先翻手册,再用标准口吻讲出来 |
| 上岗时递给他手册 | 没练过看手册,多半还背老答案 | 正是练过的流程 |
| 手册更新了一条 | 还按旧答案说 | 照新手册说 |
| 遇到没培训过的问题 | 凭印象硬答,容易编 | 照手册讲,手册没写的不乱说 |
换成本项目的同一条样本,目标回答完全相同,只是输入不同:
不带事实卡:
输入:为什么白天亮、晚上黑?
目标:地球在不停转,我们住的地方转向太阳就是白天……
带事实卡:
输入:[事实卡:自转使地球表面交替朝向和背向太阳……] 为什么白天亮、晚上黑?
目标:地球在不停转,我们住的地方转向太阳就是白天……
不带卡时,输入里没有任何依据,目标却是确定的科学结论,1150 条样本反复强化的是“听到问题,凭空给出确定结论”;带卡时,目标里的内容都能在输入里找到,反复强化的是“从给的资料里取内容,改写成孩子听得懂的话”。
如果训练时不放、只在部署时放,会出现四个问题:
- 模型没练过看资料。 部署时多出一大段
CURRENT REFERENCE FACTS,对它是陌生输入,可能被忽略,照样凭记忆回答。 - 改资料不生效。 发现某张卡写错,或想在某张卡的
scope里补一条适用边界,练法 A 的模型不会跟着变,只能改训练数据重新训练。 - 新主题更容易编。 练法 A 养成的是“凭空给结论”的习惯,遇到训练里没有的主题也照样自信地下结论。
- 输入格式对不上。 训练时提示词里没有事实卡,部署时有,模型学到的说话方式依赖它训练时见过的输入样子;这也是教程第 5.5 节强调“提示词要和训练时一致”的原因。
用代码的话说,训练是在学一个函数 answer = f(question, facts, grade_band, history)。如果训练时只给 question,学到的是 f(question);部署时再多传一个它从没用过的参数 facts,它不会自动去用。
不用事实卡也能训练、也能回答,代价就是上面这些:编错了改不了、容易过度自信、换到基座不懂的领域要推倒重来。把“事实”和“说法”分开是本项目的设计取舍,不是唯一做法;它的收益最终要靠教程第 6 章的对照来验证。
事实卡在哪里用到¶
为什么单独存一份。 60 张卡被 600 段对话按 fact_ids 引用(一段对话可引用多张),类似数据库里的外键:同一条事实只改一处,所有引用它的对话下次读入时都会用新内容。
为什么也要按集合隔离。 一张卡只能被同一个集合(train/val/test 之一)的对话引用。否则验证、测试题依据的资料,模型在训练时已经见过,评估结果会偏乐观。
| 环节 | 代码位置 | 怎样使用事实卡 |
|---|---|---|
| 读入校验(D1) | data.py validate |
id 唯一;字段齐全;source_url 为 https;kind 只能是 fact/legend/current;对话的 fact_ids 必须都存在;同一张卡不能跨 train/val/test |
| 拼提示词(D3) | common.py build_messages |
按 fact_ids 取出卡片,只保留 id/text/kind/scope,以 JSON 写在 system 提示词的 CURRENT REFERENCE FACTS: 之后;来源与审核字段不进入模型 |
| Notebook 训练与验证样本 | Notebook 第 2 步 prepare-data 格 |
1150 条 train 样本和 115 条 val 样本都经 build_messages 带上各自的事实卡 |
| Notebook 观察题 | Notebook 第 4 步 cases 格 |
三道观察题各指定一个 fact_id,训练前后用同一张卡对比回答 |
| 命令行快照 | prepare.py | 原样写入快照的 facts.jsonl;few-shot 示例记录它们的 fact_ids |
| A/B/C 对照推理 | infer.py | 从快照读 facts.jsonl,三组都附当前题目的同一张事实卡(第 6.4 节) |
| 盲评 | evaluate.py blind |
评分表带 fact_ids,评分人对照事实卡填写 fact_claims/supported(第 6.6 节) |
| 部署后调用 | 第 5.5 节调用示例 | read_dataset 读出 facts,messages_for(fact_id, ...) 拼出和训练相同的 system 提示词 |
3 第一段完整对话¶
{
"id": "day-night-01",
"group_id": "day-night",
"category": "space",
"topic": "昼夜",
"grade_band": "1-2",
"scenario": "repair_misconception",
"fact_ids": [
"day-night"
],
"provenance": {
"type": "synthetic",
"author": "Codex",
"date": "2026-09-29",
"method": "按主题逐项撰写知识、误解、追问、比喻边界和安全观察,再组合为10种教学对话路径;不是儿童真实对话,部分主题内回答复用。"
},
"review": {
"status": "draft",
"reviewer": "",
"reviewer_kind": "",
"notes": "待逐段核查科学事实、来源支持、适读学段、观察可行性、近重复与上下文衔接;成人审阅不代表已验证儿童探索兴趣。"
},
"turns": [
{
"user": "为什么白天亮、晚上黑?",
"assistant": "地球在不停转,我们住的地方转向太阳就是白天,转到背光的一面就是晚上。",
"behavior": "explain_at_grade"
},
{
"user": "太阳晚上关灯睡觉了吧?",
"assistant": "太阳没有在晚上关灯。是我们所在的地面转到了背向太阳的一侧。还可以接着想:如果地球不转,就没有黑夜了吗?",
"behavior": "repair_then_offer_specific_question"
}
]
}
孩子先问昼夜,再提出“太阳睡觉”的猜想。参考回答先解释现象,随后修正具体误解,并留下一个可选择继续思考的问题。不要只学“你真棒”等固定口头禅。
| 字段 | 是否进入模型、在哪里使用 |
|---|---|
id |
生成 day-night-01:0 等样本编号,供追踪 |
group_id |
同一科学主题统一分组;所有学段、路径、改写必须在同一 split |
| (所在数组) | 这段对话在 train 数组里;读入时标为 split="train",D2 只筛选 train;不写入提示词 |
grade_band |
D3 写入 system 中的“小学学段”,是可见输入 |
fact_ids |
D3 选择本文件中的事实卡,只传 id/text/kind/scope |
turns[].user |
当前孩子的问题;此前问题进入 history |
turns[].assistant |
当前回答是训练目标;此前回答仅作上下文 |
turns[].behavior |
供人工看本轮示范什么行为,不传给模型 |
category/topic/scenario |
统计与审核标签,不把“孩子错在哪里”偷塞进输入 |
provenance |
记录合成方法、作者与日期;不是儿童真实对话 |
review |
全部 draft;审核后填实际审核者、类型与修改记录 |
4 如何读、改和审核¶
先找 topic,再读 fact_ids、grade_band 和整段 turns。修改表达就改 assistant;增加真实反馈路径要同时调整上下文,避免孩子引用了模型根本没说过的话。更新事实后复查所有引用它的对话。
应输出 {'train': 500, 'val': 50, 'test': 50}。这是结构检查,不是科学审查。正式审核包括:来源是否支持事实、有没有过度拟人、孩子是否听得懂、比喻边界是否清楚、观察是否安全可行、是否尊重不提问和结束。
审核可以直接在这一个文件中进行,并用版本控制保留历史。只有真正审核过的条目才改 review.status="approved"、填写实际 reviewer/reviewer_kind/notes;不要批量改状态冒充审核。事实卡同步记录实际核对状态。之后重新运行不带 --allow-draft 的校验。
数据中的 10 类结构有意重复教学行为,同主题也复用部分知识性回答;仍需人工检查语义近重复和模板味。50 个 train 主题不是 500 个独立主题。val/test 与训练主题隔离,但仍共享光、力、生命等基础概念,不是知识绝对隔离。
2026-10-04 修订(仍为 draft):去掉 662 条回答中拼接句子时留下的半角空格,模型不会再学到“。 下一句”这种接缝;“多讲一点原因”(increase_depth_when_requested)的 60 条回答改为“机制 + 比方 + 比方的边界”,确实比上一轮更深入;“观察不一样”(ask_for_evidence_without_blame)的 60 条回答去掉了“我们刚才讨论的是……”套话,改为先核对条件;“邀请探索”的 60 条回答在追问前加“还可以想想:”。回答整体仍偏短(平均约 51 字),仍需人工审核。
5 修改后怎样生效¶
保存 JSON 后,Notebook 换一个新的 RUN_DIR,重新执行 D1 读取和 D2~D5 分词,再开始训练。已经在内存中的 rows 和已保存的 adapter 都不会自动变化。运行目录中的 dataset.json 是本次输入快照,不是第二份需要日常维护的源数据。
2 D1~D7:数据实际使用的位置¶
| 注释 | 位置与实际行为 |
|---|---|
| D1 | Notebook 第 2 步读 DATA_FILE,给 train/val/test 里的对话标上 split,校验事实卡引用与跨集合隔离 |
| D2 | 只选 train;expand_episode 逐轮保留过去历史 |
| D3 | build_messages 拼角色、grade_band、所选事实、history 和当前 user |
| D4 | render_pair 加聊天模板,把当前 assistant 与 EOS 标成目标 |
| D5 | 只将 input_ids/completion_mask 两列交给 SFTTrainer |
| D6 | 检查 TRL 实际 labels,上下文和 padding 必须为 -100 |
| D7 | 保存本次数据副本与指纹;trainer.train() 才真正更新参数 |
单一 dataset.json → train 对话 → 当前问题 + 已发生的历史
↓
角色 + 学段 + 对应事实 + history + user
↓
prompt / 当前 assistant + EOS
↓
input_ids + completion_mask
↓
labels → loss → 反向传播 → LoRA 参数更新
3 五个可见字段¶
rows 留有 case_id/prompt/completion/input_ids/completion_mask,方便查看。实际训练只收后两列。Notebook 会打印第一条的完整提示词、目标和真实 Token 长度;不要沿用旧场景的长度数字。
completion_mask=0 表示上下文,=1 表示当前回答及 EOS。TRL 将上下文和 padding 的 labels 设为 -100,不对这些位置直接计算损失,但模型预测答案时仍能看到上下文。
EOS 是 <|im_end|>,每个目标只出现一次,让模型学习结束;PAD 与 EOS 不同。超出 4096 Token 会报错,需改写样本,不悄悄截掉答案。
逐条科学来源在 facts[].source_url 中;事实审核与儿童适读性审核仍未完成。