- 引入 MemoryRouterXL 与 v5/v6 流式多线程训练/编码管线 - 修复 prepare_memory_router_dataset 候选池重建缺陷(mega 家族 3568x 加速,输出逐字节相同) - 修复 v5 被破坏的拒答与多跳标签(train 未知样本 319 -> 16319,multi_hop 平均正例 1.00 -> 2.00) - 同存储预算下 V2-128 v6 逐轴 22/22 通过:Top-1 41.12% -> 94.62%,未知拒答 0.00% -> 100.00% - 记录三条被实测推翻的显然优化(logits_to_keep=1 反而慢 55%、XL 容量未带来收益) - 记忆手术跨架构可移植性 14/14,读写关闭时与原生模型逐位相同
6.5 KiB
端到端瓶颈攻坚:排序混合权重(负面结论)+ 未知问题泄漏
本文件记录的是失败与自我纠错,因为这两个结果都会影响后续方向判断。
1. 先修掉一个我自己的错误判断
我一度声称"93.75% 的读取绕过 V2 库、走旧版 16 槽路径",并据此把它列为第一瓶颈。
这个结论是错的。 probe_record_selection.py 里的 legacy_prefix_used 取的是
runtime.text_prefix_used,而该方法在 V2 路径(qwen_integration.py:2483)和旧版路径
(2515 行)都会被置 True —— 字段名误导了我。
实测反证:大样本评测 64/64 用例的 stop_reason 全是 evidence_found(read() 返回了记录),
平均选中记录 6.47,写入 24 条事实后活跃记录 23–24。V2 库记录确实是被注入的证据来源。
2. 真正测出来的瓶颈:记录级排序器很弱
同一批冻结特征、250 条可回答的零重叠 episode、24 个同形候选(随机基线 4.17%):
| 排序器 | Top-1 | Recall@3 | MRR |
|---|---|---|---|
| REPLAY-128(训练过的路由器) | 59.60% | 83.20% | 73.06% |
| 冻结键余弦 | 30.40% | 49.20% | 44.84% |
打包的 text_retriever(运行时实际用来给事实记录排序) |
23.20% | 55.20% | 43.16% |
| V2-128-v6 路由器 | 18.40% | 36.00% | 35.07% |
memory_os_v2._record_scores 对带 semantic_key 的记录会用 record_scorer(即打包的
text_retriever)逐位置覆盖 MemoryRouterV2.projected_scores 的结果(残差与 retriever
匹配 18/18、与路由器匹配 0/18)。所以运行时用一个 23.20% 的排序器,而手上有 59.60% 的。
3. 实现(已落地,默认关闭)
QwenMemoryConfig.memory_record_router_blend(默认 0.0 = 历史行为):
_score_semantic_memory_records 在混合权重 > 0 时返回
(1-w)·retriever + w·router,路由器分数在同一批候选上用 encode_key + projected_scores 现算。
0.0 时路径与改动前完全一致。
4. 测量:先是假象,然后是否定结论
**第一轮(同进程内跑多个权重)**看起来有效:
| blend | 16 用例正确率 | 64 用例正确率 | 答成别的属性 |
|---|---|---|---|
| 0.00 | 68.75% | 60.38% | 37.74% |
| 0.50 | 75.00% | 66.04% | 30.19% |
我据此把默认值改成 0.5,但独立进程复核时复现不出来(0.5 只得到 68.75%),说明这两组数字被 "同一进程内先跑 0.0 再跑 0.5"的顺序污染了。
第二轮(每档一个独立进程,各跑 2 次):
| blend | 第 1 次 | 第 2 次 |
|---|---|---|
| 0.0 | 68.75% | 68.75% |
| 0.5 | 68.75% | 68.75% |
四次完全一致:混合权重在 16 用例尺度上没有任何可测效果。因此:
- 默认值已退回 0.0;
- 撤回第 4 节第一轮的全部增益数字(+6.25pp / +5.66pp / −7.55pp);
probe_record_blend.py保留,但必须每档独立进程运行,文件注释已写明这一点。
5. 为什么它本来就不该有效(与先前的取证一致)
早先的逐记录取证已经量过:_record_scores 的最终分数里神经项只占 12.0% / 14.7% / 16.9%,
而词面/地址先验占 83–88%(`0.25·lexical + 0.45·token_overlap + 1.25·rare_lexical_address
- 0.35 + 0.15
,write_fact还使0.10·conf + 0.08·imp恒定)。并且1.25·rare` 在部分候选集上 造成 1.108 的先验差,而 [0,1] 的神经分最大只能摆动 1.0 —— 那些配对结构上无法被神经分翻转。
实测印证:所有权重下 mean_records_selected 都是 6.0625,被注入的记录集合不变,只是顺序微调;
顺序变化不足以改变生成结果。所以下一个杠杆不是神经排序器,而是 _record_scores 里的先验权重
(尤其 1.25·rare_lexical_address 与那两项常数加成),以及 direct_records 的硬编码 8.0/10.0
(比神经路径最高约 2.90 高出 5.6 以上,使直接命中路径完全压过学习排序)。
6. 附带发现(已用全样本确认):未知问题泄漏率 78.00%
零重叠评测集里"问的是候选中不存在的属性、但句式完全正常"的不可回答 episode: 全部 50 条单跑一遍,泄漏率 78.00%(39/50) —— 运行时会给这些无从回答的问题返回一个候选代号。 (小样本 11 条时是 81.82%,方向一致。)
stop_reason 在 50/50 条里全是 evidence_found。对照 v6 全集(10 类别)未知泄漏率是 0.00%
(走 explicit_unknown_request 规则)。"句式正常的未知"是当前最大的体验缺口。
6.1 阈值方案已被测量排除
最直觉的修法是"最高分不够高就拒答"。为判断可行性,我采集了检索最高分(注意:这是运行时的原始 相关度分,不是概率、不是百分比,观测上限约 851)在两组的分布:
| 组 | n | min | p10 | p50 | p90 | max |
|---|---|---|---|---|---|---|
| 可回答 | 50 | 69.98 | 77.13 | 288.20 | 851.50 | 851.68 |
| 不可回答 | 10 | 77.80 | 118.86 | 285.25 | 851.37 | 851.37 |
两组分布几乎完全重叠(中位数 288.20 vs 285.25,上限 851.68 vs 851.37,且上限处明显饱和)。 所以任何标量阈值都无法分开二者 —— 压低阈值以拦住 78% 的泄漏,会同时拦掉同等比例的可回答问题。 "未知"与"可回答"在这里的差别不在检索强度,而在库里到底有没有那个属性,这需要 查询-库级别的覆盖判断(例如对可解析属性直接查结构化账本,或训练一个"该属性是否存在"的分类头), 而不是给查询-记录打分设阈值。
(附带:本次 60 用例混合样本里可回答 50 条正确率 60.00%、答成别的属性 40.00%, 加权记录 23–24 条全部存活 —— 写入路径修复在大样本下持续有效。)
7. 产物
| 文件 | 内容 |
|---|---|
compare_record_rankers.py |
四路排序器对比(余弦 / 各路由器 / 打包 retriever),--with-text-retriever |
zero_overlap_ranker_comparison_with_retriever.json |
第 2 节的数字 |
probe_record_blend.py |
混合权重端到端测量(每档独立进程) |
record_blend_proc_{0.0,0.5}_rep{1,2}.json |
第 4 节第二轮的四次干净测量 |
record_blend_dose_response.{json,md} |
第一轮(已作废,保留以说明混淆来源) |
eval_runtime_zero_overlap_e2e.py |
大样本运行时评测(零重叠 64 例 / v6 40 例),第 6 节 |
runtime_zero_overlap_e2e.{json,md}、runtime_v6_blend_e2e.{json,md} |
第 6 节数字 |