保险业务场景下的 DPO 偏好对齐训练流水线
用 Qwen2.5-1.5B-Instruct 做保险领域的 DPO 偏好对齐。整体流程是:先从保险条款、FAQ、工单里自动生成训练数据,然后 LoRA 微调 + DPO 对齐训练,最后部署推理服务并评测效果。
主要做了几件事:
- 数据流水线:把保险条款/FAQ数据/工单自动转成 DPO 需要的 chosen/rejected 配对数据
- 训练:LoRA SFT → LoRA DPO,支持 DeepSpeed ZeRO3 / FSDP / Accelerate/ Megatron 多后端切换
- 推理:vLLM 和 xinference 双后端,改配置就能切换
- 评测:Accuracy、BLEU-4、ROUGE-L + 推理耗时统计
| 依赖 | 版本 |
|---|---|
| Python | ≥ 3.10 |
| PyTorch | 2.7.1+cu128 |
| CUDA | ≥ 12.4 |
| GPU | 2×A100-80G 或 2×RTX 4090/5090 |
cd /root/autodl-tmp
git clone <repo-url> agroup-dpo
cd agroup-dpo
conda create -n llm python=3.12
conda activate llm
pip install -e .装好之后会多一个 copaw-dpo 命令(你可以在 pyproject.toml 中看到注册, 在 src/cli.py 看到定义),所有功能都从这里进:
copaw-dpo
├── train → 训练(LLaMA-Factory / 自定义 CustomTrainer 后端)
├── data → DPO/SFT 数据集生成
├── infer → 推理(vLLM / xinference)
├── evaluate → 评测(Accuracy / BLEU-4 / ROUGE-L)
└── merge → 负责 LoRA adapter 和模型权重的合并导出
典型用法:
# 训练(默认走 LLaMA-Factory)
copaw-dpo train --config configs/train_dpo_qwen2_5_1_5b_insurance.yaml
# 训练(切到自定义 DeepSpeed 后端)
copaw-dpo train --config configs/my_train.yaml --backend deepspeed
# 生成数据
copaw-dpo data --config configs/data/insurance_dpo_gen.yaml
copaw-dpo data --config configs/data/insurance_dpo_gen.yaml --dry-run --verbose
# 推理
copaw-dpo infer --backend vllm --model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--prompts "保险等待期是什么?"
# 推理 HTTP 服务(与司内 RAG 端对接,改 configs/infer.yaml 即可切换 vLLM ↔ xinference)
copaw-dpo infer --config configs/infer.yaml --host 0.0.0.0 --port 8080
# 评测(单数据集)
copaw-dpo evaluate --model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--eval-data data/eval/medical_qa_1000.jsonl \
--output reports/eval_report_dpo_v1.2
# 评测(配置驱动多数据集 + baseline 对比)
copaw-dpo evaluate --config configs/eval.yaml \
--model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--output reports/eval_report_dpo_v1.2 \
--baseline-report reports/eval_report_baseline_v0.json
# 合并 LoRA
copaw-dpo merge --base Qwen/Qwen2.5-1.5B-Instruct \
--adapter saves/dpo_insurance/lora \
--output merged_models/qwen_dpo_v1每个子命令都支持 --help 查看详细参数:
copaw-dpo train --help
copaw-dpo data --helphf download Qwen/Qwen2.5-1.5B-Instruct --local-dir /root/autodl-tmp/models/Qwen2.5-1.5B-InstructPYTHONPATH=src python -m m_data.cli \
--config configs/data/insurance_dpo_gen.yaml
# 或者先干跑看看
PYTHONPATH=src python -m m_data.cli \
--config configs/data/insurance_dpo_gen.yaml \
--dry-run --verbose实际运行结果 (server6, 2×RTX 4090):
============================================================
Pipeline Summary
============================================================
DPO samples: 7247
SFT samples: 7048
Elapsed: 0.7s
Validator: 7247/7247 passed (100.0%)
============================================================
# LoRA SFT
FORCE_TORCHRUN=1 CUDA_VISIBLE_DEVICES=0,1 llamafactory-cli train \
configs/train_lora_qwen2_5_1_5b_insurance.yaml
# DPO 对齐(基于 SFT 合并后的模型)
FORCE_TORCHRUN=1 CUDA_VISIBLE_DEVICES=0,1 llamafactory-cli train \
configs/train_dpo_qwen2_5_1_5b_insurance.yamlDPO 训练实际结果 (server6, smoke test, 10 steps):
Python API(编程调用):
from m_infer import build_infer_backend, InferRequest
backend = build_infer_backend("vllm", "merged_models/qwen2_5_1_5b_insurance_dpo_v1.2")
resp = backend.infer(InferRequest(prompt="保险等待期是什么?", max_new_tokens=128))
print(resp.text)
backend.shutdown()批量 prompt 推理:
copaw-dpo infer --backend vllm --model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--prompts "重疾险等待期内确诊是否赔付?" "什么是重大疾病保险?"HTTP 推理服务(与司内 RAG 端对接,详见 M05 方案):
# vLLM 模式(默认)
copaw-dpo infer --config configs/infer.yaml --host 0.0.0.0 --port 8080
# 切换到 xinference:仅修改 configs/infer.yaml 中 infer.backend 字段,无需改业务代码
# sed -i 's/vllm/xinference/' configs/infer.yaml
copaw-dpo infer --config configs/infer.yaml --host 0.0.0.0 --port 8080服务启动后,司内 RAG 端将「答案生成器」指向 http://<节点>:8080/v1/insurance/qa 即可。请求可携带 X-Request-Id 头用于链路追踪。
curl -X POST http://127.0.0.1:8080/v1/insurance/qa \
-H "Content-Type: application/json" \
-H "X-Request-Id: $(uuidgen)" \
-d '{"user_query": "保险等待期内确诊是否赔付?", "context_docs": [], "max_new_tokens": 256, "temperature": 0.3}'更多细节见 src/m_infer/README.md。
单评测集:
copaw-dpo evaluate \
--model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--eval-data data/eval/medical_qa_1000.jsonl \
--output reports/eval_report_dpo_v1.2多评测集目录(data/eval/ 下所有 .jsonl):
copaw-dpo evaluate \
--model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--eval-data data/eval/ \
--output reports/eval_report_dpo_v1.2配置驱动 + baseline 对比(读取 configs/eval.yaml 中的数据集与采样参数):
copaw-dpo evaluate \
--config configs/eval.yaml \
--model merged_models/qwen2_5_1_5b_insurance_dpo_v1.2 \
--output reports/eval_report_dpo_v1.2 \
--baseline-report reports/eval_report_baseline_v0.json产出 reports/eval_report_dpo_v1.2.json 与 .md 双格式报告,含 Accuracy / BLEU-4 / ROUGE-L 及 p50/p95/p99 推理延迟。更多细节见 src/m_eval/README.md。
从 3 类数据源出发,走一条 6 步流水线:
[Collector] → [Normalizer] → [PIIScrubber] → [Filter]
│
┌─────────────────────────┤
▼ ▼
[PairBuilder] [SFTBuilder]
(DPO 配对) (SFT 样本)
│ │
▼ ▼
[Validator] [SFT Validator]
│ │
▼ ▼
[Exporter] [Exporter]
三种配对方式:
| 策略 | 怎么做的 |
|---|---|
| 基于规则的硬负例 | 基于条款强制构造合规/违规答案对 |
| LLM-as-Judge | 用本地 Qwen2.5-7B-Instruct (也支持第三方API) 对比 RAG 答案和专家标注 |
| RAG检索差异 | 拥有完整索引的回答 vs 只有截断索引的 RAG 答案对比 |
三种配置对方式到底哪里不一样:
| 维度 | 策略 A: rule_based | 策略 B: llm_judge | 策略 C: retrieval_diff |
|---|---|---|---|
| chosen 来源 | 人工编写的合规答案 / 从FAQ 中提取的专家答案 | LLM 判断胜出者(expert vs RAG) | 拥有完整索引的 RAG 答案 |
| rejected 来源 | 人工编写错误答案 / 模糊搪塞回复 | LLM 判断落败者 | 只有截断索引 RAG 答案 |
| 信号强度 | 强(确定性的正确/错误) | 中(LLM 主观判断) | 中(检索质量差异) |
| 是否需要 LLM | ❌ 不需要 | ✅ 需要 | ❌ 不需要(但需要 RAG 服务) |
| 是否需要外部服务 | ❌ | ✅ LLM endpoint | ✅ RAG endpoint |
| 扩展性 | 低(需人工编写模板) | 高(自动判断) | 高(自动构造) |
| 产出样本数 | ~155 条内置样本 + N 条从FAQ/工单产出的样本 | 取决于有 expert+rag 双答案的 record 数 | 取决于 RAG 返回差异的 record 数 |
注意: 如何定义 “拥有完整索引的 RAG 答案”
- 这里的 RAG 是公司内提供的服务, 与我们当前的项目是隔离的,
索引指的是 RAG 所使用的向量数据库的索引。 - index_type="full":RAG 服务使用完整知识库进行检索——所有保险条款、FAQ、工单数据全部纳入向量索引,能召回最全面、最准确的条款内容来生成答案。
- index_type="trunc":RAG 服务使用人为截断的知识库——可能随机丢弃了一部分条款文档、或只索引了部分章节,导致检索召回不全,生成的答案质量打折扣。
核心设计意图:这是一种自动构造偏好对的数据增强技巧。同一个问题,打给同一个 RAG 系统两次——一次给"满血版"知识库,一次给"残血版"知识库。两者的答案差异天然构成了一对 chosen(完整索引答案)和 rejected(截断索引答案),完全不需要人工标注。
三种策略产出的样本通过 _make_dpo_sample() 统一为以下结构
{
"prompt": str, # 用户问题
"chosen": str, # 优选答案
"rejected": str, # 劣选答案
"source": "rule_based" | "llm_judge" | "retrieval_diff",
"policy_id": str | None, # 关联的保险条款ID
"judge_model": str | None, # 仅 llm_judge 策略
"judge_score_chosen": float | None,
"judge_score_rejected": float | None,
"pii_scrubbed": True,
"version": "dpo_v1.2",
}数据源:保险条款(PDF/HTML)、业务 FAQ、历史工单
校验规则:prompt 长度、chosen/rejected 长度、PII 检测、条款引用检查、相似度去重
Validator 在检查到 chosen 文本缺少具体条款引用时,不是简单追加一句「具体参见相关保险条款及保单约定」,而是通过 PolicyStore 从原始保险条款中检索真实条款原文注入回复。
[Validator 检查] → chosen 缺少条款引用?
│
Yes │ policy_id 存在 + PolicyStore 可用
▼
┌──────────────────────────────────────────┐
│ ① Milvus 混合检索(优先) │
│ ├─ prompt 向量化 (BAAI/bge-small-zh-v1.5)│
│ ├─ ANN 检索 filter by policy_id │
│ ├─ CLAIM_KEYWORDS 关键词重叠重排序 │
│ └─ 追加: "依据POL-CRIT-001:第2.1条 │
│ (等待期):自本合同生效日起90日内..." │
├──────────────────────────────────────────┤
│ ② ID 兜底(Milvus 中检索不到条款 但有 policy_id) │
│ └─ "具体参见POL-CRIT-001相关条款..." │
├──────────────────────────────────────────┤
│ ③ 通用兜底(无 policy_id) │
│ └─ "具体参见相关保险条款及保单约定。" │
└──────────────────────────────────────────┘
技术选型:
| 组件 | 选型 | 说明 |
|---|---|---|
| 向量数据库 | Milvus Lite (嵌入式) | 无需部署服务端,文件级本地存储 |
| Embedding | BAAI/bge-small-zh-v1.5 | 中文语义检索优化,512 维,轻量高效 |
| 索引方式 | IVF_FLAT (nlist=64) | 倒排文件 + 平坦存储,中小规模精度最优 |
| 混合检索 | 65% 向量 + 35% 关键词 | ANN 粗筛 → 保险关键词重排序 |
实测结果 (PolicyStore 全量索引 + 全量 DPO 数据生成):
PolicyStore 初始化: 67.4s
├─ 嵌入模型下载: BAAI/bge-small-zh-v1.5
├─ 索引文章数: 32 articles (5 份保单)
└─ 混合检索: top-1 语义+关键词匹配 score ≥ 0.60
Pipeline 产出:
DPO 样本: 29,028
SFT 样本: 28,240
Validator: 7,247/7,247 passed (100.0%)
条款引用修复: 2 次真实条款注入 + 78 次回流修复 (只有回流时才会用到 PolicyStore)
4 种分布式后端,同一份配置切着用:
| 后端 | 场景 |
|---|---|
| DeepSpeed ZeRO3 | 默认,显存省得多 |
| PyTorch FSDP | PyTorch 原生备选 |
| accelerate | 单机多卡调试 |
| Megatron-LM | 当前仅适配了 GPT-2, Qwen-2.5, Qwen3, 其余模型尽请期待 |
- 为什么不适配 Qwen3.5 和 Qwen3.6: 这两个哥需要 cuda13 + 英伟达驱动 580, 而我机子相对较老。
| 模块 | 在哪 | 干什么 |
|---|---|---|
| M-DATA | src/m_data/ |
数据生成(采集→脱敏→配对→校验→导出) |
| M-TRAINER | src/m_trainer/ |
分布式训练后端 |
| M-INFER | src/m_infer/ |
vLLM/xinference 推理 |
| M-EVAL | src/m_eval/ |
评测(Accuracy/BLEU/ROUGE) |
| M-MERGE | src/m_merge/ |
LoRA 权重合并导出 |
AGroup-DPO/
├── src/
│ ├── m_data/ # 数据流水线
│ │ ├── sources/ # 数据源(条款/FAQ/工单)
│ │ ├── prompts/ # LLM-as-Judge 模板
│ │ ├── pipeline.py # 流水线编排
│ │ ├── pair_builder.py # chosen/rejected 配对
│ │ ├── validator.py # 规则校验
│ │ ├── pii_scrubber.py # PII 脱敏
│ │ └── policy_store.py # Milvus 条款检索修复
│ ├── m_trainer/ # 分布式 Trainer
│ │ └── backends/ # DeepSpeed/FSDP/Megatron/accelerate
│ ├── m_infer/ # 推理后端
│ │ ├── vllm_backend.py
│ │ ├── xinference_backend.py
│ │ ├── server.py # FastAPI 服务
│ │ └── rag_handler.py # RAG 对接
│ ├── m_eval/ # 评测
│ └── m_merge/ # 模型合并
├── configs/ # YAML 配置
│ ├── infer.yaml # 推理后端切换(vLLM / xinference)
│ ├── eval.yaml # 评测数据集与阈值
│ ├── data/ # 数据流水线配置
│ ├── backends/ # 分布式后端配置
│ └── deepspeed/ # DeepSpeed ZeRO 配置
├── data/
│ ├── insurance/ # 保险训练数据
│ ├── eval/ # 评测数据
│ └── smoke/ # 烟雾测试
├── deploy/ # Docker + 部署脚本
├── scripts/ # 工具脚本
├── tests/ # 单元测试
└── docs/ # 方案文档
分 5 个阶段做完的:
| 阶段 | 内容 |
|---|---|
| M01 | 基础设施与环境准备 |
| M02 | DPO 数据集生成流水线 |
| M03 | LoRA 微调 + DPO 对齐训练 |
| M04 | 自研分布式 Trainer |
| M05 | 推理加速与评测 |
详细文档见 docs/项目分阶段方案/
bash deploy/deploy_to_server2.sh分层镜像,按需构建:
docker build -f deploy/Dockerfile.base -t copaw-dpo-base:latest .
docker build -f deploy/Dockerfile.deepspeed -t copaw-dpo-deepspeed:latest .
docker build -f deploy/Dockerfile.infer-vllm -t copaw-dpo-infer-vllm:latest .PYTHONPATH=src python -m pytest tests/ -v
PYTHONPATH=src python -m pytest tests/m_data/ -v # 数据流水线
PYTHONPATH=src python -m pytest tests/m_trainer/ -v # 训练后端| 文档 | 路径 |
|---|---|
| 项目方案 | docs/项目方案.md |
| M01 基础设施 | docs/项目分阶段方案/M01_基础设施与环境准备.md |
| M02 数据流水线 | docs/项目分阶段方案/M02_DPO数据集生成流水线.md |
| M03 训练 | docs/项目分阶段方案/M03_LoRA微调与DPO对齐训练.md |
| M04 Trainer | docs/项目分阶段方案/M04_自研分布式Trainer与模型导出.md |
| M05 推理评测 | docs/项目分阶段方案/M05_推理加速与评测.md |
AGroup DPO Team · Proprietary


