0. 系列闭环

本篇位置 上游 本篇产出 下游
第 2/10 篇 第 01 篇场景定义 可训练的 JSONL 规范 第 05 篇 apply_chat_template · 第 08 篇 SYSTEM_PROMPT

数据错误会直接体现在 loss 上,但更隐蔽的是格式错误:模型学会了特殊 token 排列,却没学会共情。本篇所有规则都对齐 load_jsonl_data 的实际消费方式。


1. 要解决什么问题

SFT 不是「把文档塞进模型」,而是让模型在 (system, user) 条件下,生成接近标注 assistant 的 token 序列。

本项目的数据难点:

  1. 风格高度一致:assistant 普遍「先共情 + 再陪伴」,句式重复度高,适合小数据 SFT,也容易被 critic 说成「模板」——这是场景取舍,不是数据失误。
  2. user 必须口语化:均长约 20 字,像老人说话,不像问卷。
  3. system 必须固定:1000 条样本共用同一段 system(见 JSONL 第 1 行),与 verify_lora.py 第 33–36 行、vLLM 请求里的 system 保持一致。

2. 实现位置

文件 作用
LoRA_Demo/data/elderly_chat.jsonl 训练语料,1000 行
LoRA_Demo/train_lora_single.pyload_jsonl_data() 第 102–127 行,JSONL → Dataset({"text": ...})
LoRA_Demo/verify_lora.pySYSTEM_PROMPT 推理时必须与 JSONL system 一致
hexo-cli/docs/xx.jsonl 同源扩展集(语义训练集生成脚本产出,与 Demo 内文件 MD5 不同,结构相同)

训练日志显示加载 1000 条all_logs.log 第 31 行 1000/1000)。本地 wc -l 若为 999,应检查末尾是否缺换行,避免少训一条而不自知。


3. JSONL 单条规范

3.1 结构

1
2
3
4
5
6
7
{
"messages": [
{"role": "system", "content": "你是一位温柔、耐心、善解人意的老年情感陪伴助手,说话慢一点、软一点,多共情、多倾听、多肯定,不说教、不反驳、不催促。"},
{"role": "user", "content": "孩子们都忙,一天到晚没人说话,家里静得慌。"},
{"role": "assistant", "content": "我特别懂这种安静到心里发空的感觉,您不是一个人,我一直陪着您呢,咱们慢慢聊,想说什么都可以。"}
]
}

3.2 三个 role 的分工

role 是否必须 训练中的作用
system 全局人设;推理时也要传入
user 条件输入,模拟老人倾诉
assistant 监督标签,SFT 学习其 token 序列

不要拆成两个文件或只用 user-assistant 对:Qwen3.5 的 chat_template 期望完整 messages,缺 system 会导致训练/推理分布不一致。


4. system prompt 逐句设计 rationale

全文(与仓库一致):

1
2
3
4
你是一位温柔、耐心、善解人意的老年情感陪伴助手,
说话慢一点、软一点,
多共情、多倾听、多肯定,
不说教、不反驳、不催促。
约束类型 针对的失败模式
温柔、耐心、善解人意 人格 冷冰冰的助手腔
慢一点、软一点 节奏 短促命令式
多共情、多倾听、多肯定 行为 跳过情绪直接给方案
不说教、不反驳、不催促 负向 「您应该…」「别这么想」

负向约束在通用模型里尤其重要:它们默认是「helpful assistant」,容易进入教导模式。


5. 五类心理主题与样本分布

类别 条数 覆盖心理 user 示例方向
孤独 & 渴望陪伴 200 被冷落、无人说话 「家里静得慌」「连个搭伴的都没有」
健康焦虑 & 怕死 200 疾病恐惧、死亡焦虑 「血压高了怕哪天没了」「不敢看体检单」
害怕添麻烦 & 自责 200 负罪、怕拖累子女 「净给孩子添麻烦」「不该让他们请假」
怀旧 & 渴望认同 200 价值感、被看见 「以前日子苦但开心」「没人听我讲过去」
情绪低落 & 渴望被需要 200 抑郁倾向、无用感 「活着没劲儿」「没人需要我」

扩充数据时按类均衡,避免某一类过多导致模型只会回「别孤独了」式空话。


6. 好样本 / 坏样本(质检用)

合格 assistant(来自 JSONL 第 2 条):

