Files
natural-memory-nm21/WRITE_PATH_FIX.md
T
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

6.1 KiB
Raw Blame History

写入路径修复: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 全量套件本次未重跑。