# 零重叠改写成语(zero-overlap paraphrase)阶段报告 本阶段针对 `E2E_FINDINGS.md` 定位到的**唯一**泛化缺口:查询与事实之间**没有任何独特字重叠**时的语义匹配。 所有速率均为百分比。所有数字均来自磁盘上保存的 scorecard / 评测 JSON,不引用滚动日志。 逐轴对照表见 `zero_overlap_phase.md`(由 `report_zero_overlap_phase.py` 从 scorecard JSON 渲染)。 --- ## 1. 结果摘要 新增了一个**可验证的**零重叠改写数据集、一个**合并特征库**,以及一个重放微调后的路由器 `REPLAY-128`(2,037,774 参数,128 维,每条记录地址 512 字节 —— 与现网路由器几何完全一致)。 | 评测 | 指标 | v6 最终版 | REPLAY-128 | 变化 | |---|---|---|---|---| | 零重叠(300 条,24 个**未见过**问法) | Top-1 正确率 | 18.40% | **59.60%** | **+41.20pp** | | 零重叠 | Recall@3 | 36.00% | **83.20%** | **+47.20pp** | | 零重叠 | MRR | 35.07% | **73.06%** | **+37.99pp** | | v6 全集(21,920 条,全部 22 轴) | Top-1 正确率 | 94.37% | 94.14% | −0.23pp | | v6 全集 | Recall@3 | 96.40% | 96.48% | +0.08pp | | v6 全集 | 多跳证据全中(Top-3) | 95.95% | 96.22% | +0.27pp | | v6 全集 | hop 正确率 | 100.00% | 100.00% | 0 | | v6 全集 | 未知拒答率 | 100.00% | 100.00% | 0 | | v6 全集 | 已知问题被误拒率 | 0.00% | 0.00% | 0 | 零重叠评测集随机猜测基线为 4.17%(24 个同形候选),因此 59.60% 约为**随机水平的 14 倍**。 v6 全集的全部策略轴**逐位不变**(见第 3 节的冻结机制),检索轴变化在 ±0.27pp 以内。 部署契约:`check_router_swap.py` 判定 **DROP-IN REPLACEMENT OK** —— 16/16 键匹配、无缺失/多余、 无形状不符、分数有限,且实测可驱动 `PagedMemoryBankV2` 路由(`bank_routed_records: 3`)。 延迟(`bench_router_latency.py`,7 轮交错取中位数):单查询 1.2042 ms → 1.2087 ms(**+0.37%**), batch=256 QPS 190,103 → 192,600(**+1.3%**)。batch=64 QPS 读数低 9.2%,但该轮自身离散度为 17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快 —— 方向不一致,判为测量噪声而非回归。 --- ## 2. 数据集:属性由构造保证,而非相信手写意图 `make_zero_overlap_paraphrase_data.py` 生成 24 个属性 × 3 个改写问法: * 每条查询与其**自身**目标事实的独特字重叠必须为 **0**(属性字符、停用字除外); * 前 2 个改写用于训练(48 个问法),第 3 个**留出**用于评测(24 个问法)——两个划分的 **查询字符串完全不相交**,因此评测衡量的是"换一种说法"的泛化,而不是记住训练字符串; * 候选是 24 条**同句式、不同属性**的事实,值为随机码,只出现在一条事实里(可检测证据混用); * 15% 为不可回答 episode(改写指向候选中不存在的属性)。 关键设计判断:改写与其他**无关**属性的字符重叠被**刻意保留**(生成器统计 54.53% 的 episode 至少 含一个这类"陷阱"候选)。因为命中错误候选会得到**错误答案**,这只会让任务更难,不会提供捷径; 真正必须为 0 的是与**自身**目标的重叠。最初把二者混为一谈时,24 个属性中有 7 个因为 "起床时间"的"时"、"办公城市"的"公"等高频字而被判为"无可用的改写"。 `verify_zero_overlap_dataset.py` 用**独立重写**的停用字集合与重叠算法复核(不 import 生成器): * 结构问题 **0**,查询与自身目标重叠 **0**,两个划分共享查询字符串 **0**; * 每条的答案码只出现在唯一一条候选中,候选属性标签唯一; * `answerable` / `need_memory` / `hop` 与正例索引一致。 (生成器报的陷阱比例 54.53% 与校验器报的 44.00%/50.00% 不同,因为两者用不同的停用字集合 定义"独特字";这是定义差异,不是矛盾。) --- ## 3. 两次微调的对照:朴素微调会造成灾难性遗忘 先做朴素微调(只用零重叠数据,全部损失项默认权重,从 v6 最终版权重出发,6000 步): * 零重叠 Top-1 18.40% → **66.80%**,但 v6 全集同时退化: `random_position` MRR 99.02% → 93.37%、多跳证据全中 100.00% → 95.65%、 hop 100.00% → 87.10%、**已知问题被误拒率 0.00% → 13.85%**、`unknown` Top-1 58.99% → 47.52%。 * 训练日志还显示策略门发生了"用误拒换拒答"的交换:未知拒答率升到 40.00%,代价是 **38.00% 的可回答问题被误拒**。 这就是单指标视角会误判为胜利、而多轴记分卡会否掉的典型情形。 修正方案由两点组成: 1. **冻结策略头**(`--need-loss-weight 0 --hop-loss-weight 0`)。在零重叠集合上,`need_memory` 门只看查询、看不到候选,而"不可回答"的改写与可回答的改写句式完全一样,因此该门在此数据上 **原理上不可学**;强行训练只会破坏 v6 学到的门。实测证明冻结生效:合并评测的 `need_specificity` 在每一次 eval 中都是 `0.9879692011549567`,逐位不变。 2. **重放**(把零重叠数据并入 v6 数据一起训练),避免排序通路被新分布带偏。 --- ## 4. 合并语料:复制已验证的行,而不是重新编码 `build_replay_corpus.py` 把 v6 库(2,124,552 行)与零重叠库(36,072 行)合并为 2,160,624 行。**没有重新编码** v6 特征 —— 那既耗时数小时,又会让历史测量所依赖的参考特征 发生无声漂移。合并的强制不变量: * 每个源库的每一行都恰好被复制一次,或因文本完全相同而跳过(本次重复 **0** 行); * `index` 条目数 == 库行数(训练器会拒绝不一致的库); * 合并后 train/eval 引用的**每一个文本**都能解析到一行(缺失 **0**); * **2000 行抽查逐字节相等**;全库扫描全零行 **0**; * manifest 携带**合并后**数据文件的 sha256(训练器会据此校验冻结输入)。 合并语料:train 88,355 episode / eval 22,220 episode; `data/router_replay_v7/`、`H:\Memory\nm_cache\nm_replay_v7\feature_cache`。 --- ## 5. 关键负面结论:路由器排序不影响端到端答案 `REPLAY-128` 与 `V2-128-v6` 在 `eval_router_critical_e2e.py`(16 个零重叠用例)上的结果**逐位相同**: | top_k_records | 路由器 | 回答正确率 | 答成别的属性 | 平均选中记录 | |---|---:|---:|---:|---:| | 默认 | V2-128-v6 / REPLAY-128 | 37.50% / 37.50% | 25.00% / 25.00% | 5.88 / 5.88 | | 1 | V2-128-v6 / REPLAY-128 | 12.50% / 12.50% | 43.75% / 43.75% | 0.94 / 0.94 | | 3 | V2-128-v6 / REPLAY-128 | 31.25% / 31.25% | 25.00% / 31.25% | 2.44 / 2.44 | 路由器级 Top-1 提升了 41.20pp,端到端却**一个用例都没变**。因此做了一个判决性实验:保留 `need_memory` / `hop_controller` / `head_gate` 全部原权重,只把排序通路 (`query_projection`、`key_projection`、`pair_scorer`)替换为同形状**随机权重** (`checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt`,非有限值 0 个)。 结果在默认 Top-K 与 Top-K=1 下**都完全不变**(37.50% / 12.50%,选中记录数相同)。 机制(已逐项实测,含两次被自己数据推翻的中间假设): * `memory_os_v2.py:1671` — 带 `semantic_key`(张量)的记录,其 `_score_candidates()`(即 `MemoryRouterV2.projected_scores`)分数会被 `self.record_scorer` **逐位置覆盖**;实测残差与 text_retriever 匹配 18/18,与 `memory_router_v2` 匹配 0/18。 * `qwen_integration.py:1359` `_score_semantic_memory_records` — 该回调走 `self.text_retriever`。**注意:最初以为"部署目录没有 `text_retriever.pt` 所以走余弦兜底" 是错的** —— 重排器被烘焙进合并包(safetensors 内有 6 个 `dynamic_memory.text_retriever.*` 张量,1,573,377 参数), `_text_retriever_ready` 实测为 **True**,余弦兜底分支根本不执行。 * 路由器在此配置下**唯一实际生效的杠杆是 `need_memory` 门**:部署权重下门开启率 **68.75%**(11/16),而三份随机初始化的路由器只有 **37.5%**(6/16)。判决性探针只随机化了 排序通路、**保留了门控头**,所以"端到端零变化"是由构造保证的,而不是巧合。 * 这 16 个用例里 **93.75%(15/16)实际注入的是旧版 16 槽路径**的证据 (`legacy_slot` + `text_memory_top_k=2`),而不是 V2 库记录;地址命中 **0/16**, 直接词面命中 6/16。`skip_semantic_expansion` 在这 6 例里直接跳过整个语义 hop 循环。 * 更严重的是:一次写入 20 条不同属性的事实后,**只剩 12 条 active,8 条在查询前就已被 retract**(16 个用例的 8 个里,目标记录本身就在被删的那批里)。**任何排序器都救不回一条 已经不存在的记录**,因此端到端正确率存在 50% 的硬上限。 结论分两层,不要混为一谈: 1. 路由器记分卡上 22/22 的统治性和本次 +41.20pp 的零重叠提升,都是路由器工件**真实**的增益; 2. 但在这套运行时里,记录级顺序由打包的 `text_retriever` 决定,路由器分数被覆盖,且相当比例的 读取根本不走 V2 库、目标记录还会被写入路径删除。所以路由器增益**不会**传递到最终答案。 要让增益落地,需要的不是继续加大路由器,而是: 1. 修写入路径:不要让 20 条不同属性的事实互相淘汰(当前最大瓶颈,直接决定 50% 上限); 2. 让 V2 库记录真正参与读取:目前 93.75% 的用例走旧版 16 槽路径; 3. 训练/替换 `text_retriever`(记录级重排器)—— 零重叠数据集可直接复用为它的训练/评测集; 4. 让路由器分数对 `semantic_key` 记录也参与排序(改动 `memory_os_v2.py:1671` 的覆盖逻辑)。 (曾据此改过一版,实测在部署配置下**不生效**,已回退并留注释。) --- ## 6. 产物清单 | 文件 | 作用 | |---|---| | `make_zero_overlap_paraphrase_data.py` | 生成零重叠数据集(属性由构造保证,划分字符串不相交) | | `verify_zero_overlap_dataset.py` | 独立复核数据集(不共享生成器代码) | | `build_replay_corpus.py` | 合并两个数据集与两个特征库,含逐字节抽查 | | `report_zero_overlap_phase.py` | 由 scorecard JSON 渲染对照表(`zero_overlap_phase.md`) | | `data/zero_overlap/` | train 1,200 / eval 300(含 `manifest.json`、`zero_overlap_verification.json`) | | `data/router_replay_v7/` | 合并语料 train 88,355 / eval 22,220 | | `H:\Memory\nm_cache\nm_replay_v7\feature_cache` | 合并特征库 2,160,624 行 | | `checkpoints/router_replay_v7_v2_128/` | `REPLAY-128` 最终版权重(可 drop-in) | | `compare_record_rankers.py` | 在同一批冻结特征上对比"余弦 / 各路由器"的排序能力 | | `measure_write_path_updates.py` | 量写入路径的更新判定(证明重排器打分不是 retract 的原因) | | `probe_record_selection.py` | 逐记录拆解打分项、门控扫描、目标记录存活状态 | | `migrate_bank_paths.py` | 把特征库路径从 D: 迁移到 `H:\Memory\nm_cache`(含校验) | | `zero_overlap_phase.md` | 本阶段的逐轴对照表 | 特征库现存于 `H:\Memory\nm_cache\`(`nm_router_v6` / `nm_router_v5` / `nm_zero_overlap` / `nm_replay_v7` / `nm_probe_bf16`,共约 35.9 GB;D 盘已不再保存项目数据)。迁移后用 `verify_feature_bank.py` 全盘复扫(2,124,552 行、全零行 0、`complete=true`)并在新路径下 复算了排序器对比(59.60 / 30.40 逐位一致)。 历史运行记录(`checkpoints/*/metrics.jsonl`、`*/training_stdout.log`、 `*/router_v5_training.json`、`feature_bank_verification.json`)**刻意保留原始 D: 路径**, 因为那是当时真实的运行事实,改写它们等于篡改记录。 对照用的负面结果与判决性探针:`checkpoints/router_zov_v2_128/`(朴素微调)、 `checkpoints/router_v6_v2_128/ranking_scrambled_probe.pt`(随机排序探针)、 `forget_check_zov.json`、`router_critical_e2e_scramble*.json`。