跳转至

单文件科学对话数据

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:小学科学是常识,基座模型早就知道,为什么还要事实卡?

确实,这些常识基座模型大多知道。只论“知识从哪来”,事实卡不是必需的。仍然要它,原因不在补知识:

  1. 让训练只学“怎么说”。 loss 算在参考回答上,而参考回答里既有说法,也有事实内容。输入里没有资料时,模型只能靠权重把“转向太阳”“背光的一面”这些内容硬记下来,训练会把“说法”和“知识”混在一起学;资料就在输入里时,内容照着资料说就能预测对,LoRA 有限的容量主要花在“短句、先纠正误解、给一个安全的小观察”上。原理见问 3。
  2. 基座的知识检查不了、也改不了,事实卡可以。 你说不准一个 4B 模型会在哪道题上出错(比如把季节归因于离太阳远近),错了也没法打开权重去改。事实卡是一段看得见的文字,有来源、有审核状态,写错了改一张卡,下一次请求就生效。类比后端:业务规则放在配置或数据库里,而不是散落在编译好的二进制里。
  3. 卡上有基座不会主动遵守的边界。 例如 day-night 的 text 写明“假定取消自转但保留公转,光照位置仍会随公转缓慢变化”,参考对话第二轮的追问就以它为依据;scope 写明“模型和比喻不能替代真实测量”;kind 区分 fact、legend、current,传说不能当事实讲。
  4. 这就是真实产品的形态,也是评估的依据。 业务里模型要讲的往往是它根本不知道的东西:产品规格、本校课本、当天的数据。常见做法是调用方查到资料放进提示词,模型负责讲清楚,也就是常说的 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 条样本反复强化的是“听到问题,凭空给出确定结论”;带卡时,目标里的内容都能在输入里找到,反复强化的是“从给的资料里取内容,改写成孩子听得懂的话”。

如果训练时不放、只在部署时放,会出现四个问题:

  1. 模型没练过看资料。 部署时多出一大段 CURRENT REFERENCE FACTS,对它是陌生输入,可能被忽略,照样凭记忆回答。
  2. 改资料不生效。 发现某张卡写错,或想在某张卡的 scope 里补一条适用边界,练法 A 的模型不会跟着变,只能改训练数据重新训练。
  3. 新主题更容易编。 练法 A 养成的是“凭空给结论”的习惯,遇到训练里没有的主题也照样自信地下结论。
  4. 输入格式对不上。 训练时提示词里没有事实卡,部署时有,模型学到的说话方式依赖它训练时见过的输入样子;这也是教程第 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;增加真实反馈路径要同时调整上下文,避免孩子引用了模型根本没说过的话。更新事实后复查所有引用它的对话。

python -m lab.data --input data/dataset.json --allow-draft

应输出 {'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 中;事实审核与儿童适读性审核仍未完成。