电商客服
记录该项目的全过程和实验数据。
流水线统计
| 步骤 |
数量 |
说明 |
| 原始数据行数 |
212,807 |
chat.txt 原始行 |
过滤空内容/? 符号 |
1,531 行 |
无意义行 |
| 原始会话数 |
10,945 |
按 session_id 聚合 |
| 原始级去重 |
1,138 行跳过 |
同 session 同角色连续相同→跳过 |
| 消息级去重 |
4,088 条合并 |
连���同内容消息合并 |
| 变量替换后 |
10,941 个会话 |
[姓名x] [金额x] 随机替换 |
质量过滤
| 项目 |
数量 |
| 删除低质量 asst 回复 |
2,951 条(空回复/纯数字/单字敷衍) |
| 因质量不合格丢弃 session |
16 个 |
| 质量过滤后保留 |
10,928 个会话 |
低质量清洗
| 规则 |
数量 |
| 非客服对话 |
527 个 |
| 过短(<55 tokens) |
453 个 |
| user 含系统订单摘要 |
1,459 个 |
| 乱码严重 |
302 个 |
| asst 平均短回复 |
16 个 |
| 低质量合计剔除 |
2,757 个会话 |
| 清洗后保留 |
8,168 个会话 |
场景分布
| 场景 |
数量 |
占比 |
| 退换货/售后 |
2,617 |
24.0% |
| 物流查询 |
3,435 |
31.4% |
| 商品咨询 |
1,206 |
11.0% |
| 投诉/抱怨 |
21 |
0.2% |
| 其他 |
889 |
8.1% |
模板套话过滤
| 项目 |
数量 |
| 删除纯模板 asst 回复 |
2,015 条 |
| 受影响的 session 数 |
1,998 |
最终
| 项目 |
数量 |
| 跨会话去重 |
0 个移除 |
| 总过滤率 |
25.4% |
输出文件:
- jddc_trl_sft_rag.jsonl(含模板完整版):8,168 条
- jddc_trl_sft_rag_no_template.jsonl(去模板纯净版):8,168 条
模型打分统计
| 指标 |
数量 |
占比 |
| 总数据 |
8,168 条 |
100% |
| 解析失败(score=-1) |
202 条 |
2.5% |
| 超范围 |
1 条 |
0.0% |
| 高质量(≥6 分) |
7,298 条 |
89.3% |
| 低质量(<6 分) |
667 条 |
8.2% |
去 RAG 噪声 + 分层切分
剔除含 RAG 注入 session:SFT 918 条 + DPO 150 条 = 1,068 条
SFT 6,381 条 → 按场景分层 80/10/10 切分
| 文件 |
条数 |
用途 |
jddc_sft_train.jsonl |
5,103 |
SFT 训练 |
jddc_sft_eval.jsonl |
636 |
验证(防过拟合) |
jddc_sft_test.jsonl |
642 |
测试(最终评测) |
jddc_dpo_pool.jsonl |
517 |
DPO 候选 |
jddc_parse_fail.jsonl |
202 |
解析失败 |
场景分布(5,103/636/642)
| 场景 |
训练 |
验证 |
测试 |
| 物流查询 |
2,092 |
261 |
263 |
| 退换货/售后 |
1,655 |
206 |
208 |
| 商品咨询 |
752 |
94 |
94 |
| 其他 |
592 |
74 |
74 |
| 投诉/抱怨 |
12 |
1 |
3 |
SFT 训练结果
消融实验:LoRA target_modules 对比
| 配置 |
方法 |
rank |
target_modules |
可训参数 |
占比 |
| V1 |
QLoRA |
16 |
q_proj, k_proj, v_proj, o_proj |
11.8M |
0.29% |
| V2 |
QLoRA |
16 |
上述 + gate_proj, up_proj, down_proj |
33.0M |
0.81% |
| V3 |
QLoRA |
8 |
q_proj, k_proj, v_proj, o_proj(同 V1) |
5.9M |
0.15% |
| V4 |
LoRA(全精度) |
16 |
q_proj, k_proj, v_proj, o_proj(同 V1) |
11.8M |
0.29% |
V1 训练分析(rank=16,仅 Attention)
| 指标 |
起始 |
最终 |
评估 |
| Training Loss |
1.76 |
1.21 |
持续下降,正常拟合 |
| Validation Loss |
1.64 |
1.24 |
同步下降,无过拟合 ✓ |
| Token Accuracy |
65% |
73% |
+8pp |
| Avg Loss |
- |
1.58 |
全程均值 |
V2 训练分析(rank=16,Attention + MLP)
| 指标 |
Step 100 |
Step 200 |
Step 300 |
最终 |
评估 |
| Training Loss |
1.39 |
1.11 |
1.05 |
1.06 |
收敛更快 |
| Validation Loss |
1.34 |
1.17 |
1.13 |
1.13 |
更低 ✓ |
| Token Accuracy |
71% |
74% |
75% |
75% |
更高 ✓ |
| Avg Loss |
- |
- |
- |
1.49 |
最优 |
V3 训练分析(rank=8,仅 Attention)
| 指标 |
Step 100 |
Step 200 |
Step 300 |
Step 400 |
Step 500 |
Step 600 |
最终 |
| Training Loss |
1.86 |
1.53 |
1.44 |
1.34 |
1.34 |
1.30 |
1.31 |
| Validation Loss |
1.72 |
1.50 |
1.41 |
1.35 |
1.33 |
1.32 |
1.32 |
| Token Accuracy |
64% |
68% |
70% |
71% |
72% |
72% |
72% |
V4 训练分析(LoRA 全精度,rank=16,仅 Attention)
| 指标 |
Step 100 |
Step 200 |
Step 300 |
最终 |
| Training Loss |
1.61 |
1.36 |
1.29 |
1.31 |
| Validation Loss |
1.56 |
1.38 |
1.33 |
1.33 |
| Token Accuracy |
67% |
70% |
71% |
71% |
四维对比
| 指标 |
V1(QLoRA) |
V2(+MLP) |
V3(r8) |
V4(LoRA) |
结论 |
| 量化 |
4-bit |
4-bit |
4-bit |
全精度 |
- |
| 最终 Train Loss |
1.21 |
1.06 |
1.31 |
1.31 |
V2 最优 |
| 最终 Eval Loss |
1.24 |
1.13 |
1.32 |
1.33 |
V2 最优 |
| Token Accuracy |
73% |
75% |
72% |
71% |
V2>V1>V4≈V3 |
| Avg Loss |
1.58 |
1.49 |
~1.50 |
~1.39 |
V2 最优 |
关键发现
- QLoRA > 全精度 LoRA:V1 和 V4 仅量化方式不同,其余完全一致。V1(4-bit)eval loss=1.24,V4(全精度)=1.33。量化噪声在小数据集上起到正则化作用,反而优于全精度
- rank=16→8 代价明显:V3 eval loss 从 1.24 退至 1.32,收敛慢约 300 步——rank 本身构成容量瓶颈
- V2 整体最优:MLP 层 + QLoRA + rank=16
- 消融结论:QLoRA 对本任务既省显存又提效果,rank=16 是容量下限
Pair-wise 盲测结果
V1 全量评测(仅 Attention,1,231 条对比)
| 指标 |
Base |
SFT |
| 胜率 |
25.3% (311) |
68.4% (842) |
| 平局 |
- |
78 |
| 平均回复长度 |
198.9 字 |
30.4 字 |
| 长度缩减 |
- |
84.7% |
V2 全量评测(Attention + MLP,1,256 条对比)
| 指标 |
Base |
SFT |
| 胜率 |
25.6% (322) |
67.0% (841) |
| 平局 |
- |
93 |
| 平均回复长度 |
180.2 字 |
25.8 字 |
| 长度缩减 |
- |
85.7% |
V3 全量评测(rank=8,仅 Attention,1,231 条对比)
| 指标 |
Base |
SFT |
| 胜率 |
25.9% (319) |
67.0% (825) |
| 平局 |
- |
87 |
| 平均回复长度 |
198.9 字 |
29.7 字 |
| 长度缩减 |
- |
85.1% |
V4 全量评测(LoRA 全精度,仅 Attention,1,256 条对比)
| 指标 |
Base |
SFT |
| 胜率 |
27.5% (346) |
64.8% (814) |
| 平局 |
- |
96 |
| 平均回复长度 |
180.9 字 |
24.8 字 |
| 长度缩减 |
- |
86.3% |
四维对比分析
| 指标 |
V1(r16 QLoRA) |
V2(+MLP) |
V3(r8) |
V4(LoRA) |
| 可训参数 |
11.8M |
33.0M |
5.9M |
11.8M |
| SFT 胜率 |
68.4% |
67.0% |
67.0% |
64.8% |
| Base 胜率 |
25.3% |
25.6% |
25.9% |
27.5% |
| SFT 长度 |
30.4 字 |
25.8 字 |
29.7 字 |
24.8 字 |
| 训练数据中位 |
19 字 |
19 字 |
19 字 |
19 字 |
发现
- V3 rank=8 胜率 67.0%,仅比 V1 rank=16 低 1.4pp——参数减半但效果接近,rank 边际收益递减
- V3 与 V2(+MLP, 33M 参数)胜率平齐(67.0%)——rank 不是 bottleneck 时,Attention 层已足够
- V4(全精度)胜率垫底(64.8%),证实 QLoRA 正则化在本任务的优势
- 训练指标排序 V2>V1>V3>V4 与盲测胜率不完全对齐:V3 训练 loss 最差但胜率与 V2 持平,说明 eval loss 不完全代表用户感知质量
-
V1/V3(transformers)产出 1231 对;V2/V4(vLLM)产出 1256 对。差 25 对来自:260 条对话中有 25 条消息数为奇数,transformers 用 max_t = len(msgs)//2 截断了尾轮(用户发送后无 assistant 回复),vLLM 未做截断而多生成了这 25 轮。胜率按比例计算,不影响横向对比
-
V1(QLoRA)胜率最高(68.4%),V4(全精度 LoRA)最低(64.8%),差了 3.6pp——与 training loss 一致,QLoRA 在小数据上的正则化效应在胜率中得到验证
- V2(+MLP)与 V1 持平,MLP 贡献边际
- 结论:QLoRA 是本任务最佳选择——既省显存又提效果,全精度反而退步
DPO 偏好数据构造
总览
| 层 |
文件 |
条数 |
占比 |
来源 |
方法 |
| 一层 |
jddc_dpo_pairwise.jsonl |
311 |
20% |
测试集 0-259 |
SFT vs Base 中 SFT 输的 + DS V4 改写 chosen |
| 二层 |
jddc_dpo_layer2_clean.jsonl |
150 |
10% |
合成 |
DS V4 按失败模式 few-shot 生成 |
| 三层 |
jddc_dpo_layer3.jsonl |
127 |
8% |
测试集 260-359 |
SFT 自对弈(修复 AB 映射 + 去重后重跑) |
| 四层 |
jddc_dpo_layer4.jsonl |
354 |
23% |
eval 集 0-299 |
SFT 自对弈(修复 AB 映射 + 去重) |
| 五层 |
jddc_dpo_layer5.jsonl |
579 |
38% |
eval 集 300-635 |
SFT vs Base 中 SFT 输的 + DS V4 改写 chosen |
| 合计 |
- |
1,521 |
100% |
- |
- |
五层详情
| 属性 |
一层 |
二层 |
三层 |
四层 |
五层 |
| 数据来源 |
SFT vs Base 负样本 |
LLM 合成 |
SFT 自对弈 |
SFT 自对弈 |
SFT vs Base 负样本 |
| 占用数据 |
测试集 0-259 |
无 |
测试集 260-359 |
eval 0-299 |
eval 300-635 |
| chosen 来源 |
DS V4 改写 |
DS V4 生成 |
SFT 生成(DS 挑优) |
SFT 生成(DS 挑优) |
DS V4 改写 |
| rejected 来源 |
SFT 生成 |
DS V4 生成 |
SFT 生成(DS 挑劣) |
SFT 生成(DS 挑劣) |
SFT 生成 |
⚠ 四层修复了 AB 映射 bug(gen1/gen2 未真正随机化),从 718 降至 354;五层已按修复后的流程生成
⚠ eval 集(636 条)在 SFT 阶段仅做 validation 监控,未参与梯度更新,用作 DPO 数据无 SFT 泄露风险
- 最终 DPO 评测使用 360-641(282 条干净数据),所有 DPO 训练数据均不与其重叠
DPO 训练结果
基于五层全量 1,521 对数据。
三组实验配置
| 参数 |
实验一 |
实验二 |
实验三 |
| 基座模型 |
SFT checkpoint |
SFT on chosen 数据 |
SFT on chosen 数据 |
| chosen 数据源 |
- |
层1+2+5(DS 生成,944 对) |
同实验二 |
| lr |
1e-5 |
1e-5 |
1.5e-5 |
| beta |
0.05 |
0.05 |
0.1 |
| Epochs |
2 |
2 |
2 |
| Steps |
342 |
342 |
342 |
三组结果对比
| 指标 |
实验一(纯 DPO) |
实验二(SFT→DPO) |
实验三(SFT→DPO+) |
结论 |
| Final Eval Loss |
0.509 |
0.572 |
0.424 |
实验三最优 |
| Reward Margin |
0.486 |
0.301 |
1.032 |
实验三 2.1x |
| Reward Accuracy |
76.3% |
82.1% |
82.1% |
实验二/三平齐 |
| Entropy |
1.77 |
1.28 |
1.17 |
实验三最"自信" |
| Logps/chosen |
-40.75 |
-33.99 |
-35.99 |
实验二最短 |
| Logps/rejected |
-30.50 |
-34.41 |
-40.70 |
实验三推最远 |
| 偏好方向 |
❌ 偏好 rejected |
✅ 偏好 chosen |
✅✅ 强偏好 chosen |
|
关键发现
- 实验一仍是方向性错误:Logps/rejected(-30.50)> Logps/chosen(-40.75),模型觉得 rejected 回复更"顺口"。DPO 拉出了 0.486 margin,但推错了方向——是在错误的起点上硬拉
- 实验二"先 SFT 后 DPO"修复了方向:SFT on chosen 数据让模型学到"什么是好回复",DPO 时 chosen 天然 logp 更高。rejected 被打到 -34.41(down 4 个点),chosen 稳在 -33.99
- 实验三 lr↑ + beta↑ 释放了 DPO 的潜力:beta 从 0.05→0.1 告诉模型"更重视偏好",lr 从 1e-5→1.5e-5 加速收敛。rejected 被推到 -40.70(down 6 个点),chosen 仅从 -33.99 降到 -35.99,margin 从 0.30 跳到 1.03
- 三组实验构成完整的消融链:纯 DPO 不行 → 两阶段范式修复方向 → 调参放大效果。reward margin 从 0.49→0.30→1.03 的走势说明:范式比调参重要,但正确范式 + 合理调参 = 效果最大化
DPO 最终盲测
使用干净测试集 360-641(282 条,从未被任何 DPO 层使用)。
| 指标 |
纯 SFT(V1) |
二轮 SFT(DS chosen) |
DPO 最终 |
变化 |
| SFT/DPO 胜率 |
64.0% |
77.9% |
88.3% |
+24.3pp |
| Base 胜率 |
32.9% |
19.1% |
10.7% |
-22.2pp |
| 平局 |
41 |
39 |
14 |
更果断 |
| SFT/DPO 长度 |
25.0 字 |
23.0 字 |
38.8 字 |
+55% |
| Base 长度 |
175.5 字 |
175.5 字 |
175.5 字 |
不变 |
| 长度缩减 |
85.7% |
86.9% |
77.9% |
- |
三阶段增量分析
- SFT → 二轮 SFT:+13.9pp(64.0%→77.9%)。仅用 DS 生成的 944 条 chosen 回复(层1+2+5)做第二轮 SFT,模型学到了"准确完整地回答"而非"极度简短",长度不变但质量显著提升
- 二轮 SFT → DPO:+10.4pp(77.9%→88.3%)。DPO 在校准偏好,长度从 23→39 字是因为模型学会了 DS chosen 的"简洁但不敷衍"风格,同时 rejected 被打压
- 三阶段消融验证了"先 SFT 后 DPO"范式:SFT 决定回答的内容质量(+13.9pp),DPO 决定偏好方向(+10.4pp),两者互补而非替代
- 回复长度从 25.0→38.8 字是有意为之:DS 生成的 chosen 回复(约 30-40 字)比纯 SFT(19 字中位)更完整,DPO 学到的是"简洁但不敷衍"。相比 Base 的 175 字,77.9% 的长度缩减仍保留了显著的简洁性
- 平局从 41→14:模型偏好处判更清晰,回答质量更稳定
SFT vs DPO 内部对决
将二轮 SFT 模型(77.9% vs Base)与最终 DPO 模型(88.3% vs Base)直接对比,看 DPO 相较 SFT 的净增量。
| 指标 |
二轮 SFT |
DPO 最终 |
| 胜率 |
39.9% (524) |
56.0% (735) |
| 平局 |
- |
54 |
| 平均长度 |
22.5 字 |
39.8 字 |
DPO 赢在哪里
| 场景 |
SFT 表现 |
DPO 表现 |
| 用户问题模糊 |
"建议申请售后"(17 字,直接跳结论) |
"请提供具体问题,我帮您核实"(29 字,先了解再处理) |
| 问题未解决时结束 |
"不客气哈 有需要随时找我"(过于随意) |
"我会继续跟进,有结果第一时间告知"(给出承诺和预期) |
| 退款到哪 |
绕弯核查询流程(77 字,没回答问题) |
"退到原支付账户"(37 字,直接给答案) |
SFT 赢在哪里
| 场景 |
SFT 表现 |
DPO 表现 |
| 简单确认 |
"亲,可以退货哦"(准确直接) |
加了无关的"无法使用"前提,显得机械 |
| 确认用户意图 |
"退2个,对吗?"(先确认再执行) |
"是可以的,只退两个"(跳过确认,风险更高) |
| 简单操作 |
直接记录订单号和上门信息 |
额外加了收费说明,多余信息稀释核心操作 |
总结
- DPO 净 +16.1pp,优势在需要主动性、核实、跟进的中等复杂度场景
- SFT 在简单确认场景仍有优势——极简回复比 DPO 的模板化表达更自然
- DPO 回复更长(39.8 vs 22.5),但长度增加来自信息增量而非废话:核实用户问题、给出具体承诺、提供准确信息
- 这说明 DPO 不是简单偏长回复,而是在"短但敷衍"和"长且完整"之间做了有信息增量的选择——SFT 的长度是"刚好够"(22.5 字),DPO 的长度是"刚好对"(39.8 字)
踩坑记录
坑 1:LoRA 权重原地覆盖 → Base/SFT 实际是同一模型
PeftModel.from_pretrained(base_model, checkpoint) 会原地修改 base_model。评测时以为加载了两个模型,实则两者都带 LoRA。
- 现象:Base 胜率异常高达 69%
- 修复:通过
model.disable_adapter_layers() / enable_adapter_layers() 动态开关 LoRA
- 修复后:SFT 胜率 68.4%,回复长度从 188 字降至 30 字
坑 2:LLM-as-Judge 全量评分无效
初始评测用逐维度打分模式,让 judge 同时看到参考回复和两个模型的输出。结果 SFT 在各维度均略低于 Base——但实际仍是同一模型的输出在比。 改为 pair-wise 盲测 + 明确「简洁直接」标准后结果回归合理,也证明了绝对评分不如相对对比可靠。
坑 3:RAG 知识注入 → 去留两难
注入的假知识与对话无关(68% 为无用物流站点),但一键删除会连带削掉退货场景(因 [金额x] 变量集中在该场景)。最终保留 RAG 数据,在 SFT 中接受 13.3% 的轻度噪声。
坑 4:随机变量替换 → 数据分布偏移
[姓名x] [金额x] [地址x] 在不同运行中产生不同随机值。验证发现 8168 vs 8158 的 10 条差异来自去 RAG 后 token 变短被过滤,需精确对齐后按行匹配置信分数。同一次 pipeline 产出的数据天然行对齐,不同运行则无法保证。
坑 5:同一模型自比 → 暴露 judge 的位置偏差
两个回复完全相同(都是同一 SFT 模型输出),但 judge 打出 69%:31%。理论上 50:50,实际严重偏向前一个。LLM-as-judge 的 A/B 对比存在位置偏见——模型倾向选靠前的回答。 评测时必须随机打乱 AB 顺序,且不能仅凭胜率绝对值做结论。
坑 6:评测结果反过来验证了之前的所有结论
修完 LoRA 覆盖 bug 后重新跑测,SFT 68.4% vs Base 31.6%。之前基于 bug 数据得出的所有结论("基座太强""SFT 无意义""数据拖后腿")全部作废。教训:评测框架本身的正确性必须先验证,否则后续分析全是在给 bug 编理由。
vLLM 推理加速对比
| 方案 |
s/it |
260 轮预估 |
技术差异 |
逐条 model.generate() |
60.02s |
4h15min |
Transformers 原生,无 KV cache 复用 |
| vLLM 单条推理 |
12.30s |
52min |
PagedAttention + KV cache 优化 |
- 加速比:4.9x,评测时间从 4 小时压缩到 1 小时以内
- 加速来自 vLLM 的 PagedAttention 显存管理和跨轮 KV cache 预取(
enable_prefix_caching=True),而非连续批处理
- 对话评测天然串行——为模拟真实部署(模型自己的回复成为下一轮上下文),无法用测试集固定上下文并行推理。这是真实性 vs 速度的权衡,vLLM 将此权衡下串行的代价降到 4.9x
材料汇总
简历项目介绍
电商客服多轮对话模型微调与偏好对齐
针对京东客服退换货、物流查询等高频场景,基于 Qwen3-4B-Instruct 完成 SFT+DPO 全流程。清洗并构建 5,103 条多轮客服指令数据,使用 QLoRA(rank=16)完成 SFT,对 rank、target_modules、量化方案展开四组消融实验。创新性地构建五层、1,521 组多维度偏好数据(真实负样本 + LLM 合成 + 自对弈),经两阶段"先 SFT 后 DPO"范式训练。最终在 pair-wise 盲测中 DPO 模型以 88.3% 胜率显著优于基座(barely 10.7%),较大幅提升 SFT baseline(64.0%)。沉淀 LoRA 权重覆盖、LLM-as-Judge 位置偏见、自对弈信号强度等多项工程踩坑经验。
面试叙事要点
| 环节 |
讲什么 |
关键数字 |
| 开头 |
京东客服场景,退换货/物流/商品咨询 |
5,103 训练 / 636 验证 / 642 测试 |
| SFT |
QLoRA rank=16,四组消融 |
QLoRA > 全精度 LoRA,rank=16 是容量底线 |
| DPO 数据 |
五层 1,521 对,三层互补 |
真实负样本 54% + 合成 10% + 自对弈 36% |
| DPO 训练 |
三组消融:纯 DPO → SFT+DPO → 调参 |
reward margin 0.49→0.30→1.03 |
| 最终结果 |
pair-wise 盲测 DPO 88.3% vs Base |
SFT 64.0% → DPO 88.3%(+24.3pp) |
| 收尾 |
踩坑经验 |
LoRA 覆盖、AB 映射、judge 位置偏见 |
面试追问预答
Q: LoRA rank 怎么选的?
跑过四组消融:V3 rank=8 eval loss 1.32 vs V1 rank=16 eval loss 1.24,rank 本身构成容量瓶颈。rank=16 在此任务上是最优的性价比平衡点。
Q: QLoRA 为什么比全精度 LoRA 效果好?
四组实验中 V1(QLoRA 4-bit)胜率 68.4%,V4(全精度 LoRA)仅 64.8%。4-bit 量化噪声在小规模客服数据上起到隐性正则化,省 60% 显存还提 3.6pp 效果。
Q: DPO 数据怎么构建的?
1,521 对来自五个独立层:一层/五层利用 SFT vs Base 评测的负样本(DS 改写 chosen),二层用 DS V4 按失败模式 few-shot 合成,三/四层利用 SFT 自对弈(DS 思考模式盲测评优)。三层自对弈发现 55% 的初始偏好来自语气词差异,引入"实质性差异"标准后重新筛选,只用质量对。
Q: 为什么选 DPO 不选 PPO?
资源角度:DPO 不需要额外构建 reward model,1,521 对偏好数据在 QLoRA 上直接收敛。PPO 需要在线采样 + 训练价值函数,4B 模型单卡部署成本过高。
Q: beta 参数如何调?
三组 DPO 消融实验:SFT→DPO(beta=0.05)reward margin 0.30;beta 提高到 0.1 + lr 提高到 1.5e-5 后 reward margin 跳至 1.03。chosen/rejected 区分度增强明显,最终盲测选择 beta=0.1 + lr=1.5e-5 为最佳。
Q: 项目中最大的坑?
评测 LoRA 覆盖 bug——PeftModel.from_pretrained 原地修改 base_model,导致 Base 和 SFT 实际是同一模型。排查后通过 disable/enable_adapter_layers 修复。教训:评测框架的正确性须先自行验证,否则后续分析全在给 bug 编理由。