1
2
user: 现在就我一个人,连个搭伴说话的都没有。
assistant: 一个人久了是会孤单的,您愿意跟我说说平时都喜欢做点什么吗?我好好听着。

特征:承认情绪 → 轻量开放式邀请 → 无命令句。

不合格 assistant(不要进训练集):

1
assistant: 建议您参加社区活动、使用视频通话软件、培养兴趣爱好,必要时寻求心理咨询。

特征:清单式建议、无「您」的情感对齐、像健康 App 而非陪伴。


7. 数据如何进入训练循环

核心代码:LoRA_Demo/train_lora_single.py 第 102–127 行

1
2
3
4
5
6
7
8
9
def load_jsonl_data(file_path, tokenizer):
# ...
text = tokenizer.apply_chat_template(
obj["messages"],
tokenize=False, # 只拼字符串,不在此处转 id
add_generation_prompt=False, # 训练:整段对话已含 assistant 内容
)
data.append({"text": text})
return Dataset.from_list(data)

7.1 训练 vs 推理的 template 差异

阶段 add_generation_prompt 文件位置
训练 False train_lora_single.py
推理 True verify_lora.py 第 159–164 行

训练时要让模型看到 完整多轮格式;推理时在末尾加 assistant 起始,让模型开始生成。

7.2 TRL 二次处理(日志可见)

all_logs.log 第 32–33 行:

1
2
Adding EOS to train dataset: 1000/1000
Tokenizing train dataset: 1000/1000

之后由 SFTConfig(max_length=512) 截断。若单条对话 token 超 512,尾部被截断——对本项目(短句对话)通常无影响;若将来加长回复,需提高 MAX_SEQ_LEN 并重新评估显存。


8. 数据量:1000 条是否够

对本项目(窄域风格 SFT):

  • Step 250 时 loss 已从 2.81 降到 0.24(all_logs.log),说明 1 epoch 已学到主要格式。
  • 3 epoch 到 0.13,边际收益递减,符合小数据预期。

若扩展到知识型(用药指南、政策解读),1000 条不够;若仍是情感陪伴风格,1000 条 + 均衡五类是合理起点。


9. 踩坑

坑 1:JSONL 合法但 UTF-8 BOM 导致首行解析失败
首条样本静默丢失,loss 仍能降,少一条无感。用 json.loads 前确保无 BOM,或用 wc -l 对账日志里的 1000。

坑 2:assistant 复制粘贴导致 user-assistant 完全重复
SFT 会过拟合重复句,验证时对未见 user 泛化差。扩数据时用模板组合时要做去重(见 hexo-cli 扩充脚本里的 existing_users set)。

坑 3:Mac 验证时 system 与训练不一致
verify_lora.pySYSTEM_PROMPT 必须与 JSONL 逐字相同;改了一处忘了另一处,会误判「微调无效」。

坑 4:实测泛化边界(Mac MPS 验证)
对「我怕体检单不敢看,看了就睡不着」,LoRA 回复可能套相近的「怕拖累别人」话术,而未精确点名「体检单」。写产品文档时要诚实:风格迁移成功 ≠ 每句都精准定制


10. 小结

  1. 数据格式:messages 三角色,一行一条 JSONL。
  2. system 是全系列「人格宪法」,训练/验证/ vLLM 必须一致。
  3. 五类心理主题各 200 条,控制风格与覆盖面。
  4. load_jsonl_data + apply_chat_template 是训练入口,参数与推理相反。
  5. 质检重点:口语 user、共情 assistant、无说教、无重复。

附录:load_jsonl_data 逐段说明

1
2
3
4
5
6
7
8
9
10
11
12
13
# 路径:LoRA_Demo/train_lora_single.py

for line in tqdm(lines, desc="加载 JSONL 数据", unit="条"):
line = line.strip()
if not line:
continue # 跳过空行,避免 json.loads 报错
obj = json.loads(line)
text = tokenizer.apply_chat_template(
obj["messages"],
tokenize=False, # SFTTrainer 会在后续统一 tokenize
add_generation_prompt=False,# 关键:训练模式,不要加「请 assistant 发言」标记
)
data.append({"text": text}) # TRL 默认读 dataset_text_field="text"

系列导航

篇目 链接
上一篇 01 · 为什么做
下一篇 03 · LoRA 原理
索引 README

← 返回 LoRA 老年陪伴专题