PART 5|如何训练:从数据构造到评测的完整管线
数据格式:一个 JSONL 搞定
一行一个请求,与 API 请求同形状,每个问题多一个 label:
jsonl
{"state": {"subject": "Charged twice", "body": "I see two charges for order #4411."},
"questions": {
"team": {"type": "choice", "instructions": "Which team should handle this ticket?",
"criteria": {"billing": "Payments and refunds", "shipping": "Delivery problems", "access": "Login and account access"}, "label": "billing"},
"angry": {"type": "noul", "instructions": "Is the customer angry?", "label": false},
"priority": {"type": "score", "instructions": "How urgent is this ticket?", "criteria": ["low", "normal", "high"], "label": 1}}}
choice 的 label 是选项名,noul 是 true/false,score 是等级序号(从 0 起)。留 10--20% 做评测。
最重要的一条:从 released checkpoint 出发要 --init_from
从底座 base 起训会把 Kev 已有能力全部扔掉。官方引用的一个实测:836 条支持工单数据上,base 起训在 Kev 自己的评测集掉到 0.33,而同样的数据 --init_from 保住 0.83 且新域到 0.88。
bash
uv run python -m kev.train --data train.jsonl --base Qwen/Qwen3.5-4B-Base --init_from jaredpalmer/kev-4b \
--epochs 2 --lr 2e-5 --batch 1 --accum 8 --dtype bf16 --checkpointing 1 --device cuda --out runs/mine
uv run python -m kev.benchmark --run runs/mine --data heldout.jsonl --out runs/mine-eval
uv run --extra serve python -m kev.serve --run runs/mine --port 8009
要点:--init_from 时学习率要降(2e-5 起步);--base 必须与 checkpoint 匹配(trainer 会校验底座、revision、LoRA rank、head size);--batch 1 --accum 8 bf16 下 0.8B 只要 4GB 显存。Mac 上一次只跑一个训练任务。
实战管线:我在知识图谱项目里的完整流程
图谱元数据 ──▶ 自产训练数据(五任务,train/heldout/eval 三集样本严格不相交)
│
▼
分段微调(Qwen3-0.6B-Base + LoRA r=16,lr 2e-4,bf16 + 梯度检查点,
batch 1 × accum 8,state.json 记进度、任意中断零损失续跑)
│
▼
统一评测(acc / confidence / ECE / AUROC / 延迟,同一把尺量所有模型)
│
▼
kev.serve 接入业务决策层
数据构造四条纪律(比训练超参重要得多)
- 标签只取客观元数据,绝不用系统自身输出做标签------否则是自证循环(用共现分数标签训练,模型学到的就是你的召回偏置)。
- 任务设计成低基数(5 选 1 或二元)。高基数分类上,这类选项打分模型会塌------我测过 77 类的分类任务直接崩掉。
- 训练与运行的问题措辞逐字同源。强烈建议定义一个共享常量,两边 import,绝不复制粘贴。这是我从 0.400 到 1.000 的那条修正。
- 任务行数不均衡时,交错后必须洗牌。固定种子的 shuffle。我踩过的坑:数据不均衡 + 顺序交错,导致训练尾段 40% 全是同一任务,把另一个任务灾难性遗忘(0.925 → 0.215);等量数据时此 bug 完全不可见。
主结果
| 任务 | 零样本底线 | 微调后 |
|---|---|---|
| 朝代归属(5 选 1) | 0.185(随机线 0.2) | 0.925(AUROC 0.89,ECE 0.018) |
| 下一跳选择(5 选 1,问法对齐后) | 0.215 | 1.000(rank 对照 20/20) |
| 作者归属(5 选 1) | 0.145 | 0.32--0.34(AUROC 0.64) |
| 体裁归属(5 选 1) | 0.185 | +0.000(选项描述口径缺陷所致) |
| 作者判定(二元 noul) | 0.475 | 0.6B 学不动(重设计数据后仍随机) |
三个可迁移的结论:
- 微调增益与任务性质强相关:纯形式可判的(朝代/体裁线索)增益大或受数据质量限制,需要实体知识的(作者判定)小模型学不动------先升级到 4B,或在 state 里注入知识,而不是加数据。
- 零样本全随机是常态不是异常 :我在诗歌域测的多个类 Jev 模型(含 Kev)零样本全部落在随机线附近。这类模型的价值在微调之后,选型时别被零样本 demo 迷惑。
- 评测口径要统一且固定:choice 看 picked==answer;noul 的 AUROC 用 Mann-Whitney U(逐阈值扫描在量化分数上会塌成 0);ECE 用 |mean(conf) − acc| 简化口径。所有模型同一份数据同一把尺。
LoRA 无 resume 的解法:分段训练
kev.train 的 --resume 只支持 full_ft 模式。LoRA 中断续跑的解法是把训练切成 N 段(--steps-per-seg),每段结束存 checkpoint,下一段 --init_from 上一段输出,外层脚本用 state.json 记进度------中断后重跑同一条命令即可续。每段约 5 分钟,对沙箱/CI 强杀长进程的场景零损失。