0. 系列闭环

本篇位置 上游 本篇产出 下游
第 1/10 篇 业务场景定义 技术选型结论、项目边界 第 02 篇数据格式 → 第 05 篇训练脚本

本系列只走一条链路:JSONL 数据 → train_lora_single.pyfinal_loraverify_lora.py → vLLM API。不包含多卡 DDP、不包含 LoRA 合并进基座(后续单开)。


1. 要解决的实际问题

1.1 基座模型的默认行为不符合场景

Qwen3.5-4B 作为通用对话模型,面对老年用户典型倾诉时,倾向于输出解决方案型回复:建议活动、建议就医、建议「保持积极」。这在逻辑上没错,但在情感陪伴场景里会被体验为说教。

训练数据里第一条 user 就是:

1
孩子们都忙,一天到晚没人说话,家里静得慌。

期望的 assistant 风格(见 LoRA_Demo/data/elderly_chat.jsonl 第 1 行)是:

1
我特别懂这种安静到心里发空的感觉,您不是一个人,我一直陪着您呢,咱们慢慢聊,想说什么都可以。

关键词不是「怎么办」,而是共情 + 陪伴 + 不催促。这是风格迁移任务,不是知识注入任务。

1.2 五类心理模型决定数据边界

1000 条训练数据按五类划分(各 200 条),覆盖:

主题 典型 user 情绪 模型应避免
孤独 & 渴望陪伴 冷清、无人说话 立刻给「去社区活动」清单
健康焦虑 & 怕死 体检、失眠、怕拖累 诊断式表述、保证疗效
害怕添麻烦 & 自责 怕请假、怕花钱 讲道理式否定感受
怀旧 & 渴望认同 过去的好、不被理解 dismiss 为「老观念」
情绪低落 & 渴望被需要 没用、多余 空洞鼓励「你要振作」

LoRA SFT 的作用:在不改变医学/法律等事实能力的前提下,把回复分布往「温柔陪伴」偏移。


2. 实现位置与仓库结构

1
2
3
4
5
6
7
8
LoRA_Demo/
├── train_lora_single.py # 单卡 SFT 入口(本系列核心)
├── verify_lora.py # loss + 推理验证
├── data/elderly_chat.jsonl # 1000 条 messages
├── models/Qwen3.5-4B/ # 基座(约 8.7 GB)
├── output/lora_elderly_single/
│ └── final_lora/ # 产出 adapter(约 41 MB)
└── all_logs.log # V100 单次训练完整日志

未纳入本系列的脚本(存在但暂不写):train_lora_multi.py(多卡)、merge_lora.py(合并权重)。


3. 为什么选 LoRA + SFT,而不是其他方案

3.1 与全量微调对比

维度 全量微调 4B LoRA(本项目)
可训练参数 ~42 亿 10,616,832(0.2518%) — 见 all_logs.log 第 34 行
单次产出体积 ~8 GB+ adapter_model.safetensors ~41 MB
实测训练时长 未做 2484 s(41 分 23 秒)
数据规模匹配 通常需更大语料 1000 条风格样本即可见效

日志原文:

1
trainable params: 10,616,832 || all params: 4,216,368,128 || trainable%: 0.2518

3.2 与 Prompt-only 对比

仅靠 system prompt(verify_lora.pySYSTEM_PROMPT 与训练一致)可以略微改善语气,但基座仍容易滑向「建议式」结构。SFT 把整段回复的 token 分布拉向数据里的共情模板,效果比纯 prompt 稳定。

3.3 与 LLaMA-Factory 等配置化框架对比

本项目刻意保留 250 行可读脚本train_lora_single.py),注释里带 LoRA 原理与单卡 device_map 说明。目标是对照代码理解 TRL + PEFT 调用链,而不是隐藏在一层 YAML 后面。

技术栈锁定:

  • TRL SFTTrainer + SFTConfig(v1.5.1,见 checkpoint README)
  • PEFT LoraConfig(v0.19.1,见 final_lora/adapter_config.json
  • Transformers 5.9.0 加载 Qwen3.5-4B

4. 端到端架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
flowchart TB
subgraph data [数据层]
J[elderly_chat.jsonl]
end
subgraph train [训练层]
T[train_lora_single.py]
L[final_lora adapter]
end
subgraph eval [验证层]
V[verify_lora.py]
end
subgraph serve [服务层]
VL[vLLM serve + --lora-modules]
API["/v1/chat/completions"]
end
J --> T --> L
L --> V
L --> VL --> API

5. 一次真实训练的关键数字(可引用)

来源:LoRA_Demo/all_logs.log

指标 说明
GPU Tesla V100S-PCIE-32GB 31.7 GB 显存
样本数 1000 JSONL 加载日志第 31 行
有效 batch 4 2 × 2 梯度累积
总步数 750 3 epochs
Step 1 loss 2.8099 token 准确率 43.6%
Step 250 loss 0.2402 第 1 epoch 结束
Step 750 loss 0.1286 训练结束
平均 train_loss 0.2587 汇总行
吞吐 1.208 samples/s

这些数字说明:小数据 + 低秩适配器可以在 40 分钟内完成可观测的风格收敛,适合个人/小团队迭代。


6. 踩坑(立项阶段就要知道)

坑 1:把陪伴做成「健康问答」
若 assistant 回复充满医学建议,loss 同样会降,但产品定位偏了。数据里刻意不写诊断、不写「保证治好」。

坑 2:以为 4B 小模型「不用微调」
实测 Mac mini 上对 LoRA 模型跑 verify_lora.py(MPS + float16),「晚上睡不着」类问题能稳定输出「我陪着您呢」式回复;基座在同 prompt 下仍更易给建议清单。是否微调差别在语气结构,不是 IQ。

坑 3:训练与推理 system 不一致
verify_lora.py 第 33–36 行 SYSTEM_PROMPT 必须与 JSONL 里 system 逐字一致,否则验证结论不可信。


7. 小结

  1. 场景是老年情感陪伴,核心是共情风格,不是通用聊天。
  2. LoRA SFT 在 1000 条数据、41 MB 权重、41 分钟训练的可复现成本下完成风格迁移。
  3. 代码入口是 train_lora_single.py,验证入口是 verify_lora.py,部署走 vLLM 动态 LoRA。
  4. 本系列 10 篇按数据 → 原理 → 环境 → 代码 → 指标 → 验证 → 踩坑 → 部署顺序展开。

附录:训练脚本文件头(设计意图)

来源:LoRA_Demo/train_lora_single.py 第 1–33 行

1
2
3
4
5
6
7
8
9
10
11
12
13
"""
【LoRA 是什么?】
- 冻结基座 40 亿参数不动
- 只在部分线性层旁路插入小型可训练矩阵(约 0.25% 参数)

【5 步流程】
1. 加载 tokenizer + 基座模型
2. 配置 LoRA(指定注入哪些层)
3. 加载 JSONL 对话数据
4. 配置 SFT 训练参数
5. 训练并保存 LoRA 权重
"""
# 文件头把「读代码顺序」写死,避免直接跳到 Trainer 看不懂 device_map

系列导航

篇目 链接
下一篇 02 · 训练集设计
索引 README

← 返回 LoRA 老年陪伴专题