Files
WpyQwq 643e22ecb9 Natural Memory NM2.1: 记忆路由器分叉、数据集缺陷修复与全轴评测证据
- 引入 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,读写关闭时与原生模型逐位相同
2026-09-19 11:11:31 +08:00

102 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 写入路径修复:20 条不同属性事实不再互相销毁
## 症状
一次写入 20 条**不同属性**的事实后,库里有 20 条记录但只剩 **12 条 active**,**8 条在查询前就已被
retract**;16 个关键零重叠用例中有 **8 个的目标记录已不存在**,因此端到端正确率存在 **50% 的硬上限**
—— 任何排序器都救不回一条已被删除的记录。
## 根因(栈追踪确证,非推断)
16 次 retract 的调用栈完全一致:
```
eval_end_to_end_memory.py:106 write_fact
-> qwen_integration.py:3807 forward
-> qwen_integration.py:2195 _write_text_memory <-- retract 调用点
-> memory_os_v2.py:2578 MemoryOSV2.retract_record
-> memory_os_v2.py:1959 PagedMemoryBankV2.retract (status = retracted)
```
`status_transition_summary == {"active->retracted": 16}`,无 `superseded`、无 `quarantined`,
20 次 `bank.write` 全部返回 `inserted` —— 写入路径的**唯一**销毁机制就是 2195 行的 retract。
授权条件 `confirmed_update` 在 20 次写入中为真 **5 次**,且**全部只走"学习重排器
`learned_best >= 0.95`"这一条**(`qwen_integration.py:2140-2142`):
| 写入 | 事实 | learned_best | 结果 |
|---|---|---:|---|
| 2 | 出生城市 | 0.998959 | confirmed_update=True |
| 3 | 办公城市 | 0.992638 | confirmed_update=True |
| 9 | 工位楼层 | 0.964987 | confirmed_update=True |
| 12 | 手机尾号 | 0.998516 | confirmed_update=True |
| 15 | 办公楼层 | 0.997487 | confirmed_update=True |
随后 2187-2197 的循环退掉 8 条:3 条走 `record.slot_index == slot`,5 条走
`score >= 0.95 and shared >= 2`;2196 行的 `break`(仅当 `score < 0.98` 才停)让写 #12 一次
级联退掉 3 条、写 #15 退掉 2 条。被退掉的正是写 1/2/3/4/9/10/11/14 —— 16 条关键事实中的 8 条。
三条辅助事实:
* `exact_slots.numel() > 0`(token 完全相同)**0/20 次**都没命中 —— 精确重复路径与此无关;
* `shared >= 2` 毫无区分力:19 个候选的 `shared` 全落在 {2,3,5,6,7}(每条事实都含"我的/是"模板词元);
* 词面兜底分支(`>= 0.30`)在重排器就绪时是**死代码**(`learned_best is not None`),
且更宽松:真跑起来 17/20 次会通过。
**根因**:retire 一条已存在记录的授权**完全来自学习打分加一个固定 0.95 阈值**,没有任何
"新文本与在位记录共享 `(entity, attribute)`"的结构性校验。而打包进来的 `text_retriever` 在真实
e2e 事实集上对**不相关**属性过度自信:190 个不相关对中 **10 个 ≥ 0.95**,最高 **0.9985**
(如"常住城市 CITY-A1B2C3" vs "出生城市 CITY-D4E5F6" = 0.9985)。
> 说明:这条结论修正了我早先的一次错误判断。我曾用自己合成的 `VAL-A%07d` 事实集测得
> "0/190 对达 0.95"并据此宣布"重排器过度自信假设被推翻"——那是**数据集不同**造成的:
> 同一测量在 e2e 协议真正写入的事实集上得到 10/190。因此调阈值不可行:假阳性高达 0.9989,
> 没有任何标量切点能分开"不相关属性"与"真更新"。
## 修复(已应用,两处)
1. `qwen_integration.py` `_write_text_memory`:`confirmed_update` 的两条**打分类**分支改为
**结构优先** —— 仅当候选文本的 `entity::attribute` 已经在库的 `active_by_conflict` 账本中
存在(即确实是同一条事实的更新)才允许由打分授权;`exact_slots`(token 完全相同)这条
结构安全的分支保持不变,以保留"重复写入幂等"的既有语义。
2. 2187-2197 的 retract 循环增加守卫:`if record.conflict_key() != candidate_conflict_key: continue`,
循环只能退掉**描述同一属性**的记录,不再因"共享热槽"或"词面重叠高"而销毁无关事实。
语义不受损:真·同属性更新本来就已经由 `PagedMemoryBankV2.write` 通过 `active_by_conflict`
做版本化(旧版本保留为 `superseded`,`version+1`),修复只是移除了那条**额外的、无监督的销毁路径**。
## 四级验证(全部通过)
| 级 | 检验 | 修复前 | 修复后 |
|---|---|---|---|
| ① | `probe_write_retraction_trace.py`:`status_transition_summary` | `{"active->retracted": 16}` | **`{}`** |
| ① | 同上:`retraction_calls` / `status_counts` | 16 次 / `{"retracted": 8, "active": 12}` | **`[]` / `{"active": 20}`** |
| ② | 正对照:同属性改值 | —— | 旧值 `superseded` + 新值 `active`,其余 19 属性不受影响,0 retract |
| ③ | 重排器缺席对照(更宽松的词面分支) | 会踩死代码分支 | 20 条全 active、0 retract |
| ④ | `probe_record_selection.py --gate-scan`:`target_record_active` | 8 / 16 | **16 / 16** |
| ④ | 同上:`target_record_retracted_or_absent` | 8 | **0** |
| ④ | 同上:`active_records_min/max` | 12 / 12 | **20 / 20** |
| ④ | 同上:probe 口径回答正确率 | 25.00%(4/16) | **43.75%**(7/16) |
`verify_write_path_fix.py` 的三阶段与 8 项检查全部通过(`write_path_fix_verification.json`)。
## 端到端效果(官方 harness,16 个零重叠用例)
| 路由器 | 修复前 | 修复后 | 答成别的属性 |
|---|---:|---:|---|
| deployed(原版 NM2) | 25.00% | **50.00%**(+25.00pp) | 18.75% → 12.50% |
| V2-128-v6 | 37.50% | **68.75%**(+31.25pp) | 25.00% → 18.75% |
| REPLAY-128 | 37.50% | **68.75%**(+31.25pp) | 25.00% → 18.75% |
`router_critical_e2e_after_write_fix.json` / `.md`。
## 注意事项与仍未解决的部分
* **路由器评分卡不受此修复影响**:评分卡直接读冻结特征库、不走运行时的写入/读取路径,
所以 `router_scorecard_final.md` 的 22 轴结论无需重跑,也不因本次改动而改变。
* 单元测试 52 项仍全部通过(`python -m unittest discover -s tests -t .`)。
* **仍未解决**(本次未触碰):这 16 个用例里 **93.75% 的读取仍走旧版 16 槽路径**
(`legacy_prefix_used_pct: 93.75`)而非 V2 库记录,地址命中 0/16;记录级排序仍由打包的
`text_retriever` 决定而非路由器。这两项是端到端正确率的下一个瓶颈。
* 220 用例的 `eval_end_to_end_memory.py` 全量套件本次未重跑。