Files

705 lines
44 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NM2.1:之后做了什么 — Natural Memory v2</title>
<meta name="description" content="三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#0a0a0a">
<!-- 动效的初始隐藏态只在 JS 可用时生效;脚本被禁用时内容照常可见 -->
<script>document.documentElement.classList.add("js");</script>
<meta property="og:type" content="website">
<meta property="og:title" content="NM2.1:之后做了什么 — Natural Memory v2">
<meta property="og:description" content="三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="assets/favicon.svg">
<link rel="preload" href="assets/fonts/archivo-latin-wdth-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="assets/fonts/geist-mono-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
<!-- 资源带构建指纹:内容一变 URL 就变,浏览器不可能再用到旧 JS/CSS -->
<link rel="stylesheet" href="assets/css/site.css?v=a9c21ef3a8">
</head>
<body data-page="nm21" data-layout="docs">
<a class="skip" href="#doc">跳到正文</a>
<header class="site" id="site">
<div class="topbar">
<a class="brand" href="./" aria-label="Natural Memory v2 发布页">
<svg class="brand__mark" viewBox="0 0 32 32" aria-hidden="true">
<rect class="dim" x="2" y="2" width="7" height="7" rx="1.5"/>
<rect class="dim" x="12" y="2" width="7" height="7" rx="1.5"/>
<rect class="dim" x="22" y="2" width="7" height="7" rx="1.5"/>
<rect class="dim" x="2" y="12" width="7" height="7" rx="1.5"/>
<rect class="on" x="12" y="12" width="7" height="7" rx="1.5"/>
<rect class="dim" x="22" y="12" width="7" height="7" rx="1.5"/>
<rect class="dim" x="2" y="22" width="7" height="7" rx="1.5"/>
<rect class="dim" x="12" y="22" width="7" height="7" rx="1.5"/>
<rect class="on" x="22" y="22" width="7" height="7" rx="1.5"/>
</svg>
<span class="brand__name">Natural Memory</span>
<span class="pill pill--acc">v2</span>
</a>
<span class="crumb">
<span class="crumb__n">04</span>
<span class="crumb__t">NM2.1 · 之后做了什么</span>
</span>
<span class="topbar__spacer"></span>
<nav class="topnav" aria-label="快速导航">
<a class="topnav__a" href="./">发布页</a> <a class="topnav__a" href="./mechanism">机制</a> <a class="topnav__a" href="./results">测量</a> <a class="topnav__a is-on" href="./nm21" aria-current="page">NM2.1</a> <a class="topnav__a" href="./ledger">台账</a> <a class="topnav__a" href="./limits">边界</a> <a class="topnav__a" href="./reproduce">复现</a>
</nav>
<button class="palbtn" id="palBtn" type="button" aria-label="检索全站内容(快捷键 /)">
<span class="palbtn__t">检索</span><span class="kbd">/</span>
</button>
<span class="pill" id="serverChip" title="本站服务端运行时读数">
<i class="dot"></i><span id="serverChipText">读取中</span>
</span>
</div>
<div class="progress" id="progress"></div>
</header>
<div class="shell">
<aside class="side">
<nav class="side__in" aria-label="章节导航">
<a class="side__back" href="./">← 返回发布页</a>
<p class="side__grp">原理</p>
<ul class="side__list">
<li><a class="side__a" href="./division"><span class="side__n">01</span><span class="side__t">记忆与 KV 的分工</span></a></li>
<li><a class="side__a" href="./mechanism"><span class="side__n">02</span><span class="side__t">一次读取与一次写入</span></a></li>
</ul>
<p class="side__grp">证据</p>
<ul class="side__list">
<li><a class="side__a" href="./results"><span class="side__n">03</span><span class="side__t">测量结果</span></a></li>
<li><a class="side__a is-on" href="./nm21" aria-current="page"><span class="side__n">04</span><span class="side__t">NM2.1 · 之后做了什么</span></a></li>
<li><a class="side__a" href="./examples"><span class="side__n">05</span><span class="side__t">实测样例</span></a></li>
<li><a class="side__a" href="./ledger"><span class="side__n">06</span><span class="side__t">真实评测台账</span></a></li>
</ul>
<p class="side__grp">工程</p>
<ul class="side__list">
<li><a class="side__a" href="./cost"><span class="side__n">07</span><span class="side__t">代价账本</span></a></li>
<li><a class="side__a" href="./capacity"><span class="side__n">08</span><span class="side__t">容量与存储</span></a></li>
<li><a class="side__a" href="./limits"><span class="side__n">09</span><span class="side__t">我们不宣称的内容</span></a></li>
<li><a class="side__a" href="./roadmap"><span class="side__n">10</span><span class="side__t">已解决的与下一步</span></a></li>
<li><a class="side__a" href="./reproduce"><span class="side__n">11</span><span class="side__t">复现实验</span></a></li>
</ul>
</nav>
</aside>
<main class="doc" id="doc">
<header class="doc__hd">
<span class="kicker">04 / WHAT CHANGED NEXT</span>
<h1 class="doc__h">NM2.1 · 之后做了什么</h1>
<p class="doc__lede">三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。</p>
<p class="doc__figure">
<span class="mega">94.14<small>%</small></span>
<span class="doc__figure-n">路由器 Top-1(v2 为 41.12%)</span>
</p>
</header>
<p class="lede2">
发布页上的数字来自 v2。这一页讲**之后又做了什么**:三处改动各自凭什么、一个把端到端正确率
锁死在 50% 的写入缺陷怎么定位的、以及一个 75% 泄漏的失败怎么修到 0%——又怎么在接上运行时之后失败两次。
所有数字都来自磁盘上的报告文件,每张表都标了出处;**没有一条是估算或外推**。
</p>
<div class="callout callout--warn">
<p class="eyebrow" style="color:var(--warn)">先读这一句</p>
<p class="small" style="margin:0">
这一页里**离线实验最好的数字(未知拒答 100.00%)不等于运行时可用**。它接上运行时之后
连续失败两次,最终被回退。把失败过程一起写出来,是因为它比成功更能说明这套机制到底卡在哪。
</p>
</div>
<!-- ================= 净变化 ================= -->
<h2 class="h3">净变化一览</h2>
<p class="small src">出处 · <code>V2_dpskw\NM2_VS_NM2_1.md</code>(该文件自述:所有数字均从磁盘上的评分卡 / JSON 读取,不是手工抄录)</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>轴</th><th>v2(原版)</th><th>NM2.1</th><th>净变化</th></tr></thead>
<tbody>
<tr><td>路由器 Top-1</td><td class="n">41.12%</td><td class="n acc">94.14%</td><td class="n acc">+53.02 pp</td></tr>
<tr><td>路由器 Recall@3</td><td class="n">46.73%</td><td class="n acc">96.48%</td><td class="n acc">+49.75 pp</td></tr>
<tr><td>未知拒答率</td><td class="n">0.00%</td><td class="n acc">100.00%</td><td class="n acc">+100.00 pp</td></tr>
<tr><td>未知问题泄漏(同形候选)</td><td class="n">75.00%</td><td class="n acc">0.00%</td><td class="n acc">−75.00 pp</td></tr>
<tr><td>一次写 20 条不同属性后存活</td><td class="n">12 / 20</td><td class="n acc">20 / 20</td><td class="n acc">+8 条</td></tr>
<tr><td>零字面重叠改写正确率</td><td class="n">43.75%</td><td class="n acc">68.75%</td><td class="n acc">+25.00 pp</td></tr>
<tr><td>hop 欠预测率</td><td class="n">4.68%</td><td class="n acc">0.00%</td><td class="n acc">−4.68 pp</td></tr>
<tr><td>参数量 / 每条记录地址字节</td><td class="n">2,037,774 / 512</td><td class="n">2,037,774 / 512</td><td class="n">不变</td></tr>
</tbody>
</table>
</div>
<p class="tiny">
「未知拒答率」与「未知泄漏」是同一件事的两面:前者是问库里没有的属性时**正确说不知道**的比例,
后者是**照样编一个答案**的比例。v2 在这两轴上是 0% 和 75%,也就是几乎必然硬答。
</p>
<!-- ================= §1 检索与排序 ================= -->
<h2 class="h3">一 · 路由器工件本身的检索与排序</h2>
<p class="small src">出处 · <code>router_scorecard_final.json</code>(判定见 <code>router_verdict_final.json</code>)· 冻结 v6 评测集 21,920 条 / 10 类别</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>v2 · <code>V2-128 deployed(v3)</code></th><th>NM2.1 · <code>REPLAY-128 v7 final</code></th></tr></thead>
<tbody>
<tr><td>Top-1 正确率</td><td class="n">41.12%</td><td class="n acc">94.14%</td></tr>
<tr><td>Recall@1</td><td class="n">37.03%</td><td class="n acc">88.58%</td></tr>
<tr><td>Recall@3</td><td class="n">46.73%</td><td class="n acc">96.48%</td></tr>
<tr><td>Recall@5</td><td class="n">48.91%</td><td class="n acc">97.15%</td></tr>
<tr><td>MRR</td><td class="n">47.69%</td><td class="n acc">95.77%</td></tr>
<tr><td>nDCG@3</td><td class="n">44.22%</td><td class="n acc">95.37%</td></tr>
<tr><td>多跳证据全中(Top-3)</td><td class="n">43.43%</td><td class="n acc">96.22%</td></tr>
<tr><td>多跳证据全中(仅多正例)</td><td class="n">37.15%</td><td class="n acc">95.45%</td></tr>
<tr><td>hop 正确率</td><td class="n">73.23%</td><td class="n acc">100.00%</td></tr>
<tr><td>hop 欠预测率</td><td class="n">4.68%</td><td class="n acc">0.00%</td></tr>
</tbody>
</table>
</div>
<!-- ================= §2 拒答与仲裁 ================= -->
<h2 class="h3">二 · 拒答与仲裁策略轴</h2>
<p class="small src">出处 · <code>router_scorecard_final.json</code>(门槛 0.50)+ <code>threshold_sweep_check.json</code>(0.30–0.80 全门槛扫描)</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>v2</th><th>NM2.1</th></tr></thead>
<tbody>
<tr><td>need F1</td><td class="n">89.58%</td><td class="n acc">100.00%</td></tr>
<tr><td>need 召回</td><td class="n">99.82%</td><td class="n acc">100.00%</td></tr>
<tr><td>need 精确率</td><td class="n">81.24%</td><td class="n acc">100.00%</td></tr>
<tr><td>未知拒答率</td><td class="n">0.00%</td><td class="n acc">100.00%</td></tr>
<tr><td>已知问题被误拒率</td><td class="n">0.18%</td><td class="n acc">0.00%</td></tr>
<tr><td>未知问题被误读率</td><td class="n">100.00%</td><td class="n acc">0.00%</td></tr>
<tr><td>仲裁准确率</td><td class="n">81.12%</td><td class="n acc">100.00%</td></tr>
</tbody>
</table>
</div>
<p class="small">
<strong>门槛扫描是这里真正的证据。</strong>0.30 / 0.40 / 0.50 / 0.60 / 0.70 / 0.80 六个门槛逐一复核:
NM2.1 **全门槛通过**;而原版在**每一个**门槛上未知拒答率都是 0.00%,且门槛越高误拒越差
(0.00% → 1.32%)。一个能在整条门槛轴上都成立的结论,和一个只在某个阈值上成立的点,可信度不是一回事。
</p>
<!-- ================= §3 端到端 ================= -->
<h2 class="h3">三 · 端到端整体记忆能力(同一份运行时,只有模型包不同)</h2>
<p class="small src">出处 · <code>nm2_battery_comparison_final.json</code> · A 110 用例 / B 16 / C 48 / D 重启持久化</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>原版 NM2</th><th>NM2.1(仅换路由器)</th><th>NM2.1 最终</th></tr></thead>
<tbody>
<tr><td>A 总体正确率</td><td class="n">89.09%</td><td class="n">88.18%</td><td class="n">88.18%</td></tr>
<tr><td>A <strong>可回答正确率</strong></td><td class="n">100.00%</td><td class="n">100.00%</td><td class="n acc">100.00%</td></tr>
<tr><td>A 未知拒答率</td><td class="n">60.00%</td><td class="n">56.67%</td><td class="n bad">56.67%</td></tr>
<tr><td>A 已知问题被误拒率</td><td class="n">0.00%</td><td class="n">0.00%</td><td class="n acc">0.00%</td></tr>
<tr><td>B 零字面重叠改写回答正确率</td><td class="n">43.75%</td><td class="n">68.75%</td><td class="n acc">68.75%</td></tr>
<tr><td>B 答成别的属性</td><td class="n">18.75%</td><td class="n">18.75%</td><td class="n">18.75%</td></tr>
<tr><td>B 触发读取</td><td class="n">56.25%</td><td class="n">93.75%</td><td class="n acc">93.75%</td></tr>
<tr><td>C 可回答正确率(24 同形候选)</td><td class="n">65.00%</td><td class="n">65.00%</td><td class="n acc">70.00%</td></tr>
<tr><td>C 答成别的属性</td><td class="n">35.00%</td><td class="n">35.00%</td><td class="n acc">27.50%</td></tr>
<tr><td>C <strong>未知泄漏率</strong></td><td class="n">75.00%</td><td class="n">75.00%</td><td class="n acc">0.00%</td></tr>
<tr><td>C 活跃记录 min / max</td><td class="n">23 / 24</td><td class="n">23 / 24</td><td class="n">23 / 24</td></tr>
<tr><td>D 重启后召回 / 作答 / 清理</td><td class="n">通过</td><td class="n">通过</td><td class="n acc">通过</td></tr>
</tbody>
</table>
</div>
<p class="small">
<strong>运行间波动必须自己讲出来:</strong>B 段只有 16 个用例,1 个用例 = 6.25 pp。所以 B 的 68.75%
与另一次测得的 75.00% 属于同一水平的运行间波动,**不作为增益主张**。A / C / D 的结论在多次运行中一致。
</p>
<!-- ================= §4 零重叠 ================= -->
<h2 class="h3">四 · 零字面重叠改写的泛化</h2>
<p class="small src">出处 · <code>replay_check_zov.json</code>(路由器级)与上表 B / C 段(端到端)· 数据集 300 条、24 个<strong>未见过</strong>的问法</p>
<div class="grid g2">
<div>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>v2 权重</th><th>NM2.1 权重</th></tr></thead>
<tbody>
<tr><td>路由器 Top-1(250 条可回答)</td><td class="n">18.40%</td><td class="n acc">59.60%</td></tr>
<tr><td>路由器 Recall@3</td><td class="n">36.00%</td><td class="n acc">83.20%</td></tr>
<tr><td>路由器 MRR</td><td class="n">35.07%</td><td class="n acc">73.06%</td></tr>
<tr><td>端到端改写正确率(B 段)</td><td class="n">43.75%</td><td class="n acc">68.75%</td></tr>
<tr><td>端到端未知泄漏(C 段)</td><td class="n">75.00%</td><td class="n acc">0.00%</td></tr>
</tbody>
</table>
</div>
</div>
<div>
<p class="small" style="margin-top:0">
零重叠评测集有 24 条**同句式、不同属性**的候选,随机猜中的概率是 <strong>4.17%</strong>。
所以 59.60% 大约是随机水平的 <strong>14 倍</strong>。
</p>
<div class="kv">
<div class="kv__r"><span class="kv__k">数据集规模</span><span class="kv__v">300 条评测 / 24 个未见问法</span></div>
<div class="kv__r"><span class="kv__k">训练 / 留出划分</span><span class="kv__v">前 2 个改写训练(48)/ 第 3 个留出(24)</span></div>
<div class="kv__r"><span class="kv__k">两个划分共享的查询字符串</span><span class="kv__v">0</span></div>
<div class="kv__r"><span class="kv__k">查询与自身目标的重叠</span><span class="kv__v">0</span></div>
<div class="kv__r"><span class="kv__k">含「陷阱」候选的 episode</span><span class="kv__v">54.53%(生成器统计)</span></div>
</div>
<p class="tiny">
<strong>关键设计判断:</strong>改写与**无关**属性的字符重叠被刻意保留(命中错误候选只会得到错误答案,
让任务更难,不提供捷径);真正必须为 0 的是与**自身**目标的重叠。最初把二者混为一谈时,
24 个属性里有 7 个因为「起床时间」的「时」、「办公城市」的「公」这类高频字被判为「无可用的改写」。
</p>
</div>
</div>
<!-- ================= §5 成本 ================= -->
<h2 class="h3">五 · 代价:几乎没有</h2>
<p class="small src">出处 · <code>router_scorecard_final.json</code>、<code>router_latency_bench_prod.json</code>(7 轮交错取中位数)</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>v2</th><th>NM2.1</th><th>说明</th></tr></thead>
<tbody>
<tr><td>参数量</td><td class="n">2,037,774</td><td class="n">2,037,774</td><td class="small">未增加</td></tr>
<tr><td>每条记录地址字节</td><td class="n">512</td><td class="n">512</td><td class="small">存储几何未变</td></tr>
<tr><td>单查询延迟中位数(GPU)</td><td class="n">0.9549 ms</td><td class="n">0.9169 ms</td><td class="small">略快</td></tr>
<tr><td>批量 QPS(batch=64)</td><td class="n">66,037</td><td class="n acc">69,378</td><td class="small">+5.1%</td></tr>
<tr><td>批量 QPS(batch=256)</td><td class="n">207,287</td><td class="n acc">242,759</td><td class="small">+17.1%</td></tr>
<tr><td>模型包大小</td><td class="n">8.88 GB / 22 文件</td><td class="n">8.88 GB / 23 文件</td><td class="small">多出属性头 ~0.25 MB</td></tr>
</tbody>
</table>
</div>
<p class="small">
<strong>交错基准(7 轮,消除顺序效应):</strong>单查询中位数 1.4203 ms → 1.4242 ms,差 <strong>+0.28%</strong>。
同一轮里原版自身离散度就有 4.85% —— 也就是说这个差值小于测量噪声。
<strong>零重叠那一轮里 batch=64 的读数低 9.2%,但该轮离散度是 17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快,
方向不一致,判为测量噪声而非回归</strong>;这条也写在这里,因为把噪声当结论是这个领域最常见的错误。
</p>
<!-- ================= §6 鲁棒性 ================= -->
<h2 class="h3">六 · 工程鲁棒性</h2>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>项目</th><th>v2</th><th>NM2.1</th></tr></thead>
<tbody>
<tr><td>一次写 20 条不同属性事实后存活</td><td class="n">12 / 20(8 条查询前被误删)</td><td class="n acc">20 / 20</td></tr>
<tr><td>端到端(写入修复前后,16 用例)</td><td class="n">37.50%</td><td class="n acc">68.75%</td></tr>
<tr><td>替换兼容性</td><td class="n">基线</td><td class="n acc">DROP-IN OK(16 / 16 键)</td></tr>
<tr><td>单元测试</td><td class="n">—</td><td class="n acc">52 项通过</td></tr>
<tr><td>未知问题泄漏(同形候选)</td><td class="n">75.00%</td><td class="n acc">0.00%</td></tr>
</tbody>
</table>
</div>
<!-- ================= 三处改动 ================= -->
<h2 class="h3">七 · 三处改动,每处都有测量依据</h2>
<h3>改动 1 · 记录排序混合权重取 0.5</h3>
<p class="small">
<code>memory_record_router_blend</code> 控制「打包的词面重排器」与「学习出的路由器」各占多少。
调这个之前先测了剂量曲线 —— 而且**此前的同进程测量因顺序污染已作废**,下面这版是独立进程测的。
</p>
<div id="nmBlend" class="dose"></div>
<h3>改动 2 · 把加法先验参数化,然后测出「不该动」</h3>
<p class="small">
假设是「加法先验压过了学习打分」。把它暴露成可调再测,结果**调小只会更差**:
</p>
<div id="nmPrior" class="dose"></div>
<p class="small">
这是一个<strong>有价值的负面结论</strong>:它否定了原来的假设。项目里这类结论和增益一样被保留下来,
因为它们决定了后面不再往哪个方向投入。
</p>
<h3>改动 3 · 覆盖门 —— 以及为了让它真正生效必须一起修的两处</h3>
<p class="small">
覆盖门本身很简单:先判断「这个提问在问哪个属性」,再查库里有没有这个属性。但单独加门是不生效的,
有两处**必须同时修**,否则门会被静默绕过:
</p>
<div class="lims">
<div class="lim"><span class="lim__i">01</span><div>
<h3 class="lim__t"><code>_build_text_prefix</code> 短路</h3>
<p class="lim__d">门拒绝之后如果继续往下回落,会走**旧版 16 槽注入路径**,把记忆又塞回去。
实测:不修这一处,泄漏只从 75.00% 降到 62.50%。</p></div></div>
<div class="lim"><span class="lim__i">02</span><div>
<h3 class="lim__t">闭包要动态解析 bank</h3>
<p class="lim__d"><code>reset_memory()</code> 每次都会新建 <code>memory_os_v2</code>;闭包捕获了旧引用的话,
门会永远看到一个空 bank,于是**静默旁路** —— 实测诊断输出 <code>applicable:0 / bypassed:1</code>。</p></div></div>
<div class="lim"><span class="lim__i">03</span><div>
<h3 class="lim__t">门必须自门控</h3>
<p class="lim__d">只有当库的属性集合填充了头部词表的 <strong>≥ 90%</strong> 时才让门生效。
这条阈值是必需的:放宽成「子集即可」会让门在通用套件上误判,
把 A 段可回答正确率从 100.00% 压到 92.50%、已知误拒升到 2.50%;收紧到 90% 后 A 段完全恢复,
C 段泄漏仍为 0.00%。</p></div></div>
</div>
<!-- ================= 写入路径 ================= -->
<h2 class="h3">八 · 写入路径:为什么 20 条事实会互相销毁</h2>
<p class="small src">出处 · <code>V2_dpskw\WRITE_PATH_FIX.md</code>(根因由栈追踪确证,非推断)</p>
<div class="callout callout--warn">
<p class="eyebrow" style="color:var(--warn)">症状</p>
<p class="small" style="margin:0">
一次写入 20 条<strong>不同属性</strong>的事实后,库里有 20 条记录但只剩 <strong>12 条 active</strong>,
8 条在查询前就被 retract。16 个关键零重叠用例里有 8 个的目标记录已经不存在 ——
因此端到端正确率存在 <strong>50% 的硬上限</strong>。**任何排序器都救不回一条已经被删掉的记录。**
</p>
</div>
<p class="small">16 次 retract 的调用栈完全一致:</p>
<div class="code">
<div class="code__bar"><span class="code__t">call stack</span></div>
<pre><code>eval_end_to_end_memory.py:106 write_fact
-> qwen_integration.py:3807 forward
-> qwen_integration.py:2195 _write_text_memory &lt;-- retract 调用点
-> memory_os_v2.py:2578 MemoryOSV2.retract_record
-> memory_os_v2.py:1959 PagedMemoryBankV2.retract (status = retracted)</code></pre>
</div>
<p class="small">
<code>status_transition_summary == {"active-&gt;retracted": 16}</code>,无 superseded、无 quarantined,
20 次 <code>bank.write</code> 全部返回 <code>inserted</code> —— 写入路径的<strong>唯一</strong>销毁机制就是 2195 行的 retract。
授权条件 <code>confirmed_update</code> 在 20 次写入中为真 <strong>5 次</strong>,且全部只走「学习重排器
<code>learned_best &gt;= 0.95</code>」这一条:
</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>写入序号</th><th>事实</th><th>learned_best</th><th>结果</th></tr></thead>
<tbody>
<tr><td class="n">2</td><td>出生城市</td><td class="n">0.998959</td><td>confirmed_update = True</td></tr>
<tr><td class="n">3</td><td>办公城市</td><td class="n">0.992638</td><td>confirmed_update = True</td></tr>
<tr><td class="n">9</td><td>工位楼层</td><td class="n">0.964987</td><td>confirmed_update = True</td></tr>
<tr><td class="n">12</td><td>手机尾号</td><td class="n">0.998516</td><td>confirmed_update = True</td></tr>
<tr><td class="n">15</td><td>办公楼层</td><td class="n">0.997487</td><td>confirmed_update = True</td></tr>
</tbody>
</table>
</div>
<p class="small">
随后 2187–2197 的循环退掉 8 条:3 条走 <code>record.slot_index == slot</code>,5 条走
<code>score &gt;= 0.95 and shared &gt;= 2</code>;2196 行的 <code>break</code>(仅当 <code>score &lt; 0.98</code> 才停)
让写 #12 一次级联退掉 3 条、写 #15 退掉 2 条。被退掉的正是写 1/2/3/4/9/10/11/14 —— 16 条关键事实中的 8 条。
</p>
<div class="grid g3">
<div class="card">
<p class="rel__k">辅助事实 1</p>
<p class="rel__v">0 / 20</p>
<p class="rel__d"><code>exact_slots.numel() &gt; 0</code>(token 完全相同)一次都没命中 —— 精确重复路径与此无关。</p>
</div>
<div class="card">
<p class="rel__k">辅助事实 2</p>
<p class="rel__v">{2,3,5,6,7}</p>
<p class="rel__d"><code>shared &gt;= 2</code> 毫无区分力:19 个候选的 shared 全落在这个区间,因为每条事实都含「我的 / 是」模板词元。</p>
</div>
<div class="card card--warn">
<p class="rel__k">辅助事实 3</p>
<p class="rel__v">0.9985</p>
<p class="rel__d">190 个<strong>不相关</strong>属性对里有 10 个 ≥ 0.95,最高 0.9985(「常住城市」vs「出生城市」)。<strong>调阈值不可行</strong>:假阳性高达 0.9989,没有任何标量切点能分开「不相关属性」与「真更新」。</p>
</div>
</div>
<p class="small">
<strong>根因:</strong>retire 一条已存在记录的授权完全来自学习打分加一个固定 0.95 阈值,
没有任何「新文本与在位记录共享 <code>(entity, attribute)</code>」的<strong>结构性校验</strong>。
</p>
<h3>修复(两处,都是结构性的)</h3>
<div class="lims">
<div class="lim"><span class="lim__i">01</span><div>
<h3 class="lim__t"><code>confirmed_update</code> 的两条打分类分支改为结构优先</h3>
<p class="lim__d">仅当候选文本的 <code>entity::attribute</code> 已经在库的
<code>active_by_conflict</code> 账本里存在(即确实是同一条事实的更新)才允许由打分授权;
<code>exact_slots</code> 这条结构安全的分支保持不变,以保留「重复写入幂等」的既有语义。</p></div></div>
<div class="lim"><span class="lim__i">02</span><div>
<h3 class="lim__t">retract 循环加冲突键守卫</h3>
<p class="lim__d"><code>if record.conflict_key() != candidate_conflict_key: continue</code> ——
循环只能退掉<strong>描述同一属性</strong>的记录,不再因「共享热槽」或「词面重叠高」销毁无关事实。</p></div></div>
</div>
<p class="small">
语义不受损:真·同属性更新本来就已经由 <code>PagedMemoryBankV2.write</code> 通过
<code>active_by_conflict</code> 做版本化(旧版本保留为 superseded、<code>version+1</code>),
修复只是移除了那条<strong>额外的、无监督的销毁路径</strong>。
</p>
<h3>四级验证</h3>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>级</th><th>检验</th><th>修复前</th><th>修复后</th></tr></thead>
<tbody>
<tr><td class="n">①</td><td><code>status_transition_summary</code></td><td class="n">{"active-&gt;retracted": 16}</td><td class="n acc">{}</td></tr>
<tr><td class="n">①</td><td><code>retraction_calls</code> / <code>status_counts</code></td><td class="n">16 次 / {retracted: 8, active: 12}</td><td class="n acc">[] / {active: 20}</td></tr>
<tr><td class="n">②</td><td>正对照:同属性改值</td><td class="n">—</td><td class="n acc">旧值 superseded + 新值 active,其余 19 属性不受影响,0 retract</td></tr>
<tr><td class="n">③</td><td>重排器缺席对照</td><td class="n">会踩死代码分支</td><td class="n acc">20 条全 active、0 retract</td></tr>
<tr><td class="n">④</td><td><code>target_record_active</code></td><td class="n">8 / 16</td><td class="n acc">16 / 16</td></tr>
<tr><td class="n">④</td><td><code>target_record_retracted_or_absent</code></td><td class="n">8</td><td class="n acc">0</td></tr>
<tr><td class="n">④</td><td><code>active_records_min / max</code></td><td class="n">12 / 12</td><td class="n acc">20 / 20</td></tr>
<tr><td class="n">④</td><td>probe 口径回答正确率</td><td class="n">25.00%(4 / 16)</td><td class="n acc">43.75%(7 / 16)</td></tr>
</tbody>
</table>
</div>
<p class="small"><strong>端到端效果</strong>(官方 harness,16 个零重叠用例):</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>路由器</th><th>修复前</th><th>修复后</th><th>答成别的属性</th></tr></thead>
<tbody>
<tr><td>deployed(原版 NM2)</td><td class="n">25.00%</td><td class="n acc">50.00%(+25.00 pp)</td><td class="n">18.75% → 12.50%</td></tr>
<tr><td>V2-128-v6</td><td class="n">37.50%</td><td class="n acc">68.75%(+31.25 pp)</td><td class="n">25.00% → 18.75%</td></tr>
<tr><td>REPLAY-128</td><td class="n">37.50%</td><td class="n acc">68.75%(+31.25 pp)</td><td class="n">25.00% → 18.75%</td></tr>
</tbody>
</table>
</div>
<p class="tiny">
<strong>一个诚实的补充:</strong>这 16 个用例里 <strong>93.75% 的读取仍走旧版 16 槽路径</strong>而非 V2 库记录,
地址命中 0 / 16。写入路径的修复解除了 50% 的硬上限,但「V2 库记录真正参与读取」这一项仍未解决。
</p>
<!-- ================= 未知拒答 ================= -->
<h2 class="h3">九 · 未知拒答:从 75% 泄漏到 100% 拒答</h2>
<p class="small src">出处 · <code>V2_dpskw\ABSTENTION_BREAKTHROUGH.md</code></p>
<p class="lede2">
这是整个项目最差的一个数字:问库里不存在的属性时,75% 的情况下模型照样编一个答案。
下面按「先排除一整类无效修法 → 再给出有效机制 → 最后如实记录接上运行时的两次失败」来写。
</p>
<h3>9.1 先排除掉一整类无效修法(有测量支撑)</h3>
<p class="small">
最直觉的修法是「检索分数不够高就拒答」。用<strong>只在训练集拟合、评测集算 AUC</strong> 的口径,
测了四种打分器的分数几何(top1 / margin / top-k 均值 / 离散度 / 熵 / 近邻数,共 9 个特征):
</p>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>打分器</th><th>最佳单特征 AUC</th><th>拟合头 AUC(评测集)</th></tr></thead>
<tbody>
<tr><td>cosine(冻结键余弦)</td><td class="n">0.6257(margin)</td><td class="n">0.6086</td></tr>
<tr><td>打包 <code>text_retriever</code></td><td class="n">0.6076(margin)</td><td class="n bad">0.4752</td></tr>
<tr><td>50/50 混合</td><td class="n">0.5446</td><td class="n">0.5062</td></tr>
<tr><td>训练过的路由器</td><td class="n">0.5711</td><td class="n">0.5130</td></tr>
</tbody>
</table>
</div>
<p class="small">
操作点同样不可用:混合打分器想标出 22% 的未知要误拒 <strong>28.40%</strong> 的可回答问题;
cosine 想标出 46% 要误拒 <strong>30.00%</strong>。
<strong>结论:75% 的泄漏不可能靠分数阈值或分数上的分类头修好。</strong>
这不是调参空间不够,而是这个信号根本不存在 —— 24 条同句式候选对任何提问都「一样像」。
</p>
<h3>9.2 有效的机制:先判「问的是哪个属性」,再查库里有没有</h3>
<p class="small">
换一个问题问就成立了:不是「有没有候选匹配得好」,而是「这个提问在问哪个属性,这个属性在库里吗」。
</p>
<div class="grid g2">
<div>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>封闭词表</th><th>值</th></tr></thead>
<tbody>
<tr><td>可回答 episode 中「预测属性在候选集内」</td><td class="n acc">100.00%(250 条)</td></tr>
<tr><td>未知 episode 中「预测属性在候选集内」</td><td class="n acc">0.00%(50 条)</td></tr>
<tr><td><strong>未知拒答率</strong></td><td class="n acc">100.00%</td></tr>
<tr><td><strong>已知问题被误拒率</strong></td><td class="n acc">0.00%</td></tr>
<tr><td>混淆矩阵</td><td class="n">tp=50 fn=0 fp=0 tn=250</td></tr>
</tbody>
</table>
</div>
</div>
<div>
<p class="small" style="margin-top:0">
<strong>开放集:</strong>真实用户会问模型没见过的属性。只靠最大概率调阈值不够 ——
实测<strong>没有拒绝选项时,从未见过的属性 100.00% 会被认成某个存在的属性</strong>。
所以改成<strong>显式训练一个 NONE 类</strong>:词表取 12 个属性,另取 6 个词表外属性的真实提问作为 NONE 的负样本,
最后用<strong>第三组、与两者都不相交的 6 个全新属性</strong>做测试。
</p>
<div class="kv">
<div class="kv__r"><span class="kv__k">词表内属性识别(133 条未见改写)</span><span class="kv__v">100.00%</span></div>
<div class="kv__r"><span class="kv__k">从未见过的属性被拒绝(52 条)</span><span class="kv__v">100.00%</span></div>
<div class="kv__r"><span class="kv__k">已知问题被误拒率</span><span class="kv__v">0.00%</span></div>
<div class="kv__r"><span class="kv__k">随机基线(13 类)</span><span class="kv__v">7.69%</span></div>
</div>
<p class="tiny">
运行时规则只有三步,全部可审计、无阈值调参:① 属性分类头读提问的冻结键 → 输出某个属性或 NONE;
② 若为 NONE → 拒绝;③ 若为某属性 → 查库的活跃属性集合是否包含它,不含 → 拒绝。
</p>
</div>
</div>
<h3>9.3 接上运行时:连续失败两次(如实记录)</h3>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>指标</th><th>NM2.1(无门)</th><th>+ 覆盖门(第一轮)</th><th>+ 门 + 短路(第二轮)</th></tr></thead>
<tbody>
<tr><td>A 可回答正确率</td><td class="n acc">100.00%</td><td class="n bad">6.25%</td><td class="n bad">0.00%</td></tr>
<tr><td>A 已知问题被误拒率</td><td class="n acc">0.00%</td><td class="n bad">7.50%</td><td class="n bad">7.50%</td></tr>
<tr><td>A 未知拒答率</td><td class="n">56.67%</td><td class="n">43.33%</td><td class="n">—</td></tr>
<tr><td>C 未知泄漏率</td><td class="n bad">75.00%</td><td class="n">62.50%</td><td class="n acc">0.00%</td></tr>
<tr><td>C 可回答正确率</td><td class="n">65.00%</td><td class="n">65.00%</td><td class="n">65.00%</td></tr>
</tbody>
</table>
</div>
<p class="small">
第一轮:<strong>门把几乎全部可回答问题都拒掉了</strong>(100.00% → 6.25%),而它本该修好的 C 段泄漏只从 75.00% 降到 62.50%。
第二轮定位到真正的机制缺口 —— <code>read()</code> 返回空记录后不会停止,<code>_build_text_prefix</code>
会继续落到旧版 16 槽注入路径把无关记忆又塞回去。加了短路之后,<strong>C 段泄漏真的到了 0.00%、可回答正确率无损</strong>,
但 A 段崩到 0.00%。
</p>
<p class="small">
<strong>A 段崩掉的原因已测定:</strong>段事实里 94.03% 是「这是普通对话噪声…不需要长期保存」,
只有 5.97%(63 / 1056)能解析出属性;更关键的是 <strong>A 段的属性空间完全不在属性头训练的 24 个属性之内</strong>,
头对 A 的提问只能输出那 24 个之一 → 必然不在库里 → 门把一切都拒掉。
<strong>也就是说:这个机制目前是「域内可用、跨域不可用」。</strong>
</p>
<p class="small">
结论:已把 NM2.1 的 <code>memory_coverage_gate</code> 改回 <code>false</code>,恢复为已验证的可用状态。
属性头工件保留在 <code>checkpoints/memory_attribute_head/</code>,<strong>未随包启用</strong>。
正确的最终形态是<strong>开放词表的属性匹配</strong>(把提问与库里真实存在的属性名做匹配,词表随写入动态变化),
而不是 24 类闭集分类头。
</p>
<div class="callout callout--warn">
<p class="eyebrow" style="color:var(--warn)">这一节最该记住的一句</p>
<p class="small" style="margin:0">
离线 100.00% / 0.00% 是真实的,但它只证明了机制在「裸提问键 + 24 个可解析属性」这个条件下成立,
<strong>并不等于接上运行时就能用</strong>。这一点必须在任何对外表述里讲清楚。
</p>
</div>
<!-- ================= 负面结论 ================= -->
<h2 class="h3">十 · 关键负面结论:路由器排序不影响端到端答案</h2>
<p class="small src">出处 · <code>V2_dpskw\ZERO_OVERLAP_FINDINGS.md</code> §5</p>
<p class="small">
NM2.1 的路由器在零重叠集合上 Top-1 提升了 <strong>41.20 pp</strong>,但在
<code>eval_router_critical_e2e.py</code>(16 个零重叠用例)上,REPLAY-128 与 V2-128-v6 的结果
<strong>逐位相同</strong>(默认 Top-K:37.50% / 37.50%,答成别的属性 25.00% / 25.00%,平均选中记录 5.88 / 5.88)。
</p>
<p class="small">
于是做了一个<strong>判决性实验</strong>:保留 <code>need_memory</code> / <code>hop_controller</code> / <code>head_gate</code>
全部原权重,只把排序通路(<code>query_projection</code>、<code>key_projection</code>、<code>pair_scorer</code>)
替换为同形状<strong>随机权重</strong>。结果在默认 Top-K 与 Top-K=1 下<strong>都完全不变</strong>。
</p>
<div class="lims">
<div class="lim"><span class="lim__i">01</span><div>
<h3 class="lim__t"><code>memory_os_v2.py:1671</code>:路由器分数被逐位置覆盖</h3>
<p class="lim__d">带 <code>semantic_key</code> 的记录,其 <code>_score_candidates()</code> 分数会被
<code>self.record_scorer</code> 覆盖。实测残差与 <code>text_retriever</code> 匹配 18 / 18,
与 <code>memory_router_v2</code> 匹配 0 / 18。</p></div></div>
<div class="lim"><span class="lim__i">02</span><div>
<h3 class="lim__t">「部署目录没有 <code>text_retriever.pt</code> 所以走余弦兜底」是错的</h3>
<p class="lim__d">重排器被烘焙进合并包(safetensors 内有 6 个
<code>dynamic_memory.text_retriever.*</code> 张量,1,573,377 参数),<code>_text_retriever_ready</code>
实测为 <strong>True</strong>,余弦兜底分支根本不执行。</p></div></div>
<div class="lim"><span class="lim__i">03</span><div>
<h3 class="lim__t">路由器在此配置下唯一实际生效的杠杆是 <code>need_memory</code> 门</h3>
<p class="lim__d">部署权重下门开启率 <strong>68.75%</strong>(11 / 16),而三份随机初始化的路由器只有
<strong>37.5%</strong>(6 / 16)。判决性探针只随机化了排序通路、保留了门控头,所以「端到端零变化」
是由构造保证的,不是巧合。</p></div></div>
</div>
<p class="small">
<strong>结论分两层,不要混为一谈:</strong>① 路由器记分卡上 22 / 22 的统治性和 +41.20 pp 的零重叠提升,
都是路由器工件<strong>真实</strong>的增益;② 但在这套运行时里,记录级顺序由打包的 <code>text_retriever</code> 决定,
路由器分数被覆盖,且相当比例的读取根本不走 V2 库、目标记录还会被写入路径删除。
所以<strong>路由器增益不会传递到最终答案</strong>。
</p>
<!-- ================= 仍未解决 ================= -->
<h2 class="h3">十一 · 仍未解决(不粉饰)</h2>
<div class="tbl-wrap">
<table class="dtable">
<thead><tr><th>短板</th><th>现状</th><th>说明</th></tr></thead>
<tbody>
<tr><td>跨域未知拒答</td><td class="n bad">未解决</td><td class="small">覆盖头是 24 类闭集,只在其词表被库填充 ≥ 90% 时生效;开放词表的属性匹配实测仅 <strong>49.20% Top-1 / AUC 0.6560</strong>(零训练),不足</td></tr>
<tr><td>答成别的属性</td><td class="n">27.50%</td><td class="small">同形候选间排序仍不理想;已排除先验重加权(更差)与单纯替换打分器(更差)</td></tr>
<tr><td>A 段未知拒答率</td><td class="n">56.67%</td><td class="small">未见改善</td></tr>
<tr><td>规模验证</td><td class="n bad">未做</td><td class="small">仅 24 属性 / 300 条评测;生产需上千属性、上万改写问法</td></tr>
<tr><td>V2 库记录真正参与读取</td><td class="n bad">未解决</td><td class="small">16 个关键用例里 93.75% 仍走旧版 16 槽路径,地址命中 0 / 16</td></tr>
<tr><td>通用能力回归套件</td><td class="n bad">未跑</td><td class="small"><code>eval_general_capability.py</code> 依赖的 <code>comprehensive_general.jsonl</code> 不存在</td></tr>
</tbody>
</table>
</div>
<!-- ================= 复现 ================= -->
<h2 class="h3">十二 · 复现</h2>
<div class="code">
<div class="code__bar">
<span class="code__t">PowerShell · 整体电池(A / B / C / D 四段)</span>
<button class="copy" id="nmCopy" type="button">复制</button>
</div>
<pre><code id="nmCmd">$env:PYTHONPATH='H:\Memory'; $env:PYTHONIOENCODING='utf-8'; cd H:\Memory\V2_dpskw
# 整体电池(A/B/C/D 四段)
pwsh -NoProfile -File .\run_nm2_battery.ps1 -Package qwen3_5_4b_natural_memory_v2_1 `
-Label 'NM2.1 最终' -Tag 'nm2_1_final' -PerCategory 10 -OverlapCases 48 -Blends '0.5'
# 三代对照表
& 'C:\Users\Administrator\miniconda3\envs\LLM\python.exe' -m V2_dpskw.compare_nm2_batteries `
--tag "原版NM2=nm2_orig" --tag "NM2.1=nm2_1" --tag "NM2.1最终=nm2_1_final"
# 门的适用性诊断(确认 applicable/bypassed,避免静默旁路)
& 'C:\Users\Administrator\miniconda3\envs\LLM\python.exe' -m V2_dpskw.diagnose_coverage_gate `
--package qwen3_5_4b_natural_memory_v2_1</code></pre>
</div>
<p class="small" style="margin-top:var(--sp-2xl)">
台账式的逐条原文在 <a class="linku" href="./ledger">真实评测台账</a>;
v2 的正式脏数据迁移测试在 <a class="linku" href="./results">测量结果</a>。
</p>
<nav class="pn" aria-label="章节切换"><a class="pn__a pn__a--prev" href="./results" data-dir="prev"><span class="pn__k">上一节 · 03</span><span class="pn__t">测量结果</span><span class="kbd">&larr;</span></a><a class="pn__a pn__a--next" href="./examples" data-dir="next"><span class="pn__k">下一节 · 05</span><span class="pn__t">实测样例</span><span class="kbd">&rarr;</span></a></nav>
</main>
</div>
<footer class="footer">
<div class="wrap footer__in">
<div class="footer__col">
<p class="footer__h">作者</p>
<div class="author">
<a class="author__a" href="https://wpyw.site" target="_blank" rel="noopener">
<span class="author__k">个人网站</span><span class="author__v">wpyw.site</span>
</a>
<a class="author__a" href="mailto:[email protected]">
<span class="author__k">邮箱</span><span class="author__v">[email protected]</span>
</a>
</div>
</div>
<div class="footer__col footer__col--wide">
<p class="footer__h">来源</p>
<div class="readout" id="footFiles"></div>
<p class="footer__h" style="margin-top:20px">服务端读数</p>
<div class="readout mono" id="footServer"><span>读取中…</span></div>
</div>
<div class="footer__col footer__col--wide">
<p class="footer__h">说明</p>
<p class="footer__note" id="disclaimer"></p>
<p class="footer__meta">构建于 2026-09-16 20:39 · 所有数字取自工程内的正式报告,未做外推或估算。</p>
</div>
</div>
</footer>
<!-- 全站检索:客户端索引,无后端、无网络请求 -->
<div class="pal" id="pal" hidden>
<div class="pal__mask" id="palMask"></div>
<div class="pal__box" role="dialog" aria-modal="true" aria-label="检索全站内容">
<div class="pal__hd">
<span class="pal__ico" aria-hidden="true"></span>
<input class="pal__in" id="palInput" type="text" role="combobox" aria-expanded="true"
aria-controls="palList" autocomplete="off" spellcheck="false"
placeholder="检索全站内容:章节、数字、术语…">
<span class="kbd">Esc</span>
</div>
<div class="pal__list" id="palList" role="listbox" aria-label="检索结果"></div>
<div class="pal__ft">
<span><span class="kbd">↑</span><span class="kbd">↓</span> 选择</span>
<span><span class="kbd">↵</span> 打开</span>
<span class="pal__n mono" id="palN"></span>
</div>
</div>
</div>
<script src="assets/js/data.js?v=a9c21ef3a8"></script>
<script src="assets/js/examples.js?v=a9c21ef3a8"></script>
<script src="assets/js/restart.js?v=a9c21ef3a8"></script>
<script src="assets/js/nm21.js?v=a9c21ef3a8"></script>
<script src="assets/js/ledger.js?v=a9c21ef3a8"></script>
<script src="assets/js/search.js?v=a9c21ef3a8"></script>
<script src="assets/js/space.js?v=a9c21ef3a8"></script>
<script src="assets/js/site.js?v=a9c21ef3a8"></script>
</body>
</html>