commit 1da4b14c19175fd88f5ca0172ff95f3e6c2e5162 Author: WpyQwq <3911625973@qq.com> Date: Sat Sep 19 11:21:45 2026 +0800 Initial commit: 自然记忆站:12 页纯标准库 Python 静态站点生成器,含真实评测台账(108 条)与全站客户端检索 diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..608b237 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,230 @@ +# DESIGN.md — Natural Memory 站点重设计 + +> 这份文件是**设计动作的产物**,写在动效之前。依据是 `motion-web` skill: +> `page-design.md`(静态帧怎么设计)、`design-slop.md`(AI 味闸门)、`data/visual-bank.json`(可选原型)、 +> `motion-tokens.md`(时长/缓动/弹簧)、`choreography-arc.md`(叙事弧)。 + +--- + +## 0. 先测量,再设计:旧版站点的问题(实测,不是印象) + +在 `assets/_gate.html` 里按 `design-slop.md` + `page-design.md §2` 的可量化条款跑了一遍旧站(1576px,12 页): + +| 闸门 | 获奖站中位数(n=12) | 旧站实测 | 判定 | +|---|---|---|---| +| 最大渲染字号 | **108 px**(52–218) | **48 px** | ❌ | +| 正文字号 | 16 px(12–16) | 12–14 px | ✅ | +| **display ÷ body** | **≈ 8×**(3.3–14×) | **3.4–4.0×** | ❌ A6 | +| 主色数量 | **4**(1–6) | 6–7 | ❌ A4 | +| 渐变 | **0 / 15** | home 2、results 10 | ❌ A4 | +| body 全局 max-width | 15/15 无 | 无 | ✅ | +| `

` 数量 | — | 1 | ✅ | + +**结论**:旧站最大的问题是**字阶没有落差**。48 px 的标题正好落在 `design-slop.md` A6 点名的 +「everything between 16 and 48px」区间里 —— 那不是「不够大」,是**整个页面没有尺度对比**。 +这一条足以解释「普通 / 没设计感」,而且**加留白、换配色都救不回来**。 +其余是次要的:颜色偏多、有几处渐变、色带落在 2–6 屏这个「中段」(获奖站是双峰的:≈1 屏或 7–57 屏)。 + +### 重设计之后(同一把尺子,同样 12 页,1576px) + +| 闸门 | 获奖站中位数 | 旧站 | **新站** | 判定 | +|---|---|---|---|---| +| 最大渲染字号 | 108 px | 48 px | **200 px**(`.mega`)/ 132 px(首屏标题) | ✅ | +| display ÷ body | ≈ 8× | 3.4–4.0× | **7.1 – 14.3×**(12/12 页 ≥ 7.1) | ✅ | +| 字体家族 | 2 | 3(含一个漏出来的 `monospace`) | **2** | ✅ | +| `

` 数量 | — | 1 | 1 | ✅ | +| 首屏按钮对 | A1 反模式 | 6 个药丸 | **0** | ✅ | +| 横向溢出 / 截断 / undefined / NaN | — | 0 | **0(12 页 × 4 档视口)** | ✅ | +| 渐变 | 0 / 15 | home 2, results 10 | 仅剩基线条的**斜纹填充**(语义编码,见下) | ⚠️ 保留 | + +**顺带修掉一个真 bug**:`
` 一直没有显式 `font-family`,掉回 UA 样式表的 `monospace` ——
+**全站代码块此前都不是 Geist Mono**。这也是「字体家族 = 3」的来源。已修。
+
+**唯一保留的渐变**:`.bar--base .bar__fill` 用的是 `repeating-linear-gradient` 斜纹,
+它编码的是「无记忆基线」这一条数据,是数据墨水不是装饰(A4 针对的是渐变**背景处理**)。
+
+---
+
+## 7. 明确的偏差与代价(不藏)
+
+1. **移动端的字阶落差改用「数字」承担 —— 这一条已经解决,不是遗留问题。**
+   中文标题在 320px 上推不到 7×:`page-design.md §4` 自己写了
+   「weight contrast carries hierarchy better than size contrast alone, because a huge 黑体 becomes a solid block」——
+   320px 宽放 98px 的中文标题,一行只有 3 个字,13 个字的标题要断成 5 行,那是排版事故。
+   所以**每个证据页的页眉由一个真实数字领起**(`.doc__figure` + `.mega`),
+   中文标题负责可读性,数字负责落差。实测结果:
+
+   | 视口 | 12 页 ratio 最小值 | ≥ 7× 的页数 | 布局问题 |
+   |---|---:|---:|---:|
+   | 1600 px | **11.1×** | 12 / 12 | 0 |
+   | 768 px | **7.8×** | 12 / 12 | 0 |
+   | 390 px | **7.1×** | 12 / 12 | 0 |
+   | 320 px | **7.1×** | 12 / 12 | 0 |
+   | 1600 px + reduced-motion | **11.1×** | 12 / 12 | 0 |
+
+   这不是把数字调到达标,而是**让每一页自己先把最强的那条数字说出来** ——
+   这些页面本来就是证据页,页眉只有一个标题反而浪费了它们最好的东西。
+2. **地址空间装置在 `≤900px` 与 `prefers-reduced-motion` 下不下降**,冻结成一帧静态地址空间
+   (窄屏冻结在 t=0.30「Qwen hidden state」,reduced-motion 冻结在 t=0.52「候选页」)。
+   页面高度同时从 340vh 收到 1 屏,不留下 3 屏的空滚动。
+3. **`--body` 用 `#9a9a92` 而不是 p-07 原型写的 `#909090`** —— 为了 12 页技术文档的连续阅读,
+   在原型值上抬了一档(对比度 7.6:1,AAA 正文标准以上)。这是对原型的**唯一**颜色偏离,特此标明。
+
+---
+
+## 8. 动效闸门自检结果(B1–B8)
+
+| 闸门 | 实测 | 处置 |
+|---|---|---|
+| B1 统一上浮 hover | `translateY(-*)` 命中 **0** | ✅ |
+| B2 全页统一 fade-up | 原本 ~20 个元素同款入场 → 现在 **3 处,三种不同手势**(擦除 / 数字冲入 / 侧向错峰)+ 路径图自己的画线 | ✅ |
+| B3 一个缓动一个时长 | 原本 5 个时长令牌有 3 个没用、**15 处硬编码 `120ms ease-out`** → 现在 **23 条 transition 全部走令牌**,实际用到 fast/std/med/slow **4 档** | ✅ |
+| B4 预算全烧在首屏 | 首屏是装置,但 ~60% 处另有头号数字、重启装置两处收网 | ✅ |
+| B5 动效与内容无关 | 机制来自「寻址」本身的物理,换到别的题材讲不通 | ✅ |
+| B6 只有视差做纵深 | 无视差图层 | ✅ |
+| B7 跟随光标的圆圈 | 无 | ✅ |
+| B8 reduced-motion 下什么都不剩 | 实测 `wipeTarget=none`、装置冻结成完整静态帧、页高收到 1 屏、12 页 0 布局问题 | ✅ |
+
+**顺带修掉的三个健壮性问题**(都不是猜的,是量出来的):
+
+1. **`
` 没有 `font-family`** → 掉回 UA 的 `monospace`,**全站代码块此前都不是 Geist Mono**。
+2. **入场只靠 `IntersectionObserver`** → IO 在被节流的 iframe / 无头虚拟时间 / 某些低端 WebView 里不保证投递;
+   而 `clip-path` 擦除一旦不触发就是**一个永远看不见的标题**。已加「按滚动位置判定」的兜底,实测未入场元素从 **5 → 0**。
+3. **首屏循环 latch**:`raf` 句柄非空但帧被冻结时,`kick()` 永远提前返回,循环永久卡死。
+   已改为超时同步走一帧 —— 实测 `spaceT` 从 **0.00 → 0.39**。
+
+
+---
+
+## 1. 叙事结构(macrostructure)
+
+**Editorial + set-pieces** —— 页面主体是**可读的正文**,动效出现 3 次、每次都有明确含义,
+是段落之间的标点,不是背景装饰。
+
+选它的理由来自内容本身:这是一个**研究发布站**,主体是 12 页文档、数据表和一份 108 条台账。
+把它硬塞进「单场景」或「Acts」会牺牲可读性;而纯粹的 Editorial 又浪费了这套内容里
+**唯一真正有空间感的东西 —— 记忆的地址空间**。
+
+- **线性内容** → 走 editorial,而不是 index / instrument(`page-design.md §1` 的两条判据之一)。
+- **相机移动还是世界移动**:世界移动(地址空间被镜头穿过),所以镜头更像「下降」而不是「横摇」。
+
+**叙事弧:`cold-open`**(安静入场 → 复杂度上升 → 峰值 → 收束)
+`0–20%` 极简、只有字;`20–70%` 地址空间下降(峰值在此);`70–100%` 结果与边界,留白放开。
+
+---
+
+## 2. 视觉系统(提交一个原型,不混搭)
+
+来自 `data/visual-bank.json`,**选一个、执行到底**:
+
+- **调色:`p-07 Bold Black + Signal Accent`**("creative agencies, award sites, experimental portfolios, bold studios")
+  - ground `#000000`(纯黑,不是近黑)
+  - ink `#fffef4`(骨白,不是纯白)
+  - ink-2 `#909090` / ink-3 `#555555`
+  - surface `rgba(255,255,255,.05)` · border `rgba(255,255,255,.12)`
+  - **信号色 `#00ff6a`,每页只出现一次**(原型原文:one per page max)
+  - 主色数量从 6–7 压到 **≤4**;**渐变全部删除**
+- **字体:两个家族**(获奖站中位数就是 2,range 1–4)
+  - `Archivo Variable`(wght 100–900 · wdth 62–125%)—— 显示与正文的拉丁部分、**全部数字**
+  - `Geist Mono` —— 标签、代码、字段名
+  - 中文回落到系统 CJK(PingFang SC / 微软雅黑 / Noto Sans CJK),**不额外加载 CJK 字体**
+
+### 2.1 字阶:落差就是设计(`page-design.md §4`)
+
+| 令牌 | 值 | 用途 |
+|---|---|---|
+| `--fs-mega` | `clamp(4.5rem, 12vw, 12.5rem)` = 72 → **200 px** | 拉丁/数字巨字 |
+| `--fs-display` | `clamp(2.75rem, 7vw, 6.5rem)` = 44 → **104 px** | 页面主标题 |
+| `--fs-h2` | `clamp(1.75rem, 3.4vw, 3rem)` | 章节标题 |
+| `--fs-lede` | `1.125rem` | 导语 |
+| `--fs-body` | `0.875rem` | **正文(14 px)** |
+
+`mega ÷ body = 200/14 ≈ 14×`(获奖站上沿),`display ÷ body ≈ 7.4×`(≈ 中位数 8×)。
+旧站是 3.4× —— **这是本次重设计的核心动作**。
+
+### 2.2 中日韩显示字的特殊处理(`page-design.md §4` 明确点名)
+
+中文大字和拉丁大字**不是一套参数**:
+
+| | 行高 | 字距 | 字重 |
+|---|---|---|---|
+| 拉丁/数字 `.dsp--lat` | 0.90 | `-0.045em` | 700,`font-stretch: 92%` |
+| 中文 `.dsp` | **1.28** | **-0.005em** | 500 |
+
+中文行高更大、负字距要收(不然字腔会闭合),层级**靠字重对比**而不是只靠字号。
+所以拉丁与中文的 display 是两个 class,在标记里显式指定,不靠回落碰运气。
+
+---
+
+## 3. 版面(layout)
+
+- **全幅页面,逐元素设 measure**(获奖站 15/15 都没有 body 全局 max-width):
+  `--measure: 62ch` 用于连续正文,`--measure-wide: 88ch` 用于说明,媒体全幅。
+- **间距不是常数**(`page-design.md §6`):`--band-tight 48–80px` / `--band 80–160px` /
+  `--band-wide 120–260px`,按「两块该不该被读成一组」来给。
+- **至少三种不同的 section 形状**:全幅巨字带 / 非对称 2:1 分栏 / 数据表带 / 卡片网格 /
+  装置带(整屏交互)/ 引用带 —— 不允许「每段都是居中标题 + 段落」。
+
+---
+
+## 4. 动效概念(来自题材自身的物理)
+
+**概念名:地址空间下降(Address Descent)**
+
+题材的物理就是**寻址**:1,048,576 个槽位、738 条活跃记录、730 个 LSH 桶、
+每页 32 条、每次只把少量记录提升到 GPU。整条读取路径本来就是**一次下降**:
+问题 → 128 维地址 → 粗索引桶 → 候选页 → 页内重排 → Top-K → 证据前缀 → 生成。
+
+**招牌场景**:一个整屏的地址空间栅格(1,048,576 个槽位画成点阵,738 个亮着)。
+滚动时镜头**一路下降五个数量级**:整个空间 → 730 个桶 → 一页 32 条 → 一条记录 →
+这条记录变成注入模型的前缀 token。终点是**一条记录**,不是一句 slogan。
+
+这个机制满足 `design-slop.md` B5(机制必须来自题材自身的物理):换到任何别的题材上都讲不通。
+
+**动效令牌**(`motion-tokens.md`,至少三档时长 + 弹簧):
+
+| 令牌 | 值 | 用途 |
+|---|---|---|
+| `--dur-fast` | 160ms | 悬停、按下、开关 |
+| `--dur-std` | 300ms | 面板、抽屉、状态切换 |
+| `--dur-med` | 460ms | 区块入场(少数几处) |
+| `--dur-slow` | 700ms | 巨字入场 |
+| `--dur-cine` | 1200ms | 冷开场 |
+| `--ease-exp` | `cubic-bezier(.16,1,.3,1)` | 默认出场 |
+| `--ease-sharp` | `cubic-bezier(.65,0,.45,1)` | 硬切、品牌感 |
+| `--ease-in-expo` | `cubic-bezier(.7,0,.84,0)` | **只用于离场** |
+
+**明确不做什么**(对应 `design-slop.md` B1–B8):
+- B1 不做「所有卡片统一上浮 4px + 阴影」
+- B2 不做「每个区块都 fade-up 20px」—— 大多数内容**本来就在那儿**,入场只留给 3 处
+- B3 不允许全站一个 `0.3s ease`
+- B4 不把预算全烧在首屏
+- B7 不做跟随光标的圆圈
+- B8 reduced-motion 下必须仍然是一张完整的页面
+
+---
+
+## 5. 移动端降级(`page-design.md §7`,显式写明,不含糊)
+
+- **地址空间装置**:`< 900px` 关闭点阵与镜头下降,改为一帧静态的地址空间图 + 数字条。
+  降级**不是**隐藏内容,是把动效换成静态帧。
+- 巨字:`12vw` 下限 72px,320px 宽时仍是 72px(不缩到看不清)。
+- 悬停不存在 —— 所有悬停信息必须有第二个落点(见设备页/台账页的显式状态)。
+- 滚动在移动端更长,节奏按移动端重读一遍,不假设等比缩放。
+
+---
+
+## 6. 设计闸门自检(交付前必须全过)
+
+`design-slop.md` A1–A8 / B1–B8 + `page-design.md §8`,用 `assets/_gate.html` 逐条量:
+
+- [ ] A1 首屏不是「徽标 + 标题 + 副标题 + 两个按钮」
+- [ ] A2 没有三张等宽居中卡片
+- [ ] A3 每页 ≥3 种不同 section 形状
+- [ ] A4 无紫/靛渐变;主色 ≤4;信号色每页只出现一次
+- [ ] A5 没有噪点/光晕/网格纸这类「填空装饰」
+- [ ] A6 display ÷ body ≥ 7×
+- [ ] A7 没有手绘的假浏览器/假手机外壳
+- [ ] A8 没有编造的证明(本站所有数字都来自工程内真实报告)
+- [ ] B1–B8 动效闸门
+- [ ] D 移动端 320/375/414/768 无横向滚动;reduced-motion 下页面完整
diff --git a/assets/css/site.css b/assets/css/site.css
new file mode 100644
index 0000000..7168612
--- /dev/null
+++ b/assets/css/site.css
@@ -0,0 +1,1448 @@
+/* ==================================================================
+   Natural Memory — site.css
+
+   设计系统见项目根目录 DESIGN.md(设计动作的产物,写在动效之前)。
+   本文件按 motion-web skill 重建:
+
+   调色    data/visual-bank.json → p-07「Bold Black + Signal Accent」
+           纯黑地面 #000000 · 骨白 #fffef4 · 次级 #9a9a92 · 三级 #5c5c56
+           信号色 #00ff6a —— 每页只出现一次(原型原文:one per page max)
+           渐变全部删除(获奖站 15/15 都不用渐变做背景)
+
+   字体    两个家族(获奖站中位数 2):
+           Archivo Variable(wght 100–900 · wdth 62–125%)—— 显示、正文拉丁、全部数字
+           Geist Mono —— 标签、代码、字段名
+           中文回落系统 CJK,不额外加载 CJK 字体
+
+   字阶    落差就是设计(page-design.md §4):
+           mega 200px / display 104px 对 body 14px = 14× / 7.4×
+           中文大字与拉丁大字参数不同(§4 明确点名):
+             拉丁 .dsp--lat  lh .90  ls -.045em  700  stretch 92%
+             中文 .dsp        lh 1.28 ls -.005em  500
+
+   版面    全幅页面、逐元素 measure(获奖站 15/15 都没有 body 全局 max-width)
+           间距不是常数:tight / normal / wide 三档按语义给
+   ================================================================== */
+
+/* ---------- 1. 字体 ---------- */
+@font-face {
+  font-family: "Archivo Variable"; font-style: normal; font-weight: 100 900;
+  font-stretch: 62% 125%; font-display: swap;
+  src: url("../fonts/archivo-latin-wdth-normal.woff2") format("woff2-variations");
+}
+@font-face {
+  font-family: "Geist Mono"; font-style: normal; font-weight: 100 900; font-display: swap;
+  src: url("../fonts/geist-mono-latin-wght-normal.woff2") format("woff2-variations");
+}
+
+/* ---------- 2. 令牌 ---------- */
+:root {
+  /* 颜色 · p-07 —— 三个灰 + 一个信号色,不再有别的色相 */
+  --canvas:        #000000;
+  --canvas-soft:   #0e0e0e;                     /* 悬停/激活面 */
+  --canvas-card:   #000000;                     /* 卡片与地面同色:层次只靠发丝线 */
+  --canvas-mid:    #262623;                     /* 轨道底 */
+  --hairline:      #1f1f1c;
+  --btn-border:    rgba(255, 255, 255, .28);
+  --ink:           #fffef4;                     /* 骨白,不是纯白 */
+  --ink-hover:     #ffffff;
+  --on-primary:    #000000;
+  --body:          #9a9a92;
+  --mute:          #5c5c56;
+  --signal:        #00ff6a;                     /* 每页只用一次:首屏画布里那颗目标记录 */
+
+  /* 圆角:p-07 是硬边原型,圆角压到最小 */
+  --r-sm: 2px;
+  --r-pill: 9999px;
+
+  /* 间距 —— 4px 基数 */
+  --sp-xxs: 2px;
+  --sp-xs:  4px;
+  --sp-sm:  8px;
+  --sp-md:  12px;
+  --sp-lg:  16px;
+  --sp-xl:  24px;
+  --sp-2xl: 32px;
+  --sp-3xl: 48px;
+  --sp-4xl: 64px;
+
+  /* 版式 */
+  --font-sans: "Archivo Variable", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", system-ui, sans-serif;
+  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
+
+  /* 字阶 —— mega ÷ body = 14×,display ÷ body = 7.4× */
+  --fs-mega:       clamp(72px, 12vw, 200px);
+  --fs-display:    clamp(44px, 7vw, 104px);
+  --fs-display-xl: var(--fs-display);
+  --fs-display-lg: clamp(32px, 4.6vw, 64px);
+  --fs-display-md: clamp(26px, 3.2vw, 44px);
+  --fs-display-sm: 24px;
+  --fs-display-xs: 18px;
+  --fs-lede:       1.125rem;
+  --fs-body:       0.875rem;                    /* 14px */
+  --fs-small:      0.875rem;                    /* 连续正文:不为了「看起来精致」压到 13px */
+  --fs-label:      0.75rem;                     /* 12px,标签/注释/字段名 */
+
+  --lh-lat:     .90;                            /* 拉丁巨字 */
+  --lh-cjk:     1.28;                           /* 中文巨字:行高更大、负字距要收 */
+  --ls-lat:     -.045em;
+  --ls-cjk:     -.005em;
+  --ls-display: var(--ls-cjk);
+  --font-stretch-display: 92%;
+
+  /* 版面 */
+  --container: 1440px;                          /* 内层栅格上限,不是 body 的 max-width */
+  --measure:      62ch;
+  --measure-wide: 88ch;
+  --nav-h: 66px;
+
+  /* 段落留白 —— 三档,按「两块该不该被读成一组」给 */
+  --band-tight: clamp(48px, 6vw, 80px);
+  --band:       clamp(80px, 10vw, 152px);
+  --band-wide:  clamp(120px, 16vw, 248px);
+
+  /* 动效令牌(motion-tokens.md) */
+  --dur-fast:  160ms;
+  --dur-std:   300ms;
+  --dur-med:   460ms;
+  --dur-slow:  700ms;
+  --dur-cine:  1200ms;
+  --ease-exp:   cubic-bezier(.16, 1, .3, 1);
+  --ease-sharp: cubic-bezier(.65, 0, .45, 1);
+  --ease-circ:  cubic-bezier(0, .55, .45, 1);
+  --ease-in-expo: cubic-bezier(.7, 0, .84, 0);
+  --ease:       var(--ease-exp);                /* 兼容旧引用 */
+}
+
+/* ---------- 3. 基础 ---------- */
+*, *::before, *::after { box-sizing: border-box; }
+html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
+
+body {
+  margin: 0;
+  background: var(--canvas);
+  color: var(--body);
+  font-family: var(--font-sans);
+  font-size: var(--fs-body);
+  line-height: 1.7;
+  font-weight: 400;
+  letter-spacing: 0;
+  font-synthesis-weight: none;
+  text-wrap: pretty;
+  -webkit-font-smoothing: antialiased;
+  overflow-x: hidden;
+}
+
+a { color: inherit; text-decoration: none; }
+button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
+::selection { background: var(--ink); color: var(--on-primary); }
+:focus-visible { outline: 1px solid var(--ink); outline-offset: 3px; }
+
+/* 行内代码 */
+code {
+  font-family: var(--font-mono);
+  font-size: 0.9em;
+  color: var(--ink);
+  background: var(--canvas-soft);
+  padding: 1px 5px;
+  border-radius: var(--r-sm);
+}
+/* pre 必须显式指定:UA 样式表给的是 font-family: monospace,
+   不覆盖的话代码块会掉回浏览器默认等宽字体(此前全站就是这样)。 */
+pre {
+  margin: 0;
+  font-family: var(--font-mono);
+  font-size: var(--fs-label);
+  line-height: 1.75;
+  tab-size: 2;
+}
+pre code { background: none; padding: 0; font-size: inherit; border-radius: 0; }
+
+.wrap { width: 100%; max-width: var(--container); margin-inline: auto; padding-inline: clamp(20px, 4vw, 56px); }
+.wrap--narrow { max-width: none; }
+.skip { position: absolute; left: -9999px; }
+.skip:focus {
+  left: var(--sp-lg); top: var(--sp-lg); z-index: 99;
+  background: var(--ink); color: var(--on-primary);
+  padding: var(--sp-sm) var(--sp-lg);
+}
+
+/* ---------- 4. 排版:落差就是设计 ---------- */
+h1, h2, h3, h4 { margin: 0; font-family: var(--font-sans); font-weight: 500; color: var(--ink); }
+
+/* 中文大字:行高放大、负字距收回,层级靠字重(page-design.md §4) */
+.dsp {
+  font-size: var(--fs-display); line-height: var(--lh-cjk);
+  letter-spacing: var(--ls-cjk); font-weight: 500;
+}
+/* 拉丁/数字巨字:同一量级但可以收紧 */
+.dsp--lat {
+  font-size: var(--fs-display); line-height: var(--lh-lat);
+  letter-spacing: var(--ls-lat); font-weight: 700;
+  font-stretch: var(--font-stretch-display);
+  font-variant-numeric: tabular-nums;
+}
+/* 超巨:只给真正的头号数字。数字是拉丁字形,可以推到 200px 而不变成一块黑体方块 ——
+   这正好绕开 page-design §4 说的「中文大字过大就成实心块」的问题,
+   所以移动端撑住 display ÷ body 落差的是它,不是中文标题。 */
+.mega {
+  font-size: clamp(84px, 26vw, 200px); line-height: .86; letter-spacing: -.05em;
+  font-weight: 700; font-stretch: 88%; color: var(--ink);
+  font-variant-numeric: tabular-nums; display: block;
+}
+.mega--cjk { font-size: var(--fs-display); line-height: var(--lh-cjk); letter-spacing: var(--ls-cjk); font-weight: 500; font-stretch: 100%; }
+
+.h1 { font-size: var(--fs-display); line-height: var(--lh-cjk); letter-spacing: var(--ls-cjk); }
+.h2 { font-size: var(--fs-display-md); line-height: 1.16; letter-spacing: -.012em; }
+.h3 { font-size: var(--fs-display-sm); line-height: 1.3; letter-spacing: -.008em; }
+.doc__h { font-size: clamp(56px, 10.5vw, 100px); line-height: 1.14; letter-spacing: var(--ls-cjk); }
+
+strong, b { font-weight: 600; color: var(--ink); }
+em { font-style: italic; }
+
+.lede, .lede2 { font-size: var(--fs-lede); line-height: 1.65; color: var(--body); max-width: var(--measure); }
+.small  { font-size: var(--fs-small); line-height: 1.65; color: var(--body); max-width: var(--measure-wide); }
+.tiny   { font-size: var(--fs-label); line-height: 1.6; color: var(--mute); max-width: 96ch; }
+.mono   { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 1.4px; }
+.src    { font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 1.2px; color: var(--mute); }
+
+/* 签名式眉标:等宽大写 + 正字距 */
+.eyebrow, .kicker, .shead__idx {
+  font-family: var(--font-mono);
+  font-size: var(--fs-label);
+  line-height: 1.6;
+  font-weight: 400;
+  letter-spacing: 1.8px;
+  text-transform: uppercase;
+  color: var(--mute);
+  margin: 0;
+  display: block;
+}
+.arrow { color: var(--mute); padding-inline: 6px; }
+.rule { height: 1px; background: var(--hairline); border: 0; margin: 0; }
+.serif-it { color: var(--ink); }
+
+/* 正文区的小节节奏:统一用 48px 上留白,避免各页内联边距不一致 */
+#doc h2, #doc h3, #doc h4 { margin-top: var(--sp-3xl); }
+#doc > *:first-child, .card h2, .card h3, .card h4,
+.panel h2, .panel h3, .panel h4, .ex h3 { margin-top: 0; }
+#doc > p + h2, #doc > p + h3 { margin-top: var(--sp-3xl); }
+#doc > p { margin: 0 0 var(--sp-lg); }
+#doc > p.small, #doc > p.tiny { margin-bottom: var(--sp-md); }
+
+/* ---------- 5. 顶栏(nav-bar: padding 12 24) ---------- */
+.site {
+  position: sticky; top: 0; z-index: 50;
+  background: var(--canvas);
+  border-bottom: 1px solid transparent;
+}
+.site.is-stuck { border-bottom-color: var(--hairline); }
+
+.topbar {
+  display: flex; align-items: center; gap: var(--sp-lg);
+  height: var(--nav-h); max-width: var(--container); margin-inline: auto;
+  padding-inline: var(--sp-xl);
+}
+.topbar__spacer { flex: 1 1 auto; }
+
+.brand { display: flex; align-items: center; gap: var(--sp-sm); flex: none; }
+.brand__mark { width: 22px; height: 22px; flex: none; }
+.brand__mark .dim { fill: var(--canvas-mid); }
+.brand__mark .on { fill: var(--ink); }
+.brand__name { font-size: 14px; line-height: 20px; color: var(--ink); }
+
+/* 药丸:整个交互形状系统 */
+.pill, .chip, .kbd {
+  display: inline-flex; align-items: center; gap: var(--sp-sm);
+  border: 1px solid var(--btn-border);
+  border-radius: var(--r-pill);
+  color: var(--ink);
+  white-space: nowrap;
+}
+.pill { height: 28px; padding: 0 var(--sp-md); font-size: 12px; letter-spacing: 1.2px; font-family: var(--font-mono); }
+.pill--acc { border-color: var(--ink); }
+.dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink); flex: none; }
+
+.crumb { display: flex; align-items: baseline; gap: var(--sp-sm); min-width: 0; }
+.crumb__n { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.crumb__t { font-size: 14px; line-height: 20px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
+.progress { position: absolute; left: 0; bottom: -1px; height: 1px; width: 0; background: var(--ink); }
+
+/* ---------- 6. 骨架:侧栏 + 内容(容器 1200px) ---------- */
+.shell {
+  display: grid; grid-template-columns: 232px minmax(0, 1fr);
+  gap: var(--sp-4xl);
+  max-width: var(--container); margin-inline: auto; padding-inline: var(--sp-xl);
+  overflow-x: clip;
+}
+.side {
+  position: sticky; top: var(--nav-h); align-self: start;
+  max-height: calc(100vh - var(--nav-h)); overflow-y: auto;
+  padding: var(--sp-2xl) 0 var(--sp-3xl);
+  border-right: 1px solid var(--hairline);
+}
+.side__in { padding-right: var(--sp-lg); }
+.side, .side__in, .side__list, .side__a { min-width: 0; }
+
+.side__grp {
+  margin: var(--sp-xl) 0 var(--sp-sm); padding-left: var(--sp-md);
+  font-family: var(--font-mono); font-size: 12px; line-height: 16px;
+  letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute);
+}
+.side__grp:first-child { margin-top: 0; }
+.side__list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-xxs); }
+/* ex-app-shell-row:active 用 primary 作为指示条 */
+.side__a {
+  display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: var(--sp-sm);
+  align-items: baseline; padding: var(--sp-sm) var(--sp-md);
+  border-radius: var(--r-sm); border-left: 1px solid transparent;
+  transition: background var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.side__a:hover { background: var(--canvas-soft); }
+.side__a.is-on { background: var(--canvas-soft); border-left-color: var(--ink); }
+.side__n { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.side__t { font-size: 14px; line-height: 20px; color: var(--body); }
+.side__a:hover .side__t { color: var(--ink); }
+.side__a.is-on .side__t { color: var(--ink); }
+.side__a.is-on .side__n { color: var(--ink); }
+
+/* hero / content band */
+.doc { position: relative; min-width: 0; max-width: 100%; padding: var(--band) 0 var(--band-tight); }
+/* 页眉:标题左、头号数字右 —— 非对称分栏,不是居中标题块 */
+.doc__hd {
+  display: grid; grid-template-columns: minmax(0, 1fr) auto;
+  gap: var(--sp-xl) clamp(24px, 5vw, 72px);
+  align-items: end;
+  margin-bottom: var(--band-tight); padding-bottom: var(--sp-xl);
+  border-bottom: 1px solid var(--hairline);
+}
+.doc__hd > .kicker { grid-column: 1 / -1; }
+.doc__h { margin-top: var(--sp-lg); }
+.doc__lede {
+  margin: var(--sp-xl) 0 0; max-width: var(--measure);
+  font-size: var(--fs-lede); line-height: 1.6; color: var(--body);
+}
+/* 头号数字:文档页自己先把最强的那条数字说出来 */
+.doc__figure { margin: 0; text-align: right; display: grid; gap: var(--sp-md); justify-items: end; }
+.doc__figure .mega { color: var(--ink); }
+.doc__figure .mega small { font-size: .30em; letter-spacing: -.01em; margin-left: .04em; font-weight: 500; }
+.doc__figure-n {
+  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 1.2px;
+  color: var(--mute); max-width: 30ch; line-height: 1.8;
+}
+
+/* 文档页的章节标题不能停在 24px:页面标题 100px、正文 14px,
+   章节标题落在中间会让层级整个塌掉。片段里写的是 class="h3",这里按上下文提上来。 */
+#doc h2 { font-size: var(--fs-display-md); line-height: 1.14; letter-spacing: -.012em; }
+
+@media (max-width: 900px) {
+  .doc__hd { grid-template-columns: minmax(0, 1fr); align-items: start; }
+  .doc__figure { text-align: left; justify-items: start; margin-top: var(--sp-xl); }
+  .doc__figure .mega { font-size: clamp(98px, 26vw, 140px); }
+}
+
+/* ---------- 7. 按钮:轮廓胶囊为常态,白色实底仅作极少数例外 ---------- */
+.btn {
+  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-sm);
+  padding: var(--sp-sm) var(--sp-lg);
+  background: transparent;
+  border: 1px solid var(--btn-border);
+  border-radius: var(--r-pill);
+  color: var(--ink);
+  font-size: 14px; line-height: 20px; font-weight: 400;
+  transition: background var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.btn:hover { background: var(--canvas-soft); border-color: var(--ink); }
+.btn--pri { background: var(--ink); border-color: var(--ink); color: var(--on-primary); }
+.btn--pri:hover { background: var(--ink-hover); border-color: var(--ink-hover); color: var(--on-primary); }
+.hero__cta { display: flex; flex-wrap: wrap; gap: var(--sp-md); align-items: center; }
+
+.chip { height: 28px; padding: 0 var(--sp-md); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--body); }
+.chip__dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink); }
+.kbd { min-width: 26px; height: 26px; padding: 0 var(--sp-sm); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--body); }
+
+/* ---------- 8. 卡片:8px 圆角 · #191919 · 发丝线 · 零阴影 ---------- */
+.grid { display: grid; gap: var(--sp-lg); }
+.g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.g3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.g4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.g-2-1 { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); }
+
+.card {
+  background: var(--canvas-card);
+  border: 1px solid var(--hairline);
+  border-radius: var(--r-sm);
+  padding: var(--sp-xl);
+}
+.card--flat { background: var(--canvas-card); }
+/* 极性翻转:白底近黑字,规范里的 featured/active 处理 */
+.card--acc { background: var(--ink); border-color: var(--ink); color: var(--on-primary); }
+.card--acc .eyebrow, .card--acc .h3, .card--acc strong, .card--acc .small, .card--acc .tiny { color: var(--on-primary); }
+.card--warn { background: var(--canvas-card); }
+
+/* ---------- 9. KPI / 数值(mono 计数器,规范里的 metric counter) ---------- */
+.kpis { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.kpi { background: var(--canvas-card); padding: var(--sp-xl); display: grid; gap: var(--sp-md); align-content: start; }
+.kpi__k { font-family: var(--font-mono); font-size: 12px; line-height: 16px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.kpi__v { font-family: var(--font-mono); font-size: 24px; line-height: 32px; color: var(--ink); letter-spacing: -.02em; }
+.kpi__v--long { font-size: 20px; line-height: 28px; }
+.kpi__v .to { color: var(--ink); }
+.kpi__v .mid { color: var(--mute); }
+.kpi__d { font-size: 14px; line-height: 20px; color: var(--mute); }
+
+.statrow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.stat { background: var(--canvas-card); padding: var(--sp-xl); display: grid; gap: var(--sp-sm); align-content: start; }
+.stat__k { font-family: var(--font-mono); font-size: 12px; line-height: 16px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.stat__v { font-family: var(--font-mono); font-size: 28px; line-height: 32px; color: var(--ink); letter-spacing: -.02em; }
+.stat__v small { font-size: 12px; letter-spacing: 1.2px; color: var(--mute); margin-left: var(--sp-xs); }
+.stat--acc .stat__v { color: var(--ink); }
+.stat__d { font-size: 14px; line-height: 20px; color: var(--mute); }
+
+.mini { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.mini > div { background: var(--canvas-card); display: flex; justify-content: space-between; gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); }
+.mini span { font-size: 14px; line-height: 20px; color: var(--body); }
+.mini b { font-family: var(--font-mono); font-size: 14px; color: var(--ink); }
+.mini--lg > div { display: block; padding: var(--sp-lg); }
+.mini--lg span { font-size: 16px; line-height: 24px; }
+
+/* ---------- 10. 导航卡片 / 作者 ---------- */
+.navcard {
+  display: grid; gap: var(--sp-sm); align-content: start;
+  background: var(--canvas-card); border: 1px solid var(--hairline);
+  border-radius: var(--r-sm); padding: var(--sp-xl);
+  transition: background var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.navcard:hover { background: var(--canvas-soft); border-color: var(--btn-border); }
+.navcard__n { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.navcard__t { font-size: 20px; line-height: 28px; color: var(--ink); letter-spacing: -.01em; }
+.navcard__d { font-size: 14px; line-height: 20px; color: var(--mute); }
+.navcard--acc { border-color: var(--btn-border); }
+
+.authorcard { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-xl); }
+.authorcard__k { margin: 0 0 var(--sp-sm); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.authorcard__a { font-size: 16px; line-height: 24px; color: var(--ink); border-bottom: 1px solid var(--btn-border); }
+.authorcard__a:hover { border-bottom-color: var(--ink); }
+.authorcard__v { margin: 0; font-family: var(--font-mono); font-size: 14px; color: var(--body); }
+
+.author { display: grid; gap: var(--sp-sm); }
+.author__a { display: inline-flex; align-items: baseline; gap: var(--sp-sm); }
+.author__k { font-size: 14px; line-height: 20px; color: var(--mute); white-space: nowrap; }
+.author__v { font-family: var(--font-mono); font-size: 14px; color: var(--ink); border-bottom: 1px solid var(--btn-border); }
+.author__a:hover .author__v { border-bottom-color: var(--ink); }
+
+/* ---------- 11. 表格(ex-data-table-cell:表头 mono 大写、内容 body-sm) ---------- */
+.tbl-wrap { overflow-x: auto; border: 1px solid var(--hairline); border-radius: var(--r-sm); background: var(--canvas-card); }
+.dtable { width: 100%; border-collapse: collapse; }
+.dtable th {
+  text-align: left; background: var(--canvas-soft);
+  font-family: var(--font-mono); font-size: 12px; line-height: 16px; font-weight: 400;
+  letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute);
+  padding: var(--sp-md) var(--sp-lg); border-bottom: 1px solid var(--hairline); white-space: nowrap;
+}
+.dtable td { padding: var(--sp-md) var(--sp-lg); border-bottom: 1px solid var(--hairline); color: var(--body); font-size: 14px; line-height: 20px; vertical-align: baseline; }
+.dtable tr:last-child td { border-bottom: 0; }
+.dtable tr:hover td { background: var(--canvas-soft); }
+.dtable td.n { font-family: var(--font-mono); color: var(--ink); white-space: nowrap; }
+.dtable td.acc { color: var(--ink); }
+.dtable td.bad { color: var(--mute); }
+
+.gt { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.gt__h, .gt__r { display: grid; gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); background: var(--canvas-card); align-items: baseline; }
+.gt__h { background: var(--canvas-soft); }
+.gt__h span { font-family: var(--font-mono); font-size: 12px; line-height: 16px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.gt--4 .gt__h, .gt--4 .gt__r { grid-template-columns: minmax(0, .8fr) minmax(0, 1.1fr) minmax(0, .75fr) minmax(0, 1.5fr); }
+.gt--3 .gt__h, .gt--3 .gt__r { grid-template-columns: minmax(0, .75fr) minmax(0, 1fr) minmax(0, 1.95fr); }
+.gt__r:hover { background: var(--canvas-soft); }
+.gt__k { font-size: 16px; line-height: 24px; color: var(--ink); }
+.gt__v { font-size: 14px; line-height: 20px; color: var(--body); }
+.gt__who { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.gt__who--acc { color: var(--ink); }
+.gt__why { font-size: 14px; line-height: 22px; color: var(--mute); }
+.scrollx { overflow-x: auto; border-radius: var(--r-sm); }
+.scrollx > .gt { min-width: 720px; }
+
+/* ---------- 12. 键值 / 容量 ---------- */
+.kv { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.kv__r { background: var(--canvas-card); display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); }
+.kv__k { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.kv__v { font-family: var(--font-mono); font-size: 14px; color: var(--body); word-break: break-all; }
+
+.cap { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.cap__r { background: var(--canvas-card); display: grid; grid-template-columns: minmax(0, 1fr) 168px; gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); align-items: baseline; }
+.cap__k { font-size: 16px; line-height: 24px; color: var(--body); }
+.cap__k small { display: block; color: var(--mute); font-size: 14px; line-height: 20px; }
+.cap__v { font-family: var(--font-mono); font-size: 16px; text-align: right; color: var(--ink); }
+.cap__r--hi .cap__v { color: var(--ink); }
+
+.pagemap { display: grid; grid-template-columns: repeat(64, 1fr); gap: 2px; }
+.pagemap i { aspect-ratio: 1; background: var(--canvas-mid); border-radius: 1px; }
+.pagemap i.on { background: var(--ink); }
+
+/* ---------- 13. 账本 / 显存 ---------- */
+.ledger { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.ledger__h, .ledger__r { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, .85fr) minmax(0, .85fr) minmax(0, .9fr); gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); align-items: baseline; }
+.ledger__h { background: var(--canvas-soft); }
+.ledger__h span { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.ledger__r { background: var(--canvas-card); }
+.ledger__r:hover { background: var(--canvas-soft); }
+.ledger__k { font-size: 16px; line-height: 24px; color: var(--body); }
+.ledger__b, .ledger__n { font-family: var(--font-mono); font-size: 14px; color: var(--mute); }
+.ledger__n { color: var(--ink); }
+.ledger__d { font-family: var(--font-mono); font-size: 14px; text-align: right; color: var(--ink); }
+.ledger__d.t-ok, .ledger__d.t-warn, .ledger__d.t-bad, .ledger__d.t-mute { color: var(--body); }
+.ledger__d.t-bad, .ledger__d.t-mute { color: var(--mute); }
+
+.vram { display: grid; gap: var(--sp-lg); }
+.vram__row { display: grid; gap: var(--sp-sm); }
+.vram__top { display: flex; justify-content: space-between; gap: var(--sp-md); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.vram__top b { color: var(--ink); }
+.vram__track { position: relative; height: 20px; background: var(--canvas-mid); border-radius: var(--r-pill); overflow: hidden; }
+.vram__fill { position: absolute; inset: 0 auto 0 0; width: 0; border-radius: var(--r-pill); transition: width 700ms cubic-bezier(.16,1,.3,1); }
+.vram__fill--base { background: repeating-linear-gradient(115deg, var(--mute) 0 3px, transparent 3px 7px); }
+.vram__fill--nm2 { background: var(--ink); }
+.vram__mark { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--btn-border); }
+.vram__card { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+
+/* ---------- 14. 条形对比(单色明度阶梯,不用彩色编码) ---------- */
+.bars { display: grid; gap: var(--sp-xl); }
+.barset { display: grid; gap: var(--sp-sm); }
+.barset__top { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--sp-sm); }
+.barset__t { font-size: 16px; line-height: 24px; color: var(--ink); }
+.barset__n { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+
+.bar { display: grid; grid-template-columns: 108px minmax(0, 1fr) 108px; gap: var(--sp-md); align-items: center; }
+.bar__l { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.bar__track { position: relative; height: 6px; background: var(--canvas-mid); border-radius: var(--r-pill); overflow: hidden; }
+.bar__fill { position: absolute; inset: 0 auto 0 0; width: 0; border-radius: var(--r-pill); transition: width 700ms cubic-bezier(.16,1,.3,1); }
+.bar--base .bar__fill { background: repeating-linear-gradient(115deg, var(--mute) 0 3px, transparent 3px 7px); }
+.bar--a .bar__fill { background: var(--mute); }
+.bar--rag .bar__fill { background: var(--mute); }
+.bar--nm2 .bar__fill { background: var(--ink); }
+.bar__v { font-family: var(--font-mono); font-size: 14px; color: var(--ink); text-align: right; }
+.bar__v small { color: var(--mute); display: block; font-size: 12px; letter-spacing: 1.2px; }
+
+.legend { display: flex; flex-wrap: wrap; gap: var(--sp-lg); }
+.legend span { display: inline-flex; align-items: center; gap: var(--sp-sm); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.legend i { width: 14px; height: 6px; border-radius: var(--r-pill); display: block; }
+.legend i.l-base { background: repeating-linear-gradient(115deg, var(--mute) 0 3px, transparent 3px 7px); }
+.legend i.l-a { background: var(--mute); }
+.legend i.l-rag { background: var(--mute); }
+.legend i.l-nm2 { background: var(--ink); }
+
+.gap { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.gap__c { background: var(--canvas-card); padding: var(--sp-xl); display: grid; gap: var(--sp-sm); }
+.gap__k { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.gap__v { font-family: var(--font-mono); font-size: clamp(28px, 4vw, 40px); line-height: 1; letter-spacing: -.02em; }
+.gap__c--recall .gap__v { color: var(--body); }
+.gap__c--answer .gap__v { color: var(--ink); }
+.gap__n { font-size: 14px; line-height: 20px; color: var(--mute); }
+
+/* ---------- 15. 标签页(药丸) ---------- */
+.tabs { display: flex; flex-wrap: wrap; gap: var(--sp-sm); max-width: 100%; }
+.tab {
+  padding: var(--sp-sm) var(--sp-lg);
+  border: 1px solid var(--btn-border); border-radius: var(--r-pill);
+  color: var(--body); font-size: 14px; line-height: 20px; white-space: nowrap;
+  transition: background var(--dur-fast) var(--ease-exp), color var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.tab:hover { background: var(--canvas-soft); color: var(--ink); }
+.tab.is-on { background: var(--ink); border-color: var(--ink); color: var(--on-primary); }
+.panel { display: none; }
+.panel.is-on { display: block; }
+
+/* ---------- 16. 机制:步进器 / 流程图 / 记录 ---------- */
+.flow { display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); gap: var(--sp-2xl); align-items: start; }
+.steps { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.step {
+  background: var(--canvas-card); text-align: left; width: 100%;
+  display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: var(--sp-md);
+  padding: var(--sp-md) var(--sp-lg); align-items: center;
+  transition: background var(--dur-fast) var(--ease-exp);
+}
+.step:hover { background: var(--canvas-soft); }
+.step__i { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.step__l { font-size: 14px; line-height: 20px; color: var(--body); }
+.step.is-on { background: var(--canvas-soft); }
+.step.is-on .step__l { color: var(--ink); }
+.step.is-on .step__i { color: var(--ink); }
+.step.is-past .step__l { color: var(--mute); }
+
+.flow__detail { position: sticky; top: calc(var(--nav-h) + var(--sp-lg)); }
+.flow__stage { margin: 0 0 var(--sp-md); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.flow__title { margin: 0 0 var(--sp-md); font-size: 24px; line-height: 32px; letter-spacing: -.02em; }
+.flow__body { margin: 0; color: var(--body); font-size: 14px; line-height: 22px; min-height: 88px; }
+.flow__io { margin: var(--sp-lg) 0 0; padding-top: var(--sp-md); border-top: 1px solid var(--hairline); font-family: var(--font-mono); font-size: 12px; line-height: 20px; color: var(--mute); }
+.flow__meta { margin-top: var(--sp-lg); padding-top: var(--sp-md); border-top: 1px solid var(--hairline); display: grid; gap: var(--sp-sm); }
+.flow__meta div { display: flex; justify-content: space-between; gap: var(--sp-lg); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.flow__meta b { color: var(--ink); font-weight: 400; }
+
+.dgm { width: 100%; height: auto; display: block; overflow: visible; }
+.dgm__box { fill: var(--canvas-card); stroke: var(--hairline); }
+.dgm__box--acc { stroke: var(--btn-border); }
+.dgm__n { font-family: var(--font-mono); font-size: 9px; fill: var(--mute); letter-spacing: 1px; }
+.dgm__l { font-size: 12px; fill: var(--ink); }
+.dgm__s { font-family: var(--font-mono); font-size: 9px; fill: var(--mute); }
+.dgm__a { stroke: var(--btn-border); stroke-width: 1; fill: none; }
+.dgm__rule { stroke: var(--hairline); stroke-width: 1; }
+.dgm__note { font-family: var(--font-mono); font-size: 9.5px; fill: var(--mute); letter-spacing: .6px; }
+.dgm__note:first-of-type { fill: var(--ink); }
+
+.rec { display: grid; grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.rec__f { background: var(--canvas-card); padding: var(--sp-md) var(--sp-lg); display: grid; gap: var(--sp-xxs); }
+.rec__f:hover { background: var(--canvas-soft); }
+.rec__k { font-family: var(--font-mono); font-size: 14px; color: var(--ink); }
+.rec__d { font-size: 12px; line-height: 16px; color: var(--mute); }
+
+.recjson { display: grid; gap: 1px; }
+.recjson__r { display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: var(--sp-md); padding: var(--sp-sm) 0; border-bottom: 1px solid var(--hairline); align-items: baseline; }
+.recjson__r:last-child { border-bottom: 0; }
+.recjson__k { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.recjson__v { font-size: 14px; line-height: 20px; color: var(--ink); word-break: break-word; overflow-wrap: anywhere; }
+
+.states { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.state { background: var(--canvas-card); display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: var(--sp-lg); padding: var(--sp-md) var(--sp-lg); align-items: baseline; }
+.state__s { font-family: var(--font-mono); font-size: 14px; color: var(--ink); }
+.state--mute .state__s, .state--bad .state__s { color: var(--mute); }
+.state--warn .state__s { color: var(--body); }
+.state__d { font-size: 14px; line-height: 20px; color: var(--body); }
+
+/* ---------- 17. 提示块 / 声明 / 局限 ---------- */
+.callout {
+  background: var(--canvas-card);
+  border: 1px solid var(--btn-border);
+  border-radius: var(--r-sm);
+  padding: var(--sp-xl);
+}
+.callout--warn { border-color: var(--hairline); }
+.callout__t { margin: 0 0 var(--sp-sm); font-family: var(--font-mono); font-size: 14px; line-height: 20px; letter-spacing: 1.4px; text-transform: uppercase; color: var(--ink); }
+.callout__b { margin: 0; font-size: 16px; line-height: 26px; color: var(--body); max-width: 78ch; }
+
+.claims { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.claim { background: var(--canvas-card); display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: var(--sp-md); padding: var(--sp-md) var(--sp-lg); align-items: center; }
+.claim:hover { background: var(--canvas-soft); }
+.claim__x { font-family: var(--font-mono); font-size: 14px; color: var(--mute); }
+.claim__t { font-size: 16px; line-height: 24px; color: var(--mute); text-decoration: line-through; text-decoration-color: var(--canvas-mid); }
+.claim__s { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.claims--loose .claim__s { display: none; }
+
+.lims { display: grid; gap: var(--sp-xl); }
+.lim { display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: var(--sp-md); }
+.lim__i { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); padding-top: 3px; }
+.lim__t { font-size: 20px; line-height: 28px; margin-bottom: var(--sp-sm); }
+.lim__d { margin: 0; font-size: 14px; line-height: 22px; color: var(--mute); max-width: 76ch; }
+
+.quote { border-left: 1px solid var(--btn-border); padding: var(--sp-xs) 0 var(--sp-xs) var(--sp-xl); margin: 0; }
+.quote q { font-size: 24px; line-height: 34px; letter-spacing: -.02em; color: var(--ink); display: block; }
+.quote cite { display: block; margin-top: var(--sp-md); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); font-style: normal; }
+
+.checks { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.check { background: var(--canvas-card); display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: var(--sp-md); padding: var(--sp-md) var(--sp-lg); align-items: baseline; }
+.check__i { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.check__t { font-size: 14px; line-height: 20px; color: var(--body); }
+
+.road { display: grid; gap: 1px; background: var(--hairline); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.road__r { background: var(--canvas-card); display: grid; grid-template-columns: 44px minmax(0, 1fr); gap: var(--sp-md); padding: var(--sp-md) var(--sp-lg); align-items: baseline; }
+.road__r:hover { background: var(--canvas-soft); }
+.road__i { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.road__t { font-size: 14px; line-height: 20px; color: var(--body); }
+
+/* ---------- 18. 复现:代码块(规范:Canvas Mid 用作代码底) ---------- */
+.code { background: var(--canvas-mid); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.code__bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-md); padding: var(--sp-md) var(--sp-lg); border-bottom: 1px solid var(--hairline); background: var(--canvas-soft); }
+.code__t { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.copy {
+  font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px;
+  padding: var(--sp-xs) var(--sp-md);
+  background: transparent; border: 1px solid var(--btn-border); border-radius: var(--r-pill);
+  color: var(--ink);
+  transition: background var(--dur-fast) var(--ease-exp);
+}
+.copy:hover { background: var(--canvas-card); }
+.copy.is-done { border-color: var(--ink); }
+.code pre { margin: 0; padding: var(--sp-xl); overflow-x: auto; }
+.code code { font-family: var(--font-mono); font-size: 14px; line-height: 24px; color: var(--ink); white-space: pre; background: none; padding: 0; }
+.code code .k { color: var(--ink); }
+.code code .c { color: var(--mute); }
+
+/* ---------- 19. 实测样例 ---------- */
+.examples { display: grid; gap: var(--sp-xl); }
+.ex { background: var(--canvas-card); border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.ex__hd { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-sm); padding: var(--sp-md) var(--sp-xl); border-bottom: 1px solid var(--hairline); background: var(--canvas-soft); }
+.ex__no { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--ink); }
+.ex__q { margin: 0; padding: var(--sp-xl) var(--sp-xl) var(--sp-md); font-size: 24px; line-height: 32px; letter-spacing: -.02em; color: var(--ink); }
+.ex__exp { margin: 0; padding: 0 var(--sp-xl) var(--sp-xl); font-family: var(--font-mono); font-size: 12px; line-height: 20px; color: var(--mute); word-break: break-word; }
+.ex__exp span { color: var(--mute); margin-right: var(--sp-sm); }
+.ex__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--hairline); }
+.ex__col { padding: var(--sp-xl); min-width: 0; }
+.ex__col + .ex__col { border-left: 1px solid var(--hairline); }
+.ex__col--nm { background: rgba(255, 255, 255, .015); }
+.ex__lab { margin: 0 0 var(--sp-md); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.ex__col--nm .ex__lab { color: var(--ink); }
+.ex__ans { margin: 0 0 var(--sp-md); font-size: 14px; line-height: 22px; color: var(--body); white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; }
+.ex__meta { margin: 0; padding-top: var(--sp-md); border-top: 1px solid var(--hairline); font-family: var(--font-mono); font-size: 12px; line-height: 20px; letter-spacing: 1.2px; color: var(--mute); }
+.v--ok { color: var(--ink); }
+.v--no { color: var(--mute); }
+.ex__ev { margin-top: var(--sp-lg); padding-top: var(--sp-md); border-top: 1px solid var(--hairline); }
+.ex__evt { margin: 0 0 var(--sp-sm); font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.ex__evr { display: grid; gap: var(--sp-xs); padding: var(--sp-sm) 0; border-bottom: 1px solid var(--hairline); }
+.ex__evr:last-child { border-bottom: 0; padding-bottom: 0; }
+.ex__evk { font-family: var(--font-mono); font-size: 14px; color: var(--ink); word-break: break-word; }
+.ex__evv { font-size: 14px; line-height: 20px; color: var(--body); word-break: break-word; }
+.ex__evs { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); word-break: break-all; }
+
+/* ---------- 20. 上一节 / 下一节 ---------- */
+.pn { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-lg); margin-top: var(--sp-4xl); padding-top: var(--sp-xl); border-top: 1px solid var(--hairline); }
+.pn__a {
+  display: grid; gap: var(--sp-sm); padding: var(--sp-lg) var(--sp-xl); position: relative;
+  background: var(--canvas-card); border: 1px solid var(--hairline); border-radius: var(--r-sm);
+  transition: background var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.pn__a:hover { background: var(--canvas-soft); border-color: var(--btn-border); }
+.pn__a--next { text-align: right; }
+.pn__k { font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.pn__t { font-size: 20px; line-height: 28px; color: var(--ink); }
+.pn__a .kbd { position: absolute; top: var(--sp-lg); right: var(--sp-lg); }
+.pn__a--prev .kbd { right: auto; left: var(--sp-lg); }
+.pn__x { display: block; }
+
+/* ---------- 21. 页脚(footer band: padding 48 24) ---------- */
+.footer { border-top: 1px solid var(--hairline); padding: var(--sp-3xl) 0; }
+.footer__in { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.05fr) minmax(0, 1.15fr); gap: var(--sp-3xl); align-items: start; }
+.footer__col--wide { min-width: 0; }
+.footer__h { margin: 0 0 var(--sp-md); font-family: var(--font-mono); font-size: 12px; line-height: 16px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.footer__note { margin: 0; font-size: 14px; line-height: 22px; color: var(--body); max-width: 64ch; }
+.footer__meta { margin: var(--sp-lg) 0 0; font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute); }
+.readout { display: flex; flex-wrap: wrap; gap: var(--sp-sm); }
+.readout > span {
+  display: inline-flex; align-items: center; padding: var(--sp-xs) var(--sp-md);
+  border: 1px solid var(--hairline); border-radius: var(--r-pill);
+  font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px; color: var(--mute);
+}
+
+/* ---------- 22. JS 渲染内容的淡入(只淡入,不位移 —— B2 的底线) ---------- */
+.reveal { opacity: 0; transition: opacity var(--dur-std) var(--ease-exp); }
+.reveal.is-in { opacity: 1; }
+
+/* 分带标题:眉标 + 标题的间距走令牌,不再用行内 style */
+.band__k { margin: 0 0 var(--sp-lg); }
+.band__k + .h2, .band__k + .h3 { margin-bottom: var(--sp-2xl); }
+
+@media (prefers-reduced-motion: reduce) {
+  html { scroll-behavior: auto; }
+  .reveal { opacity: 1; transform: none; transition: none; }
+  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
+  .bar__fill, .vram__fill { transition: none; }
+  /* B8:关掉动效时直接呈现终态,不允许有任何内容因为「还没入场」而消失 */
+  .js .arr-wipe { clip-path: none !important; }
+  .js .arr-num, .js .arr-cards > *, .js .teaser .dgm > g:not(.dgm__a) {
+    opacity: 1 !important; transform: none !important;
+  }
+  .js .teaser .dgm__a path { stroke-dashoffset: 0 !important; }
+  .rst__b { opacity: 1 !important; }
+  .pal__box { animation: none; }
+  .hero__cue { animation: none; opacity: .6; }
+  #doc h2.is-flash, #doc h3.is-flash { animation: none; box-shadow: inset 3px 0 0 var(--ink); }
+  /* 交叉淡入淡出全部关掉后,页面仍然是完整的一帧 */
+}
+
+/* ---------- 23. 响应式(<768:hero 96→48,网格 1 列) ---------- */
+@media (min-width: 1120px) {
+  .g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+  .g4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+}
+@media (max-width: 1080px) {
+  /* 顶栏在窄屏必须能收:给导航一个可横向滚动的弹性列,
+     否则 7 个胶囊 + 检索按钮 + 状态胶囊会把整行撑出视口。 */
+  .topbar { gap: var(--sp-sm); }
+  .topbar__spacer { display: none; }
+  .topnav {
+    flex: 1 1 auto; min-width: 0;
+    flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none;
+  }
+  .topnav::-webkit-scrollbar { display: none; }
+  .brand__name { display: none; }
+  .shell { grid-template-columns: minmax(0, 1fr); gap: 0; }
+  .side {
+    position: static; max-height: none; overflow: visible;
+    border-right: 0; border-bottom: 1px solid var(--hairline);
+    padding: var(--sp-lg) 0;
+  }
+  .side__in { display: flex; gap: var(--sp-sm); overflow-x: auto; padding-right: 0; scrollbar-width: none; }
+  .side__in::-webkit-scrollbar { display: none; }
+  .side__grp { display: none; }
+  .side__list { display: flex; gap: var(--sp-sm); }
+  .side__a { grid-template-columns: auto auto; white-space: nowrap; padding: var(--sp-sm) var(--sp-md); border-left: 0; border: 1px solid var(--hairline); border-radius: var(--r-pill); }
+  .side__a.is-on { border-color: var(--ink); }
+  .g-2-1 { grid-template-columns: minmax(0, 1fr); }
+  .statrow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+  .authorcard { grid-template-columns: 1fr; }
+  .footer__in { grid-template-columns: 1fr; }
+  .crumb { display: none; }
+  .flow { grid-template-columns: minmax(0, 1fr); }
+  .flow__detail { position: static; }
+}
+@media (max-width: 768px) {
+  .g2, .g3, .g4 { grid-template-columns: minmax(0, 1fr); }
+  .kpis { grid-template-columns: 1fr; }
+  .statrow { grid-template-columns: 1fr; }
+  .bar { grid-template-columns: 84px minmax(0, 1fr) 84px; gap: var(--sp-sm); }
+  .ledger__h { display: none; }
+  .ledger__r { grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-xs) var(--sp-md); }
+  .ledger__b { grid-column: 1 / -1; }
+  .ledger__d { text-align: left; }
+  .cap__r { grid-template-columns: minmax(0, 1fr); gap: var(--sp-xs); }
+  .cap__v { text-align: left; }
+  .state { grid-template-columns: minmax(0, 1fr); gap: var(--sp-xs); }
+  .gap { grid-template-columns: 1fr; }
+  .ex__grid { grid-template-columns: 1fr; }
+  .ex__col + .ex__col { border-left: 0; border-top: 1px solid var(--hairline); }
+  .pn { grid-template-columns: 1fr; }
+  .pn__a--next { text-align: left; }
+  .recjson__r { grid-template-columns: minmax(0, 1fr); gap: var(--sp-xxs); }
+  .tabs { width: 100%; }
+  .tab { flex: 1 1 auto; text-align: center; }
+  .doc { padding-top: var(--sp-3xl); }
+  .scrollx > .gt { min-width: 0; }
+  .gt--4 .gt__h, .gt--3 .gt__h { display: none; }
+  .gt--4 .gt__r, .gt--3 .gt__r { grid-template-columns: minmax(0, 1fr); gap: var(--sp-sm); padding: var(--sp-lg) var(--sp-md); }
+  .tbl-wrap, .scrollx {
+    background-image: linear-gradient(to left, rgba(255, 255, 255, .06), rgba(255, 255, 255, 0) 40px);
+    background-repeat: no-repeat; background-position: right center; background-size: 40px 100%;
+  }
+  :root { --band-tight: 40px; --band: 64px; --band-wide: 88px; }
+}
+
+/* ==================================================================
+   24. 发布页版式(layout: release)
+   按研究/模型发布页的通行骨架:发布头 → teaser → 关键数字 → 摘要 →
+   发布清单 → 结果预览 → 深入阅读 → 边界 → 引用。
+   规范约束:页面表面只用 canvas,band 之间用 divider-hairline 分隔,
+   不引入第二种页面底色。
+   ================================================================== */
+.release { min-width: 0; }
+
+/* 段落留白不是常数(page-design.md §6):三档按语义给,
+   紧的地方两块属于一组,宽的地方是留给观众的呼吸。 */
+.band { padding: var(--band) 0; }
+.band + .band { border-top: 1px solid var(--hairline); }
+.band--hero { padding-top: var(--band-tight); padding-bottom: var(--band-wide); }
+.band--tight { padding: var(--band-tight) 0; }
+.band--wide { padding: var(--band-wide) 0; }
+.band--teaser { padding: var(--band-tight) 0 var(--band); }
+.band--stats { padding: var(--band) 0; }
+.band--alt { /* 命名钩子:p-07 只有一种地面,不分带换底 */ }
+
+.wrap--narrow { max-width: 820px; }
+
+/* 顶栏快速导航(胶囊行) */
+.topnav { display: flex; flex-wrap: wrap; gap: var(--sp-sm); }
+.topnav__a {
+  padding: var(--sp-xs) var(--sp-md);
+  border: 1px solid transparent; border-radius: var(--r-pill);
+  color: var(--mute); font-size: 13px; line-height: 20px;
+  transition: color var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.topnav__a:hover { color: var(--ink); border-color: var(--hairline); }
+.topnav__a.is-on { border-color: var(--btn-border); color: var(--ink); }
+
+/* 侧栏返回发布页 */
+.side__back {
+  display: inline-flex; align-items: center;
+  margin: 0 0 var(--sp-xl) var(--sp-md);
+  padding: var(--sp-xs) var(--sp-md);
+  border: 1px solid var(--btn-border); border-radius: var(--r-pill);
+  font-size: 12px; line-height: 20px; letter-spacing: 1.2px; font-family: var(--font-mono);
+  color: var(--body);
+  transition: background var(--dur-fast) var(--ease-exp), color var(--dur-fast) var(--ease-exp);
+}
+.side__back:hover { background: var(--canvas-soft); color: var(--ink); }
+
+/* 发布头 */
+.release__badge { margin-bottom: var(--sp-xl); color: var(--mute); }
+.release__h1 { margin: 0 0 var(--sp-xl); max-width: 20ch; }
+.release__byline {
+  margin: 0 0 var(--sp-xl);
+  font-size: 14px; line-height: 20px; color: var(--body);
+  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-sm);
+}
+.release__byline a { color: var(--ink); border-bottom: 1px solid var(--btn-border); }
+.release__byline a:hover { border-bottom-color: var(--ink); }
+.release__by { color: var(--mute); }
+.release__sep { color: var(--canvas-mid); }
+.release__lede { max-width: 68ch; margin: 0 0 var(--sp-2xl); }
+
+.linkrow { display: flex; flex-wrap: wrap; gap: var(--sp-md); align-items: center; }
+
+/* Teaser 全宽图 */
+.teaser {
+  margin: 0;
+  background: var(--canvas-card);
+  border: 1px solid var(--hairline); border-radius: var(--r-sm);
+  padding: var(--sp-xl);
+}
+.teaser .dgm { width: 100%; height: auto; }
+.teaser__cap {
+  margin-top: var(--sp-lg); padding-top: var(--sp-md);
+  border-top: 1px solid var(--hairline);
+  font-size: 14px; line-height: 22px; color: var(--mute); max-width: 92ch;
+}
+.teaser__k {
+  display: inline-block; margin-right: var(--sp-sm);
+  font-family: var(--font-mono); font-size: 12px; line-height: 16px;
+  letter-spacing: 1.2px; text-transform: uppercase; color: var(--ink);
+}
+
+/* 发布清单 */
+.rel__k { margin: 0 0 var(--sp-sm); font-family: var(--font-mono); font-size: 12px; line-height: 16px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--mute); }
+.rel__v { margin: 0 0 var(--sp-sm); font-family: var(--font-mono); font-size: 28px; line-height: 32px; letter-spacing: -.02em; color: var(--ink); }
+.rel__v small { font-size: 12px; letter-spacing: 1.2px; color: var(--mute); margin-left: var(--sp-xs); }
+.rel__d { margin: 0; font-size: 14px; line-height: 22px; color: var(--mute); }
+
+/* 正文内链接 */
+.linku { color: var(--ink); border-bottom: 1px solid var(--btn-border); }
+.linku:hover { border-bottom-color: var(--ink); }
+
+/* ==================================================================
+   24. 抵达编排 —— 三种不同的抵达方式,不是全页统一 fade-up
+
+   design-slop B2:所有区块都 `opacity 0→1, translateY 20px` 会让页面变成
+   一串相同的到达,读者到第三节就学会了无视它。所以这里只保留三处入场,
+   而且**三处的手势互不相同**(擦除 / 数字冲入 / 侧向错峰),
+   其余内容一律「本来就在那儿」。
+
+   初始隐藏态全部写在 .js 下:脚本挂了内容不会被藏起来。
+   ================================================================== */
+
+/* 1 · 编辑式擦除:给「一句结论」用(choreography-arc 的 editorial 弧) */
+.js .arr-wipe {
+  clip-path: inset(0 0 106% 0);
+  transition: clip-path var(--dur-slow) cubic-bezier(.77, 0, .18, 1);
+}
+.arr-wipe.is-in { clip-path: inset(0 0 -2% 0); }
+
+/* 2 · 数字冲入:只给头号数字,带一点点压缩再回弹 */
+.js .arr-num {
+  opacity: 0; transform: translateY(16px) scale(.982);
+  transform-origin: 0 70%;
+  transition: opacity var(--dur-slow) var(--ease-exp), transform var(--dur-slow) var(--ease-sharp);
+}
+.arr-num.is-in { opacity: 1; transform: none; }
+
+/* 3 · 侧向错峰:只给一处列表,手势是从装订线推出来,不是往上浮 */
+.js .arr-cards > * {
+  opacity: 0; transform: translateX(-12px);
+  transition: opacity var(--dur-med) var(--ease-circ), transform var(--dur-med) var(--ease-circ);
+}
+.js .arr-cards.is-in > * { opacity: 1; transform: none; }
+.js .arr-cards.is-in > *:nth-child(1) { transition-delay: 0ms; }
+.js .arr-cards.is-in > *:nth-child(2) { transition-delay: 60ms; }
+.js .arr-cards.is-in > *:nth-child(3) { transition-delay: 120ms; }
+.js .arr-cards.is-in > *:nth-child(4) { transition-delay: 180ms; }
+.js .arr-cards.is-in > *:nth-child(5) { transition-delay: 240ms; }
+.js .arr-cards.is-in > *:nth-child(6) { transition-delay: 300ms; }
+
+/* 读取路径图:连接线自左向右绘出,环节随后依次点亮 —— 这一处是内容自己的
+   语法(一条路径本来就是按顺序被走完的),所以保留,且它的手势与上面三种都不同 */
+.js .teaser .dgm__a path {
+  stroke-dasharray: 1; stroke-dashoffset: 1;
+  transition: stroke-dashoffset var(--dur-slow) var(--ease-exp);
+}
+.js .teaser.is-in .dgm__a path { stroke-dashoffset: 0; }
+.js .teaser.is-in .dgm__a path:nth-child(1) { transition-delay: 60ms; }
+.js .teaser.is-in .dgm__a path:nth-child(2) { transition-delay: 130ms; }
+.js .teaser.is-in .dgm__a path:nth-child(3) { transition-delay: 200ms; }
+.js .teaser.is-in .dgm__a path:nth-child(4) { transition-delay: 270ms; }
+.js .teaser.is-in .dgm__a path:nth-child(5) { transition-delay: 340ms; }
+.js .teaser.is-in .dgm__a path:nth-child(6) { transition-delay: 410ms; }
+.js .teaser.is-in .dgm__a path:nth-child(7) { transition-delay: 480ms; }
+.js .teaser .dgm > g:not(.dgm__a) { opacity: 0; transition: opacity var(--dur-med) var(--ease-exp); }
+.js .teaser.is-in .dgm > g:not(.dgm__a) { opacity: 1; }
+.js .teaser.is-in .dgm > g:nth-child(2) { transition-delay: 120ms; }
+.js .teaser.is-in .dgm > g:nth-child(3) { transition-delay: 200ms; }
+.js .teaser.is-in .dgm > g:nth-child(4) { transition-delay: 280ms; }
+.js .teaser.is-in .dgm > g:nth-child(5) { transition-delay: 360ms; }
+.js .teaser.is-in .dgm > g:nth-child(6) { transition-delay: 440ms; }
+.js .teaser.is-in .dgm > g:nth-child(7) { transition-delay: 520ms; }
+.js .teaser.is-in .dgm > g:nth-child(8) { transition-delay: 600ms; }
+.js .teaser.is-in .dgm > g:nth-child(9) { transition-delay: 680ms; }
+
+/* ==================================================================
+   25. 重启实验装置
+   ================================================================== */
+.band--rst { padding-top: var(--sp-4xl); }
+.rst__lede { margin-bottom: var(--sp-2xl); }
+
+.rst {
+  border: 1px solid var(--hairline);
+  border-radius: var(--r-sm);
+  background: var(--canvas-card);
+  overflow: hidden;
+}
+
+/* --- 顶部控制条 --- */
+.rst__head {
+  display: flex; align-items: center; gap: var(--sp-lg);
+  padding: var(--sp-md) var(--sp-lg);
+  border-bottom: 1px solid var(--hairline);
+  flex-wrap: wrap;
+}
+.rst__file {
+  margin-left: auto; color: var(--mute); text-transform: none;
+  letter-spacing: 0; font-size: 11px;
+  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
+}
+.seg {
+  display: inline-flex; padding: var(--sp-xxs);
+  border: 1px solid var(--hairline); border-radius: var(--r-pill);
+  background: var(--canvas);
+}
+.seg__b {
+  appearance: none; border: 0; cursor: pointer;
+  background: transparent; color: var(--mute);
+  font: inherit; font-size: 13px; letter-spacing: .01em;
+  padding: var(--sp-xs) var(--sp-lg); border-radius: var(--r-pill);
+  transition: color var(--dur-fast) var(--ease-exp), background-color var(--dur-fast) var(--ease-exp);
+}
+.seg__b:hover { color: var(--body); }
+.seg__b.is-on { background: var(--ink); color: var(--on-primary); }
+.seg__b.is-on[data-v="before"] { background: var(--canvas-mid); color: var(--ink); }
+.seg__b:focus-visible { outline: 2px solid var(--btn-border); outline-offset: 2px; }
+
+.rst__play { padding: var(--sp-xs) var(--sp-lg); font-size: 13px; }
+.rst__play::before {
+  content: ""; width: 0; height: 0; margin-right: var(--sp-sm);
+  border-left: 7px solid currentColor;
+  border-top: 5px solid transparent; border-bottom: 5px solid transparent;
+  display: inline-block; vertical-align: 1px;
+}
+.rst__play.is-playing::before { border-left-width: 6px; border-right: 6px solid currentColor; border-top-width: 6px; border-bottom-width: 6px; }
+
+/* --- 主体两栏 --- */
+.rst__grid {
+  display: grid; grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
+  gap: 0;
+}
+
+/* --- 左栏:情境 + 状态 --- */
+.rst__aside {
+  padding: var(--sp-xl);
+  border-right: 1px solid var(--hairline);
+  background: var(--canvas);
+  min-width: 0;
+}
+.rst__k {
+  margin: 0 0 var(--sp-sm);
+  font-family: var(--font-mono); font-size: 11px; letter-spacing: 1.4px;
+  text-transform: uppercase; color: var(--mute);
+}
+.rst__k + .rst__quote { margin-bottom: var(--sp-xl); }
+.rst__quote {
+  margin: 0; padding: 0 0 0 var(--sp-md);
+  border-left: 1px solid var(--btn-border);
+  font-size: 14px; line-height: 22px; color: var(--body);
+  text-wrap: pretty;
+}
+.rst__quote--q { color: var(--ink); }
+
+.rst__gauges { display: grid; gap: var(--sp-xl); }
+.rst__g { display: grid; gap: var(--sp-sm); }
+.rst__gt { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-md); }
+.rst__gt > span { font-size: 13px; color: var(--mute); }
+.rst__gt > b { font-size: 15px; letter-spacing: 1px; color: var(--ink); font-weight: 400; }
+.rst__gt > b small { font-size: 11px; color: var(--mute); letter-spacing: 1px; }
+.rst__track {
+  position: relative; height: 4px; border-radius: var(--r-pill);
+  background: var(--canvas-mid); overflow: hidden;
+}
+.rst__fill {
+  position: absolute; inset: 0 auto 0 0; width: 0;
+  border-radius: var(--r-pill); background: var(--mute);
+  transition: width var(--dur-slow) var(--ease-exp), background-color var(--dur-std) var(--ease-exp);
+}
+.rst__fill--store { background: var(--ink); }
+.rst__hint { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .6px; color: var(--mute); text-wrap: pretty; }
+
+/* --- 右栏:四个节拍 --- */
+.rst__beats { list-style: none; margin: 0; padding: 0; min-width: 0; }
+.rst__b {
+  position: relative;
+  display: grid; grid-template-columns: 36px minmax(0, 1fr);
+  gap: var(--sp-lg);
+  padding: var(--sp-xl);
+  border-bottom: 1px solid var(--hairline);
+  opacity: .28;
+  transition: opacity var(--dur-med) var(--ease-exp), background-color var(--dur-med) var(--ease-exp);
+}
+.rst__b:last-child { border-bottom: 0; }
+.rst__b.is-on { opacity: 1; background: var(--canvas-soft); }
+.rst__b.is-past { opacity: .62; }
+.rst__i {
+  font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.4px;
+  color: var(--mute); padding-top: 3px;
+}
+.rst__b.is-on .rst__i { color: var(--ink); }
+.rst__bt { margin: 0 0 var(--sp-sm); font-size: 16px; line-height: 22px; color: var(--ink); }
+.rst__bd { margin: 0; font-size: 13px; line-height: 21px; color: var(--mute); text-wrap: pretty; }
+/* 字段表始终参与布局:用 max-height 做展开动画在窄屏会截断较长的行,
+   而且会造成跳动,所以层级只用节拍自身的透明度表达。 */
+.rst__data {
+  margin: var(--sp-md) 0 0; display: grid; gap: var(--sp-xs);
+  font-family: var(--font-mono); font-size: 12px; line-height: 19px;
+  letter-spacing: .4px; color: var(--body);
+}
+.rst__d { display: flex; gap: var(--sp-sm); }
+.rst__d > i { font-style: normal; color: var(--mute); min-width: 132px; flex: 0 0 auto; }
+.rst__d > span { min-width: 0; word-break: break-word; }
+
+/* --- 底部:模型输出 --- */
+.rst__out {
+  padding: var(--sp-xl);
+  border-top: 1px solid var(--hairline);
+  background: var(--canvas);
+  transition: opacity var(--dur-slow) var(--ease-exp);
+}
+.rst__out.is-idle { opacity: .38; }
+.rst__outhead {
+  display: flex; align-items: center; gap: var(--sp-md); margin-bottom: var(--sp-md);
+}
+.rst__outhead > .mono { color: var(--mute); }
+.rst__verdict { margin-left: auto; letter-spacing: 1.4px; }
+.rst__verdict.is-ok { color: var(--ink); }
+.rst__verdict.is-bad { color: var(--mute); }
+.rst__resp {
+  margin: 0; font-size: 22px; line-height: 32px; letter-spacing: -.01em;
+  color: var(--ink); text-wrap: pretty;
+  min-height: 32px;
+}
+/* 「答对了」这一帧不再用信号色:p-07 原型写明 one per page max,
+   而信号色已经用在首屏画布里那颗目标记录上(accent 属于插图)。
+   这里的含义由 ✓ 字形承担,跟「修复前 / 修复后」的对比一样,靠文字不靠颜色。 */
+.rst.is-payoff .rst__resp { color: var(--ink); }
+.rst.is-payoff .rst__verdict::before { content: ""; }
+.rst__kv {
+  display: flex; flex-wrap: wrap; gap: var(--sp-sm);
+  margin-top: var(--sp-lg); min-height: 26px;
+}
+.rst__kv > span {
+  display: inline-flex; align-items: baseline; gap: var(--sp-sm);
+  padding: var(--sp-xs) var(--sp-md);
+  border: 1px solid var(--hairline); border-radius: var(--r-pill);
+  font-family: var(--font-mono); font-size: 11px; letter-spacing: 1px;
+  color: var(--mute);
+}
+.rst__kv > span b { font-weight: 400; color: var(--body); }
+.rst__kv > span.is-hot { border-color: var(--btn-border); }
+.rst__kv > span.is-hot b { color: var(--ink); }
+
+.rst__note { margin-top: var(--sp-xl); max-width: 88ch; }
+
+@media (max-width: 1080px) {
+  .rst__grid { grid-template-columns: minmax(0, 1fr); }
+  .rst__aside { border-right: 0; border-bottom: 1px solid var(--hairline); }
+}
+@media (max-width: 768px) {
+  .rst__head { gap: var(--sp-md); }
+  /* 出处路径是证据本身,窄屏不截断,换行显示 */
+  .rst__file { margin-left: 0; width: 100%; white-space: normal; overflow: visible; text-overflow: clip; word-break: break-all; }
+  .rst__aside, .rst__b, .rst__out { padding: var(--sp-lg); }
+  .rst__b { grid-template-columns: 28px minmax(0, 1fr); gap: var(--sp-md); }
+  .rst__d { flex-direction: column; gap: 0; }
+  .rst__d > i { min-width: 0; }
+  .rst__resp { font-size: 18px; line-height: 26px; }
+}
+
+@media (max-width: 768px) {
+  .band { padding: var(--band-tight) 0; }
+  .release__h1 { max-width: none; }
+  .release__lede { margin-bottom: var(--sp-xl); }
+  .linkrow .btn { flex: 1 1 auto; justify-content: center; }
+  .teaser { padding: var(--sp-lg); }
+  .rel__v { font-size: 24px; line-height: 28px; }
+}
+
+/* ==================================================================
+   29. 首屏:地址空间(本页的招牌机制)
+   版面取舍来自 page-design §1/§3:
+     · hero **不是**「徽标 + 标题 + 副标题 + 两个按钮」的文字块,
+       它就是这个机制本身,字被排进场景里(左上元信息 / 中间标题 / 右下读数)
+     · 不居中、没有按钮对、没有置顶徽标 —— A1 的四个特征一个都不占
+   机制层见 assets/js/space.js;机制借用 press-stack 的「进度从文档读回来」,
+   皮(配色 / 字体 / 结构)全部是本项目自己的。
+   ================================================================== */
+.hero { position: relative; height: 150vh; background: var(--canvas); }
+.hero__pin { position: sticky; top: 0; height: 100vh; overflow: hidden; }
+.hero__cv { position: absolute; inset: 0; display: block; }
+
+.hero__layer {
+  position: relative; z-index: 1;
+  height: 100%;
+  display: grid; grid-template-rows: auto 1fr auto;
+  padding: clamp(18px, 3.2vw, 44px) clamp(20px, 4vw, 56px) clamp(22px, 3.6vw, 48px);
+  pointer-events: none;
+}
+.hero__row { display: flex; min-width: 0; }
+.hero__row--top { justify-content: space-between; align-items: baseline; gap: var(--sp-xl); }
+.hero__row--mid { align-items: flex-end; justify-content: space-between; gap: clamp(24px, 5vw, 72px); }
+.hero__row--bot { justify-content: space-between; align-items: flex-end; gap: clamp(24px, 5vw, 72px); }
+
+.hero__geo { text-align: right; }
+.hero__h { max-width: 14ch; margin: 0; font-size: clamp(56px, 15vw, 132px); }
+
+/* 第一屏里的头号数字:读者不用滚三屏才知道这页在说什么 */
+.hero__fig { margin: 0; display: grid; gap: var(--sp-md); justify-items: end; text-align: right; max-width: 34ch; }
+.hero__fig-row { display: flex; align-items: baseline; gap: var(--sp-md); }
+.hero__fig-v {
+  font-size: clamp(38px, 4.4vw, 84px); line-height: .95;
+  letter-spacing: -.04em; font-weight: 700; font-stretch: 92%;
+  color: var(--ink); font-variant-numeric: tabular-nums;
+}
+.hero__fig-v small { font-size: .38em; font-weight: 500; letter-spacing: 0; margin-left: .04em; }
+.hero__fig-a { font-family: var(--font-mono); font-size: var(--fs-label); color: var(--mute); }
+.hero__fig-n {
+  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: .8px;
+  line-height: 1.8; color: var(--mute);
+}
+.hero__lede-wrap { max-width: 44ch; min-width: 0; }
+.hero__lede { margin: 0 0 var(--sp-md); font-size: var(--fs-lede); line-height: 1.6; color: var(--body); }
+.hero__byline { margin: 0; color: var(--mute); }
+
+.hero__hud { display: grid; gap: var(--sp-xs); min-width: 250px; max-width: 34ch; text-align: right; }
+.hero__hud-i { color: var(--mute); }
+.hero__hud-n { font-size: 1rem; line-height: 1.4; color: var(--ink); }
+.hero__hud-v { color: var(--mute); }
+.hero__hud-track { display: block; height: 1px; margin-top: var(--sp-sm); background: var(--hairline); }
+.hero__hud-track > i { display: block; height: 100%; width: 0; background: var(--ink); }
+
+.hero__cue {
+  position: absolute; left: 50%; bottom: clamp(22px, 3.6vw, 48px);
+  transform: translateX(-50%);
+  margin: 0; color: var(--mute); white-space: nowrap;
+  animation: cue 2.6s var(--ease-exp) infinite;
+}
+@keyframes cue { 0%, 100% { opacity: .30; } 45% { opacity: .95; } }
+
+/* reduced-motion / 窄屏:不下降,就是一帧静态的地址空间 */
+.space--still .hero { height: auto; }
+.space--still .hero__pin { position: relative; height: 100vh; min-height: 560px; }
+.space--still .hero__cue { display: none; }
+
+@media (max-width: 900px) {
+  .hero { height: auto; }
+  .hero__pin { position: relative; height: auto; min-height: 92vh; padding-block: var(--sp-4xl); }
+  .hero__layer { grid-template-rows: auto auto auto; gap: var(--sp-3xl); }
+  .hero__row--mid { align-items: flex-start; flex-direction: column; }
+  .hero__fig { justify-items: start; text-align: left; max-width: none; }
+  .hero__fig-v { font-size: clamp(52px, 15vw, 84px); }
+  .hero__row--bot { flex-direction: column; align-items: flex-start; gap: var(--sp-xl); }
+  .hero__hud { text-align: left; min-width: 0; }
+  .hero__geo, .hero__cue { display: none; }
+}
+
+/* ==================================================================
+   26. 全站检索(命令面板)
+   ================================================================== */
+.palbtn {
+  display: inline-flex; align-items: center; gap: var(--sp-sm);
+  height: 30px; padding: 0 var(--sp-md);
+  appearance: none; cursor: pointer; flex: none;
+  background: transparent; color: var(--mute);
+  border: 1px solid var(--hairline); border-radius: var(--r-pill);
+  font: inherit; font-size: 13px;
+  transition: color var(--dur-fast) var(--ease-exp), border-color var(--dur-fast) var(--ease-exp);
+}
+.palbtn:hover { color: var(--ink); border-color: var(--btn-border); }
+.palbtn:focus-visible { outline: 2px solid var(--btn-border); outline-offset: 2px; }
+.palbtn .kbd { min-width: 20px; height: 20px; font-size: 11px; }
+
+html.pal-open { overflow: hidden; }
+
+.pal { position: fixed; inset: 0; z-index: 80; }
+.pal__mask {
+  position: absolute; inset: 0;
+  background: rgba(0, 0, 0, .72);
+  backdrop-filter: blur(3px);
+}
+.pal__box {
+  position: relative;
+  width: min(720px, calc(100vw - 32px));
+  margin: 12vh auto 0;
+  background: var(--canvas-card);
+  border: 1px solid var(--hairline); border-radius: var(--r-sm);
+  overflow: hidden;
+  display: flex; flex-direction: column;
+  max-height: 70vh;
+  animation: palIn var(--dur-fast) var(--ease-exp);
+}
+@keyframes palIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
+
+.pal__hd {
+  display: flex; align-items: center; gap: var(--sp-md);
+  padding: var(--sp-md) var(--sp-lg);
+  border-bottom: 1px solid var(--hairline);
+}
+.pal__ico {
+  width: 12px; height: 12px; flex: none;
+  border: 1.5px solid var(--mute); border-radius: 50%;
+  position: relative;
+}
+.pal__ico::after {
+  content: ""; position: absolute; right: -5px; bottom: -4px;
+  width: 6px; height: 1.5px; background: var(--mute);
+  transform: rotate(45deg);
+}
+.pal__in {
+  flex: 1 1 auto; min-width: 0;
+  appearance: none; background: transparent; border: 0; outline: 0;
+  color: var(--ink); font: inherit; font-size: 16px; letter-spacing: -.005em;
+  padding: var(--sp-sm) 0;
+}
+.pal__in::placeholder { color: var(--mute); }
+
+.pal__list { overflow-y: auto; overscroll-behavior: contain; flex: 1 1 auto; }
+.pal__i {
+  display: grid; grid-template-columns: 34px minmax(0, 1fr) auto;
+  gap: var(--sp-lg); align-items: baseline;
+  padding: var(--sp-md) var(--sp-lg);
+  border-bottom: 1px solid var(--hairline);
+  color: inherit;
+}
+.pal__i:last-child { border-bottom: 0; }
+.pal__i.is-cur { background: var(--canvas-soft); }
+.pal__i-n { color: var(--mute); font-size: 11px; }
+.pal__i.is-cur .pal__i-n { color: var(--ink); }
+.pal__i-b { display: grid; gap: var(--sp-xs); min-width: 0; }
+.pal__i-h { font-size: 15px; line-height: 22px; color: var(--body); }
+.pal__i.is-cur .pal__i-h { color: var(--ink); }
+.pal__i-t {
+  font-size: 13px; line-height: 20px; color: var(--mute);
+  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
+}
+.pal__i-g { font-family: var(--font-mono); font-size: 11px; letter-spacing: 1.2px; color: var(--mute); }
+.pal__i mark { background: transparent; color: var(--ink); box-shadow: inset 0 -1px 0 var(--ink); }
+.pal__none { margin: 0; padding: var(--sp-xl) var(--sp-lg); font-size: 14px; color: var(--mute); }
+.pal__ft {
+  display: flex; align-items: center; gap: var(--sp-lg);
+  padding: var(--sp-sm) var(--sp-lg);
+  border-top: 1px solid var(--hairline);
+  font-size: 12px; color: var(--mute);
+}
+.pal__ft .kbd { min-width: 20px; height: 20px; font-size: 11px; }
+.pal__n { margin-left: auto; }
+
+/* 检索跳转后闪一下目标标题 */
+#doc h2.is-flash, #doc h3.is-flash {
+  animation: flash 1700ms var(--ease);
+}
+@keyframes flash {
+  0%, 100% { box-shadow: inset 3px 0 0 transparent; }
+  12%      { box-shadow: inset 3px 0 0 var(--ink); }
+  70%      { box-shadow: inset 3px 0 0 var(--ink); }
+}
+
+/* ==================================================================
+   27. 剂量曲线(NM2.1 页)
+   ================================================================== */
+.dose {
+  margin: var(--sp-xl) 0 var(--sp-2xl);
+  padding: var(--sp-xl);
+  border: 1px solid var(--hairline); border-radius: var(--r-sm);
+  background: var(--canvas-card);
+}
+.dose__hd { display: flex; flex-wrap: wrap; gap: var(--sp-md); align-items: baseline; margin-bottom: var(--sp-lg); }
+.dose__hd .mono { color: var(--ink); text-transform: none; letter-spacing: .6px; font-size: 12px; }
+.dose__x, .dose__r {
+  display: grid; grid-template-columns: minmax(0, 190px) minmax(0, 1fr);
+  gap: var(--sp-lg); align-items: center;
+}
+.dose__x { margin-bottom: var(--sp-md); }
+.dose__x > span {
+  font-family: var(--font-mono); font-size: 12px; letter-spacing: 1.2px;
+  color: var(--mute); text-align: center;
+}
+.dose__x > span.is-pick { color: var(--ink); }
+.dose__r { padding: var(--sp-md) 0; border-top: 1px solid var(--hairline); }
+.dose__l { font-size: 14px; line-height: 20px; color: var(--body); display: grid; gap: var(--sp-xxs); }
+.dose__l small { font-family: var(--font-mono); font-size: 11px; letter-spacing: 1.2px; color: var(--mute); }
+.dose__cells { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--sp-xl); }
+.dose__c { display: grid; gap: var(--sp-sm); justify-items: center; }
+.dose__bar {
+  width: 100%; height: 6px; border-radius: var(--r-pill);
+  background: var(--canvas-mid); overflow: hidden;
+}
+.dose__fill { display: block; height: 100%; border-radius: var(--r-pill); background: var(--mute); }
+.dose__c.is-pick .dose__fill { background: var(--ink); }
+.dose__c b { font-family: var(--font-mono); font-size: 12px; font-weight: 400; letter-spacing: .6px; color: var(--mute); }
+.dose__c.is-pick b { color: var(--ink); }
+.dose__note { margin: var(--sp-lg) 0 0; color: var(--body); max-width: 88ch; }
+.dose__src { margin: var(--sp-sm) 0 0; color: var(--mute); }
+
+/* ==================================================================
+   28. 真实评测台账
+   ================================================================== */
+.ledger-bar { display: flex; flex-wrap: wrap; gap: var(--sp-md); margin: var(--sp-2xl) 0 var(--sp-lg); }
+.ledger-bar .seg { flex-wrap: wrap; }
+.ledger-count {
+  display: flex; flex-wrap: wrap; gap: var(--sp-md); align-items: baseline;
+  padding-bottom: var(--sp-lg); border-bottom: 1px solid var(--hairline);
+}
+.ledger-count .mono { color: var(--ink); text-transform: none; letter-spacing: .6px; }
+.ledger { display: grid; }
+.lg { padding: var(--sp-xl) 0; border-bottom: 1px solid var(--hairline); }
+.lg:last-child { border-bottom: 0; }
+.lg__hd { display: flex; flex-wrap: wrap; gap: var(--sp-md); align-items: center; margin-bottom: var(--sp-md); }
+.lg__i { color: var(--mute); font-size: 11px; }
+.lg__id { margin-left: auto; }
+.lg__exp { margin: 0 0 var(--sp-sm); font-size: 14px; line-height: 22px; color: var(--mute); }
+.lg__exp > span { margin-right: var(--sp-md); font-family: var(--font-mono); font-size: 11px; letter-spacing: 1.2px; }
+.lg__exp code { color: var(--body); }
+.lg__rep {
+  margin: 0 0 var(--sp-md); font-size: 13px; line-height: 21px; color: var(--body);
+  padding-left: var(--sp-md); border-left: 1px solid var(--btn-border);
+  word-break: break-word;
+}
+.lg__meta { margin: 0; font-size: 11px; letter-spacing: .6px; color: var(--mute); }
+.lg__meta b { font-weight: 400; color: var(--body); }
+
+/* 发布页上「NM2.1」那一段的紧凑对照 */
+.nm21grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; border: 1px solid var(--hairline); border-radius: var(--r-sm); overflow: hidden; }
+.nm21grid > div { padding: var(--sp-xl); border-right: 1px solid var(--hairline); }
+.nm21grid > div:last-child { border-right: 0; }
+.nm21grid .rel__v { font-size: 30px; line-height: 34px; }
+.nm21grid .rel__v em { font-style: normal; color: var(--mute); font-size: 20px; margin-right: var(--sp-sm); }
+
+/* 头号数字(money frame):整页唯一一个 .mega,放在 ~60% 滚动深度收网 */
+.money {
+  margin-top: var(--band-tight);
+  padding: var(--band-tight) 0;
+  border-top: 1px solid var(--hairline);
+  border-bottom: 1px solid var(--hairline);
+}
+.money__row {
+  display: flex; align-items: baseline; flex-wrap: wrap;
+  gap: clamp(14px, 3vw, 44px); margin: var(--sp-lg) 0 0;
+}
+.money .mega { color: var(--ink); }
+.money .mega small { font-size: .34em; letter-spacing: -.02em; margin-left: .03em; font-weight: 500; }
+.money__from {
+  font-family: var(--font-mono); font-size: var(--fs-label); letter-spacing: 1.2px;
+  color: var(--mute); max-width: 34ch; line-height: 1.8;
+}
+.money__cap { margin: var(--sp-xl) 0 0; max-width: var(--measure); font-size: var(--fs-small); line-height: 1.7; color: var(--body); }
+
+@media (max-width: 1080px) {
+  .dose__x, .dose__r { grid-template-columns: minmax(0, 1fr); gap: var(--sp-sm); }
+  .dose__x .dose__x0 { display: none; }
+  .dose__x > span { text-align: left; }
+  .nm21grid { grid-template-columns: minmax(0, 1fr); }
+  .nm21grid > div { border-right: 0; border-bottom: 1px solid var(--hairline); }
+  .nm21grid > div:last-child { border-bottom: 0; }
+}
+@media (max-width: 768px) {
+  .palbtn__t { display: none; }
+  .pal__box { margin-top: 6vh; max-height: 82vh; }
+  .pal__i { grid-template-columns: 26px minmax(0, 1fr); }
+  .pal__i-g { display: none; }
+  .dose, .lg { padding-left: 0; padding-right: 0; }
+  .dose { padding: var(--sp-lg); }
+}
diff --git a/assets/favicon.svg b/assets/favicon.svg
new file mode 100644
index 0000000..11ddc9e
--- /dev/null
+++ b/assets/favicon.svg
@@ -0,0 +1,15 @@
+
+  
+  
+  
+    
+    
+    
+    
+    
+    
+    
+  
+  
+  
+
diff --git a/assets/fonts/archivo-latin-wdth-normal.woff2 b/assets/fonts/archivo-latin-wdth-normal.woff2
new file mode 100644
index 0000000..e2448d0
Binary files /dev/null and b/assets/fonts/archivo-latin-wdth-normal.woff2 differ
diff --git a/assets/fonts/geist-mono-latin-wght-normal.woff2 b/assets/fonts/geist-mono-latin-wght-normal.woff2
new file mode 100644
index 0000000..0750682
Binary files /dev/null and b/assets/fonts/geist-mono-latin-wght-normal.woff2 differ
diff --git a/assets/js/data.js b/assets/js/data.js
new file mode 100644
index 0000000..bdb6b6f
--- /dev/null
+++ b/assets/js/data.js
@@ -0,0 +1,334 @@
+/* ------------------------------------------------------------------
+   Natural Memory v2 — 站点数据层
+   所有数字均直接摘自工程内的正式报告,src 字段标注出处文件。
+   本文件不包含任何推算、外推或未测量的数字。
+   ------------------------------------------------------------------ */
+window.NM_DATA = {
+
+  meta: {
+    name: "Natural Memory v2",
+    version: "v2",
+    base: "Qwen3.5-4B",
+    date: "2026-09",
+    repo: "H:\\Memory",
+    disclaimer:
+      "本文只描述当前实现已经完成并实际测量的内容。它不把系统描述成通用智能,也不把实验室结果表述成线上服务承诺。"
+  },
+
+  /* ---------- 一句话定位 ---------- */
+  positioning: {
+    headline: "模型重启之后,它仍然记得。",
+    latin: "A memory layer inside the model.",
+    body:
+      "Natural Memory v2 是接在本地 Qwen3.5-4B 上的一个模型内记忆层:把适合长期复用的个人事实、项目事实和短对话片段写进带地址的记忆记录," +
+      "当前问题只读取少量相关记录,再把这些记录作为模型内部前缀交给 Qwen 生成答案。",
+    problem:
+      "要解决的具体问题:模型重启后不携带历史聊天记录,仍能访问已经保存的个人或项目事实,同时不把全部历史转换成 GPU 上的长 KV Cache。"
+  },
+
+  /* ---------- 记忆与 KV 的分工 ---------- */
+  division: [
+    { k: "最近对话", v: "Qwen 热 KV", note: "顺序关系仍由普通上下文负责" },
+    { k: "长期个人/项目事实", v: "Natural Memory 记录", note: "事实、版本、来源、短证据、语义地址" },
+    { k: "当前问题", v: "有界路由与 Top-K 读取", note: "不做全量 slot 注意力" },
+    { k: "原始持久化状态", v: "嵌入式 memory safetensors", note: "无 SQLite、无磁盘分页" }
+  ],
+
+  /* ---------- 读取路径(可交互的分步) ---------- */
+  readPath: [
+    { id: "q",   label: "用户问题",        detail: "普通自然语言,不要求 /remember 之类的显式指令。", io: "输入 · 一句口语化的问题" },
+    { id: "hs",  label: "Qwen hidden state", detail: "由模型隐状态生成紧凑查询地址,而不是独立的外部 embedding 服务。", io: "问题 → 紧凑查询地址(单条 batch 编码,避免显存瞬时峰值)" },
+    { id: "lsh", label: "LSH / 地址粗索引", detail: "精确桶 + Hamming-1/2 探针,把候选收敛到少量页面。", io: "查询地址 → 候选页(批次 A 粗索引 730 个桶,最近一次粗筛返回 24 个候选)" },
+    { id: "pg",  label: "候选页",           detail: "页容量 32 条记录;默认最多读取 4 页。", io: "候选页 → 少量页面(本次 723 条 active records 分布在 23 个页面上)" },
+    { id: "rr",  label: "页内记录重排",      detail: "记录级重排由独立的 text_retriever 完成(6 张量、1,573,377 参数)。", io: "页内记录 → 有序候选(text_retriever 自身 Top-1 只有 23.20%,是当前主要瓶颈)" },
+    { id: "tk",  label: "Top-K 记录",       detail: "正式脏数据测试中的读取上限为 2 条。", io: "有序候选 → 少量证据(目标召回:批次 A 166/246,批次 B 228/246)" },
+    { id: "inj", label: "证据前缀注入 Qwen", detail: "命中的短证据被提升到 GPU,作为模型内部前缀。", io: "证据 → 模型内部前缀(批次 B 96.97% 的题实际注入,平均 410.34 token)" },
+    { id: "gen", label: "普通生成",          detail: "记忆主体留在进程内存,只有热点记录进入有界 GPU cache。", io: "前缀 + 问题 → 回答(读取额外耗时:批次 A 61.10 ms → 批次 B 26.35 ms)" }
+  ],
+  readPathCost: { value: 61.1, unit: "ms", label: "读取路径额外耗时", src: "对外技术总结 §4" },
+
+  /* ---------- 记忆记录解剖 ---------- */
+  recordFields: [
+    { f: "raw_text",       d: "原始短文本" },
+    { f: "semantic_addr",  d: "语义地址" },
+    { f: "entity",         d: "实体" },
+    { f: "attribute",      d: "属性" },
+    { f: "value",          d: "值" },
+    { f: "timestamp",      d: "时间" },
+    { f: "source",         d: "来源" },
+    { f: "confidence",     d: "置信度" },
+    { f: "importance",     d: "重要性" },
+    { f: "access_count",   d: "访问次数" },
+    { f: "status",         d: "状态" },
+    { f: "supersedes",     d: "版本关系" },
+    { f: "related_ids",    d: "关联记录" },
+    { f: "audit",          d: "审计信息" }
+  ],
+  states: [
+    { s: "active",      d: "当前有效事实,参与正常读取",     tone: "ok" },
+    { s: "superseded",  d: "同一实体+属性出现新值,旧值退役", tone: "mute" },
+    { s: "quarantined", d: "低置信度写入,需显式批准",       tone: "warn" },
+    { s: "retracted",   d: "已撤回,但不从审计中消失",       tone: "bad" }
+  ],
+
+  /* ---------- 主结果:正式脏数据迁移测试 ---------- */
+  dirty: {
+    title: "正式脏数据迁移测试",
+    src: "对外技术总结 §3",
+    subtitle: "真实工程源码 / 文档 + 本项目对话中用户明确说过的短事实",
+    corpus: [
+      { k: "真实源码与文档", v: "65 个文件 · 1,110,524 字节 · 620 个分片" },
+      { k: "用户对话事实",   v: "15 条记录" },
+      { k: "由记录派生的问题", v: "44 题(38 可回答 / 6 未知)" },
+      { k: "来自源码与文档", v: "220 题" },
+      { k: "题目总数",       v: "264 题 · 246 可回答 · 18 未知" }
+    ],
+    protocol: "Qwen3.5-4B · 4-bit NF4 · greedy 解码 · 最大新生成 64 token · 进程显存上限 10 GiB · 读取上限 2 条记录 · 同一 tokenizer",
+    batchNote: "同一题集(264 题 / 246 可回答 / 18 未知)、同一协议、同一仓库,先后跑了两个独立批次。批次配置不同,因此两个批次的分数不可合并、不可平均,只能并列读。",
+    headline: [
+      { metric: "可回答正确率", base: 1.22,  a: 62.60, b: 82.52, baseRaw: "3/246",  aRaw: "154/246", bRaw: "203/246" },
+      { metric: "目标记忆召回率", base: 0.00,  a: 67.48, b: 92.68, baseRaw: "0/246",  aRaw: "166/246", bRaw: "228/246" },
+      { metric: "总体正确率",   base: 7.58,  a: 64.39, b: 82.95, baseRaw: "20/264", aRaw: "170/264", bRaw: "219/264" },
+      { metric: "未知拒答正确率", base: 94.44, a: 88.89, b: 88.89, baseRaw: "17/18",  aRaw: "16/18",   bRaw: "16/18" }
+    ],
+    batches: [
+      {
+        id: "A", label: "批次 A", date: "2026-09-05 23:22", file: "dirty_real_corpus_compare_4b.json",
+        tag: "对外技术总结采用",
+        reader: 61.10, promptTokens: 641.42, prefixRate: 90.91, prefixTokens: 610.67,
+        latencyBase: 2650.4, latencyNm: 2718.9, decodeBase: 24.06, decodeNm: 21.68,
+        allocNm: 3.134, resvNm: 5.021, records: 723, pages: 23, cacheTokens: 15885
+      },
+      {
+        id: "B", label: "批次 B", date: "2026-09-06 11:36", file: "dirty_real_corpus_compare_4b_router_final.json",
+        tag: "接入记忆路由器后",
+        reader: 26.35, promptTokens: 441.19, prefixRate: 96.97, prefixTokens: 410.34,
+        latencyBase: 2854.9, latencyNm: 2878.3, decodeBase: 22.34, decodeNm: 20.68,
+        allocNm: 3.134, resvNm: 5.180, records: 738, pages: 24, cacheTokens: 17990
+      }
+    ],
+    categories: [
+      { k: "symbol_navigation",      label: "符号定位(函数在哪个文件)", n: 192, base: "0/192",   a: "129/192", b: "181/192" },
+      { k: "unknown_refusal",        label: "未知拒答",                    n: 18,  base: "17/18",   a: "16/18",   b: "16/18" },
+      { k: "constraint",             label: "约束类问题",                  n: 15,  base: "0/15",    a: "5/15",    b: "6/15" },
+      { k: "paraphrase",             label: "自然改写",                    n: 9,   base: "0/9",     a: "5/9",     b: "5/9" },
+      { k: "cross_session",          label: "跨会话",                      n: 6,   base: "1/6",     a: "5/6",     b: "5/6" },
+      { k: "temporal_conflict",      label: "时间冲突目录查询",             n: 4,   base: "0/4",     a: "4/4",     b: "4/4" },
+      { k: "reasoning",              label: "推理",                        n: 4,   base: "0/4",     a: "2/4",     b: "0/4" },
+      { k: "write_safety",           label: "写入安全解释",                 n: 2,   base: "0/2",     a: "2/2",     b: "0/2" },
+      { k: "baseline_limitation",    label: "基线局限说明",                 n: 2,   base: "2/2",     a: "2/2",     b: "2/2" },
+      { k: "operational_architecture", label: "操作性架构",                n: 2,   base: "0/2",     a: "0/2",     b: "0/2" },
+      { k: "architecture_tradeoff",  label: "架构取舍",                    n: 2,   base: "0/2",     a: "0/2",     b: "0/2" },
+      { k: "memory_tiers",           label: "记忆分层",                    n: 2,   base: "0/2",     a: "0/2",     b: "0/2" },
+      { k: "benchmark_protocol",     label: "基准协议",                    n: 2,   base: "0/2",     a: "0/2",     b: "0/2" },
+      { k: "control_path",           label: "控制路径",                    n: 2,   base: "0/2",     a: "0/2",     b: "0/2" },
+      { k: "router_path",            label: "路由器路径",                  n: 2,   base: "0/2",     a: "0/2",     b: "0/2" }
+    ],
+    categoryNote: "类别题量差异极大:符号定位 192 题,而 11 个类别各只有 2 题——2 题的类别里错 1 题就是 50 个百分点,不构成结论。批次 B 也并非全面提升:符号定位从 129 升到 181,但写入安全从 2/2 掉到 0/2、推理从 2/4 掉到 0/4。",
+    domains: [
+      { domain: "用户对话事实", n: 38,  base: 2.63,  baseRaw: "1/38",  a: 55.26, aRaw: "21/38",  b: 52.63, bRaw: "20/38" },
+      { domain: "真实源码/文档", n: 208, base: 0.96,  baseRaw: "2/208", a: 63.94, aRaw: "133/208", b: 87.98, bRaw: "183/208" }
+    ],
+    gap: {
+      recallA: 67.48, recallARaw: "166/246",
+      answerA: 62.60, answerARaw: "154/246",
+      recallB: 92.68, recallBRaw: "228/246",
+      answerB: 82.52, answerBRaw: "203/246",
+      note: "目标记忆召回与最终回答正确必须分开报告——检索准确率不等于回答准确率。批次 A 的召回 67.48%、回答 62.60%(差 4.88pp);批次 B 召回 92.68%、回答 82.52%(差 10.16pp):路由器把该查的记忆查回来了,但证据到答案的闭环仍有丢失。"
+    }
+  },
+
+  /* ---------- 实测样例(逐字取自评测原始结果) ---------- */
+  examples: {
+    title: "实测样例",
+    src: "dirty_real_corpus_compare_4b_router_final.json(批次 B 逐条记录)",
+    subtitle: "下面五条题目与回答全部逐字取自评测原始结果,未经改写或润色。包含两条我们判错的样例。",
+    note:
+      "判分规则:可回答题要求 expected_anchors 里的锚点全部出现在回答中(去空白、转小写后做子串匹配);未知题则看模型是否拒答。" +
+      "输出上限为 64 token,标注「达上限截断」的回答是被硬截断的原文,不是完整句——我们保留原样,不做补全。" +
+      "另外:第 1 条命中的记忆记录里写的是「Natural Memory v1」,那是这条记录写入时的项目名,记录内容按原样保留、未做改写;" +
+      "第 5 条则是证据明明已经召回、回答却没能把锚点说全,因此判为未命中——这类「检索对、答案错」正是当前最主要的失分来源。"
+  },
+
+  /* ---------- Stage 5 结构化工程基准 ---------- */
+  stage5: {
+    title: "Stage 5 结构化工程基准",
+    src: "对外技术总结 §5",
+    subtitle: "与脏数据迁移集不是同一题集,不能合并成一个总分",
+    sets: [
+      {
+        name: "通用个人事实",
+        n: 107,
+        rows: [
+          { sys: "原版 Qwen3.5-4B", acc: 3.74,  raw: "4/107",   lat: 1233.2, tok: 23.51, abstain: "21/21" },
+          { sys: "强 RAG 基线",     acc: 99.07, raw: "106/107", lat: 885.8,  tok: 21.67, abstain: "20/21" },
+          { sys: "Natural Memory v2", acc: 100.00, raw: "107/107", lat: 994.3, tok: 21.70, abstain: "21/21" }
+        ]
+      },
+      {
+        name: "项目库",
+        n: 64,
+        rows: [
+          { sys: "原版 Qwen3.5-4B", acc: 70.31, raw: "45/64", lat: 1351.5, tok: 23.70, abstain: null },
+          { sys: "强 RAG 基线",     acc: 100.00, raw: "64/64", lat: 1395.3, tok: 22.94, abstain: null },
+          { sys: "Natural Memory v2", acc: 100.00, raw: "64/64", lat: 1463.2, tok: 21.85, abstain: null }
+        ]
+      }
+    ],
+    note: "强 RAG 使用本地 CPU bert-base-chinese 向量、稀疏特征和固定规则重排。它不是训练好的公开 cross-encoder,因此可以作为更强的本地基线,但不能代表所有生产 RAG 服务。"
+  },
+
+  /* ---------- 代价账本 ---------- */
+  cost: {
+    title: "代价账本",
+    src: "对外技术总结 §4",
+    subtitle: "以下数字来自同一次正式脏数据测试。显存字段是每次请求结束后的 allocated/reserved 快照,不是 torch.cuda.max_memory_allocated() 记录的严格峰值。",
+    rows: [
+      { k: "平均总延迟",     base: "2650.4 ms", nm2: "2718.9 ms", delta: "+2.58%",  tone: "warn" },
+      { k: "平均解码速度",   base: "24.06 tok/s", nm2: "21.68 tok/s", delta: "-9.90%", tone: "bad" },
+      { k: "平均输入 token", base: "30.8",       nm2: "641.4",      delta: "证据前缀增加", tone: "mute" },
+      { k: "读取额外耗时",   base: "无",         nm2: "61.1 ms",     delta: "新增路径", tone: "mute" },
+      { k: "allocated 快照峰值", base: "3.084 GiB", nm2: "3.134 GiB", delta: "+0.050 GiB", tone: "ok" },
+      { k: "reserved 快照峰值",  base: "3.234 GiB", nm2: "5.021 GiB", delta: "+1.787 GiB", tone: "warn" }
+    ],
+    vram: { card: 12, allocBase: 3.084, allocNm2: 3.134, resvBase: 3.234, resvNm2: 5.021, unit: "GiB" },
+    quote: "这个代价来自 Qwen 查询编码、稀疏路由、证据前缀和热点缓存,而不是免费获得的能力。"
+  },
+
+  /* ---------- 容量与存储 ---------- */
+  capacity: {
+    title: "容量与存储",
+    src: "对外技术总结 §4",
+    stats: [
+      { k: "active records",      v: "723",        d: "本次运行实际装载" },
+      { k: "pages",               v: "23",         d: "页容量 32 records/page" },
+      { k: "设计容量",             v: "1,048,576",  d: "地址空间容量,非已装入数量" },
+      { k: "GPU cache 上限",       v: "256 records", d: "且 131,072 token" },
+      { k: "实际 GPU cache token", v: "15,885",     d: "" },
+      { k: "GPU cache fallback",  v: "0",          d: "分配失败 0" },
+      { k: "cold page",           v: "0",          d: "本次为 embedded / process-RAM 模式" }
+    ],
+    storage: {
+      mode: "embedded weight-shard",
+      text: "记忆快照、路由器参数、页面元数据和短文本证据写入模型包的 memory safetensors 切片。启动时加载到进程内存,命中的热点记录才进入有界 GPU cache。",
+      tradeoff: "当前默认路径不使用 SQLite,也不使用磁盘分页。该设计减少了运行时依赖,但意味着模型包会随持久化记忆增长而变大,且需要重新保存权重切片才能固化更新。"
+    },
+    grid: { pagesTotal: 32768, perPage: 32, usedPages: 23 }
+  },
+
+  /* ---------- 记忆路由器工程线 ---------- */
+  router: {
+    title: "记忆路由器工程线",
+    src: "PRODUCTION_ROUTER.md · router_scorecard_final.md · V6_FINAL_REPORT.md · ZERO_OVERLAP_FINDINGS.md · WRITE_PATH_FIX.md",
+    subtitle: "128 维路由器替换原 512 维方案。以下为工程报告中的实测值。",
+    spec: [
+      { k: "参数量",     v: "2,037,774" },
+      { k: "张量数",     v: "16 (float32)" },
+      { k: "地址字节",   v: "512 B" },
+      { k: "结构",       v: "dim=128 · heads=8 · hidden=2560 · max_hops=3" },
+      { k: "权重摘要",   v: "sha256 69f8295e…d189deb · 8,156,921 B" },
+      { k: "冻结评测集", v: "21,920 episode / 10 类" }
+    ],
+    metrics: [
+      { k: "Top-1 命中",        base: "41.12%", nm2: "94.14%", note: "REPLAY-128" },
+      { k: "Recall@1 / @3 / @5", base: "37.03 / 46.73 / 48.91", nm2: "88.58 / 96.48 / 97.15", note: "" },
+      { k: "MRR / nDCG@3",      base: "47.69 / 44.22", nm2: "95.77 / 95.37", note: "" },
+      { k: "多跳证据全中 (Top-3)", base: "43.43%", nm2: "96.22%", note: "" },
+      { k: "hop 正确率",        base: "73.23%", nm2: "100.00%", note: "欠预测率 4.68% → 0.00%" },
+      { k: "未知拒答率",        base: "0.00%",  nm2: "100.00%", note: "门槛 0.30–0.80 每点均为 100%" },
+      { k: "单查询延迟",        base: "1.0995 ms", nm2: "0.9663 ms", note: "路由 QPS 909.5 → 1034.8" },
+      { k: "batch256 吞吐",     base: "207,287", nm2: "242,759", note: "texts/s 口径" },
+      { k: "零重叠改写 Top-1",   base: "18.40%", nm2: "59.60%", note: "+41.20pp,随机基线 4.17%" },
+      { k: "写入路径存活记录",   base: "12/20",  nm2: "20/20",  note: "retract 16 → 0;e2e 37.50% → 68.75%" },
+      { k: "逐轴判定",          base: "—",      nm2: "99 通过 / 11 未通过", note: "128 维两条线 22/22 全方位超越" }
+    ],
+    caveats: [
+      "端到端答案不由路由器决定:93.75% 的读取仍走旧版 16 槽注入路径,地址命中 0/16,记录级排序由 text_retriever 决定(其 Top-1 仅 23.20%)。路由器 +41.20pp 不传递到最终答案。",
+      "512 维方案「全部指标不劣于基线」在数学上不可达(地址存储 ×4 与 batch-256 吞吐两条独立障碍);XL 架构准确率最高,但吞吐真实低 26–52%。",
+      "拒答机制只在域内成立:跨域 A 段可回答 0.00%,集成门已回退为 gate=false;开放词表属性匹配仅 49.20% / AUC 0.6560。",
+      "属性头词表固定 24 类闭集:训练后新写入的属性会被判为 NONE 而误拒。",
+      "同一指标在多份报告中的延迟口径不一致(1.0995→0.9663、0.9549→0.9169 等),本页只列其中一套。",
+      "长上下文:8K 可跑,16K / 32K OOM。不得据此宣称百万级 token 上下文。"
+    ]
+  },
+
+  /* ---------- 边界:不能宣称的内容 ---------- */
+  limits: {
+    title: "我们不宣称的内容",
+    subtitle: "以下说法目前没有足够证据支持。把它们放在这里,是因为它们和上面的数字一样重要。",
+    claims: [
+      "Natural Memory 已经等价于百万 token 的完整 KV",
+      "Natural Memory 已经击败生产级 embedding + reranker RAG",
+      "任何自然语言表达都能稳定召回正确记忆",
+      "记忆写入永远不会出错",
+      "一百万条记录已经完成端到端验证",
+      "当前 4B 结果可以直接外推到 14B、32B 或更大模型",
+      "reserved VRAM 就是实际模型峰值显存",
+      "本次用户事实测试代表大规模真实用户群体"
+    ],
+    limitations: [
+      { t: "训练数据仍不够自然", d: "路由器主体训练仍来自本地构造的实体—属性—值事实和 hard negatives。自然改写仍是人工编写的测试变体,不是大规模脱敏真人会话。" },
+      { t: "真实仓库的语义索引覆盖有限", d: "620 个真实分片全部进库,但为控制 GPU 压力,只有 96 个符号目标和 8 个操作性事实建立了 Qwen 语义地址。这不等于整个仓库都被高质量索引。" },
+      { t: "开放式解释弱于明确定位", d: "「某个函数在哪个文件」明显好于「结合多个文件解释系统为什么这样设计」。后者需要多跳证据合并,仍会出现证据已召回但回答不完整。" },
+      { t: "速度仍有真实代价", d: "平均延迟高约 2.58%,解码速度低约 9.90%,输入前缀显著变长。" },
+      { t: "生产级基线还不完整", d: "本次对照聚焦原版 Qwen 无记忆基线;要作服务经济学结论,还需加入强 embedding 检索器、cross-encoder、滑动窗口和分页 KV。" }
+    ],
+    quotes: [
+      { q: "Memory Slot 不模拟完整 KV。它只接管 KV 中最昂贵、最适合长期保存、最容易重复利用的部分。", s: "README_NATURAL_MEMORY_V2.md" },
+      { q: "这些限制是设计边界,不是用一个「无限上下文」数字掩盖的未验证假设。", s: "README_NATURAL_MEMORY_V2.md" },
+      { q: "任何排序器都救不回一条已经不存在的记录。", s: "WRITE_PATH_FIX.md" },
+      { q: "这不是调参空间不够,而是这个信号根本不存在。", s: "ABSTENTION_BREAKTHROUGH.md" }
+    ]
+  },
+
+  /* ---------- 已解决的问题 ---------- */
+  solved: [
+    "普通自然语言触发自动记忆读取,不要求用户输入 /remember",
+    "模型重启后不回放历史聊天,只加载已保存的 memory shard",
+    "旧值和新值保留版本关系,旧值不会继续作为 active 事实参与正常读取",
+    "记忆读取器、地址索引和证据注入位于模型适配架构内部",
+    "记忆主体留在进程内存,热点记录使用有界 GPU cache",
+    "当前问题只读取 Top-K 记录,不对全量 slot 做注意力",
+    "低置信度写入、撤回和审计状态有独立表示",
+    "在真实工程源码定位任务上,相比无记忆 Qwen 有明显的可回答性提升"
+  ],
+
+  /* ---------- 路线图 ---------- */
+  roadmap: [
+    "使用有用户同意的脱敏对话,增加自然省略、指代、错别字、时间间隔、互相矛盾的旧说法和多轮项目协作样本",
+    "重新训练路由器,重点覆盖「同一事实的多种问法」和「相似但错误的记录」",
+    "对「检索正确但回答错误」的样本单独训练证据使用与拒答策略",
+    "将真实仓库的语义地址覆盖从目标记录扩展到完整索引,仍保持单条/小批次编码以保护显存",
+    "加入多跳证据合并和当前版本优先规则",
+    "将写入、冲突解决和摘要从生成热路径移到回合边界或后台线程",
+    "把显存测试改为记录真正的 CUDA high-water mark,并分别报告 live allocated、reserved 和 cache payload",
+    "在同一协议下补齐强 RAG、滑动窗口和分页 KV 的成本—质量曲线"
+  ],
+
+  /* ---------- 复现 ---------- */
+  reproduce: {
+    cwd: "H:\\Memory",
+    python: "C:\\Users\\Administrator\\miniconda3\\envs\\LLM\\python.exe",
+    cmd: [
+      "& C:\\Users\\Administrator\\miniconda3\\envs\\LLM\\python.exe `",
+      "  -m dynamic_memory_lab.benchmark_dirty_real_corpus_4b `",
+      "  --project-records 8192 `",
+      "  --project-targets 96 `",
+      "  --max-new-tokens 64 `",
+      "  --encode-batch-size 1 `",
+      "  --skip-chunk-rag `",
+      "  --output H:\\Memory\\dynamic_memory_lab\\dirty_real_corpus_compare_4b.json"
+    ],
+    files: [
+      { k: "原始结果", v: "H:\\Memory\\dynamic_memory_lab\\dirty_real_corpus_compare_4b.json" },
+      { k: "评测脚本", v: "H:\\Memory\\dynamic_memory_lab\\benchmark_dirty_real_corpus_4b.py" },
+      { k: "对外技术说明", v: "H:\\Memory\\dynamic_memory_lab\\Natural_Memory_v2_对外技术总结.md" }
+    ]
+  },
+
+  /* ---------- 结论 ---------- */
+  conclusion:
+    "Natural Memory v2 当前已经不是只有构造数据上的演示:在真实工程源码、文档和本项目用户要求组成的脏数据迁移测试中," +
+    "它显著提高了窗口外事实的可回答性,并在 10 GiB GPU 进程上完成了 723 条 active records 的读取运行。" +
+    "同时,62.60% 的可回答正确率、88.89% 的未知拒答正确率和约 10% 的解码速度下降说明它仍处于研究原型到工程验证之间。"
+};
diff --git a/assets/js/examples.js b/assets/js/examples.js
new file mode 100644
index 0000000..b117a9f
--- /dev/null
+++ b/assets/js/examples.js
@@ -0,0 +1,226 @@
+/* 本文件由脚本从评测原始结果逐字提取,请勿手改。
+   来源:H:\Memory\dynamic_memory_lab\dirty_real_corpus_compare_4b_router_final.json
+   创建时间:2026-09-06T11:36:26+0800  协议:greedy / max_new_tokens=64 / top_k=2 */
+window.NM_EXAMPLES = [
+  {
+    "id": "user-004",
+    "domain": "user_dialogue",
+    "category": "paraphrase",
+    "answerable": true,
+    "query": "我不想翻聊天记录,当前这个记忆模型叫什么?",
+    "expected": "Natural Memory v1",
+    "expected_anchors": [
+      "Natural Memory v1"
+    ],
+    "nm": {
+      "response": "当前这个记忆模型叫 **Natural Memory v1**。",
+      "correct": true,
+      "tokens": 14,
+      "recall": true,
+      "retrieved_values": [
+        "Natural Memory v1",
+        "重要信息写入,禁止胡编乱造,并学习写入、清理和总结时机"
+      ],
+      "records": [
+        {
+          "record_id": "mem_da9418d179467fba",
+          "entity": "user",
+          "attribute": "模型名称",
+          "value": "Natural Memory v1",
+          "status": "active",
+          "version": 0,
+          "source": "user_provided_dialogue",
+          "text_preview": "用户给当前模型定名:Natural Memory v1。"
+        },
+        {
+          "record_id": "mem_862617d79b828161",
+          "entity": "user",
+          "attribute": "记忆质量要求",
+          "value": "重要信息写入,禁止胡编乱造,并学习写入、清理和总结时机",
+          "status": "active",
+          "version": 0,
+          "source": "user_provided_dialogue",
+          "text_preview": "用户要求:重要信息都要存进去,但不能让模型胡编乱造记忆,还要学会什么时候写入和清理总结。"
+        }
+      ]
+    },
+    "base": {
+      "response": "您提到的“当前这个记忆模型”具体指代什么,取决于您使用的平台或应用。如果您是在使用某个特定的 AI 助手或应用程序,那么该应用可能内置了某种形式的记忆功能,用于记住您的偏好、历史对话或其他相关信息。\n\n不过,从技术角度来看,并没有一个统一的“记忆",
+      "correct": false,
+      "tokens": 64
+    },
+    "note": "actual user-stated fact with a hand-written natural paraphrase"
+  },
+  {
+    "id": "code-symbol-010-a",
+    "domain": "real_repository",
+    "category": "symbol_navigation",
+    "answerable": true,
+    "query": "我现在不想翻完整个屎山代码库,_generate_student 这个 def 定义落在哪个文件?",
+    "expected": "benchmark_memory_vs_full_kv.py",
+    "expected_anchors": [
+      "benchmark_memory_vs_full_kv.py",
+      "_generate_student"
+    ],
+    "nm": {
+      "response": "根据已保存的长期记忆,`_generate_student` 这个函数定义落在文件 **`benchmark_memory_vs_full_kv.py`** 中(具体在第 171 行)。",
+      "correct": true,
+      "tokens": 43,
+      "recall": true,
+      "retrieved_values": [
+        "benchmark_memory_vs_full_kv.py",
+        "benchmark_compare_baselines_4b.py",
+        "test_restart_memory_dialogue.py"
+      ],
+      "records": [
+        {
+          "record_id": "mem_df66c88a7e2ea9da",
+          "entity": "benchmark_memory_vs_full_kv.py",
+          "attribute": "symbol:_generate_student",
+          "value": "benchmark_memory_vs_full_kv.py",
+          "status": "active",
+          "version": 0,
+          "source": "real_repository_symbol_index",
+          "text_preview": "真实代码库事实:文件 benchmark_memory_vs_full_kv.py 的第 171 行定义了 def _generate_student。原始代码行:def _generate_student("
+        },
+        {
+          "record_id": "mem_7fb124c563472b07",
+          "entity": "benchmark_compare_baselines_4b.py",
+          "attribute": "symbol:_base_generate",
+          "value": "benchmark_compare_baselines_4b.py",
+          "status": "active",
+          "version": 0,
+          "source": "real_repository_symbol_index",
+          "text_preview": "真实代码库事实:文件 benchmark_compare_baselines_4b.py 的第 116 行定义了 def _base_generate。原始代码行:def _base_generate("
+        }
+      ]
+    },
+    "base": {
+      "response": "我无法直接访问您的本地文件系统或代码库,因此无法告诉您 `_generate_student` 这个函数具体落在哪个文件中。\n\n不过,您可以尝试以下几种方法来快速定位:\n\n1. **使用 IDE 的搜索功能**:在您的代码编辑器(如 VS Code、PyCharm 等)中",
+      "correct": false,
+      "tokens": 64
+    },
+    "note": "real symbol extracted from repository source"
+  },
+  {
+    "id": "user-unknown-004",
+    "domain": "user_dialogue",
+    "category": "unknown_refusal",
+    "answerable": false,
+    "query": "我在这次对话里有没有说过生日?",
+    "expected": "",
+    "expected_anchors": [],
+    "nm": {
+      "response": "在这次对话中,**您还没有提到过生日**。\n\n截至目前,我们的交流内容中没有任何关于您生日、年龄或庆祝生日的提及。如果您想分享,随时可以告诉我!",
+      "correct": true,
+      "tokens": 41,
+      "recall": false,
+      "retrieved_values": [],
+      "records": []
+    },
+    "base": {
+      "response": "在这次对话中,**您还没有提到过生日**。\n\n截至目前,我们的交流内容中没有任何关于您生日、年龄或庆祝生日的提及。如果您想分享,随时可以告诉我!",
+      "correct": true,
+      "tokens": 41
+    },
+    "note": "unknown or deliberately sensitive field absent from the supplied dialogue slice"
+  },
+  {
+    "id": "code-unknown-008",
+    "domain": "real_repository",
+    "category": "unknown_refusal",
+    "answerable": false,
+    "query": "请告诉我没有出现在仓库里的部署区域。",
+    "expected": "",
+    "expected_anchors": [],
+    "nm": {
+      "response": "根据您提供的长期记忆证据(特别是 `README_NATURAL_MEMORY_V2.md` 的内容),**该文档中并没有列出任何具体的“部署区域”(Deployment Regions)**。\n\n文档主要关注的是以下技术架构和逻辑,而非地理或云端的部署位置:\n\n1.  **核心设计",
+      "correct": false,
+      "tokens": 64,
+      "recall": false,
+      "retrieved_values": [
+        "README_NATURAL_MEMORY_V2.md",
+        "README_NATURAL_MEMORY_V2.md"
+      ],
+      "records": [
+        {
+          "record_id": "mem_796c6e8c86afce71",
+          "entity": "README_NATURAL_MEMORY_V2.md",
+          "attribute": "operational:1",
+          "value": "README_NATURAL_MEMORY_V2.md",
+          "status": "active",
+          "version": 0,
+          "source": "real_repository_snapshot",
+          "text_preview": "真实项目文件:README_NATURAL_MEMORY_V2.md\n## 核心设计\n\n### KV 与 Memory Slot 的分工\n\n```text\n最近 32K token(最多可配置到 128K) -> GPU 热 KV:保留精确顺序和局部连贯性\n长期个人事实 / 项目决策 / 纠错版本     -> Memory Slot:保存压缩、可寻址的证据\n当前问题                             -> 稀疏路由:只读取少量相关记录\n```\n\nMemory Slot 不模拟完整 KV。它只接管 KV 中最昂贵、最适合长期保存、最容易重复利用的部分。这里的目标是比长 K"
+        },
+        {
+          "record_id": "mem_32331793e07a8a4f",
+          "entity": "README_NATURAL_MEMORY_V2.md",
+          "attribute": "operational:0",
+          "value": "README_NATURAL_MEMORY_V2.md",
+          "status": "active",
+          "version": 0,
+          "source": "real_repository_snapshot",
+          "text_preview": "真实项目文件:README_NATURAL_MEMORY_V2.md\n把一百万个 slot 当作一张巨大的 KV Cache,而是把历史中最适合长期保存的部分压缩成有地址的记忆记录;当前对话仍由 Qwen 的热 KV 负责连续理解。\n\n## 先看结论\n\n当前版本已经完成以下闭环:\n\n```text\n普通用户输入\n    -> 自动写入策略\n    -> 置信度 / 重要性 / 来源审计\n    -> 紧凑地址投影\n    -> 分页存储\n    -> LSH 粗索引\n    -> 候选页重排\n    -> 候选记录精排\n    -> Top-K 证据注入 Qwen\n    -> 版本冲突 / "
+        }
+      ]
+    },
+    "base": {
+      "response": "您好!由于我无法访问您的仓库系统或具体业务数据,无法直接列出“未出现在仓库里的部署区域”。\n\n为了帮您准确定位,请提供以下信息:\n1. 仓库系统名称(如 GitLab、Jenkins、阿里云容器服务等)\n2. 您关注的部署区域范围",
+      "correct": true,
+      "tokens": 64
+    },
+    "note": "target deliberately absent from the real repository snapshot"
+  },
+  {
+    "id": "user-009",
+    "domain": "user_dialogue",
+    "category": "constraint",
+    "answerable": true,
+    "query": "你记得我对持久化文件和分页的限制吗?",
+    "expected": "第三个权重文件",
+    "expected_anchors": [
+      "第三个权重文件",
+      "不使用 SQLite",
+      "不使用磁盘分页"
+    ],
+    "nm": {
+      "response": "是的,我记住了。\n\n根据您之前的要求,关于持久化方案和分页的限制如下:\n*   **持久化方案**:不使用 SQLite 或磁盘分页,所有信息完全由**第三个权重文件**储存。\n*   **实体**:user\n*   **属性**:记忆持久化",
+      "correct": false,
+      "tokens": 64,
+      "recall": true,
+      "retrieved_values": [
+        "第三个权重文件,不使用 SQLite 或磁盘分页",
+        "Qwen3.5 4B"
+      ],
+      "records": [
+        {
+          "record_id": "mem_c3886345cac35147",
+          "entity": "user",
+          "attribute": "记忆持久化方案",
+          "value": "第三个权重文件,不使用 SQLite 或磁盘分页",
+          "status": "active",
+          "version": 0,
+          "source": "user_provided_dialogue",
+          "text_preview": "用户要求:不使用 SQLite 或磁盘分页,完全由第三个权重文件储存所有信息。"
+        },
+        {
+          "record_id": "mem_149a0c2ab04c0f56",
+          "entity": "user",
+          "attribute": "对照模型",
+          "value": "Qwen3.5 4B",
+          "status": "active",
+          "version": 0,
+          "source": "user_provided_dialogue",
+          "text_preview": "用户要求和原版 qwen3.5 4b 做跑分对比,并纠正为 4B。"
+        }
+      ]
+    },
+    "base": {
+      "response": "作为一个人工智能模型,我**无法访问您的个人历史对话记录、系统配置或您之前设定的具体规则**(例如具体的持久化文件大小限制、分页数量限制等)。每次对话对我来说都是全新的开始,除非您在当前的对话中明确告知我这些限制。\n\n不过,我可以帮您**重新设定",
+      "correct": false,
+      "tokens": 64
+    },
+    "note": "actual user-stated fact with a hand-written natural paraphrase"
+  }
+];
diff --git a/assets/js/ledger.js b/assets/js/ledger.js
new file mode 100644
index 0000000..87282b0
--- /dev/null
+++ b/assets/js/ledger.js
@@ -0,0 +1,11 @@
+/* ==================================================================
+   Natural Memory v2 — ledger.js
+   「真实评测台账」的数据层。
+
+   ⚠️ 本文件由 tools/make_ledger.py 从磁盘上的原始评测 JSON 生成,
+      请勿手改。下面是全部 108 条 episode,字段逐字复制、未做改写或换算。
+
+     runtime_score_bands.json
+     nm2_1_final_runtime_e2e.json
+   ================================================================== */
+window.NM_LEDGER = [{"key":"bands","label":"运行时评分带扫描","note":"60 用例 · 24 同形候选 · prior=1.00 blend=0.00","n":60,"items":[{"id":"zero-overlap-000047","attr":"咖啡口味","ans":true,"exp":"VAL-PW46K9K9","reply":"关于您早上饮用的咖啡偏好哪种豆子,当前记忆中没有相关记录。已知信息仅包含您的午餐偏好是 VAL-BFW4LVTV。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000144","attr":"入职年份","ans":false,"exp":"VAL-GLHQMRN6","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;档案标识;VAL-AVX95V7L;手机尾号;VAL-LBRPQJRT","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":2.8524921381220096,"active":23,"need":true},{"id":"zero-overlap-000259","attr":"出生城市","ans":true,"exp":"VAL-M9VSJAEX","reply":"您的出生城市是 VAL-M9VSJAEX。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.564332926884773,"active":24,"need":true},{"id":"zero-overlap-000143","attr":"档案标识","ans":true,"exp":"VAL-NPXYS6SC","reply":"您的紧急联系人姓氏是 VAL-DDN4WAD4,客户名称是 VAL-7UJT3QQA,课程名称是 VAL-V3TE4HHW,团队名称是 VAL-TLE8GCW3,阅读","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":4.211679219488425,"active":24,"need":true},{"id":"zero-overlap-000266","attr":"出生城市","ans":true,"exp":"VAL-K7FT5XFB","reply":"您的出生城市是 VAL-K7FT5XFB。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.3051346188937454,"active":24,"need":true},{"id":"zero-overlap-000204","attr":"阅读工具","ans":true,"exp":"VAL-ZRLSPU5V","reply":"您的阅读工具是 VAL-ZRLSPU5V。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.9886870104974497,"active":24,"need":true},{"id":"zero-overlap-000061","attr":"常住城市","ans":true,"exp":"VAL-3SUNPNRD","reply":"您的常住城市是 VAL-3SUNPNRD,通勤方式是 VAL-2CXTE3TB,出生城市是 VAL-GE828D22,办公城市是 VAL-XYRY8BUQ,入职年份","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.6213454890251158,"active":24,"need":true},{"id":"zero-overlap-000091","attr":"运动习惯","ans":true,"exp":"VAL-92MQHEDL","reply":"您的运动习惯是 VAL-92MQHEDL。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.0058205354213714,"active":24,"need":true},{"id":"zero-overlap-000193","attr":"午餐偏好","ans":true,"exp":"VAL-HG84H5TD","reply":"您的午餐偏好是 user;午餐偏好;VAL-HG84H5TD。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.6998433045297862,"active":24,"need":true},{"id":"zero-overlap-000088","attr":"设备型号","ans":true,"exp":"VAL-JY6S7EYF","reply":"关于您使用的机器档次,当前长期记忆中未包含此信息,无法回答。已知信息仅涉及:user;常用编辑器;VAL-3KPKRM4J。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.516791512356487,"active":24,"need":true},{"id":"zero-overlap-000121","attr":"手机尾号","ans":true,"exp":"VAL-LHY34JYF","reply":"您随身那台设备的末端数字是 XX2。设备型号:VAL-JFSYSXX2。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000282","attr":"咖啡口味","ans":true,"exp":"VAL-Q2AQQMUP","reply":"关于您早上那杯咖啡的豆子偏好,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-5ATEHVSM。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000261","attr":"课程名称","ans":false,"exp":"VAL-Q6ZMN3XU","reply":"关于您的上级信息,当前长期记忆中未包含相关记录。已知信息包括:紧急联系人姓氏是 VAL-QT3YM55W;客户名称是 VAL-7VDS2BRU;默认语言是 VAL-QB","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":1.9421720507257656,"active":23,"need":true},{"id":"zero-overlap-000104","attr":"午餐偏好","ans":true,"exp":"VAL-SRFC842D","reply":"您的午餐偏好是 VAL-SRFC842D。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7423390823602677,"active":24,"need":true},{"id":"zero-overlap-000214","attr":"客户名称","ans":true,"exp":"VAL-MV382EEF","reply":"我服务的对象是 VAL-MV382EEF。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7693164377659559,"active":24,"need":true},{"id":"zero-overlap-000175","attr":"客户名称","ans":true,"exp":"VAL-QJNRRE8X","reply":"您的客户名称是 VAL-QJNRRE8X。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7712558816373348,"active":24,"need":true},{"id":"zero-overlap-000078","attr":"常住城市","ans":true,"exp":"VAL-TQBY65U8","reply":"我的常住城市是 VAL-TQBY65U8。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.2650369691848753,"active":24,"need":true},{"id":"zero-overlap-000281","attr":"咖啡口味","ans":true,"exp":"VAL-3GP4STZX","reply":"关于您早上饮用的咖啡偏好哪种豆子,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-XNN7T8EK。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000285","attr":"起床时间","ans":true,"exp":"VAL-4K3RQQKF","reply":"我的闹钟设定时间是 VAL-4K3RQQKF。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.359343914853775,"active":24,"need":true},{"id":"zero-overlap-000092","attr":"宿舍楼号","ans":true,"exp":"VAL-PLZZ659B","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-PLZZ659B。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.8819549524530643,"active":24,"need":true},{"id":"zero-overlap-000000","attr":"出生城市","ans":true,"exp":"VAL-PHB8FNF9","reply":"您的出生城市是 VAL-PHB8FNF9。user;出生城市;VAL-PHB8FNF9","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.8505235558902053,"active":24,"need":true},{"id":"zero-overlap-000080","attr":"入职年份","ans":true,"exp":"VAL-GFT4HRX2","reply":"关于您何时开始在这家公司上班,当前记忆中没有相关记录。已知信息:user;办公城市;VAL-TCNMN9HW。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.51429933919514,"active":24,"need":true},{"id":"zero-overlap-000198","attr":"手机尾号","ans":true,"exp":"VAL-7T3XVMVH","reply":"您随身那台设备的末端数字是 4N,该设备的完整型号为 VAL-9MWPDS4N。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000140","attr":"宿舍楼号","ans":true,"exp":"VAL-RMDSB297","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-RMDSB297。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.942847951625275,"active":24,"need":true},{"id":"zero-overlap-000006","attr":"档案标识","ans":true,"exp":"VAL-8EFN3KGR","reply":"您的系统分配字符包括:紧急联系人姓氏 VAL-KLPQW2L2;课程名称 VAL-PVMQLCL9;团队名称 VAL-V4BVFB2N;客户名称 VAL-92LH7CHG","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":4.169416844133658,"active":24,"need":true},{"id":"zero-overlap-000180","attr":"客户名称","ans":false,"exp":"VAL-3F87H6UT","reply":"您随身那台设备的末端数字是 VAL-X5VZ48QF。","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":23,"need":true},{"id":"zero-overlap-000258","attr":"阅读工具","ans":true,"exp":"VAL-SHRY3GNG","reply":"我翻书时依赖的阅读工具是 VAL-SHRY3GNG。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.9932317454522837,"active":24,"need":true},{"id":"zero-overlap-000030","attr":"课程名称","ans":true,"exp":"VAL-BQY3K5EV","reply":"您的课程名称是 VAL-BQY3K5EV。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7088197509944439,"active":24,"need":true},{"id":"zero-overlap-000196","attr":"手机尾号","ans":true,"exp":"VAL-ZHN2NMZV","reply":"您随身那台设备的末端数字是 W9,该设备的完整型号为 VAL-5VBJ22W9。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000246","attr":"常住城市","ans":false,"exp":"VAL-6BWJYDE4","reply":"关于您小时候待的地方,当前长期记忆中未包含此信息。已知信息如下:user;常住城市;VAL-6BWJYDE4;办公城市;VAL-88V99MPM;宿舍楼号","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":2.6527658945475845,"active":23,"need":true},{"id":"zero-overlap-000268","attr":"宿舍楼号","ans":true,"exp":"VAL-QVYMWTLK","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-QVYMWTLK。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.7659836388747943,"active":24,"need":true},{"id":"zero-overlap-000219","attr":"咖啡口味","ans":true,"exp":"VAL-M3W7MRJR","reply":"关于您早上那杯咖啡的豆子偏好,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-Y4Z5H2FG。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000056","attr":"主管姓名","ans":true,"exp":"VAL-VDL8H7Y9","reply":"您的主管姓名是 user;主管姓名;VAL-VDL8H7Y9。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.9513123025616852,"active":24,"need":true},{"id":"zero-overlap-000195","attr":"午餐偏好","ans":false,"exp":"VAL-LHKPHTRT","reply":"关于您坐在哪一间房的信息,当前长期记忆中未包含该数据。已知信息包括:user;宿舍楼号;VAL-JCMMXZA3。","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":3.194271833706213,"active":23,"need":true},{"id":"zero-overlap-000183","attr":"出生城市","ans":true,"exp":"VAL-TT5RG83H","reply":"您的出生城市是 VAL-TT5RG83H。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.549031222478035,"active":24,"need":true},{"id":"zero-overlap-000168","attr":"常住城市","ans":true,"exp":"VAL-8TQ8737C","reply":"您的常住城市是 VAL-8TQ8737C。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.4585979866981504,"active":24,"need":true},{"id":"zero-overlap-000184","attr":"项目代号","ans":true,"exp":"VAL-3H5TQEC4","reply":"您的团队名称是 VAL-Z98JC7BG。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":3.191310502301591,"active":24,"need":true},{"id":"zero-overlap-000051","attr":"项目代号","ans":false,"exp":"VAL-KG4RZYH5","reply":"您的办公城市是 VAL-W9KGRD9U。","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":1.2606107878684996,"active":23,"need":true},{"id":"zero-overlap-000164","attr":"客户名称","ans":true,"exp":"VAL-BB55YHW5","reply":"您的客户名称是 VAL-BB55YHW5。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.77552460052073,"active":24,"need":true},{"id":"zero-overlap-000213","attr":"通勤方式","ans":true,"exp":"VAL-M58DDQX2","reply":"您的通勤方式是 VAL-M58DDQX2。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.1366783114608343,"active":24,"need":true},{"id":"zero-overlap-000122","attr":"通勤方式","ans":true,"exp":"VAL-VULSNGJ3","reply":"您的通勤方式是 VAL-VULSNGJ3。user;通勤方式;VAL-VULSNGJ3","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.130069571965557,"active":24,"need":true},{"id":"zero-overlap-000106","attr":"咖啡口味","ans":false,"exp":"VAL-837DYYWV","reply":"关于您的闹钟设置时间,当前长期记忆中未包含相关证据,无法回答。已知信息包括:user;闹钟设置时间;未知。","ok":false,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.273330240117752,"active":23,"need":true},{"id":"zero-overlap-000215","attr":"档案标识","ans":true,"exp":"VAL-B44BGU2X","reply":"您的系统分配字符为:user;紧急联系人姓氏;VAL-SEUHFJR6;阅读工具;VAL-4WUUDSWA;团队名称;VAL-4LFCHQ5W;课程名称;VAL","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":4.2204755537550875,"active":24,"need":true},{"id":"zero-overlap-000173","attr":"客户名称","ans":false,"exp":"VAL-P84HTKZR","reply":"您随身那台设备的末端数字是 Y。\n设备型号:VAL-NARXVSSY","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":23,"need":true},{"id":"zero-overlap-000155","attr":"主管姓名","ans":true,"exp":"VAL-NTC877NR","reply":"您的主管姓名是 VAL-NTC877NR。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.9677336550748197,"active":24,"need":true},{"id":"zero-overlap-000045","attr":"办公城市","ans":false,"exp":"VAL-CG4X6ZWD","reply":"您的办公城市是 VAL-SZ4R5GT3。","ok":false,"wrong":false,"leak":true,"stop":"evidence_found","sel":8,"score":1.1886139500141142,"active":23,"need":true},{"id":"zero-overlap-000002","attr":"午餐偏好","ans":true,"exp":"VAL-HEE2QMH2","reply":"您的午餐偏好是 user;午餐偏好;VAL-HEE2QMH2。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7196446585655213,"active":24,"need":true},{"id":"zero-overlap-000171","attr":"常住城市","ans":true,"exp":"VAL-KZA246K9","reply":"您的常住城市是 VAL-KZA246K9。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.286561994552612,"active":24,"need":true},{"id":"zero-overlap-000154","attr":"入职年份","ans":true,"exp":"VAL-7Z9CBUX8","reply":"关于您何时开始在这家公司上班,当前记忆中没有相关记录。已知信息:user;办公城市;VAL-BP7253WL。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.51429933919514,"active":24,"need":true},{"id":"zero-overlap-000105","attr":"客户名称","ans":true,"exp":"VAL-SNB45547","reply":"我服务的对象是 VAL-SNB45547。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7797372966259718,"active":24,"need":true},{"id":"zero-overlap-000021","attr":"默认语言","ans":true,"exp":"VAL-DUQ9DJ8G","reply":"您的交流语言是 VAL-9QAURGAS。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":1.9399472272499265,"active":24,"need":true},{"id":"zero-overlap-000009","attr":"咖啡口味","ans":true,"exp":"VAL-7PQL6BVT","reply":"关于您早上饮用的咖啡偏好哪种豆子,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-WS8VG4B5。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000234","attr":"工位楼层","ans":true,"exp":"VAL-87M7HVC7","reply":"关于您坐在哪一间房的信息,当前长期记忆中未包含该数据。已知信息如下:user;宿舍楼号;VAL-JTSHA7PM。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":3.1937497879965546,"active":24,"need":true},{"id":"zero-overlap-000244","attr":"午餐偏好","ans":false,"exp":"VAL-2VLHC26V","reply":"关于您服务的对象,当前长期记忆中未包含相关信息,无法回答。","ok":false,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.7779876451194286,"active":23,"need":true},{"id":"zero-overlap-000083","attr":"办公城市","ans":true,"exp":"VAL-YBVYV97X","reply":"您的办公城市是 VAL-YBVYV97X。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.8081361769552853,"active":24,"need":true},{"id":"zero-overlap-000079","attr":"出生城市","ans":true,"exp":"VAL-7R8WVAFG","reply":"您的出生城市是 VAL-7R8WVAFG。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.7214163786326675,"active":24,"need":true},{"id":"zero-overlap-000235","attr":"项目代号","ans":true,"exp":"VAL-DCA9FZL4","reply":"您的团队名称是 VAL-QZLU73KN。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":3.1913125220273892,"active":24,"need":true},{"id":"zero-overlap-000025","attr":"紧急联系人姓氏","ans":true,"exp":"VAL-STECQ3PX","reply":"根据长期记忆证据,当出现问题时,应联系主管,其姓名代码为 VAL-ZS6KE6E2。可直接复述的关键短语:user;主管姓名;VAL-ZS6KE6E2。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":8.516015553315961,"active":24,"need":true},{"id":"zero-overlap-000187","attr":"手机尾号","ans":true,"exp":"VAL-PXRHJ49F","reply":"您随身那台设备的末端数字是 H9,该设备的完整型号为 VAL-ZPUPV9H9。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000005","attr":"项目代号","ans":true,"exp":"VAL-BLPA66RK","reply":"您的项目代号是 VAL-BLPA66RK。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.191348113781957,"active":24,"need":true}]},{"key":"e2e","label":"NM2.1 最终运行时","note":"48 用例 · 24 同形候选 · prior=1.00 blend=0.50","n":48,"items":[{"id":"zero-overlap-000259","attr":"出生城市","ans":true,"exp":"VAL-M9VSJAEX","reply":"您的出生城市是 VAL-M9VSJAEX。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.313681895152214,"active":24,"need":true},{"id":"zero-overlap-000219","attr":"咖啡口味","ans":true,"exp":"VAL-M3W7MRJR","reply":"关于您早上那杯咖啡的豆子偏好,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-Y4Z5H2FG。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000193","attr":"午餐偏好","ans":true,"exp":"VAL-HG84H5TD","reply":"您的午餐偏好是 user;午餐偏好;VAL-HG84H5TD。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.953831069469452,"active":24,"need":true},{"id":"zero-overlap-000080","attr":"入职年份","ans":true,"exp":"VAL-GFT4HRX2","reply":"关于您何时开始在这家公司上班,当前记忆中没有相关记录。已知信息:user;办公城市;VAL-TCNMN9HW。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.51429933919514,"active":24,"need":true},{"id":"zero-overlap-000285","attr":"起床时间","ans":true,"exp":"VAL-4K3RQQKF","reply":"我的闹钟设定时间是 VAL-4K3RQQKF。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.2757300050727958,"active":24,"need":true},{"id":"zero-overlap-000246","attr":"常住城市","ans":false,"exp":"VAL-6BWJYDE4","reply":"作为一个人工智能助手,我并没有“小时候”的概念,也无法知道您具体待过哪里。\n\n不过,如果您是想询问**您自己**小时候待的地方,或者想聊聊关于某个特定地点的童年回忆,我很乐意","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000204","attr":"阅读工具","ans":true,"exp":"VAL-ZRLSPU5V","reply":"我翻书时依赖的长期记忆中没有关于“翻书”习惯的记录,因此无法回答您依赖什么。","ok":false,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.989592991362775,"active":24,"need":true},{"id":"zero-overlap-000078","attr":"常住城市","ans":true,"exp":"VAL-TQBY65U8","reply":"您的常住城市是 VAL-TQBY65U8。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.4848385143280027,"active":24,"need":true},{"id":"zero-overlap-000104","attr":"午餐偏好","ans":true,"exp":"VAL-SRFC842D","reply":"您的午餐偏好是 user;午餐偏好;VAL-SRFC842D。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.1867529082298276,"active":24,"need":true},{"id":"zero-overlap-000281","attr":"咖啡口味","ans":true,"exp":"VAL-3GP4STZX","reply":"关于您早上饮用的咖啡偏好哪种豆子,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-XNN7T8EK。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000106","attr":"咖啡口味","ans":false,"exp":"VAL-837DYYWV","reply":"作为一个人工智能助手,我**无法直接访问您的设备、查看您的闹钟设置或获取您的实时时间信息**。\n\n要找到您的闹钟设定时间,您可以尝试以下方法:\n\n1.  **查看手机/电脑屏幕","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000091","attr":"运动习惯","ans":true,"exp":"VAL-92MQHEDL","reply":"您的运动习惯是 VAL-92MQHEDL。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.8434395718574524,"active":24,"need":true},{"id":"zero-overlap-000088","attr":"设备型号","ans":true,"exp":"VAL-JY6S7EYF","reply":"关于您使用的机器档次,当前长期记忆中未包含此信息,无法回答。已知信息仅涉及:user;常用编辑器;VAL-3KPKRM4J。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.516791512356487,"active":24,"need":true},{"id":"zero-overlap-000244","attr":"午餐偏好","ans":false,"exp":"VAL-2VLHC26V","reply":"您好!作为一个人工智能助手,我并没有直接服务于特定某一家公司或机构。我的设计目标是**为所有用户**提供通用、安全、有帮助的信息和服务。\n\n不过,如果您是在某个具体平台(如企业微信","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000282","attr":"咖啡口味","ans":true,"exp":"VAL-Q2AQQMUP","reply":"关于您早上那杯咖啡的豆子偏好,当前记忆中没有相关记录。已知信息仅包含:user;午餐偏好;VAL-5ATEHVSM。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000056","attr":"主管姓名","ans":true,"exp":"VAL-VDL8H7Y9","reply":"您的主管姓名是 VAL-VDL8H7Y9。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.049178622612938,"active":24,"need":true},{"id":"zero-overlap-000184","attr":"项目代号","ans":true,"exp":"VAL-3H5TQEC4","reply":"您的项目代号是 VAL-3H5TQEC4。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.4711107313126073,"active":24,"need":true},{"id":"zero-overlap-000198","attr":"手机尾号","ans":true,"exp":"VAL-7T3XVMVH","reply":"您随身那台设备的末端数字是 4N,该设备的完整型号为 VAL-9MWPDS4N。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000258","attr":"阅读工具","ans":true,"exp":"VAL-SHRY3GNG","reply":"我翻书时依赖的实体是 user,属性是阅读工具,已确认值为 VAL-SHRY3GNG。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.9794869642364628,"active":24,"need":true},{"id":"zero-overlap-000030","attr":"课程名称","ans":true,"exp":"VAL-BQY3K5EV","reply":"您的课程名称是 VAL-BQY3K5EV。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.1944098997116086,"active":24,"need":true},{"id":"zero-overlap-000196","attr":"手机尾号","ans":true,"exp":"VAL-ZHN2NMZV","reply":"您随身那台设备的末端数字是 W9,该设备的完整型号为 VAL-5VBJ22W9。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000268","attr":"宿舍楼号","ans":true,"exp":"VAL-QVYMWTLK","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-QVYMWTLK;常住城市;VAL-NW2E2ZWE;","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.7256882035478824,"active":24,"need":true},{"id":"zero-overlap-000006","attr":"档案标识","ans":true,"exp":"VAL-8EFN3KGR","reply":"您的档案标识是 VAL-8EFN3KGR。user;档案标识;VAL-8EFN3KGR","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.7116483740847537,"active":24,"need":true},{"id":"zero-overlap-000047","attr":"咖啡口味","ans":true,"exp":"VAL-PW46K9K9","reply":"关于您早上饮用的咖啡偏好哪种豆子,当前记忆中没有相关记录。已知信息仅包含您的午餐偏好是 VAL-BFW4LVTV。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.515033109300843,"active":24,"need":true},{"id":"zero-overlap-000173","attr":"客户名称","ans":false,"exp":"VAL-P84HTKZR","reply":"作为一个人工智能助手,我**无法知道您随身设备的型号或数字**,因为我没有访问您个人设备信息的权限,也无法看到您正在使用的具体设备。\n\n如果您指的是:\n- **手机/电脑型号**:","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000000","attr":"出生城市","ans":true,"exp":"VAL-PHB8FNF9","reply":"您的出生城市是 VAL-PHB8FNF9。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.455323672186914,"active":24,"need":true},{"id":"zero-overlap-000266","attr":"出生城市","ans":true,"exp":"VAL-K7FT5XFB","reply":"您的出生城市是 VAL-K7FT5XFB。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.1730912274276046,"active":24,"need":true},{"id":"zero-overlap-000051","attr":"项目代号","ans":false,"exp":"VAL-KG4RZYH5","reply":"作为一个人工智能助手,我并没有实体,也没有“待得最久”的物理空间或地理位置。我的“存在”体现在每一次与您的对话中。\n\n不过,如果您是在寻找**您自己**待得最久的地方","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000061","attr":"常住城市","ans":true,"exp":"VAL-3SUNPNRD","reply":"您的常住城市是 VAL-3SUNPNRD,办公城市是 VAL-XYRY8BUQ,出生城市是 VAL-GE828D22,通勤方式是 VAL-2CXTE3TB,入职年份","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.6610547590255735,"active":24,"need":true},{"id":"zero-overlap-000214","attr":"客户名称","ans":true,"exp":"VAL-MV382EEF","reply":"您的服务对象是 VAL-MV382EEF。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.2588214325904845,"active":24,"need":true},{"id":"zero-overlap-000092","attr":"宿舍楼号","ans":true,"exp":"VAL-PLZZ659B","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-PLZZ659B。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.809071942542481,"active":24,"need":true},{"id":"zero-overlap-000140","attr":"宿舍楼号","ans":true,"exp":"VAL-RMDSB297","reply":"关于您睡觉的地儿是第几排,当前长期记忆中未包含此信息。已知信息如下:user;宿舍楼号;VAL-RMDSB297。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.8702512824281925,"active":24,"need":true},{"id":"zero-overlap-000164","attr":"客户名称","ans":true,"exp":"VAL-BB55YHW5","reply":"您的客户名称是 VAL-BB55YHW5。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.2650055932998656,"active":24,"need":true},{"id":"zero-overlap-000213","attr":"通勤方式","ans":true,"exp":"VAL-M58DDQX2","reply":"您的通勤方式是 VAL-M58DDQX2。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.0363681795533712,"active":24,"need":true},{"id":"zero-overlap-000122","attr":"通勤方式","ans":true,"exp":"VAL-VULSNGJ3","reply":"您的通勤方式是 VAL-VULSNGJ3。user;通勤方式;VAL-VULSNGJ3","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.0330638098057325,"active":24,"need":true},{"id":"zero-overlap-000168","attr":"常住城市","ans":true,"exp":"VAL-8TQ8737C","reply":"您的常住城市是 VAL-8TQ8737C。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.4606248426437376,"active":24,"need":true},{"id":"zero-overlap-000175","attr":"客户名称","ans":true,"exp":"VAL-QJNRRE8X","reply":"您的客户名称是 VAL-QJNRRE8X。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.2628431367874144,"active":24,"need":true},{"id":"zero-overlap-000144","attr":"入职年份","ans":false,"exp":"VAL-GLHQMRN6","reply":"作为一个人工智能助手,我**无法知道您睡觉的具体位置**,因为我无法感知现实世界中的物理环境,也无法获取您的个人生活信息。\n\n如果您是在询问某个特定场所(如学校教室、电影院、酒店或","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000002","attr":"午餐偏好","ans":true,"exp":"VAL-HEE2QMH2","reply":"您的午餐偏好是 user;午餐偏好;VAL-HEE2QMH2。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":0.9400680589675904,"active":24,"need":true},{"id":"zero-overlap-000171","attr":"常住城市","ans":true,"exp":"VAL-KZA246K9","reply":"您的常住城市是 VAL-KZA246K9,办公城市是 VAL-Z8QBE5HQ,出生城市是 VAL-N8SWTE6N,宿舍楼号是 VAL-XZY4WPYH,通勤","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":1.49963095664978,"active":24,"need":true},{"id":"zero-overlap-000154","attr":"入职年份","ans":true,"exp":"VAL-7Z9CBUX8","reply":"关于您何时开始在这家公司上班,当前记忆中没有相关记录。已知信息:user;办公城市;VAL-BP7253WL。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.51429933919514,"active":24,"need":true},{"id":"zero-overlap-000234","attr":"工位楼层","ans":true,"exp":"VAL-87M7HVC7","reply":"您的工位楼层是 VAL-87M7HVC7。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":3.1934859675651266,"active":24,"need":true},{"id":"zero-overlap-000083","attr":"办公城市","ans":true,"exp":"VAL-YBVYV97X","reply":"您的办公城市是 VAL-YBVYV97X。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.3717270611639645,"active":24,"need":true},{"id":"zero-overlap-000079","attr":"出生城市","ans":true,"exp":"VAL-7R8WVAFG","reply":"您的出生城市是 VAL-7R8WVAFG。","ok":true,"wrong":false,"leak":false,"stop":"evidence_found","sel":8,"score":2.4271069115554122,"active":24,"need":true},{"id":"zero-overlap-000180","attr":"客户名称","ans":false,"exp":"VAL-3F87H6UT","reply":"作为一个人工智能助手,我**无法知道您随身设备的型号或数字**,因为我没有访问您个人设备信息的权限,也无法看到您正在使用的具体设备。\n\n如果您指的是:\n- **手机/电脑型号**:","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false},{"id":"zero-overlap-000025","attr":"紧急联系人姓氏","ans":true,"exp":"VAL-STECQ3PX","reply":"您的默认语言是 VAL-PRVTUCP7,课程名称是 VAL-93ME7BJ5,起床时间是 VAL-PJC3D3W9,出生城市是 VAL-KYBRBBKV,咖啡口味是","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":8,"score":8.516015553315961,"active":24,"need":true},{"id":"zero-overlap-000187","attr":"手机尾号","ans":true,"exp":"VAL-PXRHJ49F","reply":"您随身那台设备的末端数字是 H9,该设备的完整型号为 VAL-ZPUPV9H9。","ok":false,"wrong":true,"leak":false,"stop":"evidence_found","sel":1,"score":8.513663491707547,"active":24,"need":true},{"id":"zero-overlap-000045","attr":"办公城市","ans":false,"exp":"VAL-CG4X6ZWD","reply":"作为一个人工智能助手,我并没有实体,也没有“待得最久”的物理空间或地理位置。我的“存在”体现在每一次与您的对话中。\n\n不过,如果您是在寻找**您自己**待得最久的地方","ok":false,"wrong":false,"leak":false,"stop":"attribute_not_covered","sel":0,"score":0.0,"active":23,"need":false}]}];
diff --git a/assets/js/nm21.js b/assets/js/nm21.js
new file mode 100644
index 0000000..e4fc5d6
--- /dev/null
+++ b/assets/js/nm21.js
@@ -0,0 +1,50 @@
+/* ==================================================================
+   Natural Memory v2 — nm21.js
+   「v2.1 之后做了什么」页面上需要交互的那部分数据。
+
+   只放剂量曲线:它们是这一轮唯一需要「拖动/悬停看数值」的东西,
+   其余表格都直接写在正文里并标注了来源文件,便于逐条核对。
+
+   ⚠️ 数值逐字取自工程内报告,未做改写或换算:
+     V2_dpskw\NM2_1_FINAL.md              第 3 节(两处改动 + 剂量曲线)
+     V2_dpskw\record_blend_dose_response.md / prior_scale_dose_response.md  原始产物
+   ================================================================== */
+window.NM21 = {
+  /* 改动一:记录排序混合权重(独立进程测量,48 用例,先验保持 1.0)
+     此前的同进程测量因顺序污染已作废,这里用的是独立进程那一版。 */
+  blend: {
+    title: "记录排序混合权重 · 剂量曲线",
+    axis: "memory_record_router_blend",
+    xLabel: "blend 值(0 = 仅 retriever,1 = 仅路由器)",
+    pick: 0.5,
+    metrics: [
+      { key: "answer", label: "可回答正确率", unit: "%", higherBetter: true,
+        v: [65.00, 70.00, 62.50] },
+      { key: "wrong", label: "答成别的属性", unit: "%", higherBetter: false,
+        v: [35.00, 27.50, 37.50] },
+      { key: "leak", label: "未知泄漏率", unit: "%", higherBetter: false,
+        v: [75.00, 75.00, 87.50] }
+    ],
+    x: [0, 0.5, 1],
+    note: "两个打分器互补:0.50 是可回答正确率最高、答成别的属性最低的点。" +
+          "blend=1.00 反而全面变差,说明这不是单调关系 —— 单看某一端会得出错的结论。"
+  },
+
+  /* 改动二:把加法先验暴露成参数后测出来「不该动」。
+     这是一个有价值的负面结论:它否定了「先验压过学习打分」这条假设。 */
+  prior: {
+    title: "记录打分先验权重 · 剂量曲线",
+    axis: "加法先验的缩放系数",
+    xLabel: "先验缩放(原值为 1.00)",
+    pick: 1.0,
+    metrics: [
+      { key: "answer", label: "可回答正确率", unit: "%", higherBetter: true,
+        v: [65.00, 47.50, 42.50, 40.00] }
+    ],
+    x: [1, 0.5, 0.25, 0],
+    note: "调小先验只会更差(65.00 → 47.50 → 42.50 → 40.00),所以保持 1.00 不动。" +
+          "这条曲线的作用是排除一个假设,而不是带来增益。"
+  },
+
+  srcLabel: "V2_dpskw\\NM2_1_FINAL.md §3 · 原始产物 prior1_blend_{0.0,0.5,1.0}_proc.json 与 prior_scale_dose_response.json"
+};
diff --git a/assets/js/restart.js b/assets/js/restart.js
new file mode 100644
index 0000000..c0fe87e
--- /dev/null
+++ b/assets/js/restart.js
@@ -0,0 +1,115 @@
+/* ==================================================================
+   Natural Memory v2 — restart.js
+   「重启实验」互动装置的数据层。
+
+   ⚠️ 本文件的每一个数值都逐字取自工程内的原始 JSON,未做任何改写、
+      换算或估算。两个来源文件:
+        A. dynamic_memory_lab\natural_memory_v2_restart_test_v7.json        (修复前)
+        B. dynamic_memory_lab\natural_memory_v2_restart_test_v7_final.json  (修复后)
+      两次运行使用同一条事实、同一个问题、同一份协议,唯一差别是
+      「写入是否真的落库」(write_changed: false → true)。
+
+   修改此处等于修改一个「证据」页面,请先核对原始 JSON 再改。
+   ================================================================== */
+window.NM_RESTART = {
+  eyebrow: "Evidence · 重启实验",
+  h2: "把历史扔掉,它还记得吗?",
+  lede: "下面不是示意动画,是工程里两次真实运行的逐字回放。同一条事实、同一个问题、同一份协议;" +
+        "两次之间唯一的差别,是写入有没有真的落库。",
+
+  fact: "我正在开发一个长期项目,项目内部代号是NM-V2-RESTART,使用中文。",
+  query: "我正在开发的长期项目内部代号是什么?只回答代号。",
+  expected: "NM-V2-RESTART",
+  historyPassedToRestart: false,
+
+  /* 读取路径的四个节拍(两次运行共用同一套节拍名) */
+  beats: [
+    { i: "01", t: "写入",  s: "automatic_write: true —— 事实作为一条带地址的记录进入记忆库" },
+    { i: "02", t: "重启",  s: "history_passed_to_restart: false —— 重启不携带任何对话历史" },
+    { i: "03", t: "召回",  s: "路由器在当前问题与有界候选之间定位证据记录" },
+    { i: "04", t: "作答",  s: "命中的短证据作为模型内部前缀注入 Qwen,再由普通生成产出答案" }
+  ],
+
+  variants: [
+    {
+      key: "before",
+      label: "修复前",
+      tone: "bad",
+      file: "natural_memory_v2_restart_test_v7.json",
+      /* --- 写入 --- */
+      writeChanged: false,
+      /* --- 重启后模型看到的内部状态 --- */
+      prefixLength: 0,
+      prefixUsed: false,
+      /* --- 路由决策 --- */
+      needMemory: false,
+      pageIds: [],
+      recordIds: [],
+      pageScores: [],
+      recordScores: [],
+      hopCount: 1,
+      confidence: 0.5,
+      stopReason: "below_read_threshold",
+      /* --- 生成结果 --- */
+      response: "Project Phoenix",
+      recalled: false,
+      correct: false,
+      /* --- 存储快照(before_save) --- */
+      store: {
+        records: 0, active_records: 0, pages: 0, resident_records: 0,
+        coarse_index_buckets: 0, gpu_cache_records: 0, gpu_cache_tokens: 0,
+        gpu_cache_bytes: 0
+      }
+    },
+    {
+      key: "after",
+      label: "修复后",
+      tone: "ok",
+      file: "natural_memory_v2_restart_test_v7_final.json",
+      writeChanged: true,
+      prefixLength: 45,
+      prefixUsed: true,
+      needMemory: true,
+      pageIds: ["page_00000001"],
+      recordIds: ["mem_3f5823eb77810b49"],
+      pageScores: [0.48235471503517796],
+      recordScores: [0.5923714575763567],
+      hopCount: 1,
+      confidence: 0.7961857287881784,
+      stopReason: "evidence_found",
+      response: "NM-V2-RESTART",
+      recalled: true,
+      correct: true,
+      store: {
+        records: 1, active_records: 1, pages: 1, resident_records: 1,
+        coarse_index_buckets: 1, gpu_cache_records: 1, gpu_cache_tokens: 45,
+        gpu_cache_bytes: 1429
+      }
+    }
+  ],
+
+  /* 常量:出自同一份 JSON 的存储与预算字段(两次运行相同) */
+  limits: {
+    maxPages: 32768,
+    pageCapacity: 32,
+    capacityRecords: 1048576,
+    kvBudgetTokens: 32768,
+    kvTriggerTokens: 29491,
+    gpuCacheLimitRecords: 256,
+    gpuCacheLimitTokens: 131072,
+    gpuCacheReserveMB: 2048,
+    gpuCacheDevice: "cuda:0"
+  },
+
+  /* 同一个实验的另外两次运行,用于说明「不是每次都成功」 */
+  otherRuns: [
+    { file: "natural_memory_v2_restart_test.json", date: "2026-09-04 23:16",
+      writeChanged: true, prefixLength: 36, recalled: true, quarantined: 2 },
+    { file: "restart_memory_natural_v7.json", date: "2026-09-04 09:15",
+      note: "另一条事实(工作地点代号 R7)。复位 token 后 memory_norm 归零,模型随即答不出——" +
+            "这条记录的是「清掉记忆会怎样」的对照组。" }
+  ],
+
+  note: "两次运行的判定字段(router_recalled_after_restart / generated_contains_expected)分别为 " +
+        "false / false 与 true / true。我们没有为发布页润色过任何一条模型输出,包括修复前那次编造出来的答案。"
+};
diff --git a/assets/js/search.js b/assets/js/search.js
new file mode 100644
index 0000000..38f6283
--- /dev/null
+++ b/assets/js/search.js
@@ -0,0 +1,2 @@
+/* 由 build.py 生成:顶栏命令面板的全站检索索引。请勿手改。 */
+window.NM_SEARCH = [{"s":"index","p":"./","g":"概览","n":"00","pg":"总览","h":"","t":"接在本地 Qwen3.5-4B 上的模型内记忆层。模型重启后不携带历史聊天记录,仍能访问已保存的个人与项目事实,且不把全部历史转换成 GPU 上的长 KV Cache。","d":1},{"s":"index","p":"./","g":"概览","n":"00","pg":"总览","h":"总览","t":"Natural Memory · NM2.1 1,048,576 槽位 · 738 条活跃记录 · 730 个索引桶 模型重启之后, 它仍然记得。 1.22 % → 82.52 % 同一 264 题题集的可回答正确率:无记忆基线 → 批次 B。 批次 A 为 62.60%,两批配置不同、不可合并。 wpyw.site · wpy@wpy.email · 本地 Qwen3.5-4B · 4-bit NF4 01 / 08 地址空间 1,0"},{"s":"index","p":"./","g":"概览","n":"00","pg":"总览","h":"把历史扔掉,它还记得吗?","t":"下面不是示意动画,是工程里两次真实运行的逐字回放。同一条事实、同一个问题、同一份协议; 两次之间唯一的差别,是写入有没有真的落库。 修复前 修复后 重放 写入的事实 重启之后问 记忆库记录 0 重启时传入的历史 false 注入模型内部的前缀 0 tok 模型输出 · 逐字 图 1 · 读取路径 一次问答里记忆层做了什么。用户问题只与少量页面和记录交互, 全程没有对全量记忆做注意力 ; 命中的短证据作为模型内部前缀注入 Qwen。图为示"},{"s":"index","p":"./","g":"概览","n":"00","pg":"总览","h":"这是什么东西","t":"它接在本地 Qwen3.5-4B 上,是一个 模型内的记忆层 :把适合长期复用的个人事实、项目事实和短对话片段 写进 带地址的记忆记录 ;当前问题只读取少量相关记录,再把这些记录作为 模型内部前缀 交给 Qwen 生成答案。 它要解决的具体问题是: 模型重启后不携带历史聊天记录,仍能访问已经保存的个人或项目事实, 同时不把全部历史转换成 GPU 上的长 KV Cache。 当前状态:研究原型到工程验证之间。同一 264 题题集上,可回答"},{"s":"index","p":"./","g":"概览","n":"00","pg":"总览","h":"发布之后又做了什么","t":"上面所有数字都来自 v2。之后的 NM2.1 换掉了记忆路由器、修掉写入路径上一个会把事实互相销毁的缺陷, 并把「问库里没有的属性时照样编答案」这个 75% 的失败修到 0%。 未知拒答率 · 24 条同形候选 100.00 % 从 0.00% 起 —— 修好之前,模型 75% 的情况下照样编一个答案 这是整个项目最该被引用的一个数字。它回答的不是「能不能检索到」,而是 「不知道的时候会不会不懂装懂」 。 未知拒答率 · 同形候选 0."},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"","t":"为什么不是简单加长上下文:哪些东西该留在 Qwen 的热 KV 里,哪些该写入带地址的记忆记录,各自的边界在哪里。","d":1},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"记忆与 KV 的分工","t":"这一节回答一个问题: 既然模型已经有上下文窗口,为什么还要单独做一层记忆? 答案不是「窗口不够长」,而是 把长期事实塞进上下文,会以线性成本换来极低的复用率 。 下面把「哪些东西留在 Qwen 的热 KV 里、哪些写进记忆记录」这条分界线逐层讲清楚。"},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"先看没有这层记忆时会发生什么","t":"同一套题、同一协议下,原版 Qwen3.5-4B 在 246 道 可回答 题上只答对 3 道(1.22%)。 它的典型回答不是答错,而是明确表示拿不到信息: 作为一个人工智能模型,我无法访问您的个人历史对话记录、系统配置或您之前设定的具体规则……每次对话对我来说都是全新的开始。 原版 Qwen3.5-4B · 评测样本 user-009 原始回答 这句话在事实上完全正确——模型确实没有这些信息。问题出在:这些信息 本来就应该被保存下来 "},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"四层分工:谁存什么、为什么","t":"这是整份设计里最核心的一张表。判断标准只有一条: 这份信息是「这一轮才有意义」还是「跨轮次可复用」 。 层级 存放什么 由谁负责 为什么放在这一层 最近对话 这一轮前后的原话与顺序 Qwen 热 KV 指代、省略、语气、\"你刚才说的那句\"都依赖严格的先后顺序。这类信息复用价值低、时效性强,放 KV 最自然。 长期个人 / 项目事实 事实、版本、来源、置信度、短文本证据 Natural Memory 记录 跨会话反复被问到,且需要保留\"哪"},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"分工落到数字上是什么样","t":"下面这组对照最能说明「不是把历史灌进去,而是把命中的少量证据灌进去」。两组数字分别来自批次 A 与批次 B(同一题集、同一协议)。 无记忆时的平均输入 30.75 token 批次 A 基线:只有问题本身,没有历史 有记忆时的平均输入 441.19 token 批次 B:命中的证据前缀(批次 A 为 641.42) 前缀命中率 96.97 % 批次 B 实际注入证据的题目占比(批次 A 为 90.91%) GPU cache 实际用量 "},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"这条分工线换来了什么、付出了什么","t":"换来的 可回答正确率 1.22% → 82.52% 目标记忆召回 0% → 92.68% 符号定位(192 题) 0/192 → 181/192 跨会话问题 1/6 → 5/6 付出的 平均输入前缀 30.75 → 441.19 token 平均总延迟(批次 B) 2854.9 → 2878.3 ms 平均解码速度(批次 B) 22.34 → 20.68 tok/s reserved 显存峰值 3.234 → 5.180 GiB"},{"s":"division","p":"./division","g":"原理","n":"01","pg":"记忆与 KV 的分工","h":"这条线不能推到哪里","t":"✕ Natural Memory 已经等价于百万 token 的完整 KV UNSUPPORTED ✕ 长上下文限制已被解除(实测 8K 可跑,16K / 32K OOM) UNSUPPORTED ✕ 设计容量 1,048,576 条记录等于已经装入并验证了这么多 UNSUPPORTED 第三条尤其要注意: 1,048,576 是地址空间与分页结构的容量 , 本次两个批次实际装载的是 723 / 738 条 active records"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"","t":"从用户问题到证据前缀注入的八个环节,记录包含哪些字段,以及 active / superseded / quarantined / retracted 四个状态如何演化。","d":1},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"一次读取与一次写入","t":"这一节把「 一次读取 」和「 一次写入 」拆开讲透:读取路径有哪八个环节、每一步的输入输出是什么、 一条记录里到底存了什么、四个状态如何演化,以及我们为这些决定付出了什么代价。"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"一、读取路径:从提问到证据注入","t":"点左侧任一环节查看它在做什么,也可以依次点一遍——注意整条链路上 没有一步是「和全部记忆做注意力」 , 这是显存可控的根本原因。 STAGE 01 / 08"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"一次读取与一次写入","t":"读取上限 2 records 候选页上限 4 pages 页容量 32 / page 全量注意力 否"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"每一步的输入、输出与约束","t":"括号里的数字是实测值,不是设计目标。 环节 输入 → 输出 约束或实测 01 用户问题 普通自然语言 不要求任何显式指令,不需要输入 /remember 之类的标记。 02 Qwen hidden state 问题 → 紧凑查询地址 地址由模型隐状态生成,不依赖外部 embedding 服务;语义地址编码用单条 batch 执行,避免编码阶段出现显存瞬时峰值。 03 LSH / 地址粗索引 查询地址 → 候选页 精确桶 + Hammin"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"二、一条记录里到底存了什么","t":"记录不是一段文本,而是一条带地址、带版本关系、带状态的结构化数据。下面左侧是全部字段, 右侧是 一条真实记录 (逐字取自评测结果里的命中记录)。"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"三、状态机:旧的答案不会凭空消失","t":"同一实体和属性出现新值时, 旧值被标记为 superseded,而不是被覆盖或删除 。 这是「时间冲突」类问题能答对的前提:既要给出当前有效值,也要能说清旧值去哪了。 写入路径的教训:检索救不回不存在的记录 早期版本的退役授权完全依赖一个学习打分(阈值 ≥0.95), 没有任何结构校验 。 结果是新写入把已有记录误判为旧值并退役:一次 20 条记录的测试里 active 只剩 12 条、retract 高达 16 次、目标记录只存活 8"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"四、四个设计决定与它们的代价","t":"决定 1 · 两段式读取 粗索引筛页 → 页内重排 ,而不是对全量记录做注意力。 好处:单次读取的候选规模被压到固定上限(≤4 页 / ≤2 条),成本与记忆总量解耦。 代价:两段都可能出错,且错误会叠加——粗索引错了就再没有机会。 决定 2 · 用地址索引而非向量库 语义地址 + LSH 桶放在模型包内, 不引入外部检索服务 。 好处:部署时没有额外进程与网络依赖,读取耗时稳定(批次 B 26.35 ms)。 代价:召回质量受地址质量"},{"s":"mechanism","p":"./mechanism","g":"原理","n":"02","pg":"一次读取与一次写入","h":"五、这套机制仍然做不到的事","t":"✕ 任何自然语言表达都能稳定召回正确记忆 UNSUPPORTED ✕ 记忆写入永远不会出错 UNSUPPORTED ✕ 检索召回率等于回答正确率(批次 B 召回 92.68% / 回答 82.52%,差 10.16pp) UNSUPPORTED"},{"s":"results","p":"./results","g":"证据","n":"03","pg":"测量结果","h":"","t":"同一 264 题题集的两个独立批次、结构化工程基准与记忆路由器工程线的全部实测数字,均标注出处与口径。","d":1},{"s":"results","p":"./results","g":"证据","n":"03","pg":"测量结果","h":"测量结果","t":"三个面板对应三组不同的题集与口径, 不能合并成一个总分 。 面板一是同一题集先后跑的两个独立批次(配置不同、分数不可合并,只能并列读); 面板二是结构化工程基准;面板三是记忆路由器工程线。每组数据都标注了出处、日期与口径。 读这些数字时请留意两件事: 所有对照都使用同一 tokenizer、同一 greedy 解码、同一 64 token 输出上限 ; 而 检索召回率与回答正确率是两个必须分开报告的量 ——这一点在面板一里体现得最清楚。"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"","t":"三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。","d":1},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"NM2.1 · 之后做了什么","t":"发布页上的数字来自 v2。这一页讲**之后又做了什么**:三处改动各自凭什么、一个把端到端正确率 锁死在 50% 的写入缺陷怎么定位的、以及一个 75% 泄漏的失败怎么修到 0%——又怎么在接上运行时之后失败两次。 所有数字都来自磁盘上的报告文件,每张表都标了出处;**没有一条是估算或外推**。 先读这一句 这一页里**离线实验最好的数字(未知拒答 100.00%)不等于运行时可用**。它接上运行时之后 连续失败两次,最终被回退。把失败"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"净变化一览","t":"出处 · V2_dpskw\\NM2_VS_NM2_1.md (该文件自述:所有数字均从磁盘上的评分卡 / JSON 读取,不是手工抄录) 轴 v2(原版) NM2.1 净变化 路由器 Top-1 41.12% 94.14% +53.02 pp 路由器 Recall@3 46.73% 96.48% +49.75 pp 未知拒答率 0.00% 100.00% +100.00 pp 未知问题泄漏(同形候选) 75.00% 0.00% −75."},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"一 · 路由器工件本身的检索与排序","t":"出处 · router_scorecard_final.json (判定见 router_verdict_final.json )· 冻结 v6 评测集 21,920 条 / 10 类别 指标 v2 · V2-128 deployed(v3) NM2.1 · REPLAY-128 v7 final Top-1 正确率 41.12% 94.14% Recall@1 37.03% 88.58% Recall@3 46.73% 96.48% "},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"二 · 拒答与仲裁策略轴","t":"出处 · router_scorecard_final.json (门槛 0.50)+ threshold_sweep_check.json (0.30–0.80 全门槛扫描) 指标 v2 NM2.1 need F1 89.58% 100.00% need 召回 99.82% 100.00% need 精确率 81.24% 100.00% 未知拒答率 0.00% 100.00% 已知问题被误拒率 0.18% 0.00% 未知问题被误读率"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"三 · 端到端整体记忆能力(同一份运行时,只有模型包不同)","t":"出处 · nm2_battery_comparison_final.json · A 110 用例 / B 16 / C 48 / D 重启持久化 指标 原版 NM2 NM2.1(仅换路由器) NM2.1 最终 A 总体正确率 89.09% 88.18% 88.18% A 可回答正确率 100.00% 100.00% 100.00% A 未知拒答率 60.00% 56.67% 56.67% A 已知问题被误拒率 0.00% 0.00% "},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"四 · 零字面重叠改写的泛化","t":"出处 · replay_check_zov.json (路由器级)与上表 B / C 段(端到端)· 数据集 300 条、24 个 未见过 的问法 指标 v2 权重 NM2.1 权重 路由器 Top-1(250 条可回答) 18.40% 59.60% 路由器 Recall@3 36.00% 83.20% 路由器 MRR 35.07% 73.06% 端到端改写正确率(B 段) 43.75% 68.75% 端到端未知泄漏(C 段) 75.0"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"五 · 代价:几乎没有","t":"出处 · router_scorecard_final.json 、 router_latency_bench_prod.json (7 轮交错取中位数) 指标 v2 NM2.1 说明 参数量 2,037,774 2,037,774 未增加 每条记录地址字节 512 512 存储几何未变 单查询延迟中位数(GPU) 0.9549 ms 0.9169 ms 略快 批量 QPS(batch=64) 66,037 69,378 +5.1% 批"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"六 · 工程鲁棒性","t":"项目 v2 NM2.1 一次写 20 条不同属性事实后存活 12 / 20(8 条查询前被误删) 20 / 20 端到端(写入修复前后,16 用例) 37.50% 68.75% 替换兼容性 基线 DROP-IN OK(16 / 16 键) 单元测试 — 52 项通过 未知问题泄漏(同形候选) 75.00% 0.00%"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"七 · 三处改动,每处都有测量依据","t":""},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"改动 1 · 记录排序混合权重取 0.5","t":"memory_record_router_blend 控制「打包的词面重排器」与「学习出的路由器」各占多少。 调这个之前先测了剂量曲线 —— 而且**此前的同进程测量因顺序污染已作废**,下面这版是独立进程测的。"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"改动 2 · 把加法先验参数化,然后测出「不该动」","t":"假设是「加法先验压过了学习打分」。把它暴露成可调再测,结果**调小只会更差**: 这是一个 有价值的负面结论 :它否定了原来的假设。项目里这类结论和增益一样被保留下来, 因为它们决定了后面不再往哪个方向投入。"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"改动 3 · 覆盖门 —— 以及为了让它真正生效必须一起修的两处","t":"覆盖门本身很简单:先判断「这个提问在问哪个属性」,再查库里有没有这个属性。但单独加门是不生效的, 有两处**必须同时修**,否则门会被静默绕过: 01"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"_build_text_prefix 短路","t":"门拒绝之后如果继续往下回落,会走**旧版 16 槽注入路径**,把记忆又塞回去。 实测:不修这一处,泄漏只从 75.00% 降到 62.50%。 02"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"闭包要动态解析 bank","t":"reset_memory() 每次都会新建 memory_os_v2 ;闭包捕获了旧引用的话, 门会永远看到一个空 bank,于是**静默旁路** —— 实测诊断输出 applicable:0 / bypassed:1 。 03"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"门必须自门控","t":"只有当库的属性集合填充了头部词表的 ≥ 90% 时才让门生效。 这条阈值是必需的:放宽成「子集即可」会让门在通用套件上误判, 把 A 段可回答正确率从 100.00% 压到 92.50%、已知误拒升到 2.50%;收紧到 90% 后 A 段完全恢复, C 段泄漏仍为 0.00%。"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"八 · 写入路径:为什么 20 条事实会互相销毁","t":"出处 · V2_dpskw\\WRITE_PATH_FIX.md (根因由栈追踪确证,非推断) 症状 一次写入 20 条 不同属性 的事实后,库里有 20 条记录但只剩 12 条 active , 8 条在查询前就被 retract。16 个关键零重叠用例里有 8 个的目标记录已经不存在 —— 因此端到端正确率存在 50% 的硬上限 。**任何排序器都救不回一条已经被删掉的记录。** 16 次 retract 的调用栈完全一致: call"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"修复(两处,都是结构性的)","t":"01"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"confirmed_update 的两条打分类分支改为结构优先","t":"仅当候选文本的 entity::attribute 已经在库的 active_by_conflict 账本里存在(即确实是同一条事实的更新)才允许由打分授权; exact_slots 这条结构安全的分支保持不变,以保留「重复写入幂等」的既有语义。 02"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"retract 循环加冲突键守卫","t":"if record.conflict_key() != candidate_conflict_key: continue —— 循环只能退掉 描述同一属性 的记录,不再因「共享热槽」或「词面重叠高」销毁无关事实。 语义不受损:真·同属性更新本来就已经由 PagedMemoryBankV2.write 通过 active_by_conflict 做版本化(旧版本保留为 superseded、 version+1 ), 修复只是移除了那条 "},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"四级验证","t":"级 检验 修复前 修复后 ① status_transition_summary {\"active->retracted\": 16} {} ① retraction_calls / status_counts 16 次 / {retracted: 8, active: 12} [] / {active: 20} ② 正对照:同属性改值 — 旧值 superseded + 新值 active,其余 19 属性不受影响,0 retract "},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"九 · 未知拒答:从 75% 泄漏到 100% 拒答","t":"出处 · V2_dpskw\\ABSTENTION_BREAKTHROUGH.md 这是整个项目最差的一个数字:问库里不存在的属性时,75% 的情况下模型照样编一个答案。 下面按「先排除一整类无效修法 → 再给出有效机制 → 最后如实记录接上运行时的两次失败」来写。"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"9.1 先排除掉一整类无效修法(有测量支撑)","t":"最直觉的修法是「检索分数不够高就拒答」。用 只在训练集拟合、评测集算 AUC 的口径, 测了四种打分器的分数几何(top1 / margin / top-k 均值 / 离散度 / 熵 / 近邻数,共 9 个特征): 打分器 最佳单特征 AUC 拟合头 AUC(评测集) cosine(冻结键余弦) 0.6257(margin) 0.6086 打包 text_retriever 0.6076(margin) 0.4752 50/50 混合 "},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"9.2 有效的机制:先判「问的是哪个属性」,再查库里有没有","t":"换一个问题问就成立了:不是「有没有候选匹配得好」,而是「这个提问在问哪个属性,这个属性在库里吗」。 封闭词表 值 可回答 episode 中「预测属性在候选集内」 100.00%(250 条) 未知 episode 中「预测属性在候选集内」 0.00%(50 条) 未知拒答率 100.00% 已知问题被误拒率 0.00% 混淆矩阵 tp=50 fn=0 fp=0 tn=250 开放集: 真实用户会问模型没见过的属性。只靠最大概率调阈值不"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"9.3 接上运行时:连续失败两次(如实记录)","t":"指标 NM2.1(无门) + 覆盖门(第一轮) + 门 + 短路(第二轮) A 可回答正确率 100.00% 6.25% 0.00% A 已知问题被误拒率 0.00% 7.50% 7.50% A 未知拒答率 56.67% 43.33% — C 未知泄漏率 75.00% 62.50% 0.00% C 可回答正确率 65.00% 65.00% 65.00% 第一轮: 门把几乎全部可回答问题都拒掉了 (100.00% → 6.25%),而它本"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"十 · 关键负面结论:路由器排序不影响端到端答案","t":"出处 · V2_dpskw\\ZERO_OVERLAP_FINDINGS.md §5 NM2.1 的路由器在零重叠集合上 Top-1 提升了 41.20 pp ,但在 eval_router_critical_e2e.py (16 个零重叠用例)上,REPLAY-128 与 V2-128-v6 的结果 逐位相同 (默认 Top-K:37.50% / 37.50%,答成别的属性 25.00% / 25.00%,平均选中记录 5.88 / 5"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"memory_os_v2.py:1671 :路由器分数被逐位置覆盖","t":"带 semantic_key 的记录,其 _score_candidates() 分数会被 self.record_scorer 覆盖。实测残差与 text_retriever 匹配 18 / 18, 与 memory_router_v2 匹配 0 / 18。 02"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"「部署目录没有 text_retriever.pt 所以走余弦兜底」是错的","t":"重排器被烘焙进合并包(safetensors 内有 6 个 dynamic_memory.text_retriever.* 张量,1,573,377 参数), _text_retriever_ready 实测为 True ,余弦兜底分支根本不执行。 03"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"路由器在此配置下唯一实际生效的杠杆是 need_memory 门","t":"部署权重下门开启率 68.75% (11 / 16),而三份随机初始化的路由器只有 37.5% (6 / 16)。判决性探针只随机化了排序通路、保留了门控头,所以「端到端零变化」 是由构造保证的,不是巧合。 结论分两层,不要混为一谈: ① 路由器记分卡上 22 / 22 的统治性和 +41.20 pp 的零重叠提升, 都是路由器工件 真实 的增益;② 但在这套运行时里,记录级顺序由打包的 text_retriever 决定, 路由器分数"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"十一 · 仍未解决(不粉饰)","t":"短板 现状 说明 跨域未知拒答 未解决 覆盖头是 24 类闭集,只在其词表被库填充 ≥ 90% 时生效;开放词表的属性匹配实测仅 49.20% Top-1 / AUC 0.6560 (零训练),不足 答成别的属性 27.50% 同形候选间排序仍不理想;已排除先验重加权(更差)与单纯替换打分器(更差) A 段未知拒答率 56.67% 未见改善 规模验证 未做 仅 24 属性 / 300 条评测;生产需上千属性、上万改写问法 V2 库记录真"},{"s":"nm21","p":"./nm21","g":"证据","n":"04","pg":"NM2.1 · 之后做了什么","h":"十二 · 复现","t":"PowerShell · 整体电池(A / B / C / D 四段) 复制 $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_v"},{"s":"examples","p":"./examples","g":"证据","n":"05","pg":"实测样例","h":"","t":"五条逐字取自评测原始结果的问答对照,包含两条我们判错的样例。","d":1},{"s":"examples","p":"./examples","g":"证据","n":"05","pg":"实测样例","h":"实测样例","t":""},{"s":"ledger","p":"./ledger","g":"证据","n":"06","pg":"真实评测台账","h":"","t":"全部 108 条真实评测 episode 的逐条台账:模型逐字回复、判定结果、停止原因、选中记录数与分数,可按来源与判定筛选。","d":1},{"s":"ledger","p":"./ledger","g":"证据","n":"06","pg":"真实评测台账","h":"真实评测台账","t":"这一页不是挑出来的样例,是 全部 108 条真实评测 episode 的逐条台账 。 每条都带模型的逐字回复、判定结果、停止原因、选中记录数与分数。 数据来自 runtime_score_bands.json 与 nm2_1_final_runtime_e2e.json , 由 tools/make_ledger.py 生成 assets/js/ledger.js , 字段逐字复制,未做改写、润色或换算 。模型输出里的错字、冗余和编造的"},{"s":"cost","p":"./cost","g":"工程","n":"07","pg":"代价账本","h":"","t":"记忆带来的延迟、解码速度、前缀长度与显存开销,以及显存快照的口径说明。","d":1},{"s":"cost","p":"./cost","g":"工程","n":"07","pg":"代价账本","h":"代价账本","t":"VRAM · 12 GiB CARD allocated 与 reserved 均为每次请求结束后的快照, 不是 CUDA high-water mark ; reserved 还包含 PyTorch 分配器保留的缓存。因此这里不能当作\"模型峰值显存\"来引用。"},{"s":"capacity","p":"./capacity","g":"工程","n":"08","pg":"容量与存储","h":"","t":"地址空间、分页结构、有界 GPU cache 与嵌入式 weight-shard 持久化的实际数字与取舍。","d":1},{"s":"capacity","p":"./capacity","g":"工程","n":"08","pg":"容量与存储","h":"容量与存储","t":"「一百万记录容量」是 地址空间和分页结构的容量 ,不是本次已经装入了一百万条语义记忆, 也不是一百万条记录已经通过端到端测试。这一节把容量、实际用量与存储方式分开列清楚。 PAGE MAP · 32768 PAGES × 32 SLOTS 高亮部分为本次运行实际使用的页面(批次 A:23 页 / 723 条 active records;批次 B:24 页 / 738 条)。其余为地址空间余量。 PERSISTENCE"},{"s":"capacity","p":"./capacity","g":"工程","n":"08","pg":"容量与存储","h":"容量与存储","t":"TRADE-OFF"},{"s":"capacity","p":"./capacity","g":"工程","n":"08","pg":"容量与存储","h":"取舍","t":""},{"s":"limits","p":"./limits","g":"工程","n":"09","pg":"我们不宣称的内容","h":"","t":"没有足够证据支持的八条说法、五项主要局限,以及来自工程文档的原文引用。","d":1},{"s":"limits","p":"./limits","g":"工程","n":"09","pg":"我们不宣称的内容","h":"五项主要局限","t":""},{"s":"limits","p":"./limits","g":"工程","n":"09","pg":"我们不宣称的内容","h":"来自工程文档的原文","t":""},{"s":"roadmap","p":"./roadmap","g":"工程","n":"10","pg":"已解决的与下一步","h":"","t":"在 12 GiB GPU 上已经跑通的闭环,以及按对正确率最有帮助的顺序排出的下一阶段任务。","d":1},{"s":"roadmap","p":"./roadmap","g":"工程","n":"10","pg":"已解决的与下一步","h":"已解决的与下一步","t":"左侧是在 12 GiB GPU 上已经跑通的闭环;右侧是按 对正确率最有帮助的顺序 排出的下一阶段工程任务。 这里刻意不写时间表——顺序比日期更可信。"},{"s":"roadmap","p":"./roadmap","g":"工程","n":"10","pg":"已解决的与下一步","h":"已经解决的问题","t":""},{"s":"roadmap","p":"./roadmap","g":"工程","n":"10","pg":"已解决的与下一步","h":"下一阶段的工程任务","t":""},{"s":"reproduce","p":"./reproduce","g":"工程","n":"11","pg":"复现实验","h":"","t":"正式脏数据迁移测试的完整命令、协议参数与产物路径。","d":1},{"s":"reproduce","p":"./reproduce","g":"工程","n":"11","pg":"复现实验","h":"复现实验","t":"正式脏数据迁移测试的完整命令与产物路径。协议参数与批次之间的差异见下方说明—— 复现同一批次才能得到同一组数字 。 PowerShell · 工作目录 复制命令 PROTOCOL 基座模型 量化 4-bit NF4 解码 greedy 最大新生成 64 token 进程显存上限 10 GiB 记忆读取上限 2 records 地址编码 batch size 1 两个批次的差别 上面的命令复现的是批次 A(对外技术总结采用的口径,可答题 "}];
diff --git a/assets/js/site.js b/assets/js/site.js
new file mode 100644
index 0000000..a65e972
--- /dev/null
+++ b/assets/js/site.js
@@ -0,0 +1,1188 @@
+/* ==================================================================
+   Natural Memory v2 — site.js(多页版)
+   共享外壳(顶栏读数、页脚、入场动效)+ 按 body[data-page] 分派的页面渲染。
+   所有内容来自 window.NM_DATA / window.NM_EXAMPLES,保证与数据层单一来源。
+   ================================================================== */
+(function () {
+  "use strict";
+  var D = window.NM_DATA;
+  if (!D) return;
+
+  /* ---------- 工具 ---------- */
+  var $ = function (s, r) { return (r || document).querySelector(s); };
+  var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
+  var el = function (tag, cls, html) {
+    var n = document.createElement(tag);
+    if (cls) n.className = cls;
+    if (html != null) n.innerHTML = html;
+    return n;
+  };
+  var esc = function (s) {
+    return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
+      return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
+    });
+  };
+  var fmt = function (n, d) { return Number(n).toFixed(d == null ? 2 : d); };
+  var num = function (s) { var p = String(s).split("/"); return { c: Number(p[0]), n: Number(p[1]) }; };
+  var arrow = '→';
+  var setText = function (sel, v) { var n = $(sel); if (n) n.textContent = v; };
+  var copyText = function (btn, text) {
+    var done = function () {
+      btn.textContent = "已复制";
+      btn.classList.add("is-done");
+      setTimeout(function () { btn.textContent = "复制"; btn.classList.remove("is-done"); }, 1900);
+    };
+    var fallback = function () {
+      var ta = document.createElement("textarea");
+      ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
+      document.body.appendChild(ta); ta.select();
+      try { document.execCommand("copy"); done(); } catch (e) { btn.textContent = "复制失败"; }
+      document.body.removeChild(ta);
+    };
+    if (navigator.clipboard && navigator.clipboard.writeText) {
+      navigator.clipboard.writeText(text).then(done, fallback);
+    } else { fallback(); }
+  };
+
+  /* ---------- 入场与数值动画 ----------
+     注意:这里**不把 IntersectionObserver 当唯一触发源**。
+     IO 在无头虚拟时间、被节流的 iframe、以及某些低端 WebView 里都不保证投递,
+     而「内容卡在隐藏态」是绝不能出现的故障 —— 尤其是 clip-path 擦除,
+     一次不触发就是一个永远看不见的标题。所以 IO 之外还有一条按滚动位置判定的兜底。 */
+  var pending = [];
+  var revealNow = function (node) {
+    if (!node || (node.classList && node.classList.contains("is-in"))) return;
+    node.classList.add("is-in");
+    $$("[data-fill]", node).forEach(function (f) { f.style.width = f.getAttribute("data-fill") + "%"; });
+    if (node.hasAttribute && node.hasAttribute("data-fill")) {
+      node.style.width = node.getAttribute("data-fill") + "%";
+    }
+  };
+  var io = ("IntersectionObserver" in window)
+    ? new IntersectionObserver(function (entries) {
+        entries.forEach(function (e) {
+          if (!e.isIntersecting) return;
+          revealNow(e.target);
+          io.unobserve(e.target);
+          var k = pending.indexOf(e.target);
+          if (k > -1) pending.splice(k, 1);
+        });
+      }, { rootMargin: "0px 0px -8% 0px", threshold: 0.06 })
+    : null;
+
+  /* 兜底:每帧/每次滚动再按位置判一次,进视口就放出来 */
+  var sweepPending = function () {
+    if (!pending.length) return;
+    var vh = window.innerHeight || document.documentElement.clientHeight;
+    pending = pending.filter(function (n) {
+      var r = n.getBoundingClientRect();
+      if (r.top < vh * 0.94 && r.bottom > -60) {
+        revealNow(n);
+        if (io) io.unobserve(n);
+        return false;
+      }
+      return true;
+    });
+  };
+  window.addEventListener("scroll", sweepPending, { passive: true });
+  window.addEventListener("resize", sweepPending);
+
+  var watch = function (node) {
+    if (!node) return;
+    if (io) { pending.push(node); io.observe(node); }
+    else { revealNow(node); return; }
+    sweepPending();                       /* 已经在视口里的立刻放出来 */
+  };
+  var bar = function (cls, pct, label, valueHtml) {
+    return '
' + + '' + esc(label) + "" + + '' + + '' + valueHtml + "" + + "
"; + }; + + /* ================= 共享外壳 ================= */ + (function shell() { + var header = $("#site"), progress = $("#progress"); + var onScroll = function () { + var y = window.scrollY || document.documentElement.scrollTop; + if (header) header.classList.toggle("is-stuck", y > 8); + var h = document.documentElement.scrollHeight - window.innerHeight; + if (progress) progress.style.width = (h > 0 ? Math.min(100, (y / h) * 100) : 0) + "%"; + }; + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + + // 键盘导航:← / → 切换上一节、下一节(线性风格里的快捷键触感) + document.addEventListener("keydown", function (e) { + if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return; + var t = e.target; + if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return; + var sel = e.key === "ArrowLeft" ? ".pn__a--prev" : (e.key === "ArrowRight" ? ".pn__a--next" : null); + if (!sel) return; + var a = $(sel); + if (a && a.getAttribute("href")) { e.preventDefault(); location.href = a.getAttribute("href"); } + }); + + // 页脚:来源文件 + 免责声明 + var ff = $("#footFiles"); + if (ff) { + D.reproduce.files.forEach(function (x) { + ff.appendChild(el("span", "src", esc(x.v.split("\\").pop()))); + }); + } + setText("#disclaimer", D.meta.disclaimer); + + // 服务端运行时读数 + var chip = $("#serverChipText"), foot = $("#footServer"); + fetch("api/status", { cache: "no-store" }) + .then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); }) + .then(function (s) { + if (chip) { + chip.textContent = "LIVE · UP " + s.uptime; + var c = $("#serverChip"); + if (c) c.title = "本站服务端运行时读数 · bind " + s.bind; + } + if (foot) { + foot.innerHTML = + "host " + esc(s.host) + "" + + "bind " + esc(s.bind) + "" + + "python " + esc(s.python) + "" + + "uptime " + esc(s.uptime) + "" + + "requests " + esc(s.requests) + ""; + } + }) + .catch(function () { + if (chip) chip.textContent = "STATIC"; + if (foot) foot.innerHTML = "运行时读数不可用(/api/status 无响应)"; + }); + + /* ---------- 全站检索(命令面板) ---------- + 索引由 build.py 生成到 assets/js/search.js,纯客户端,不发任何网络请求。 + 快捷键:/ 或 Ctrl/Cmd+K 打开,Esc 关闭,↑↓ 选择,↵ 打开。 */ + (function palette() { + var pal = $("#pal"), input = $("#palInput"), list = $("#palList"), n = $("#palN"); + if (!pal || !input || !list) return; + var IDX = window.NM_SEARCH || []; + var HERE = document.body.getAttribute("data-page") || ""; + var hits = [], cur = 0, open = false; + + var low = function (s) { return String(s == null ? "" : s).toLowerCase(); }; + function mark(text, q) { + if (!q) return esc(text); + var i = low(text).indexOf(q); + if (i < 0) return esc(text); + return esc(text.slice(0, i)) + "" + esc(text.slice(i, i + q.length)) + "" + + esc(text.slice(i + q.length)); + } + function score(e, q) { + var h = low(e.h), t = low(e.t), p = low(e.pg), s = 0; + if (h === q) s += 100; + else if (h.indexOf(q) === 0) s += 60; + else if (h.indexOf(q) > -1) s += 40; + if (p.indexOf(q) > -1) s += 25; + if (t.indexOf(q) > -1) s += 14; + return s; + } + function query(raw) { + var q = low(raw).trim(); + if (!q) return IDX.filter(function (e) { return e.d; }).map(function (e) { return { e: e, q: "" }; }); + var out = []; + IDX.forEach(function (e) { var s = score(e, q); if (s > 0) out.push({ e: e, q: q, s: s }); }); + out.sort(function (a, b) { return b.s - a.s || String(a.e.n).localeCompare(String(b.e.n)); }); + return out.slice(0, 40); + } + function slugOf(href) { return href === "./" ? "index" : String(href).replace(/^\.\//, ""); } + + function findHeading(text) { + var want = String(text).replace(/\s+/g, ""); + var found = null; + $$("#doc h2, #doc h3").forEach(function (h) { + if (!found && h.textContent.replace(/\s+/g, "") === want) found = h; + }); + return found; + } + function jumpTo(node) { + var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); + var y = node.getBoundingClientRect().top + (window.scrollY || 0) - 84; + window.scrollTo({ top: y < 0 ? 0 : y, behavior: reduce ? "auto" : "smooth" }); + node.classList.add("is-flash"); + setTimeout(function () { node.classList.remove("is-flash"); }, 1700); + } + + function render(q) { + hits = query(q); + cur = 0; + list.innerHTML = hits.length ? hits.map(function (h, i) { + var e = h.e, title = e.d ? e.pg : (e.h || e.pg); + var sub = e.h ? e.t : (e.d ? e.t : ""); + return '' + + '' + esc(e.n) + "" + + '' + mark(title, h.q) + "" + + (sub ? '' + mark(sub, h.q) + "" : "") + "" + + '' + esc(e.g) + ""; + }).join("") : '

没有匹配的内容。试试「拒答」「写入路径」「召回」「SHA256」这类词。

'; + if (n) n.textContent = hits.length ? hits.length + " 条" : ""; + list.scrollTop = 0; + } + + function select(i) { + var items = $$(".pal__i", list); + if (!items.length) return; + cur = Math.max(0, Math.min(items.length - 1, i)); + items.forEach(function (el2, k) { + el2.classList.toggle("is-cur", k === cur); + el2.setAttribute("aria-selected", k === cur ? "true" : "false"); + }); + var it = items[cur]; + if (it.offsetTop < list.scrollTop) list.scrollTop = it.offsetTop - 6; + else if (it.offsetTop + it.offsetHeight > list.scrollTop + list.clientHeight) + list.scrollTop = it.offsetTop + it.offsetHeight - list.clientHeight + 6; + } + + function activate(a) { + var heading = a.getAttribute("data-h") || ""; + var href = a.getAttribute("href") || "./"; + var target = slugOf(href); + // 同页结果一律就地处理,绝不重新加载 + if (target === HERE) { + var node = heading ? findHeading(heading) : null; + close(); + if (node) { jumpTo(node); return; } + window.scrollTo({ top: 0, behavior: "smooth" }); + var hd = $("#doc .doc__hd") || $("#doc h1"); + if (hd) { + hd.classList.add("is-flash"); + setTimeout(function () { hd.classList.remove("is-flash"); }, 1700); + } + return; + } + try { sessionStorage.setItem("nm.pal", JSON.stringify({ s: target, h: heading })); } + catch (e) { /* 无存储时退化为普通跳转 */ } + // 键盘回车不会触发 的默认激活,所以这里显式跳转 + location.href = href; + } + + function show() { + open = true; + pal.hidden = false; + document.documentElement.classList.add("pal-open"); + input.value = ""; + render(""); + input.focus(); + } + function close() { + open = false; + pal.hidden = true; + document.documentElement.classList.remove("pal-open"); + } + + var btn = $("#palBtn"), mask = $("#palMask"); + if (btn) btn.addEventListener("click", show); + if (mask) mask.addEventListener("click", close); + input.addEventListener("input", function () { render(input.value); }); + list.addEventListener("mousemove", function (e) { + var a = e.target && e.target.closest ? e.target.closest(".pal__i") : null; + if (a) select(Number(a.getAttribute("data-i"))); + }); + list.addEventListener("click", function (e) { + var a = e.target && e.target.closest ? e.target.closest(".pal__i") : null; + if (!a) return; + e.preventDefault(); + activate(a); + }); + input.addEventListener("keydown", function (e) { + if (e.key === "ArrowDown") { e.preventDefault(); select(cur + 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); select(cur - 1); } + else if (e.key === "Enter") { + var a = $$(".pal__i", list)[cur]; + if (a) { e.preventDefault(); activate(a); } + } + }); + document.addEventListener("keydown", function (e) { + var t = e.target; + var typing = !!(t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)); + if (e.key === "Escape" && open) { close(); if (t === input) input.blur(); return; } + if (open) return; + if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); show(); return; } + if (e.key === "/" && !typing && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); show(); } + }); + + /* 从检索跨页跳进来时,落到对应小节并闪一下 */ + try { + var saved = JSON.parse(sessionStorage.getItem("nm.pal") || "null"); + sessionStorage.removeItem("nm.pal"); + if (saved && saved.h && saved.s === HERE) { + var node2 = findHeading(saved.h); + if (node2) setTimeout(function () { jumpTo(node2); }, 60); + } + } catch (e) { /* 忽略 */ } + })(); + })(); + + /* ================= 各页面渲染 ================= */ + var PAGES = {}; + + /* ================= 重启实验装置 ================= + 数据全部来自 window.NM_RESTART(逐字取自工程原始 JSON)。 + 装置只做一件事:把「写入 → 重启 → 召回 → 作答」四个节拍的真实字段 + 按顺序放出来。修复前 / 修复后 两个版本可以随时对照。 + ================================================= */ + function restartInstrument() { + var R = window.NM_RESTART, host = $("#rst"); + if (!R || !host) return; + + var beatsBox = $("#rstBeats"), playBtn = $("#rstPlay"), out = $("#rstOut"); + var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); + var vi = 0, timer = null, items = []; + + /* 默认停在哪一版:由数据决定,不靠 HTML 里手写的 is-on(两者不一致过一次) */ + R.variants.forEach(function (v, k) { if (v.key === "after") vi = k; }); + + /* --- 四个节拍的骨架 --- */ + R.beats.forEach(function (b, i) { + var li = el("li", "rst__b"); + li.dataset.i = i; + li.innerHTML = + '' + esc(b.i) + "" + + "
" + + '

' + esc(b.t) + "

" + + '

' + esc(b.s) + "

" + + '
' + + "
"; + li.addEventListener("click", function () { stop(); show(i); }); + beatsBox.appendChild(li); + items.push(li); + }); + + /* --- 每个节拍要露出的真实字段 --- */ + function beatData(v, i) { + var L = R.limits, after = v.key === "after"; + if (i === 0) return [ + ["automatic_write", "true"], + ["write_changed", String(v.writeChanged) + (v.writeChanged ? " → 记录已落库" : " → 记录没有落库")], + ["records", "0 → " + v.store.records], + ["coarse_index_buckets", "0 → " + v.store.coarse_index_buckets] + ]; + if (i === 1) return [ + ["history_passed_to_restart", "false → 重启不携带任何对话历史"], + ["resident_records", v.store.resident_records + " 条留在进程内存"], + ["gpu_cache records / tokens", v.store.gpu_cache_records + " / " + v.store.gpu_cache_tokens], + ["gpu_cache_bytes", v.store.gpu_cache_bytes + " B"], + ["容量上限", L.capacityRecords.toLocaleString("en-US") + " 条 · " + L.maxPages.toLocaleString("en-US") + " 页 × " + L.pageCapacity + " 条"] + ]; + if (i === 2) { + if (!after) return [ + ["need_memory", "false"], + ["stop_reason", v.stopReason], + ["confidence", fmt(v.confidence, 4)], + ["page_ids / record_ids", "(空)"] + ]; + return [ + ["need_memory", "true"], + ["page_ids", v.pageIds.join(", ") + " score " + fmt(v.pageScores[0], 4)], + ["record_ids", v.recordIds.join(", ") + " score " + fmt(v.recordScores[0], 4)], + ["hop_count", String(v.hopCount)], + ["confidence", fmt(v.confidence, 4)], + ["stop_reason", v.stopReason] + ]; + } + return [ + ["prefix_length", v.prefixLength + " tok (KV 预算 " + L.kvBudgetTokens.toLocaleString("en-US") + " tok)"], + ["prefix_used", String(v.prefixUsed)], + ["response", v.response], + ["router_recalled_after_restart", String(v.recalled)], + ["generated_contains_expected", String(v.correct)] + ]; + } + + function variant() { return R.variants[vi]; } + + function fillData() { + var v = variant(); + items.forEach(function (li, i) { + var d = $("#rstData" + i, li); + if (!d) return; + d.innerHTML = beatData(v, i).map(function (r) { + return '' + esc(r[0]) + "" + esc(r[1]) + ""; + }).join(""); + }); + setText("#rstFile", (D.meta.repo || "") + "\\dynamic_memory_lab\\" + v.file); + } + + function applyGauges(i) { + var v = variant(), L = R.limits; + var restarted = i >= 1, answered = i >= 3; + + var rec = i >= 0 ? v.store.records : 0; + setText("#rstRec", String(rec)); + $("#rstRecBar").style.width = (rec > 0 ? 100 : 0) + "%"; + + setText("#rstHist", restarted ? "false" : "true(本轮对话)"); + $("#rstHistBar").style.width = (restarted ? 0 : 100) + "%"; + + var pf = answered ? v.prefixLength : 0; + $("#rstPf").innerHTML = pf + " tok"; + var pct = pf ? (pf / L.kvBudgetTokens) * 100 : 0; + $("#rstPfBar").style.width = pf ? Math.max(0.8, pct) + "%" : "0%"; + setText("#rstPfHint", pf + ? "占 KV 预算 " + pct.toFixed(2) + "% —— 记忆走的是前缀,不是把历史塞进上下文" + : "尚未注入证据前缀"); + + out.classList.toggle("is-idle", !answered); + var vd = $("#rstVerdict"); + vd.className = "rst__verdict mono " + (answered ? (v.correct ? "is-ok" : "is-bad") : ""); + vd.textContent = answered ? (v.correct ? "✓ 命中 " + R.expected : "✗ 未命中 " + R.expected) : ""; + setText("#rstResp", answered ? v.response : "…"); + + var kv = $("#rstKv"); + kv.innerHTML = answered ? [ + ["stop_reason", v.stopReason, false], + ["prefix_used", String(v.prefixUsed), v.prefixUsed], + ["confidence", fmt(v.confidence, 4), v.confidence >= 0.7], + ["recalled_after_restart", String(v.recalled), v.recalled], + ["contains_expected", String(v.correct), v.correct] + ].map(function (r) { + return '' + esc(r[0]) + " " + esc(r[1]) + ""; + }).join("") : ""; + + host.classList.toggle("is-payoff", answered && v.correct); + } + + function show(i) { + items.forEach(function (li, j) { + li.classList.toggle("is-on", j === i); + li.classList.toggle("is-past", j < i); + li.setAttribute("aria-current", j === i ? "step" : "false"); + }); + applyGauges(i); + } + + function stop() { + if (timer) { clearTimeout(timer); timer = null; } + playBtn.classList.remove("is-playing"); + } + + function play() { + stop(); + show(-1); + if (reduce) { show(R.beats.length - 1); return; } + playBtn.classList.add("is-playing"); + var i = 0; + var step = function () { + show(i); + i++; + if (i < R.beats.length) timer = setTimeout(step, 1050); + else { timer = null; playBtn.classList.remove("is-playing"); } + }; + timer = setTimeout(step, 260); + } + + playBtn.addEventListener("click", play); + + $$(".seg__b", host).forEach(function (b, k) { + b.classList.toggle("is-on", k === vi); + b.addEventListener("click", function () { + vi = k; + $$(".seg__b", host).forEach(function (x) { x.classList.toggle("is-on", x === b); }); + fillData(); + play(); + }); + }); + + fillData(); + show(-1); + + // 进入视口后自动放一次。 + // 用滚动位置判定而不是 IntersectionObserver 的可见比例:装置比视口高时 + // 比例阈值可能永远达不到,这是很常见的坑。触发一次后即摘掉监听。 + var played = false; + function autoPlay() { + if (played) return; + var r = host.getBoundingClientRect(); + var vh = window.innerHeight || document.documentElement.clientHeight; + if (r.top < vh * 0.72 && r.bottom > vh * 0.08) { + played = true; + window.removeEventListener("scroll", autoPlay); + window.removeEventListener("resize", autoPlay); + play(); + } + } + window.addEventListener("scroll", autoPlay, { passive: true }); + window.addEventListener("resize", autoPlay); + autoPlay(); + } + + /* ---- 00 总览 ---- */ + PAGES.index = function () { + setText("#heroLede", D.positioning.body); + setText("#heroSrc", D.meta.repo + "\\dynamic_memory_lab"); + setText("#heroDisclaimer", D.meta.disclaimer); + + var H = {}; + D.dirty.headline.forEach(function (h) { H[h.metric] = h; }); + var ans = H["可回答正确率"], rec = H["目标记忆召回率"]; + var A = D.dirty.batches[0], B = D.dirty.batches[1]; + var chain = function (h) { + return fmt(h.base) + "%" + arrow + '' + fmt(h.a) + "%" + arrow + + '' + fmt(h.b) + "%"; + }; + var items = [ + { k: "可回答正确率 · 基线 → A → B", v: chain(ans), + d: "同一 264 题题集的两个独立批次。基线 " + ans.baseRaw + ",批次 A " + ans.aRaw + ",批次 B " + ans.bRaw + ";两批次配置不同,不可合并。" }, + { k: "目标记忆召回率 · 基线 → A → B", v: chain(rec), + d: "基线没有记忆机制,召回恒为 0。批次 B 把该查的记忆查回来的比例达 " + rec.bRaw + "。" }, + { k: "读取路径额外耗时 A → B", v: fmt(A.reader) + arrow + '' + fmt(B.reader) + " ms", + d: "记忆读取相对无记忆基线新增的耗时;批次 A " + fmt(A.reader) + " ms,批次 B 降到 " + fmt(B.reader) + " ms。" } + ]; + var box = $("#heroKpis"); + if (box) { + items.forEach(function (it) { + box.appendChild(el("div", "kpi", + '' + esc(it.k) + "" + + '' + it.v + "" + + '' + esc(it.d) + "")); + }); + watch(box); + } + // 发布页:结果条、读取路径图绘制、三种抵达编排、重启装置 + watch($("#releaseResult")); + watch($(".teaser")); + $$(".arr-wipe, .arr-num, .arr-cards").forEach(watch); + restartInstrument(); + + var cite = $("#citeCopy"); + if (cite) { + var text = ($("#citeBlock") && $("#citeBlock").textContent) || ""; + cite.addEventListener("click", function () { + var done = function () { + cite.textContent = "已复制"; cite.classList.add("is-done"); + setTimeout(function () { cite.textContent = "复制"; cite.classList.remove("is-done"); }, 1900); + }; + var fallback = function () { + var ta = document.createElement("textarea"); + ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; + document.body.appendChild(ta); ta.select(); + try { document.execCommand("copy"); done(); } catch (e) { cite.textContent = "复制失败"; } + document.body.removeChild(ta); + }; + if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, fallback); + else fallback(); + }); + } + }; + + /* ---- 02 机制 ---- */ + PAGES.mechanism = function () { + // 读取路径步进器 + (function flow() { + var steps = D.readPath, box = $("#steps"); + if (!box) return; + steps.forEach(function (s, i) { + var b = el("button", "step", + '' + String(i + 1).padStart(2, "0") + "" + + '' + esc(s.label) + ""); + b.type = "button"; + b.dataset.i = i; + b.setAttribute("aria-selected", i === 0 ? "true" : "false"); + b.addEventListener("click", function () { show(i); }); + b.addEventListener("mouseenter", function () { show(i); }); + box.appendChild(b); + }); + function show(i) { + $$(".step", box).forEach(function (b) { + var j = Number(b.dataset.i); + b.classList.toggle("is-on", j === i); + b.classList.toggle("is-past", j < i); + b.setAttribute("aria-selected", j === i ? "true" : "false"); + }); + setText("#flowIdx", "STAGE " + String(i + 1).padStart(2, "0") + " / " + String(steps.length).padStart(2, "0")); + setText("#flowTitle", steps[i].label); + setText("#flowBody", steps[i].detail); + setText("#flowIo", steps[i].io || ""); + } + show(0); + watch(box); + })(); + + // 记录字段 + var rf = $("#recFields"); + if (rf) { + D.recordFields.forEach(function (f) { + rf.appendChild(el("div", "rec__f", + '' + esc(f.f) + "" + + '' + esc(f.d) + "")); + }); + watch(rf); + } + + // 真实记录示例(取自 examples.js,逐字) + var rr = $("#realRecord"); + var E = window.NM_EXAMPLES || []; + if (rr && E.length && E[0].nm.records && E[0].nm.records.length) { + var r = E[0].nm.records[0]; + var rows = [ + ["record_id", r.record_id], ["entity", r.entity], ["attribute", r.attribute], + ["value", r.value], ["status", r.status], ["version", r.version], + ["source", r.source], ["text_preview", r.text_preview] + ].filter(function (x) { return x[1] !== undefined && x[1] !== null && x[1] !== ""; }); + rr.innerHTML = '

一条真实记录 · 逐字取自评测结果

' + + '
' + rows.map(function (x) { + return '
' + esc(x[0]) + '' + + '' + esc(x[1]) + "
"; + }).join("") + "
" + + '

这条记录在评测中命中了问题 user-004,' + + '内容是用户给当时模型定的名字。注意「Natural Memory v1」是写入时的项目名,记录按原样保留、未做改写。

'; + } else if (rr) { + rr.textContent = "真实记录示例不可用(examples.js 未加载)。"; + } + + // 状态机 + var sl = $("#stateList"); + if (sl) { + D.states.forEach(function (s) { + sl.appendChild(el("div", "state state--" + s.tone, + '' + esc(s.s) + "" + + '' + esc(s.d) + "")); + }); + watch(sl); + } + }; + + /* ---- 03 测量结果 ---- */ + PAGES.results = function () { + var tabs = $("#resultTabs"), panels = $("#resultPanels"); + if (!tabs || !panels) return; + var DT = D.dirty; + + var defs = [ + { id: "dirty", label: "批次 A / B 对照" }, + { id: "stage5", label: "Stage 5 结构化基准" }, + { id: "router", label: "记忆路由器工程线" } + ]; + var want = (new URLSearchParams(location.search).get("tab") || "").toLowerCase(); + var startId = defs.some(function (d) { return d.id === want; }) ? want : defs[0].id; + + var activate = function (id, push) { + $$(".tab", tabs).forEach(function (x) { + var on = x.dataset.id === id; + x.classList.toggle("is-on", on); + x.setAttribute("aria-selected", on ? "true" : "false"); + }); + $$(".panel", panels).forEach(function (p) { + var on = p.dataset.id === id; + p.classList.toggle("is-on", on); + p.setAttribute("aria-hidden", on ? "false" : "true"); + if (on) $$("[data-fill]", p).forEach(function (f) { f.style.width = f.getAttribute("data-fill") + "%"; }); + }); + if (push) { + try { + var u = new URL(location.href); + u.searchParams.set("tab", id); + history.replaceState(null, "", u.pathname + u.search + u.hash); + } catch (e) { /* 仅影响地址栏 */ } + } + }; + + defs.forEach(function (d) { + var t = el("button", "tab" + (d.id === startId ? " is-on" : ""), esc(d.label)); + t.type = "button"; t.setAttribute("role", "tab"); t.dataset.id = d.id; + t.setAttribute("aria-selected", d.id === startId ? "true" : "false"); + t.addEventListener("click", function () { activate(d.id, true); }); + tabs.appendChild(t); + }); + + /* --- 面板 1 --- */ + var p1 = el("div", "panel"); p1.dataset.id = "dirty"; + var h = '

' + + "" + esc(DT.title) + " · " + esc(DT.subtitle) + "

" + + '

' + esc(DT.protocol) + "

" + + '

' + + '出处 ' + esc(DT.src) + "
" + esc(DT.batchNote) + "

"; + + h += '
' + + '原版 Qwen3.5-4B' + + '批次 A' + + '批次 B
'; + + h += '
'; + DT.headline.forEach(function (m) { + h += '
' + + '
' + esc(m.metric) + "" + + '批次 A ' + fmt(m.a) + "% → 批次 B " + fmt(m.b) + "%
" + + bar("bar--base", m.base, "原版 Qwen", fmt(m.base) + "%" + esc(m.baseRaw) + "") + + bar("bar--a", m.a, "批次 A", fmt(m.a) + "%" + esc(m.aRaw) + "") + + bar("bar--nm2", m.b, "批次 B", fmt(m.b) + "%" + esc(m.bRaw) + "") + + "
"; + }); + h += "
"; + h += '
' + DT.corpus.map(function (c) { + return '
' + esc(c.k) + '' + esc(c.v) + "
"; + }).join("") + "
"; + + h += '

两个批次的运行明细

' + + '
' + + DT.batches.map(function (b) { + return ""; + }).join("") + ""; + [ + { k: "读取路径额外耗时", v: function (b) { return b.reader + " ms"; } }, + { k: "平均输入前缀 token", v: function (b) { return fmt(b.promptTokens); } }, + { k: "前缀命中率 / 平均前缀 token", v: function (b) { return fmt(b.prefixRate) + "% / " + fmt(b.prefixTokens); } }, + { k: "平均总延迟(基线 → 记忆)", v: function (b) { return b.latencyBase + " → " + b.latencyNm + " ms"; } }, + { k: "平均解码速度(基线 → 记忆)", v: function (b) { return b.decodeBase + " → " + b.decodeNm + " tok/s"; } }, + { k: "NM 显存快照峰值 allocated / reserved", v: function (b) { return fmt(b.allocNm, 3) + " / " + fmt(b.resvNm, 3) + " GiB"; } }, + { k: "active records / pages", v: function (b) { return b.records + " / " + b.pages; } }, + { k: "GPU cache token", v: function (b) { return b.cacheTokens.toLocaleString("en-US"); } } + ].forEach(function (r) { + h += "" + DT.batches.map(function (b) { + return '"; + }).join("") + ""; + }); + h += "
指标" + esc(b.label) + '
' + esc(b.date) + " · " + esc(b.tag) + "
" + esc(r.k) + "' + esc(r.v(b)) + "
"; + + h += '

按数据域拆分(可回答正确率)

'; + DT.domains.forEach(function (d) { + h += '
' + esc(d.domain) + + '' + d.n + " 题
" + + bar("bar--base", d.base, "原版 Qwen", fmt(d.base) + "%" + esc(d.baseRaw) + "") + + bar("bar--a", d.a, "批次 A", fmt(d.a) + "%" + esc(d.aRaw) + "") + + bar("bar--nm2", d.b, "批次 B", fmt(d.b) + "%" + esc(d.bRaw) + "") + "
"; + }); + h += "
"; + + h += '

按类别拆分(正确 / 题量)

' + + '
'; + DT.categories.forEach(function (c) { + var na = num(c.a), nb = num(c.b), diff = nb.c - na.c; + h += "" + + '" + + '" + + '"; + }); + h += "
类别题量原版 Qwen批次 A批次 BB 相对 A
" + esc(c.label) + '
' + esc(c.k) + "
' + c.n + '' + esc(c.base) + '' + esc(c.a) + "' + esc(c.b) + "' + (diff > 0 ? "+" : "") + diff + " 题

" + esc(DT.categoryNote) + "

"; + + h += '

路由与生成的差距

' + + '
' + + '
批次 A · 目标记忆召回' + fmt(DT.gap.recallA) + '%' + esc(DT.gap.recallARaw) + "
" + + '
批次 B · 目标记忆召回' + fmt(DT.gap.recallB) + '%' + esc(DT.gap.recallBRaw) + "
" + + '
批次 A · 最终回答正确' + fmt(DT.gap.answerA) + '%' + esc(DT.gap.answerARaw) + "
" + + '
批次 B · 最终回答正确' + fmt(DT.gap.answerB) + '%' + esc(DT.gap.answerBRaw) + "
" + + '

' + esc(DT.gap.note) + "

"; + p1.innerHTML = h; + + /* --- 面板 2:Stage 5 --- */ + var p2 = el("div", "panel"); p2.dataset.id = "stage5"; + var h2 = '

' + + esc(D.stage5.title) + " · " + esc(D.stage5.subtitle) + + '
出处 ' + esc(D.stage5.src) + "

" + + '
' + + '原版 Qwen3.5-4B强 RAG 基线Natural Memory v2
' + + '
'; + D.stage5.sets.forEach(function (set) { + h2 += '

' + esc(set.name) + " · " + set.n + " 题

"; + set.rows.forEach(function (r) { + var cls = /Natural Memory/.test(r.sys) ? "bar--nm2" : (/RAG/.test(r.sys) ? "bar--rag" : "bar--base"); + h2 += '
' + esc(r.sys) + + '' + r.lat + " ms · " + r.tok + " tok/s
" + + bar(cls, r.acc, "正确率", fmt(r.acc) + "%" + esc(r.raw) + "") + "
"; + }); + h2 += "
"; + }); + h2 += "
"; + h2 += '
' + + ""; + D.stage5.sets.forEach(function (set) { + set.rows.forEach(function (r, i) { + h2 += "" + (i === 0 ? '" : "") + + "" + + '" + + '"; + }); + }); + h2 += "
题集系统正确率原始平均延迟解码速度未知拒答
' + esc(set.name) + " · " + set.n + "" + esc(r.sys) + '' + fmt(r.acc) + "%' + esc(r.raw) + '' + r.lat + ' ms' + r.tok + " tok/s' + (r.abstain ? esc(r.abstain) : "—") + "

" + esc(D.stage5.note) + "

"; + p2.innerHTML = h2; + + /* --- 面板 3:路由器工程线 --- */ + var p3 = el("div", "panel"); p3.dataset.id = "router"; + var h3 = '

' + + esc(D.router.title) + " · " + esc(D.router.subtitle) + + '
出处 ' + esc(D.router.src) + "

" + + '
' + + "" + + D.router.metrics.map(function (m) { + return ""; + }).join("") + "
指标原 512 维 / 基线128 维路由器备注
" + esc(m.k) + '' + esc(m.base) + '' + + esc(m.nm2) + '' + esc(m.note || "—") + "
" + + '
' + D.router.spec.map(function (s) { + return '
' + esc(s.k) + '' + esc(s.v) + "
"; + }).join("") + "
" + + '
' + + '

这张表必须连这些一起读

' + + D.router.caveats.map(function (c, i) { + return '
' + String(i + 1).padStart(2, "0") + + '

' + esc(c) + "

"; + }).join("") + "
"; + p3.innerHTML = h3; + + panels.appendChild(p1); panels.appendChild(p2); panels.appendChild(p3); + [p1, p2, p3].forEach(function (p) { p.setAttribute("role", "tabpanel"); p.setAttribute("aria-hidden", "true"); }); + watch(p1); + activate(startId, false); + }; + + /* ---- 04 实测样例 ---- */ + PAGES.examples = function () { + var box = $("#exampleList"); + if (!box) return; + var E = window.NM_EXAMPLES || []; + setText("#exampleIntro", D.examples.subtitle); + setText("#exampleNote", D.examples.note); + if (!E.length) { box.appendChild(el("p", "small", "样例数据未加载。")); return; } + + var DOMAIN = { user_dialogue: "用户对话事实", real_repository: "真实源码/文档" }; + var CAT = { + symbol_navigation: "符号定位", unknown_refusal: "未知拒答", paraphrase: "自然改写", + constraint: "约束类问题", cross_session: "跨会话", temporal_conflict: "时间冲突" + }; + var TOKMAX = 64; + var tok = function (t) { + return t >= TOKMAX ? '' + t + " token(达上限截断)" : t + " token"; + }; + var verdict = function (ok) { + return '' + (ok ? "命中" : "未命中") + ""; + }; + var col = function (title, cls, r, meta, extra) { + return '
' + + '

' + esc(title) + "

" + + '

' + esc(r.response || "(空)") + "

" + + '

' + meta + "
" + tok(r.tokens || 0) + "

" + (extra || "") + "
"; + }; + + E.forEach(function (e, i) { + var identical = (e.nm.response || "") === (e.base.response || ""); + var ev = ""; + if (e.nm.records && e.nm.records.length) { + ev = '

命中的记忆记录

' + + e.nm.records.map(function (r) { + return '
' + + '' + esc(r.attribute || r.entity || r.record_id) + "" + + '' + esc(r.text_preview || r.value || "") + "" + + '' + esc(r.record_id) + " · " + esc(r.source || "") + + (r.status ? " · " + esc(r.status) : "") + "
"; + }).join("") + "
"; + } else if (!e.answerable) { + ev = '

命中的记忆记录

' + + '
无 —— 该题目标本身不在库里,考察的是模型会不会硬答。
'; + } + box.appendChild(el("article", "ex reveal", + '
' + + '' + String(i + 1).padStart(2, "0") + "" + + '' + esc(DOMAIN[e.domain] || e.domain) + " · " + esc(CAT[e.category] || e.category) + "" + + (e.answerable ? "" : '未知题') + + (identical ? '两系统回答逐字相同' : "") + + 'id=' + esc(e.id) + "
" + + '

' + esc(e.query) + "

" + + '

期望锚点' + (e.expected + ? esc(e.expected) + (e.expected_anchors && e.expected_anchors.length > 1 + ? " · 其余:" + esc(e.expected_anchors.slice(1).join("、")) : "") + : "无(未知题,考察拒答)") + "

" + + '
' + + col("原版 Qwen3.5-4B", "base", e.base, "判分 " + verdict(e.base.correct)) + + col("Natural Memory v2", "nm", e.nm, + "判分 " + verdict(e.nm.correct) + (e.answerable ? " · 目标召回 " + (e.nm.recall ? "命中" : "未命中") : ""), ev) + + "
")); + }); + $$(".ex", box).forEach(watch); + }; + + /* ---- 05 代价 ---- */ + PAGES.cost = function () { + setText("#costIntro", D.cost.subtitle); + setText("#costQuote", D.cost.quote); + var box = $("#ledger"); + if (box) { + box.appendChild(el("div", "ledger__h", "指标原版 QwenNM v2变化")); + D.cost.rows.forEach(function (r) { + box.appendChild(el("div", "ledger__r", + '' + esc(r.k) + "" + + '' + esc(r.base) + "" + + '' + esc(r.nm2) + "" + + '' + esc(r.delta) + "")); + }); + watch(box); + } + var v = D.cost.vram, vb = $("#vram"); + if (vb) { + [ + { k: "allocated · 无记忆基线", val: v.allocBase, cls: "base" }, + { k: "allocated · NM v2", val: v.allocNm2, cls: "nm2" }, + { k: "reserved · 无记忆基线", val: v.resvBase, cls: "base" }, + { k: "reserved · NM v2", val: v.resvNm2, cls: "nm2" } + ].forEach(function (r) { + vb.innerHTML += '
' + esc(r.k) + "" + + fmt(r.val, 3) + " " + v.unit + "
" + + '
'; + }); + vb.innerHTML += '
012 GiB 显卡
'; + watch(vb); + } + }; + + /* ---- 06 容量 ---- */ + PAGES.capacity = function () { + var box = $("#capList"); + if (box) { + D.capacity.stats.forEach(function (s) { + var hi = /设计容量|GPU cache 上限/.test(s.k); + box.appendChild(el("div", "cap__r" + (hi ? " cap__r--hi" : ""), + '' + esc(s.k) + (s.d ? "" + esc(s.d) + "" : "") + "" + + '' + esc(s.v) + "")); + }); + watch(box); + } + var g = $("#pageMap"), total = 512, used = D.capacity.grid.usedPages; + if (g) { + var step = Math.max(1, Math.floor(total / used)), out = ""; + for (var i = 0; i < total; i++) { + var on = i % step === 0 && (i / step) < used; + out += ""; + } + g.innerHTML = out; + } + setText("#storageMode", D.capacity.storage.mode); + setText("#storageText", D.capacity.storage.text); + setText("#storageTradeoff", D.capacity.storage.tradeoff); + }; + + /* ---- 07 边界 ---- */ + PAGES.limits = function () { + setText("#limitsIntro", D.limits.subtitle); + var c = $("#claims"); + if (c) { + D.limits.claims.forEach(function (t, i) { + c.appendChild(el("div", "claim", + '✕' + esc(t) + "" + + 'UNSUPPORTED ' + String(i + 1).padStart(2, "0") + "")); + }); + watch(c); + } + var l = $("#lims"); + if (l) { + D.limits.limitations.forEach(function (x, i) { + l.appendChild(el("div", "lim", + '' + String(i + 1).padStart(2, "0") + "" + + "

" + esc(x.t) + '

' + esc(x.d) + "

")); + }); + watch(l); + } + var q = $("#quotes"); + if (q) { + D.limits.quotes.forEach(function (x) { + q.appendChild(el("div", "quote", "" + esc(x.q) + "" + esc(x.s) + "")); + }); + watch(q); + } + }; + + /* ---- 08 路线图 ---- */ + PAGES.roadmap = function () { + var s = $("#solved"); + if (s) { + D.solved.forEach(function (t, i) { + s.appendChild(el("div", "check", + '✓ ' + String(i + 1).padStart(2, "0") + "" + + '' + esc(t) + "")); + }); + watch(s); + } + var r = $("#roadmap-list"); + if (r) { + D.roadmap.forEach(function (t, i) { + r.appendChild(el("div", "road__r", + '→ ' + String(i + 1).padStart(2, "0") + "" + + '' + esc(t) + "")); + }); + watch(r); + } + }; + + /* ---- 09 复现 ---- */ + PAGES.reproduce = function () { + var R = D.reproduce; + setText("#cwd", R.cwd); + setText("#pBase", D.meta.base); + var cmd = R.cmd.join("\n"), block = $("#cmdBlock"); + if (block) { + block.innerHTML = esc(cmd) + .replace(/(--[a-z-]+)/g, '$1') + .replace(/("[^&]*")/g, '$1'); + } + var f = $("#files"); + if (f) { + R.files.forEach(function (x) { + f.appendChild(el("div", "kv__r", + '' + esc(x.k) + '' + esc(x.v) + "")); + }); + watch(f); + } + var btn = $("#copyBtn"); + if (btn) { + btn.addEventListener("click", function () { + var done = function () { + btn.textContent = "已复制"; + btn.classList.add("is-done"); + setTimeout(function () { btn.textContent = "复制命令"; btn.classList.remove("is-done"); }, 1900); + }; + var fallback = function () { + var ta = document.createElement("textarea"); + ta.value = cmd; ta.style.position = "fixed"; ta.style.opacity = "0"; + document.body.appendChild(ta); ta.select(); + try { document.execCommand("copy"); done(); } catch (e) { btn.textContent = "复制失败"; } + document.body.removeChild(ta); + }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(cmd).then(done, fallback); + } else { fallback(); } + }); + } + }; + + /* ---- 04 NM2.1 ---- */ + function doseChart(host, d) { + if (!host || !d) return; + var head = '
' + esc(d.axis) + "" + + '' + esc(d.xLabel) + "
"; + var axis = '
' + + d.x.map(function (v) { + return '' + esc(String(v)) + ""; + }).join("") + "
"; + var rows = d.metrics.map(function (m) { + var max = Math.max.apply(null, m.v) || 1; + return '
' + esc(m.label) + + "" + (m.higherBetter ? "越高越好" : "越低越好") + "" + + '' + m.v.map(function (v, i) { + var pick = d.x[i] === d.pick; + return '' + + '' + + "" + v.toFixed(2) + esc(m.unit) + ""; + }).join("") + "
"; + }).join(""); + host.innerHTML = head + axis + rows + + '

' + esc(d.note) + "

" + + '

' + esc(window.NM21 ? window.NM21.srcLabel : "") + "

"; + } + + PAGES.nm21 = function () { + var M = window.NM21; + if (M) { + doseChart($("#nmBlend"), M.blend); + doseChart($("#nmPrior"), M.prior); + } + var b = $("#nmCopy"), cmd = $("#nmCmd"); + if (b && cmd) b.addEventListener("click", function () { copyText(b, cmd.textContent || ""); }); + }; + + /* ---- 06 真实评测台账 ---- */ + PAGES.ledger = function () { + var L = window.NM_LEDGER, box = $("#ledgerList"); + if (!box || !L) return; + + var total = 0, all = []; + L.forEach(function (s) { + total += s.n; + s.items.forEach(function (it) { all.push({ s: s, it: it }); }); + }); + setText("#ledgerIntro", "共 " + total + " 条真实 episode,来自 " + L.length + + " 次正式运行(24 条同句式候选、值为随机码,随机猜中概率 4.17%)。"); + + var verdictOf = function (it) { + if (!it.ans) return it.leak ? "leak" : "abstain"; + if (it.ok) return "ok"; + return it.wrong ? "wrong" : "miss"; + }; + var V = { + ok: { label: "答对", tone: "ok" }, + wrong: { label: "答成别的属性", tone: "no" }, + miss: { label: "答错", tone: "no" }, + leak: { label: "未知泄漏", tone: "no" }, + abstain: { label: "未知已拒答", tone: "ok" } + }; + + var fSrc = "all", fV = "all"; + + var srcDefs = [{ k: "all", label: "全部来源" }].concat(L.map(function (s) { + return { k: s.key, label: s.label + " · " + s.n }; + })); + var vDefs = [{ k: "all", label: "全部判定" }].concat(["ok", "wrong", "miss", "leak", "abstain"].map(function (k) { + return { k: k, label: V[k].label }; + })); + + function buildSeg(host, defs, get, set) { + host.innerHTML = ""; + defs.forEach(function (d) { + var b = el("button", "seg__b" + (get() === d.k ? " is-on" : ""), esc(d.label)); + b.type = "button"; + b.addEventListener("click", function () { + set(d.k); + $$(".seg__b", host).forEach(function (x, i) { x.classList.toggle("is-on", defs[i].k === d.k); }); + paint(); + }); + host.appendChild(b); + }); + } + + function paint() { + var rows = all.filter(function (r) { + if (fSrc !== "all" && r.s.key !== fSrc) return false; + if (fV !== "all" && verdictOf(r.it) !== fV) return false; + return true; + }); + var tally = { ok: 0, wrong: 0, miss: 0, leak: 0, abstain: 0 }; + rows.forEach(function (r) { tally[verdictOf(r.it)]++; }); + setText("#ledgerCount", "显示 " + rows.length + " / " + total + " 条"); + setText("#ledgerBreak", Object.keys(V).map(function (k) { + return V[k].label + " " + tally[k]; + }).join(" · ")); + + box.innerHTML = rows.length ? rows.map(function (r, i) { + var it = r.it, v = V[verdictOf(it)]; + var src = r.s.note; + return '
' + + '
' + + '' + String(i + 1).padStart(3, "0") + "" + + '' + esc(v.label) + "" + + '属性 · ' + esc(it.attr || "—") + "" + + (it.ans ? "" : '未知题') + + '' + esc(it.id) + " · " + esc(src) + "" + + "
" + + '

期望锚点' + + (it.ans ? "" + esc(it.exp) + "" : "无(未知题,考察拒答)") + "

" + + '

' + esc(it.reply || "(空)") + "

" + + '

stop_reason=' + esc(it.stop || "—") + "" + + " · records_selected=" + esc(String(it.sel)) + "" + + " · top_score=" + esc(Number(it.score).toFixed(4)) + "" + + " · records_active=" + esc(String(it.active)) + "" + + " · need_memory=" + esc(String(it.need)) + "

" + + "
"; + }).join("") : '

当前筛选下没有记录。

'; + } + + buildSeg($("#ledgerSrc"), srcDefs, function () { return fSrc; }, function (k) { fSrc = k; }); + buildSeg($("#ledgerVerdict"), vDefs, function () { return fV; }, function (k) { fV = k; }); + paint(); + }; + + /* ---------- 分派 ---------- */ + var page = document.body.getAttribute("data-page") || ""; + if (PAGES[page]) { + try { PAGES[page](); } + catch (err) { + if (window.console) console.error("[NM] 页面渲染失败: " + page, err); + } + } +})(); diff --git a/assets/js/space.js b/assets/js/space.js new file mode 100644 index 0000000..e4c41f7 --- /dev/null +++ b/assets/js/space.js @@ -0,0 +1,256 @@ +/* ================================================================== + Natural Memory — space.js + 「地址空间下降」—— 本页的招牌机制。 + + 机制来自题材自身的物理(motion-web: design-slop B5):这套系统的读取路径 + 本来就是一次寻址下降 —— 问题 → 128 维地址 → LSH 粗索引 → 候选页 → + 页内重排 → Top-K → 证据前缀 → 生成。所以镜头一路下降,而不是在旁边飘粒子。 + + 实现取 case 的**机制**、不取 case 的皮(cases/ 的照搬边界): + 滚动进度从文档里读回来((scrollY - offsetTop) / 可滚动距离),不用 sticky 累加 —— + 同 press-stack 的做法;原生滚动完全不动它。 + + 全部数字都是真的:1,048,576 槽位 / 738 条活跃记录 / 730 个索引桶 / + 24 页 × 页容量 32 / 最多 2 条记录 / 前缀 45 token。 + ================================================================== */ +(function () { + "use strict"; + + var cv = document.getElementById("spaceCv"); + if (!cv) return; + + var D = window.NM_DATA; + var ctx = cv.getContext("2d"); + var host = document.getElementById("hero"); + var pin = cv.parentNode; + + var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); + var narrow = window.matchMedia && window.matchMedia("(max-width: 900px)").matches; + + /* ---- 真实的地址空间几何 ---- */ + var GEO = { + slots: 1048576, /* 地址空间设计容量 */ + records: 738, /* 批次 B 实际装载的 active records */ + buckets: 730, /* LSH 粗索引桶数 */ + pages: 24, /* 实际使用的页数 */ + pageCap: 32, /* 页容量 */ + topK: 2, /* 单次读取最多记录数 */ + prefix: 45 /* 注入模型内部的前缀 token 数 */ + }; + + /* ---- 可复现的伪随机(不用 Math.random:同一张图每次都一样) ---- */ + function rng(seed) { + var s = seed >>> 0; + return function () { + s = (s * 1664525 + 1013904223) >>> 0; + return s / 4294967296; + }; + } + + var N = 128; /* 空间栅格 128×128 = 16,384 格 */ + var SPACE = 1024; /* 离屏位图边长 */ + var CELL = SPACE / N; + + /* 738 条记录散布在 16,384 格里 —— 每格代表 64 个真实槽位 */ + var lit = []; + var rand = rng(20260914); + var want = Math.round(N * N * (GEO.records / 16384)); /* ≈ 738 格 */ + var seen = {}; + while (lit.length < want) { + var i = Math.floor(rand() * N * N); + if (seen[i]) continue; + seen[i] = 1; + lit.push(i); + } + /* 降落的终点:其中一条记录 */ + var target = lit[Math.floor(lit.length * 0.42)]; + var tx = (target % N) * CELL + CELL / 2; + var ty = Math.floor(target / N) * CELL + CELL / 2; + + /* ---- 离屏烘焙整张地址空间(每秒重画 16k 个点太贵,烘一次然后只做缩放着色) ---- */ + var off = document.createElement("canvas"); + off.width = off.height = SPACE; + (function bake() { + var g = off.getContext("2d"); + g.fillStyle = "#000"; + g.fillRect(0, 0, SPACE, SPACE); + /* 底噪:整个地址空间的槽位骨架 */ + g.fillStyle = "rgba(255,254,244,0.10)"; + for (var y = 0; y < N; y++) { + for (var x = 0; x < N; x++) { + g.fillRect(x * CELL + CELL * 0.42, y * CELL + CELL * 0.42, 0.9, 0.9); + } + } + /* 活跃记录 */ + g.fillStyle = "#fffef4"; + lit.forEach(function (i) { + g.fillRect((i % N) * CELL + CELL * 0.34, Math.floor(i / N) * CELL + CELL * 0.34, 1.7, 1.7); + }); + /* 目标记录:唯一一个用信号色标出的点,全页只在这里出现 */ + g.fillStyle = "#00ff6a"; + g.fillRect(tx - 1.6, ty - 1.6, 3.2, 3.2); + })(); + + /* ---- 八个环节(标签直接取自 data.js 的读取路径,单一来源) ---- */ + var path = (D && D.readPath && D.readPath.length) ? D.readPath : []; + var STEPS = [ + { label: "地址空间", value: "1,048,576 槽位 · 738 条活跃记录", zoom: 1.00 }, + { label: path[0] ? path[0].label : "用户问题", value: "自然语言,不接触全量记忆", zoom: 0.62 }, + { label: path[1] ? path[1].label : "隐状态地址", value: "128 维紧凑查询地址", zoom: 0.38 }, + { label: path[2] ? path[2].label : "LSH 粗索引", value: "730 个桶,只取少量候选", zoom: 0.20 }, + { label: path[3] ? path[3].label : "候选页", value: "24 页 × 页容量 32", zoom: 0.105 }, + { label: path[4] ? path[4].label : "页内重排", value: "页内逐条打分", zoom: 0.052 }, + { label: path[5] ? path[5].label : "Top-K 记录", value: "最多 2 条", zoom: 0.026 }, + { label: path[6] ? path[6].label : "证据前缀", value: "45 token 注入模型内部", zoom: 0.014 } + ]; + + var W = 0, H = 0, dpr = 1; + function resize() { + dpr = Math.min(window.devicePixelRatio || 1, 2); + W = pin.clientWidth; + H = pin.clientHeight; + cv.width = Math.round(W * dpr); + cv.height = Math.round(H * dpr); + cv.style.width = W + "px"; + cv.style.height = H + "px"; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.imageSmoothingEnabled = true; + } + + /* ---- t ∈ [0,1] → 画一帧 ---- */ + function draw(t) { + if (!W || !H) return; + ctx.clearRect(0, 0, W, H); + ctx.fillStyle = "#000"; + ctx.fillRect(0, 0, W, H); + + /* 镜头窗口:从整张空间缩到 32×32(约五个数量级) */ + var i = Math.min(STEPS.length - 1, Math.max(0, t * STEPS.length)); + var a = STEPS[Math.floor(i)]; + var b = STEPS[Math.min(STEPS.length - 1, Math.floor(i) + 1)]; + var f = i - Math.floor(i); + var e = f * f * (3 - 2 * f); /* smoothstep,镜头不做生硬跳变 */ + var zoom = a.zoom + (b.zoom - a.zoom) * e; + + /* 保持长宽比并保证源矩形始终落在图内 */ + var sw, sh; + if (W >= H) { sw = SPACE * zoom; sh = sw * (H / W); } + else { sh = SPACE * zoom; sw = sh * (W / H); } + var cx = (1 - e) * (SPACE / 2) + e * tx; + var cy = (1 - e) * (SPACE / 2) + e * ty; + var sx = Math.max(0, Math.min(SPACE - sw, cx - sw / 2)); + var sy = Math.max(0, Math.min(SPACE - sh, cy - sh / 2)); + + ctx.globalAlpha = 1; + ctx.drawImage(off, sx, sy, sw, sh, 0, 0, W, H); + + /* 下降越深,越暗:让叠加的字有安静的地面(page-design §3) */ + var dim = Math.min(0.84, 0.30 + t * 0.62); + ctx.fillStyle = "rgba(0,0,0," + dim.toFixed(3) + ")"; + ctx.fillRect(0, 0, W, H); + + /* 越深,槽位骨架的网格线越显形 —— 空间「resolved」成结构 */ + if (t > 0.30) { + var ga = Math.min(0.22, (t - 0.30) * 0.5); + ctx.strokeStyle = "rgba(255,254,244," + ga.toFixed(3) + ")"; + ctx.lineWidth = 1; + var step = Math.max(18, 46 / (0.4 + t)); + var ox = ((tx - sx) / sw) * W, oy = ((ty - sy) / sh) * H; + ctx.beginPath(); + for (var x = ox % step; x < W; x += step) { ctx.moveTo(Math.round(x) + .5, 0); ctx.lineTo(Math.round(x) + .5, H); } + for (var y = oy % step; y < H; y += step) { ctx.moveTo(0, Math.round(y) + .5); ctx.lineTo(W, Math.round(y) + .5); } + ctx.stroke(); + } + + /* 终点:目标记录被圈出来(信号色,全页唯一一次) */ + if (t > 0.62) { + var pa = Math.min(1, (t - 0.62) / 0.18); + var px = ((tx - sx) / sw) * W, py = ((ty - sy) / sh) * H; + var r = 10 + 26 * (1 - pa) + 8 * Math.sin(performance.now() / 620); + ctx.strokeStyle = "rgba(0,255,106," + (0.90 * pa).toFixed(3) + ")"; + ctx.lineWidth = 1.25; + ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.stroke(); + ctx.beginPath(); ctx.arc(px, py, r * 0.34, 0, Math.PI * 2); ctx.stroke(); + } + } + + /* ---- HUD:把当前环节读出来(同时是这台机器的探针面) ---- */ + var hudIdx = document.getElementById("hudIdx"); + var hudName = document.getElementById("hudName"); + var hudVal = document.getElementById("hudVal"); + var hudBar = document.getElementById("hudBar"); + var lastStep = -1; + function hud(t) { + var s = Math.min(STEPS.length - 1, Math.floor(t * STEPS.length)); + if (s === lastStep) return; + lastStep = s; + var st = STEPS[s]; + if (hudIdx) hudIdx.textContent = String(s + 1).padStart(2, "0") + " / " + String(STEPS.length).padStart(2, "0"); + if (hudName) hudName.textContent = st.label; + if (hudVal) hudVal.textContent = st.value; + } + + /* ---- 一个循环:只在首屏可见时跑,不可见就停(省电,也便于量 FPS) ---- */ + var t = 0, raf = 0, visible = true, lastFrame = 0; + function progress() { + var r = host.getBoundingClientRect(); + var span = host.offsetHeight - pin.offsetHeight; + if (span <= 0) return 0; + return Math.max(0, Math.min(1, -r.top / span)); + } + function step() { + var want = progress(); + t += (want - t) * 0.12; /* 指数趋近:帧率无关的近似 */ + if (Math.abs(want - t) < 0.0004) t = want; + draw(t); + hud(t); + if (hudBar) hudBar.style.width = (t * 100).toFixed(1) + "%"; + lastFrame = (window.performance || Date).now(); + } + function loop() { raf = 0; step(); if (visible) raf = requestAnimationFrame(loop); } + /* rAF 会被节流或冻结(后台标签页、无头虚拟时间、低端 WebView)。 + 只有句柄没有帧会让循环永久卡住,所以超时就同步走一帧 —— + 滚动事件本身就能把画面推着走。 */ + function kick() { + if (!visible) return; + var now = (window.performance || Date).now(); + if (raf && now - lastFrame < 140) return; + step(); + if (!raf) raf = requestAnimationFrame(loop); + } + + /* ---- 静态帧:reduced-motion 与窄屏都走这里 ---- */ + function still() { + document.documentElement.classList.add("space--still"); + resize(); + var frozen = narrow ? 0.30 : 0.52; + draw(frozen); + hud(frozen); + } + + /* ---- 探针面(verification-harness §1):能被 seek / hold,才好量 ---- */ + window.__space = { + seek: function (v) { t = Math.max(0, Math.min(1, v)); draw(t); hud(t); }, + hold: function () { if (raf) { cancelAnimationFrame(raf); raf = 0; } }, + progress: progress, + steps: STEPS.length, + geo: GEO, + t: function () { return t; } + }; + + if (reduce || narrow) { + still(); + window.addEventListener("resize", function () { resize(); draw(narrow ? 0.30 : 0.52); }); + } else { + resize(); + if ("IntersectionObserver" in window) { + new IntersectionObserver(function (es) { + visible = es[0].isIntersecting; + if (visible) kick(); else if (raf) { cancelAnimationFrame(raf); raf = 0; } + }, { rootMargin: "120px" }).observe(host); + } + window.addEventListener("scroll", kick, { passive: true }); + window.addEventListener("resize", function () { resize(); kick(); }); + kick(); /* 循环常转:静止时目标点的呼吸也要活着 */ + } +})(); diff --git a/build.py b/build.py new file mode 100644 index 0000000..bb51c13 --- /dev/null +++ b/build.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Natural Memory v2 — 站点构建脚本 + +两种版式: + release —— 发布页(总览):无侧栏、全宽分段,按研究/模型发布页的通行骨架组织 + docs —— 文档页(其余 9 页):左侧栏 + 窄栏正文 + +把 `src/pages/*.body.html` 的正文片段套进 `templates/base.html` 骨架; +正文里可用 `{{include:partials/xxx.html}}` 引入 `src/partials/` 下的共用片段。 + +用法: + python build.py # 生成全部页面 + python build.py --check # 只校验,不写文件 +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import sys +from datetime import datetime + +ROOT = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(ROOT, "src", "pages") +PARTIALS = os.path.join(ROOT, "src", "partials") +TPL = os.path.join(ROOT, "templates", "base.html") + +# slug, 序号, 中文标题, 英文小标, 分组, , 摘要 +PAGES = [ + ("index", "00", "总览", "OVERVIEW", "概览", + "Natural Memory v2 — 模型内的记忆层", + "接在本地 Qwen3.5-4B 上的模型内记忆层。模型重启后不携带历史聊天记录,仍能访问已保存的个人与项目事实,且不把全部历史转换成 GPU 上的长 KV Cache。"), + ("division", "01", "记忆与 KV 的分工", "DIVISION OF LABOUR", "原理", + "记忆与 KV 的分工 — Natural Memory v2", + "为什么不是简单加长上下文:哪些东西该留在 Qwen 的热 KV 里,哪些该写入带地址的记忆记录,各自的边界在哪里。"), + ("mechanism", "02", "一次读取与一次写入", "MECHANISM", "原理", + "机制:读取路径、记录结构与写入状态机 — Natural Memory v2", + "从用户问题到证据前缀注入的八个环节,记录包含哪些字段,以及 active / superseded / quarantined / retracted 四个状态如何演化。"), + ("results", "03", "测量结果", "MEASUREMENTS", "证据", + "测量结果 — Natural Memory v2", + "同一 264 题题集的两个独立批次、结构化工程基准与记忆路由器工程线的全部实测数字,均标注出处与口径。"), + ("nm21", "04", "NM2.1 · 之后做了什么", "WHAT CHANGED NEXT", "证据", + "NM2.1:之后做了什么 — Natural Memory v2", + "三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。"), + ("examples", "05", "实测样例", "GROUND TRUTH", "证据", + "实测样例 — Natural Memory v2", + "五条逐字取自评测原始结果的问答对照,包含两条我们判错的样例。"), + ("ledger", "06", "真实评测台账", "FULL LEDGER", "证据", + "真实评测台账 — Natural Memory v2", + "全部 108 条真实评测 episode 的逐条台账:模型逐字回复、判定结果、停止原因、选中记录数与分数,可按来源与判定筛选。"), + ("cost", "07", "代价账本", "COST", "工程", + "代价账本 — Natural Memory v2", + "记忆带来的延迟、解码速度、前缀长度与显存开销,以及显存快照的口径说明。"), + ("capacity", "08", "容量与存储", "CAPACITY & STORAGE", "工程", + "容量与存储 — Natural Memory v2", + "地址空间、分页结构、有界 GPU cache 与嵌入式 weight-shard 持久化的实际数字与取舍。"), + ("limits", "09", "我们不宣称的内容", "BOUNDARIES", "工程", + "边界:我们不宣称的内容 — Natural Memory v2", + "没有足够证据支持的八条说法、五项主要局限,以及来自工程文档的原文引用。"), + ("roadmap", "10", "已解决的与下一步", "STATUS & NEXT", "工程", + "已解决的与下一步 — Natural Memory v2", + "在 12 GiB GPU 上已经跑通的闭环,以及按对正确率最有帮助的顺序排出的下一阶段任务。"), + ("reproduce", "11", "复现实验", "REPRODUCE", "工程", + "复现实验 — Natural Memory v2", + "正式脏数据迁移测试的完整命令、协议参数与产物路径。"), +] + +AUTHOR_SITE = "https://wpyw.site" +AUTHOR_MAIL = "wpy@wpy.email" + +# 每页的头号数字。文档页的页眉由一个**真实数字**领起,而不是只有标题: +# 这些页面本来就是「证据页」,让它自己先把最强的那条数字说出来。 +# 顺带承担移动端的字阶落差 —— 中文标题在 320px 上推不到 7×,数字可以。 +KEYS = { + "division": ("441.19", "tok", "批次 B 平均输入前缀 token"), + "mechanism": ("1,048,576", "", "地址空间设计容量"), + "results": ("82.52", "%", "批次 B 可回答正确率 · 批次 A 为 62.60%,两批配置不同、不可合并"), + "nm21": ("94.14", "%", "路由器 Top-1(v2 为 41.12%)"), + "examples": ("5", "", "逐字原文,其中 2 条是我们主动放进去的判错样例"), + "ledger": ("108", "", "条真实评测 episode,含全部失败"), + "cost": ("26.35", "ms", "读取路径额外耗时(批次 B;批次 A 为 61.10 ms)"), + "capacity": ("512", "B", "每条记录地址字节 · 100 万条约 512 MB"), + "limits": ("75.00", "%", "跨域未知泄漏率 —— 仍未解决,覆盖门已回退"), + "roadmap": ("52", "", "项单元测试通过"), + "reproduce": ("264", "", "题题集 · 246 可回答 / 18 未知"), +} + +GROUP_ORDER = ["概览", "原理", "证据", "工程"] +RELEASE_SLUG = "index" # 这一页用发布页版式,其余用文档版式 + + +def asset_version() -> str: + """assets 静态资源的短指纹:内容一变 URL 就变,浏览器不会再用到旧资源。""" + h = hashlib.md5() + files = [] + for sub in ("css", "js"): + d = os.path.join(ROOT, "assets", sub) + if os.path.isdir(d): + files += [os.path.join(d, f) for f in sorted(os.listdir(d)) if f.endswith((".css", ".js"))] + for p in files: + with open(p, "rb") as fh: + h.update(fh.read()) + return h.hexdigest()[:10] + + +def expand_includes(text: str) -> str: + """把 {{include:partials/x.svg}} 展开成 src/partials/x.svg 的内容。""" + def repl(m): + rel = m.group(1).strip().lstrip("/") + p = os.path.join(ROOT, "src", rel) + if not os.path.isfile(p): + sys.stderr.write("[警告] 找不到 include: %s\n" % rel) + return "<!-- include missing: %s -->" % rel + with open(p, "r", encoding="utf-8") as fh: + return fh.read().rstrip() + return re.sub(r"\{\{include:([^}]+)\}\}", repl, text) + + +def href_for(slug: str) -> str: + return "./" if slug == RELEASE_SLUG else "./%s" % slug + + +def render_sidebar(active: str) -> str: + out = ['<a class="side__back" href="./">← 返回发布页</a>'] + for group in GROUP_ORDER: + items = [p for p in PAGES if p[4] == group and p[0] != RELEASE_SLUG] + if not items: + continue + out.append('<p class="side__grp">%s</p>' % group) + out.append('<ul class="side__list">') + for slug, num, zh, en, _g, _t, _d in items: + cls = "side__a is-on" if slug == active else "side__a" + cur = ' aria-current="page"' if slug == active else "" + out.append('<li><a class="%s" href="%s"%s><span class="side__n">%s</span>' + '<span class="side__t">%s</span></a></li>' % (cls, href_for(slug), cur, num, zh)) + out.append("</ul>") + return "\n".join(out) + + +def render_prevnext(active: str) -> str: + """上一节 / 下一节:只在文档页出现,且把发布页作为首尾的邻页。""" + if active == RELEASE_SLUG: + return "" + order = [p[0] for p in PAGES] + idx = order.index(active) + prev_p = PAGES[idx - 1] if idx > 0 else None + next_p = PAGES[idx + 1] if idx < len(PAGES) - 1 else None + + def link(p, direction): + if not p: + return '<span class="pn__x"></span>' + slug, num, zh = p[0], p[1], p[2] + label = "上一节" if direction == "prev" else "下一节" + kbd = "←" if direction == "prev" else "→" + return ('<a class="pn__a pn__a--%s" href="%s" data-dir="%s">' + '<span class="pn__k">%s · %s</span><span class="pn__t">%s</span>' + '<span class="kbd">%s</span></a>' % (direction, href_for(slug), direction, label, num, zh, kbd)) + + return '<nav class="pn" aria-label="章节切换">%s%s</nav>' % (link(prev_p, "prev"), link(next_p, "next")) + + +def render_topnav(active: str) -> str: + """顶栏快速导航:发布页与文档页共用,当前页高亮。""" + items = [ + ("index", "发布页"), + ("mechanism", "机制"), + ("results", "测量"), + ("nm21", "NM2.1"), + ("ledger", "台账"), + ("limits", "边界"), + ("reproduce", "复现"), + ] + out = [] + for slug, label in items: + cls = "topnav__a is-on" if slug == active else "topnav__a" + cur = ' aria-current="page"' if slug == active else "" + out.append('<a class="%s" href="%s"%s>%s</a>' % (cls, href_for(slug), cur, label)) + return " ".join(out) + + +def render_main(slug: str, num: str, zh: str, en: str, desc: str, content: str) -> str: + if slug == RELEASE_SLUG: + # 发布页:无侧栏,全宽分段 + return ('<main class="release" id="doc">\n%s\n</main>' % content) + fig = "" + if slug in KEYS: + k, unit, note = KEYS[slug] + fig = (' <p class="doc__figure">\n' + ' <span class="mega">%s<small>%s</small></span>\n' + ' <span class="doc__figure-n">%s</span>\n' + ' </p>\n' % (k, unit, note)) + return ( + '<div class="shell">\n' + ' <aside class="side">\n' + ' <nav class="side__in" aria-label="章节导航">\n%s\n </nav>\n' + ' </aside>\n' + ' <main class="doc" id="doc">\n' + ' <header class="doc__hd">\n' + ' <span class="kicker">%s / %s</span>\n' + ' <h1 class="doc__h">%s</h1>\n' + ' <p class="doc__lede">%s</p>\n%s' + ' </header>\n%s\n%s\n </main>\n' + '</div>' % (render_sidebar(slug), num, en, zh, desc, fig, content, render_prevnext(slug)) + ) + + +_SVG_RE = re.compile(r"<svg\b.*?</svg>", re.S | re.I) +_TAG_RE = re.compile(r"<[^>]+>") +_WS_RE = re.compile(r"\s+") +_ENT = (("<", "<"), (">", ">"), ("&", "&"), (""", '"'), + ("'", "'"), ("←", "←"), ("→", "→"), (" ", " ")) + + +def _plain(html: str) -> str: + html = _SVG_RE.sub(" ", html) + html = _TAG_RE.sub(" ", html) + for a, b in _ENT: + html = html.replace(a, b) + return _WS_RE.sub(" ", html).strip() + + +def build_search_index() -> int: + """把每页正文按 h2/h3 切成条目,生成顶栏命令面板用的客户端检索索引。 + + 在 asset_version() 之前调用,这样 search.js 的内容也会进资源指纹。 + """ + entries = [] + for slug, num, zh, en, group, title, desc in PAGES: + body_path = os.path.join(SRC, "%s.body.html" % slug) + if not os.path.isfile(body_path): + continue + with open(body_path, "r", encoding="utf-8") as fh: + body = expand_includes(fh.read()) + + chunks = [] + # 以 h2/h3 为锚点切段:parts = [前导, level, 标题, 正文, level, 标题, 正文, ...] + parts = re.split(r"<h([23])\b[^>]*>(.*?)</h\1>", body, flags=re.S | re.I) + if len(parts) > 1: + lead = _plain(parts[0]) + if lead: + chunks.append((zh, lead)) + for i in range(1, len(parts) - 1, 3): + head = _plain(parts[i + 1]) or zh + rest = _plain(parts[i + 2]) if i + 2 < len(parts) else "" + chunks.append((head, rest)) + else: + chunks.append((zh, _plain(body))) + + entries.append({"s": slug, "p": href_for(slug), "g": group, "n": num, + "pg": zh, "h": "", "t": desc, "d": 1}) + for head, text in chunks: + entries.append({"s": slug, "p": href_for(slug), "g": group, "n": num, + "pg": zh, "h": head, "t": text[:220]}) + + out = os.path.join(ROOT, "assets", "js", "search.js") + with open(out, "w", encoding="utf-8", newline="\n") as fh: + fh.write("/* 由 build.py 生成:顶栏命令面板的全站检索索引。请勿手改。 */\n") + fh.write("window.NM_SEARCH = %s;\n" + % json.dumps(entries, ensure_ascii=False, separators=(",", ":"))) + return len(entries) + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--check", action="store_true", help="只校验,不写文件") + args = ap.parse_args() + + if not os.path.isfile(TPL): + sys.stderr.write("缺少模板 %s\n" % TPL) + return 2 + with open(TPL, "r", encoding="utf-8") as fh: + base = fh.read() + + n_idx = 0 if args.check else build_search_index() + av = asset_version() + missing, written = [], [] + for slug, num, zh, en, group, title, desc in PAGES: + body_path = os.path.join(SRC, "%s.body.html" % slug) + if not os.path.isfile(body_path): + missing.append("src/pages/%s.body.html" % slug) + continue + with open(body_path, "r", encoding="utf-8") as fh: + body = expand_includes(fh.read()).rstrip() + "\n" + + layout = "release" if slug == RELEASE_SLUG else "docs" + html = (base + .replace("{{TITLE}}", title) + .replace("{{DESC}}", desc) + .replace("{{PAGE_ID}}", slug) + .replace("{{LAYOUT}}", layout) + .replace("{{NUM}}", num) + .replace("{{ZH}}", zh) + .replace("{{EN}}", en) + .replace("{{GROUP}}", group) + .replace("{{ASSET_V}}", av) + .replace("{{TOPBAR_NAV}}", render_topnav(slug)) + .replace("{{MAIN}}", render_main(slug, num, zh, en, desc, body)) + .replace("{{BUILT}}", datetime.now().strftime("%Y-%m-%d %H:%M"))) + left = re.findall(r"\{\{[A-Z_]+\}\}", html) + if left: + sys.stderr.write("[警告] %s 仍有未替换占位符: %s\n" % (slug, set(left))) + + if not args.check: + out = os.path.join(ROOT, "index.html" if slug == RELEASE_SLUG else "%s.html" % slug) + with open(out, "w", encoding="utf-8", newline="\n") as fh: + fh.write(html) + written.append("index.html" if slug == RELEASE_SLUG else "%s.html" % slug) + + if missing: + sys.stderr.write("[缺失正文片段]\n " + "\n ".join(missing) + "\n") + print("页面 %d 个%s:" % (len(written), "(校验模式,未写盘)" if args.check else "")) + for w in written: + print(" " + w) + if n_idx: + print("检索索引 %d 条 → assets/js/search.js" % n_idx) + return 1 if missing else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/capacity.html b/capacity.html new file mode 100644 index 0000000..c1980a4 --- /dev/null +++ b/capacity.html @@ -0,0 +1,184 @@ +<!DOCTYPE html> +<html lang="zh-CN"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>容量与存储 — Natural Memory v2 + + + + + + + + + + + + + + + + + +
+ +
+
+ + + Natural Memory + v2 + + + 08 + 容量与存储 + + + + + + 读取中 + +
+
+
+ +
+ +
+
+ 08 / CAPACITY & STORAGE +

容量与存储

+

地址空间、分页结构、有界 GPU cache 与嵌入式 weight-shard 持久化的实际数字与取舍。

+

+ 512B + 每条记录地址字节 · 100 万条约 512 MB +

+
+

+ 「一百万记录容量」是地址空间和分页结构的容量,不是本次已经装入了一百万条语义记忆, + 也不是一百万条记录已经通过端到端测试。这一节把容量、实际用量与存储方式分开列清楚。 +

+
+
+

PAGE MAP · 32768 PAGES × 32 SLOTS

+ +

高亮部分为本次运行实际使用的页面(批次 A:23 页 / 723 条 active records;批次 B:24 页 / 738 条)。其余为地址空间余量。

+
+
+
+
+
+
+
+

PERSISTENCE

+

+

+
+
+

TRADE-OFF

+

取舍

+

+
+
+ + +
+
+ + + + + + + + + + + + + + + + diff --git a/cost.html b/cost.html new file mode 100644 index 0000000..b6dcf42 --- /dev/null +++ b/cost.html @@ -0,0 +1,173 @@ + + + + + +代价账本 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
+
+ + + Natural Memory + v2 + + + 07 + 代价账本 + + + + + + 读取中 + +
+
+
+ +
+ +
+
+ 07 / COST +

代价账本

+

记忆带来的延迟、解码速度、前缀长度与显存开销,以及显存快照的口径说明。

+

+ 26.35ms + 读取路径额外耗时(批次 B;批次 A 为 61.10 ms) +

+
+

+
+
+
+

+
+
+

VRAM · 12 GiB CARD

+
+

+ allocated 与 reserved 均为每次请求结束后的快照,不是 CUDA high-water mark; + reserved 还包含 PyTorch 分配器保留的缓存。因此这里不能当作"模型峰值显存"来引用。 +

+
+
+ + +
+
+ + + + + + + + + + + + + + + + diff --git a/division.html b/division.html new file mode 100644 index 0000000..a3c9eb9 --- /dev/null +++ b/division.html @@ -0,0 +1,284 @@ + + + + + +记忆与 KV 的分工 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
+
+ + + Natural Memory + v2 + + + 01 + 记忆与 KV 的分工 + + + + + + 读取中 + +
+
+
+ +
+ +
+
+ 01 / DIVISION OF LABOUR +

记忆与 KV 的分工

+

为什么不是简单加长上下文:哪些东西该留在 Qwen 的热 KV 里,哪些该写入带地址的记忆记录,各自的边界在哪里。

+

+ 441.19tok + 批次 B 平均输入前缀 token +

+
+

+ 这一节回答一个问题:既然模型已经有上下文窗口,为什么还要单独做一层记忆? + 答案不是「窗口不够长」,而是把长期事实塞进上下文,会以线性成本换来极低的复用率。 + 下面把「哪些东西留在 Qwen 的热 KV 里、哪些写进记忆记录」这条分界线逐层讲清楚。 +

+ +

先看没有这层记忆时会发生什么

+

+ 同一套题、同一协议下,原版 Qwen3.5-4B 在 246 道可回答题上只答对 3 道(1.22%)。 + 它的典型回答不是答错,而是明确表示拿不到信息: +

+
+ 作为一个人工智能模型,我无法访问您的个人历史对话记录、系统配置或您之前设定的具体规则……每次对话对我来说都是全新的开始。 + 原版 Qwen3.5-4B · 评测样本 user-009 原始回答 +
+

+ 这句话在事实上完全正确——模型确实没有这些信息。问题出在:这些信息本来就应该被保存下来, + 而它们不属于「当前这轮对话」,所以每轮都重新粘一遍历史是最笨的保存方式。 +

+ +

四层分工:谁存什么、为什么

+

+ 这是整份设计里最核心的一张表。判断标准只有一条:这份信息是「这一轮才有意义」还是「跨轮次可复用」。 +

+
+
+
层级存放什么由谁负责为什么放在这一层
+
+ 最近对话 + 这一轮前后的原话与顺序 + Qwen 热 KV + 指代、省略、语气、"你刚才说的那句"都依赖严格的先后顺序。这类信息复用价值低、时效性强,放 KV 最自然。 +
+
+ 长期个人 / 项目事实 + 事实、版本、来源、置信度、短文本证据 + Natural Memory 记录 + 跨会话反复被问到,且需要保留"哪个版本才是当前有效"的关系。它们不依赖顺序,只依赖地址与内容。 +
+
+ 当前问题 + 由问题生成的有界读取 + 路由 + Top-K + 不是所有记忆都该被读。当前问题只与少量页面和记录交互,避免"记忆越多、噪声越大"。 +
+
+ 原始持久化状态 + 记忆快照与索引 + 嵌入式 memory safetensors + 随模型包一起加载,运行时不需要额外数据库进程,也避免了磁盘分页带来的延迟抖动。 +
+
+
+ +
+

关键判断

+

+ Memory Slot 不试图逐 token 模拟完整 KV。它接管的是 KV 里最昂贵、最适合长期保存、 + 也最容易重复利用的那一部分;而当前对话的顺序关系,仍然由 Qwen 的原生上下文负责。 +

+
+ +

分工落到数字上是什么样

+

+ 下面这组对照最能说明「不是把历史灌进去,而是把命中的少量证据灌进去」。两组数字分别来自批次 A 与批次 B(同一题集、同一协议)。 +

+
+
+ 无记忆时的平均输入 + 30.75token + 批次 A 基线:只有问题本身,没有历史 +
+
+ 有记忆时的平均输入 + 441.19token + 批次 B:命中的证据前缀(批次 A 为 641.42) +
+
+ 前缀命中率 + 96.97% + 批次 B 实际注入证据的题目占比(批次 A 为 90.91%) +
+
+ GPU cache 实际用量 + 17,990token + 有界上限 131,072 token,用到约 13.7% +
+
+

+ 注意最后一项:上限是 131,072 token,实际只用了 17,990。 + 这说明读取路径被"有界"约束住了——不会因为记忆库变大就把显存吃满。 +

+ +

这条分工线换来了什么、付出了什么

+
+
+

换来的

+
+
可回答正确率1.22% → 82.52%
+
目标记忆召回0% → 92.68%
+
符号定位(192 题)0/192 → 181/192
+
跨会话问题1/6 → 5/6
+
+
+
+

付出的

+
+
平均输入前缀30.75 → 441.19 token
+
平均总延迟(批次 B)2854.9 → 2878.3 ms
+
平均解码速度(批次 B)22.34 → 20.68 tok/s
+
reserved 显存峰值3.234 → 5.180 GiB
+
+
+
+ +

这条线不能推到哪里

+
+
✕Natural Memory 已经等价于百万 token 的完整 KVUNSUPPORTED
+
✕长上下文限制已被解除(实测 8K 可跑,16K / 32K OOM)UNSUPPORTED
+
✕设计容量 1,048,576 条记录等于已经装入并验证了这么多UNSUPPORTED
+
+

+ 第三条尤其要注意:1,048,576 是地址空间与分页结构的容量, + 本次两个批次实际装载的是 723 / 738 条 active records,分别是 23 / 24 个页面。 + 容量上限与已装入量是两个概念,我们不在页面上把它们混为一谈。 +

+ + +
+
+ + + + + + + + + + + + + + + + diff --git a/examples.html b/examples.html new file mode 100644 index 0000000..503992c --- /dev/null +++ b/examples.html @@ -0,0 +1,161 @@ + + + + + +实测样例 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
+
+ + + Natural Memory + v2 + + + 05 + 实测样例 + + + + + + 读取中 + +
+
+
+ +
+ +
+
+ 05 / GROUND TRUTH +

实测样例

+

五条逐字取自评测原始结果的问答对照,包含两条我们判错的样例。

+

+ 5 + 逐字原文,其中 2 条是我们主动放进去的判错样例 +

+
+

+
+

+ + +
+
+ + + + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b9af1ee --- /dev/null +++ b/index.html @@ -0,0 +1,585 @@ + + + + + +Natural Memory v2 — 模型内的记忆层 + + + + + + + + + + + + + + + + + + + +
+
+ + + Natural Memory + v2 + + + 00 + 总览 + + + + + + 读取中 + +
+
+
+ +
+ +
+
+ +
+
+ Natural Memory · NM2.1 + 1,048,576 槽位 · 738 条活跃记录 · 730 个索引桶 +
+ +
+

模型重启之后,
它仍然记得。

+ +

+ + 1.22% + + 82.52% + + 同一 264 题题集的可回答正确率:无记忆基线 → 批次 B。 + 批次 A 为 62.60%,两批配置不同、不可合并。 +

+
+ +
+
+

+ +
+
+ 01 / 08 + 地址空间 + 1,048,576 槽位 · 738 条活跃记录 + +
+
+
+

向下滚动 · 走一遍读取路径

+
+
+ + +
+ +
+ + +
+
+

Evidence · 重启实验

+

把历史扔掉,它还记得吗?

+

+ 下面不是示意动画,是工程里两次真实运行的逐字回放。同一条事实、同一个问题、同一份协议; + 两次之间唯一的差别,是写入有没有真的落库。 +

+ +
+
+
+ + +
+ + +
+ +
+
+

写入的事实

+

+

重启之后问

+

+ +
+
+
记忆库记录0
+
+
+
+
重启时传入的历史false
+
+
+
+
注入模型内部的前缀0 tok
+
+

+
+
+
+ +
    +
    + +
    +
    + 模型输出 · 逐字 + +
    +

    +
    +
    +
    + +

    +
    +
    + + +
    +
    +
    + + + + + + + + + + + 01 + 用户问题 + 自然语言 + + + + 02 + 隐状态地址 + Qwen hidden + + + + 03 + LSH 粗索引 + 730 桶 + + + + 04 + 候选页 + ≤4 页 · 32/页 + + + + 05 + 页内重排 + text_retriever + + + + 06 + Top-K 记录 + ≤2 条 + + + + 07 + 证据前缀 + 注入 Qwen + + + + 08 + 普通生成 + 回答 + + + + 无全量注意力 + BOUNDED READ + 当前问题不与全部记忆记录做注意力,只与少量页面和记录交互 + 读取额外耗时 61.10 ms → 26.35 ms(批次 A → B) · 前缀命中率 96.97% · GPU cache 实际 17,990 / 上限 131,072 token + +
    + 图 1 · 读取路径 + 一次问答里记忆层做了什么。用户问题只与少量页面和记录交互,全程没有对全量记忆做注意力; + 命中的短证据作为模型内部前缀注入 Qwen。图为示意,环节顺序与实现一致。 +
    +
    +
    +
    + + +
    +
    +

    Abstract

    +

    这是什么东西

    +

    + 它接在本地 Qwen3.5-4B 上,是一个模型内的记忆层:把适合长期复用的个人事实、项目事实和短对话片段 + 写进带地址的记忆记录;当前问题只读取少量相关记录,再把这些记录作为模型内部前缀交给 Qwen 生成答案。 +

    +

    + 它要解决的具体问题是:模型重启后不携带历史聊天记录,仍能访问已经保存的个人或项目事实, + 同时不把全部历史转换成 GPU 上的长 KV Cache。 +

    +

    + 当前状态:研究原型到工程验证之间。同一 264 题题集上,可回答正确率从 1.22% 提升到 62.60%(批次 A) + 与 82.52%(批次 B);代价是输入前缀变长、解码速度下降约 10%。我们不在页面上把它说成通用智能, + 也不把实验室结果表述成线上服务承诺。 +

    +
    +
    + + +
    +
    +

    What's in this release

    +
    +
    +

    记忆层与索引

    +

    738 active records

    +

    批次 B 实际装载;24 个页面,页容量 32 条。地址空间设计容量 1,048,576。

    +
    +
    +

    记忆路由器

    +

    2,037,774 参数

    +

    16 张量 / float32 / 512 B 地址;结构 dim=128 · heads=8 · hidden=2560 · max_hops=3。

    +
    +
    +

    有界 GPU cache

    +

    256 records

    +

    上限 131,072 token;批次 B 实际用到 17,990 token,fallback 与分配失败均为 0。

    +
    +
    +

    评测题集

    +

    264 题

    +

    246 可回答 / 18 未知;真实仓库 220 题 + 项目对话事实 44 题。

    +
    +
    +

    可复核性

    +

    逐字原文

    +

    样例页的问答全部取自评测原始结果、未经润色,并包含两条我们判错的样例。

    +
    +
    +

    复现

    +

    一条命令

    +

    正式脏数据迁移测试的完整命令、协议参数与产物路径见复现页。

    +

    去复现 →

    +
    +
    +
    +
    + + +
    +
    +

    Headline result

    +
    +
    +
    +
    + 可回答正确率 + 同一 264 题题集 · 246 可回答 +
    +
    原版 Qwen + + 1.22%3/246
    +
    批次 A + + 62.60%154/246
    +
    批次 B + + 82.52%203/246
    +
    +
    + 原版 Qwen3.5-4B + 批次 A + 批次 B +
    +

    + 两个批次同一题集、同一协议,但配置不同,分数不可合并,只能并列读。 + 目标记忆召回:批次 A 67.48%、批次 B 92.68%——检索召回率不等于回答正确率。 +

    +
    +
    +
    +
    总体正确率7.58% → 64.39% → 82.95%
    +
    符号定位0/192 → 129/192 → 181/192
    +
    未知拒答17/18 → 16/18 → 16/18
    +
    读取额外耗时— → 61.10 → 26.35 ms
    +
    解码速度24.06 → 21.68 → 20.68 tok/s
    +
    +

    全部测量结果 →

    +
    +
    +
    +
    + + +
    +
    +

    After this release · NM2.1

    +

    发布之后又做了什么

    +

    + 上面所有数字都来自 v2。之后的 NM2.1 换掉了记忆路由器、修掉写入路径上一个会把事实互相销毁的缺陷, + 并把「问库里没有的属性时照样编答案」这个 75% 的失败修到 0%。 +

    + + +
    +

    未知拒答率 · 24 条同形候选

    +

    + 100.00% + 从 0.00% 起 —— 修好之前,模型 75% 的情况下照样编一个答案 +

    +

    + 这是整个项目最该被引用的一个数字。它回答的不是「能不能检索到」,而是 + 「不知道的时候会不会不懂装懂」。 +

    +
    + +
    +
    +

    未知拒答率 · 同形候选

    +

    0.00% →100.00%

    +

    问库里不存在的属性时,正确说「不知道」的比例。修好之前,模型 75% 的情况下照样编一个答案。

    +
    +
    +

    写 20 条不同属性后存活

    +

    12/20 →20/20

    +

    v2 的写入路径会把 8 条无关事实互相 retract,让端到端正确率存在 50% 的硬上限——任何排序器都救不回一条已被删掉的记录。

    +
    +
    +

    零字面重叠改写

    +

    43.75% →68.75%

    +

    查询与事实之间没有任何独特字重叠时的回答正确率;24 条同句式候选的随机水平是 4.17%。

    +
    +
    + +
    +
    +

    + 代价几乎为零:参数量 2,037,774 不变、每条记录地址 512 字节不变、模型包 8.88 GB 不变; + 交错基准(7 轮,消除顺序效应)单查询延迟 1.4203 → 1.4242 ms,差 +0.28%, + 小于同一轮里原版自身 4.85% 的离散度。 +

    +

    + 失败也一并写出来:那套「先判属性、再查库」的机制在离线实验里是 100.00% 拒答 / 0.00% 误拒, + 但接上运行时连续失败两次——第一次把可回答正确率从 100.00% 压到 6.25%; + 第二次修好了目标域(泄漏真的到 0.00%、可回答无损)却让另一个域崩到 0.00%。 + 最终该项已回退,原因也定位到了。 +

    +

    + 完整技术论证 → + 108 条真实台账 → +

    +
    +
    +

    路由器工件本身的提升

    +
    +
    Top-1 正确率41.12% → 94.14%
    +
    Recall@346.73% → 96.48%
    +
    MRR47.69% → 95.77%
    +
    hop 正确率73.23% → 100.00%
    +
    hop 欠预测率4.68% → 0.00%
    +
    替换兼容性DROP-IN OK(16/16 键)
    +
    单元测试52 项通过
    +
    +

    + 这些是路由器工件在冻结评测集上的增益。它不会自动传递到最终答案 —— + 原因见技术论证页的第十节。 +

    +
    +
    +
    +
    + + +
    + +
    + + +
    +
    +

    Boundaries · 我们不宣称的内容

    +
    +
    ✕已经等价于百万 token 的完整 KV
    +
    ✕已经击败生产级 embedding + reranker RAG
    +
    ✕任何自然语言表达都能稳定召回正确记忆
    +
    ✕记忆写入永远不会出错
    +
    ✕一百万条记录已经完成端到端验证
    +
    ✕当前 4B 结果可以直接外推到 14B、32B 或更大模型
    +
    +

    + 完整的八条与五项主要局限见 边界页。 + 把它们放在发布页正面,是因为它们和上面的数字一样重要。 +

    +
    +
    + + +
    +
    +

    Citation

    +
    +
    + BibTeX + +
    +
    @misc{naturalmemoryv2,
    +  title  = {Natural Memory v2: an in-model memory layer for local LLMs},
    +  author = {wpy},
    +  year   = {2026},
    +  note   = {Research prototype on Qwen3.5-4B. 264-question dirty-corpus
    +            transfer test; two independent batches.},
    +  url    = {https://wpyw.site}
    +}
    +
    +

    + 引用时请一并注明「批次 A 与批次 B 配置不同、分数不可合并」,否则单个数字会被误读。 +

    +
    +
    + +
    + + + + + + + + + + + + + + + + diff --git a/ledger.html b/ledger.html new file mode 100644 index 0000000..8b88f6f --- /dev/null +++ b/ledger.html @@ -0,0 +1,183 @@ + + + + + +真实评测台账 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 06 + 真实评测台账 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 06 / FULL LEDGER +

    真实评测台账

    +

    全部 108 条真实评测 episode 的逐条台账:模型逐字回复、判定结果、停止原因、选中记录数与分数,可按来源与判定筛选。

    +

    + 108 + 条真实评测 episode,含全部失败 +

    +
    +

    + 这一页不是挑出来的样例,是全部 108 条真实评测 episode 的逐条台账。 + 每条都带模型的逐字回复、判定结果、停止原因、选中记录数与分数。 +

    +

    + +
    +
    +
    +
    + +
    + + +
    + +
    + +

    + 数据来自 runtime_score_bands.json 与 nm2_1_final_runtime_e2e.json, + 由 tools/make_ledger.py 生成 assets/js/ledger.js, + 字段逐字复制,未做改写、润色或换算。模型输出里的错字、冗余和编造的内容都按原样保留。 + 期望锚点是评测里的随机码(形如 VAL-XXXXXXX),它只出现在唯一一条候选事实里, + 因此可以用来检测「证据混用」。 +

    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/limits.html b/limits.html new file mode 100644 index 0000000..45d816b --- /dev/null +++ b/limits.html @@ -0,0 +1,164 @@ + + + + + +边界:我们不宣称的内容 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 09 + 我们不宣称的内容 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 09 / BOUNDARIES +

    我们不宣称的内容

    +

    没有足够证据支持的八条说法、五项主要局限,以及来自工程文档的原文引用。

    +

    + 75.00% + 跨域未知泄漏率 —— 仍未解决,覆盖门已回退 +

    +
    +

    +
    +

    五项主要局限

    +
    +

    来自工程文档的原文

    +
    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/mechanism.html b/mechanism.html new file mode 100644 index 0000000..2c18b5b --- /dev/null +++ b/mechanism.html @@ -0,0 +1,335 @@ + + + + + +机制:读取路径、记录结构与写入状态机 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 02 + 一次读取与一次写入 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 02 / MECHANISM +

    一次读取与一次写入

    +

    从用户问题到证据前缀注入的八个环节,记录包含哪些字段,以及 active / superseded / quarantined / retracted 四个状态如何演化。

    +

    + 1,048,576 + 地址空间设计容量 +

    +
    +

    + 这一节把「一次读取」和「一次写入」拆开讲透:读取路径有哪八个环节、每一步的输入输出是什么、 + 一条记录里到底存了什么、四个状态如何演化,以及我们为这些决定付出了什么代价。 +

    + +

    一、读取路径:从提问到证据注入

    +
    + + + + + + + + + + + 01 + 用户问题 + 自然语言 + + + + 02 + 隐状态地址 + Qwen hidden + + + + 03 + LSH 粗索引 + 730 桶 + + + + 04 + 候选页 + ≤4 页 · 32/页 + + + + 05 + 页内重排 + text_retriever + + + + 06 + Top-K 记录 + ≤2 条 + + + + 07 + 证据前缀 + 注入 Qwen + + + + 08 + 普通生成 + 回答 + + + + 无全量注意力 + BOUNDED READ + 当前问题不与全部记忆记录做注意力,只与少量页面和记录交互 + 读取额外耗时 61.10 ms → 26.35 ms(批次 A → B) · 前缀命中率 96.97% · GPU cache 实际 17,990 / 上限 131,072 token + +
    + +

    + 点左侧任一环节查看它在做什么,也可以依次点一遍——注意整条链路上没有一步是「和全部记忆做注意力」, + 这是显存可控的根本原因。 +

    + +
    +
    +
    +

    STAGE 01 / 08

    +

    +

    +

    +
    +
    读取上限2 records
    +
    候选页上限4 pages
    +
    页容量32 / page
    +
    全量注意力否
    +
    +
    +
    + +

    每一步的输入、输出与约束

    +

    + 括号里的数字是实测值,不是设计目标。 +

    +
    +
    +
    环节输入 → 输出约束或实测
    +
    01 用户问题普通自然语言不要求任何显式指令,不需要输入 /remember 之类的标记。
    +
    02 Qwen hidden state问题 → 紧凑查询地址地址由模型隐状态生成,不依赖外部 embedding 服务;语义地址编码用单条 batch 执行,避免编码阶段出现显存瞬时峰值。
    +
    03 LSH / 地址粗索引查询地址 → 候选页精确桶 + Hamming-1/2 探针;批次 A 的记忆状态里粗索引有 730 个桶、最近一次粗筛返回 24 个候选。
    +
    04 候选页候选页 → 少量页面页容量 32 条记录,默认最多读取 4 页;本次 active records 723 条落在 23 个页面上。
    +
    05 页内记录重排页内记录 → 有序候选记录级重排由独立的 text_retriever 完成(6 张量、1,573,377 参数)。它的 Top-1 命中率本身只有 23.20%,是当前的主要瓶颈之一。
    +
    06 Top-K 记录有序候选 → 少量证据正式测试中的读取上限为 2 条记录。目标记忆召回:批次 A 166/246,批次 B 228/246。
    +
    07 证据前缀注入证据 → 模型内部前缀命中的短证据被提升到 GPU 作为前缀;批次 B 有 96.97% 的题目实际注入了前缀,平均 410.34 token(批次 A 为 610.67)。
    +
    08 普通生成前缀 + 问题 → 回答记忆主体始终留在进程内存,只有热点记录进入有界 GPU cache。读取路径额外耗时:批次 A 61.10 ms → 批次 B 26.35 ms。
    +
    +
    + +

    二、一条记录里到底存了什么

    +

    + 记录不是一段文本,而是一条带地址、带版本关系、带状态的结构化数据。下面左侧是全部字段, + 右侧是一条真实记录(逐字取自评测结果里的命中记录)。 +

    +
    +
    +
    +
    +
    +
    +
    +
    + +

    三、状态机:旧的答案不会凭空消失

    +

    + 同一实体和属性出现新值时,旧值被标记为 superseded,而不是被覆盖或删除。 + 这是「时间冲突」类问题能答对的前提:既要给出当前有效值,也要能说清旧值去哪了。 +

    +
    + +
    +

    写入路径的教训:检索救不回不存在的记录

    +

    + 早期版本的退役授权完全依赖一个学习打分(阈值 ≥0.95),没有任何结构校验。 + 结果是新写入把已有记录误判为旧值并退役:一次 20 条记录的测试里 active 只剩 12 条、retract 高达 16 次、目标记录只存活 8/16。 + 修好之后:active 12/20 → 20/20,retract 16 → 0,目标记录存活 8/16 → 16/16,端到端 37.50% → 68.75%。 + 这件事的结论很直白——任何排序器都救不回一条已经不存在的记录。 +

    +
    + +

    四、四个设计决定与它们的代价

    +
    +
    +

    决定 1 · 两段式读取

    +

    粗索引筛页 → 页内重排,而不是对全量记录做注意力。

    +

    好处:单次读取的候选规模被压到固定上限(≤4 页 / ≤2 条),成本与记忆总量解耦。 + 代价:两段都可能出错,且错误会叠加——粗索引错了就再没有机会。

    +
    +
    +

    决定 2 · 用地址索引而非向量库

    +

    语义地址 + LSH 桶放在模型包内,不引入外部检索服务。

    +

    好处:部署时没有额外进程与网络依赖,读取耗时稳定(批次 B 26.35 ms)。 + 代价:召回质量受地址质量限制,零字面重叠的改写问法曾只有 18.40% 的 Top-1。

    +
    +
    +

    决定 3 · 有界 GPU cache

    +

    热点记录进 GPU,上限 256 条 / 131,072 token,另有动态保留。

    +

    好处:显存占用可预测,两个批次的实际用量分别是 15,885 / 17,990 token,fallback 与分配失败均为 0。 + 代价:cold page 会带来额外取数路径——不过本次两个批次都是 embedded / process-RAM 模式,cold page 为 0。

    +
    +
    +

    决定 4 · 嵌入式 weight-shard 持久化

    +

    记忆快照写进模型包的 memory safetensors 切片,默认不用 SQLite、不用磁盘分页。

    +

    好处:运行时依赖最少,重启只需加载权重切片、不回放历史聊天。 + 代价:模型包会随记忆增长变大,且固化更新需要重新保存权重切片——这是明确的工程负担,不是白拿的。

    +
    +
    + +

    五、这套机制仍然做不到的事

    +
    +
    ✕任何自然语言表达都能稳定召回正确记忆UNSUPPORTED
    +
    ✕记忆写入永远不会出错UNSUPPORTED
    +
    ✕检索召回率等于回答正确率(批次 B 召回 92.68% / 回答 82.52%,差 10.16pp)UNSUPPORTED
    +
    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/nm21.html b/nm21.html new file mode 100644 index 0000000..32dcb6a --- /dev/null +++ b/nm21.html @@ -0,0 +1,704 @@ + + + + + +NM2.1:之后做了什么 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 04 + NM2.1 · 之后做了什么 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 04 / WHAT CHANGED NEXT +

    NM2.1 · 之后做了什么

    +

    三处改动各自的测量依据、把端到端正确率锁死在 50% 的写入缺陷如何定位、75% 未知泄漏如何修到 0%,以及它接上运行时连续失败两次的完整记录。

    +

    + 94.14% + 路由器 Top-1(v2 为 41.12%) +

    +
    +

    + 发布页上的数字来自 v2。这一页讲**之后又做了什么**:三处改动各自凭什么、一个把端到端正确率 + 锁死在 50% 的写入缺陷怎么定位的、以及一个 75% 泄漏的失败怎么修到 0%——又怎么在接上运行时之后失败两次。 + 所有数字都来自磁盘上的报告文件,每张表都标了出处;**没有一条是估算或外推**。 +

    + +
    +

    先读这一句

    +

    + 这一页里**离线实验最好的数字(未知拒答 100.00%)不等于运行时可用**。它接上运行时之后 + 连续失败两次,最终被回退。把失败过程一起写出来,是因为它比成功更能说明这套机制到底卡在哪。 +

    +
    + + +

    净变化一览

    +

    出处 · V2_dpskw\NM2_VS_NM2_1.md(该文件自述:所有数字均从磁盘上的评分卡 / JSON 读取,不是手工抄录)

    + +
    + + + + + + + + + + + + +
    轴v2(原版)NM2.1净变化
    路由器 Top-141.12%94.14%+53.02 pp
    路由器 Recall@346.73%96.48%+49.75 pp
    未知拒答率0.00%100.00%+100.00 pp
    未知问题泄漏(同形候选)75.00%0.00%−75.00 pp
    一次写 20 条不同属性后存活12 / 2020 / 20+8 条
    零字面重叠改写正确率43.75%68.75%+25.00 pp
    hop 欠预测率4.68%0.00%−4.68 pp
    参数量 / 每条记录地址字节2,037,774 / 5122,037,774 / 512不变
    +
    +

    + 「未知拒答率」与「未知泄漏」是同一件事的两面:前者是问库里没有的属性时**正确说不知道**的比例, + 后者是**照样编一个答案**的比例。v2 在这两轴上是 0% 和 75%,也就是几乎必然硬答。 +

    + + +

    一 · 路由器工件本身的检索与排序

    +

    出处 · router_scorecard_final.json(判定见 router_verdict_final.json)· 冻结 v6 评测集 21,920 条 / 10 类别

    + +
    + + + + + + + + + + + + + + +
    指标v2 · V2-128 deployed(v3)NM2.1 · REPLAY-128 v7 final
    Top-1 正确率41.12%94.14%
    Recall@137.03%88.58%
    Recall@346.73%96.48%
    Recall@548.91%97.15%
    MRR47.69%95.77%
    nDCG@344.22%95.37%
    多跳证据全中(Top-3)43.43%96.22%
    多跳证据全中(仅多正例)37.15%95.45%
    hop 正确率73.23%100.00%
    hop 欠预测率4.68%0.00%
    +
    + + +

    二 · 拒答与仲裁策略轴

    +

    出处 · router_scorecard_final.json(门槛 0.50)+ threshold_sweep_check.json(0.30–0.80 全门槛扫描)

    + +
    + + + + + + + + + + + +
    指标v2NM2.1
    need F189.58%100.00%
    need 召回99.82%100.00%
    need 精确率81.24%100.00%
    未知拒答率0.00%100.00%
    已知问题被误拒率0.18%0.00%
    未知问题被误读率100.00%0.00%
    仲裁准确率81.12%100.00%
    +
    +

    + 门槛扫描是这里真正的证据。0.30 / 0.40 / 0.50 / 0.60 / 0.70 / 0.80 六个门槛逐一复核: + NM2.1 **全门槛通过**;而原版在**每一个**门槛上未知拒答率都是 0.00%,且门槛越高误拒越差 + (0.00% → 1.32%)。一个能在整条门槛轴上都成立的结论,和一个只在某个阈值上成立的点,可信度不是一回事。 +

    + + +

    三 · 端到端整体记忆能力(同一份运行时,只有模型包不同)

    +

    出处 · nm2_battery_comparison_final.json · A 110 用例 / B 16 / C 48 / D 重启持久化

    + +
    + + + + + + + + + + + + + + + + +
    指标原版 NM2NM2.1(仅换路由器)NM2.1 最终
    A 总体正确率89.09%88.18%88.18%
    A 可回答正确率100.00%100.00%100.00%
    A 未知拒答率60.00%56.67%56.67%
    A 已知问题被误拒率0.00%0.00%0.00%
    B 零字面重叠改写回答正确率43.75%68.75%68.75%
    B 答成别的属性18.75%18.75%18.75%
    B 触发读取56.25%93.75%93.75%
    C 可回答正确率(24 同形候选)65.00%65.00%70.00%
    C 答成别的属性35.00%35.00%27.50%
    C 未知泄漏率75.00%75.00%0.00%
    C 活跃记录 min / max23 / 2423 / 2423 / 24
    D 重启后召回 / 作答 / 清理通过通过通过
    +
    +

    + 运行间波动必须自己讲出来:B 段只有 16 个用例,1 个用例 = 6.25 pp。所以 B 的 68.75% + 与另一次测得的 75.00% 属于同一水平的运行间波动,**不作为增益主张**。A / C / D 的结论在多次运行中一致。 +

    + + +

    四 · 零字面重叠改写的泛化

    +

    出处 · replay_check_zov.json(路由器级)与上表 B / C 段(端到端)· 数据集 300 条、24 个未见过的问法

    + +
    +
    +
    + + + + + + + + + +
    指标v2 权重NM2.1 权重
    路由器 Top-1(250 条可回答)18.40%59.60%
    路由器 Recall@336.00%83.20%
    路由器 MRR35.07%73.06%
    端到端改写正确率(B 段)43.75%68.75%
    端到端未知泄漏(C 段)75.00%0.00%
    +
    +
    +
    +

    + 零重叠评测集有 24 条**同句式、不同属性**的候选,随机猜中的概率是 4.17%。 + 所以 59.60% 大约是随机水平的 14 倍。 +

    +
    +
    数据集规模300 条评测 / 24 个未见问法
    +
    训练 / 留出划分前 2 个改写训练(48)/ 第 3 个留出(24)
    +
    两个划分共享的查询字符串0
    +
    查询与自身目标的重叠0
    +
    含「陷阱」候选的 episode54.53%(生成器统计)
    +
    +

    + 关键设计判断:改写与**无关**属性的字符重叠被刻意保留(命中错误候选只会得到错误答案, + 让任务更难,不提供捷径);真正必须为 0 的是与**自身**目标的重叠。最初把二者混为一谈时, + 24 个属性里有 7 个因为「起床时间」的「时」、「办公城市」的「公」这类高频字被判为「无可用的改写」。 +

    +
    +
    + + +

    五 · 代价:几乎没有

    +

    出处 · router_scorecard_final.json、router_latency_bench_prod.json(7 轮交错取中位数)

    + +
    + + + + + + + + + + +
    指标v2NM2.1说明
    参数量2,037,7742,037,774未增加
    每条记录地址字节512512存储几何未变
    单查询延迟中位数(GPU)0.9549 ms0.9169 ms略快
    批量 QPS(batch=64)66,03769,378+5.1%
    批量 QPS(batch=256)207,287242,759+17.1%
    模型包大小8.88 GB / 22 文件8.88 GB / 23 文件多出属性头 ~0.25 MB
    +
    +

    + 交错基准(7 轮,消除顺序效应):单查询中位数 1.4203 ms → 1.4242 ms,差 +0.28%。 + 同一轮里原版自身离散度就有 4.85% —— 也就是说这个差值小于测量噪声。 + 零重叠那一轮里 batch=64 的读数低 9.2%,但该轮离散度是 17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快, + 方向不一致,判为测量噪声而非回归;这条也写在这里,因为把噪声当结论是这个领域最常见的错误。 +

    + + +

    六 · 工程鲁棒性

    +
    + + + + + + + + + +
    项目v2NM2.1
    一次写 20 条不同属性事实后存活12 / 20(8 条查询前被误删)20 / 20
    端到端(写入修复前后,16 用例)37.50%68.75%
    替换兼容性基线DROP-IN OK(16 / 16 键)
    单元测试—52 项通过
    未知问题泄漏(同形候选)75.00%0.00%
    +
    + + +

    七 · 三处改动,每处都有测量依据

    + +

    改动 1 · 记录排序混合权重取 0.5

    +

    + memory_record_router_blend 控制「打包的词面重排器」与「学习出的路由器」各占多少。 + 调这个之前先测了剂量曲线 —— 而且**此前的同进程测量因顺序污染已作废**,下面这版是独立进程测的。 +

    +
    + +

    改动 2 · 把加法先验参数化,然后测出「不该动」

    +

    + 假设是「加法先验压过了学习打分」。把它暴露成可调再测,结果**调小只会更差**: +

    +
    +

    + 这是一个有价值的负面结论:它否定了原来的假设。项目里这类结论和增益一样被保留下来, + 因为它们决定了后面不再往哪个方向投入。 +

    + +

    改动 3 · 覆盖门 —— 以及为了让它真正生效必须一起修的两处

    +

    + 覆盖门本身很简单:先判断「这个提问在问哪个属性」,再查库里有没有这个属性。但单独加门是不生效的, + 有两处**必须同时修**,否则门会被静默绕过: +

    +
    +
    01
    +

    _build_text_prefix 短路

    +

    门拒绝之后如果继续往下回落,会走**旧版 16 槽注入路径**,把记忆又塞回去。 + 实测:不修这一处,泄漏只从 75.00% 降到 62.50%。

    +
    02
    +

    闭包要动态解析 bank

    +

    reset_memory() 每次都会新建 memory_os_v2;闭包捕获了旧引用的话, + 门会永远看到一个空 bank,于是**静默旁路** —— 实测诊断输出 applicable:0 / bypassed:1。

    +
    03
    +

    门必须自门控

    +

    只有当库的属性集合填充了头部词表的 ≥ 90% 时才让门生效。 + 这条阈值是必需的:放宽成「子集即可」会让门在通用套件上误判, + 把 A 段可回答正确率从 100.00% 压到 92.50%、已知误拒升到 2.50%;收紧到 90% 后 A 段完全恢复, + C 段泄漏仍为 0.00%。

    +
    + + +

    八 · 写入路径:为什么 20 条事实会互相销毁

    +

    出处 · V2_dpskw\WRITE_PATH_FIX.md(根因由栈追踪确证,非推断)

    + +
    +

    症状

    +

    + 一次写入 20 条不同属性的事实后,库里有 20 条记录但只剩 12 条 active, + 8 条在查询前就被 retract。16 个关键零重叠用例里有 8 个的目标记录已经不存在 —— + 因此端到端正确率存在 50% 的硬上限。**任何排序器都救不回一条已经被删掉的记录。** +

    +
    + +

    16 次 retract 的调用栈完全一致:

    +
    +
    call stack
    +
    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」这一条: +

    + +
    + + + + + + + + + +
    写入序号事实learned_best结果
    2出生城市0.998959confirmed_update = True
    3办公城市0.992638confirmed_update = True
    9工位楼层0.964987confirmed_update = True
    12手机尾号0.998516confirmed_update = True
    15办公楼层0.997487confirmed_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 条。 +

    + +
    +
    +

    辅助事实 1

    +

    0 / 20

    +

    exact_slots.numel() > 0(token 完全相同)一次都没命中 —— 精确重复路径与此无关。

    +
    +
    +

    辅助事实 2

    +

    {2,3,5,6,7}

    +

    shared >= 2 毫无区分力:19 个候选的 shared 全落在这个区间,因为每条事实都含「我的 / 是」模板词元。

    +
    +
    +

    辅助事实 3

    +

    0.9985

    +

    190 个不相关属性对里有 10 个 ≥ 0.95,最高 0.9985(「常住城市」vs「出生城市」)。调阈值不可行:假阳性高达 0.9989,没有任何标量切点能分开「不相关属性」与「真更新」。

    +
    +
    + +

    + 根因:retire 一条已存在记录的授权完全来自学习打分加一个固定 0.95 阈值, + 没有任何「新文本与在位记录共享 (entity, attribute)」的结构性校验。 +

    + +

    修复(两处,都是结构性的)

    +
    +
    01
    +

    confirmed_update 的两条打分类分支改为结构优先

    +

    仅当候选文本的 entity::attribute 已经在库的 + active_by_conflict 账本里存在(即确实是同一条事实的更新)才允许由打分授权; + exact_slots 这条结构安全的分支保持不变,以保留「重复写入幂等」的既有语义。

    +
    02
    +

    retract 循环加冲突键守卫

    +

    if record.conflict_key() != candidate_conflict_key: continue —— + 循环只能退掉描述同一属性的记录,不再因「共享热槽」或「词面重叠高」销毁无关事实。

    +
    +

    + 语义不受损:真·同属性更新本来就已经由 PagedMemoryBankV2.write 通过 + active_by_conflict 做版本化(旧版本保留为 superseded、version+1), + 修复只是移除了那条额外的、无监督的销毁路径。 +

    + +

    四级验证

    +
    + + + + + + + + + + + + +
    级检验修复前修复后
    ①status_transition_summary{"active->retracted": 16}{}
    ①retraction_calls / status_counts16 次 / {retracted: 8, active: 12}[] / {active: 20}
    ②正对照:同属性改值—旧值 superseded + 新值 active,其余 19 属性不受影响,0 retract
    ③重排器缺席对照会踩死代码分支20 条全 active、0 retract
    ④target_record_active8 / 1616 / 16
    ④target_record_retracted_or_absent80
    ④active_records_min / max12 / 1220 / 20
    ④probe 口径回答正确率25.00%(4 / 16)43.75%(7 / 16)
    +
    + +

    端到端效果(官方 harness,16 个零重叠用例):

    +
    + + + + + + + +
    路由器修复前修复后答成别的属性
    deployed(原版 NM2)25.00%50.00%(+25.00 pp)18.75% → 12.50%
    V2-128-v637.50%68.75%(+31.25 pp)25.00% → 18.75%
    REPLAY-12837.50%68.75%(+31.25 pp)25.00% → 18.75%
    +
    +

    + 一个诚实的补充:这 16 个用例里 93.75% 的读取仍走旧版 16 槽路径而非 V2 库记录, + 地址命中 0 / 16。写入路径的修复解除了 50% 的硬上限,但「V2 库记录真正参与读取」这一项仍未解决。 +

    + + +

    九 · 未知拒答:从 75% 泄漏到 100% 拒答

    +

    出处 · V2_dpskw\ABSTENTION_BREAKTHROUGH.md

    + +

    + 这是整个项目最差的一个数字:问库里不存在的属性时,75% 的情况下模型照样编一个答案。 + 下面按「先排除一整类无效修法 → 再给出有效机制 → 最后如实记录接上运行时的两次失败」来写。 +

    + +

    9.1 先排除掉一整类无效修法(有测量支撑)

    +

    + 最直觉的修法是「检索分数不够高就拒答」。用只在训练集拟合、评测集算 AUC 的口径, + 测了四种打分器的分数几何(top1 / margin / top-k 均值 / 离散度 / 熵 / 近邻数,共 9 个特征): +

    +
    + + + + + + + + +
    打分器最佳单特征 AUC拟合头 AUC(评测集)
    cosine(冻结键余弦)0.6257(margin)0.6086
    打包 text_retriever0.6076(margin)0.4752
    50/50 混合0.54460.5062
    训练过的路由器0.57110.5130
    +
    +

    + 操作点同样不可用:混合打分器想标出 22% 的未知要误拒 28.40% 的可回答问题; + cosine 想标出 46% 要误拒 30.00%。 + 结论:75% 的泄漏不可能靠分数阈值或分数上的分类头修好。 + 这不是调参空间不够,而是这个信号根本不存在 —— 24 条同句式候选对任何提问都「一样像」。 +

    + +

    9.2 有效的机制:先判「问的是哪个属性」,再查库里有没有

    +

    + 换一个问题问就成立了:不是「有没有候选匹配得好」,而是「这个提问在问哪个属性,这个属性在库里吗」。 +

    +
    +
    +
    + + + + + + + + + +
    封闭词表值
    可回答 episode 中「预测属性在候选集内」100.00%(250 条)
    未知 episode 中「预测属性在候选集内」0.00%(50 条)
    未知拒答率100.00%
    已知问题被误拒率0.00%
    混淆矩阵tp=50 fn=0 fp=0 tn=250
    +
    +
    +
    +

    + 开放集:真实用户会问模型没见过的属性。只靠最大概率调阈值不够 —— + 实测没有拒绝选项时,从未见过的属性 100.00% 会被认成某个存在的属性。 + 所以改成显式训练一个 NONE 类:词表取 12 个属性,另取 6 个词表外属性的真实提问作为 NONE 的负样本, + 最后用第三组、与两者都不相交的 6 个全新属性做测试。 +

    +
    +
    词表内属性识别(133 条未见改写)100.00%
    +
    从未见过的属性被拒绝(52 条)100.00%
    +
    已知问题被误拒率0.00%
    +
    随机基线(13 类)7.69%
    +
    +

    + 运行时规则只有三步,全部可审计、无阈值调参:① 属性分类头读提问的冻结键 → 输出某个属性或 NONE; + ② 若为 NONE → 拒绝;③ 若为某属性 → 查库的活跃属性集合是否包含它,不含 → 拒绝。 +

    +
    +
    + +

    9.3 接上运行时:连续失败两次(如实记录)

    +
    + + + + + + + + + +
    指标NM2.1(无门)+ 覆盖门(第一轮)+ 门 + 短路(第二轮)
    A 可回答正确率100.00%6.25%0.00%
    A 已知问题被误拒率0.00%7.50%7.50%
    A 未知拒答率56.67%43.33%—
    C 未知泄漏率75.00%62.50%0.00%
    C 可回答正确率65.00%65.00%65.00%
    +
    +

    + 第一轮:门把几乎全部可回答问题都拒掉了(100.00% → 6.25%),而它本该修好的 C 段泄漏只从 75.00% 降到 62.50%。 + 第二轮定位到真正的机制缺口 —— read() 返回空记录后不会停止,_build_text_prefix + 会继续落到旧版 16 槽注入路径把无关记忆又塞回去。加了短路之后,C 段泄漏真的到了 0.00%、可回答正确率无损, + 但 A 段崩到 0.00%。 +

    +

    + A 段崩掉的原因已测定:段事实里 94.03% 是「这是普通对话噪声…不需要长期保存」, + 只有 5.97%(63 / 1056)能解析出属性;更关键的是 A 段的属性空间完全不在属性头训练的 24 个属性之内, + 头对 A 的提问只能输出那 24 个之一 → 必然不在库里 → 门把一切都拒掉。 + 也就是说:这个机制目前是「域内可用、跨域不可用」。 +

    +

    + 结论:已把 NM2.1 的 memory_coverage_gate 改回 false,恢复为已验证的可用状态。 + 属性头工件保留在 checkpoints/memory_attribute_head/,未随包启用。 + 正确的最终形态是开放词表的属性匹配(把提问与库里真实存在的属性名做匹配,词表随写入动态变化), + 而不是 24 类闭集分类头。 +

    + +
    +

    这一节最该记住的一句

    +

    + 离线 100.00% / 0.00% 是真实的,但它只证明了机制在「裸提问键 + 24 个可解析属性」这个条件下成立, + 并不等于接上运行时就能用。这一点必须在任何对外表述里讲清楚。 +

    +
    + + +

    十 · 关键负面结论:路由器排序不影响端到端答案

    +

    出处 · V2_dpskw\ZERO_OVERLAP_FINDINGS.md §5

    + +

    + NM2.1 的路由器在零重叠集合上 Top-1 提升了 41.20 pp,但在 + eval_router_critical_e2e.py(16 个零重叠用例)上,REPLAY-128 与 V2-128-v6 的结果 + 逐位相同(默认 Top-K:37.50% / 37.50%,答成别的属性 25.00% / 25.00%,平均选中记录 5.88 / 5.88)。 +

    +

    + 于是做了一个判决性实验:保留 need_memory / hop_controller / head_gate + 全部原权重,只把排序通路(query_projection、key_projection、pair_scorer) + 替换为同形状随机权重。结果在默认 Top-K 与 Top-K=1 下都完全不变。 +

    +
    +
    01
    +

    memory_os_v2.py:1671:路由器分数被逐位置覆盖

    +

    带 semantic_key 的记录,其 _score_candidates() 分数会被 + self.record_scorer 覆盖。实测残差与 text_retriever 匹配 18 / 18, + 与 memory_router_v2 匹配 0 / 18。

    +
    02
    +

    「部署目录没有 text_retriever.pt 所以走余弦兜底」是错的

    +

    重排器被烘焙进合并包(safetensors 内有 6 个 + dynamic_memory.text_retriever.* 张量,1,573,377 参数),_text_retriever_ready + 实测为 True,余弦兜底分支根本不执行。

    +
    03
    +

    路由器在此配置下唯一实际生效的杠杆是 need_memory 门

    +

    部署权重下门开启率 68.75%(11 / 16),而三份随机初始化的路由器只有 + 37.5%(6 / 16)。判决性探针只随机化了排序通路、保留了门控头,所以「端到端零变化」 + 是由构造保证的,不是巧合。

    +
    +

    + 结论分两层,不要混为一谈:① 路由器记分卡上 22 / 22 的统治性和 +41.20 pp 的零重叠提升, + 都是路由器工件真实的增益;② 但在这套运行时里,记录级顺序由打包的 text_retriever 决定, + 路由器分数被覆盖,且相当比例的读取根本不走 V2 库、目标记录还会被写入路径删除。 + 所以路由器增益不会传递到最终答案。 +

    + + +

    十一 · 仍未解决(不粉饰)

    +
    + + + + + + + + + + +
    短板现状说明
    跨域未知拒答未解决覆盖头是 24 类闭集,只在其词表被库填充 ≥ 90% 时生效;开放词表的属性匹配实测仅 49.20% Top-1 / AUC 0.6560(零训练),不足
    答成别的属性27.50%同形候选间排序仍不理想;已排除先验重加权(更差)与单纯替换打分器(更差)
    A 段未知拒答率56.67%未见改善
    规模验证未做仅 24 属性 / 300 条评测;生产需上千属性、上万改写问法
    V2 库记录真正参与读取未解决16 个关键用例里 93.75% 仍走旧版 16 槽路径,地址命中 0 / 16
    通用能力回归套件未跑eval_general_capability.py 依赖的 comprehensive_general.jsonl 不存在
    +
    + + +

    十二 · 复现

    +
    +
    + PowerShell · 整体电池(A / B / C / D 四段) + +
    +
    $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
    +
    + +

    + 台账式的逐条原文在 真实评测台账; + v2 的正式脏数据迁移测试在 测量结果。 +

    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/reproduce.html b/reproduce.html new file mode 100644 index 0000000..571ca5b --- /dev/null +++ b/reproduce.html @@ -0,0 +1,195 @@ + + + + + +复现实验 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 11 + 复现实验 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 11 / REPRODUCE +

    复现实验

    +

    正式脏数据迁移测试的完整命令、协议参数与产物路径。

    +

    + 264 + 题题集 · 246 可回答 / 18 未知 +

    +
    +

    + 正式脏数据迁移测试的完整命令与产物路径。协议参数与批次之间的差异见下方说明—— + 复现同一批次才能得到同一组数字。 +

    +
    +
    +
    + PowerShell · 工作目录 + +
    +
    +
    +
    +
    +
    +

    PROTOCOL

    +
    +
    基座模型
    +
    量化4-bit NF4
    +
    解码greedy
    +
    最大新生成64 token
    +
    进程显存上限10 GiB
    +
    记忆读取上限2 records
    +
    地址编码batch size 1
    +
    +
    +
    +
    +
    +

    两个批次的差别

    +

    + 上面的命令复现的是批次 A(对外技术总结采用的口径,可答题 154/246)。 + 批次 B 来自同目录的 dirty_real_corpus_compare_4b_router_final.json, + 是接入记忆路由器之后的另一次运行(可答题 203/246)。两次运行的配置不同, + 因此它们的分数不能平均、不能合并,也不能互相替代。 +

    +
    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/results.html b/results.html new file mode 100644 index 0000000..6e4ea4c --- /dev/null +++ b/results.html @@ -0,0 +1,170 @@ + + + + + +测量结果 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 03 + 测量结果 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 03 / MEASUREMENTS +

    测量结果

    +

    同一 264 题题集的两个独立批次、结构化工程基准与记忆路由器工程线的全部实测数字,均标注出处与口径。

    +

    + 82.52% + 批次 B 可回答正确率 · 批次 A 为 62.60%,两批配置不同、不可合并 +

    +
    +

    + 三个面板对应三组不同的题集与口径,不能合并成一个总分。 + 面板一是同一题集先后跑的两个独立批次(配置不同、分数不可合并,只能并列读); + 面板二是结构化工程基准;面板三是记忆路由器工程线。每组数据都标注了出处、日期与口径。 +

    +

    + 读这些数字时请留意两件事:所有对照都使用同一 tokenizer、同一 greedy 解码、同一 64 token 输出上限; + 而检索召回率与回答正确率是两个必须分开报告的量——这一点在面板一里体现得最清楚。 +

    + +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/roadmap.html b/roadmap.html new file mode 100644 index 0000000..35ca63a --- /dev/null +++ b/roadmap.html @@ -0,0 +1,166 @@ + + + + + +已解决的与下一步 — Natural Memory v2 + + + + + + + + + + + + + + + + + + + +
    +
    + + + Natural Memory + v2 + + + 10 + 已解决的与下一步 + + + + + + 读取中 + +
    +
    +
    + +
    + +
    +
    + 10 / STATUS & NEXT +

    已解决的与下一步

    +

    在 12 GiB GPU 上已经跑通的闭环,以及按对正确率最有帮助的顺序排出的下一阶段任务。

    +

    + 52 + 项单元测试通过 +

    +
    +

    + 左侧是在 12 GiB GPU 上已经跑通的闭环;右侧是按对正确率最有帮助的顺序排出的下一阶段工程任务。 + 这里刻意不写时间表——顺序比日期更可信。 +

    +

    已经解决的问题

    +
    +

    下一阶段的工程任务

    +
    + + +
    +
    + + + + + + + + + + + + + + + + diff --git a/server.py b/server.py new file mode 100644 index 0000000..1a7fa06 --- /dev/null +++ b/server.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Natural Memory v2 — 站点服务端 + +纯标准库实现,无第三方依赖(目标环境 Python 3.8+)。 + +默认监听 127.0.0.1:7443(仅本机可达,供 Cloudflare Tunnel 等反向代理使用)。 +如需对外直接监听,显式指定 --host 0.0.0.0。 + +用法: + python server.py + python server.py --host 0.0.0.0 --port 7443 + set NM_PORT=8443 && python server.py + +设计约定(安全默认): + 只放行 index.html 与 assets/ 下白名单扩展名的文件,其余全部 404。 + server.py、启动脚本、说明文档等一律不通过 HTTP 暴露;目录列表关闭。 + 任何包含 .. / . / 反斜杠 / NUL 的路径直接拒绝。 +""" +from __future__ import annotations + +import argparse +import json +import mimetypes +import os +import sys +import time +from datetime import datetime, timezone +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import unquote, urlparse + +ROOT = os.path.dirname(os.path.abspath(__file__)) +SITE_VERSION = "v2" +STARTED = time.time() + +# Python 3.8 的 mimetypes 不认 woff2,需要显式登记 +mimetypes.add_type("font/woff2", ".woff2") +mimetypes.add_type("font/woff", ".woff") +mimetypes.add_type("application/javascript", ".js") +mimetypes.add_type("image/svg+xml", ".svg") + +ALLOWED_EXT = { + ".html", ".css", ".js", ".mjs", ".json", + ".woff2", ".woff", ".ttf", + ".svg", ".png", ".jpg", ".jpeg", ".webp", ".avif", ".ico", +} +ALLOWED_ROOT_FILES = {"index.html", "favicon.ico", "robots.txt"} + +# 生成出来的分页(build.py 的 PAGES 列表)。支持 /division 与 /division.html 两种写法。 +PAGE_SLUGS = { + "index", "division", "mechanism", "results", "nm21", "examples", + "ledger", "cost", "capacity", "limits", "roadmap", "reproduce", +} + +LONG_CACHE = (".woff2", ".woff", ".ttf", ".png", ".jpg", ".jpeg", ".webp", ".avif", ".svg", ".ico") +NO_CACHE = (".html", ".css", ".js", ".json") + + +def human_uptime(seconds: float) -> str: + seconds = int(seconds) + d, rem = divmod(seconds, 86400) + h, rem = divmod(rem, 3600) + m, s = divmod(rem, 60) + if d: + return "%dd %dh" % (d, h) + if h: + return "%dh %dm" % (h, m) + if m: + return "%dm %ds" % (m, s) + return "%ds" % s + + +class Handler(SimpleHTTPRequestHandler): + server_version = "NaturalMemorySite/" + SITE_VERSION + protocol_version = "HTTP/1.1" + + # ---------------- 路径安全 ---------------- + @staticmethod + def _safe_rel(path: str): + """把请求路径解析为站点内的相对路径;不合规返回 None。""" + raw = unquote(path or "") + if "\x00" in raw or "\\" in raw: + return None + segs = raw.split("/") + if any(s in (".", "..") for s in segs): + return None + segs = [s for s in segs if s] + if not segs: + return "index.html" + if len(segs) == 1: + name = segs[0] + if name in ALLOWED_ROOT_FILES: + return name + # 干净 URL:/division → division.html + if name in PAGE_SLUGS: + return "index.html" if name == "index" else "%s.html" % name + if name.endswith(".html") and name[:-5] in PAGE_SLUGS: + return name + return None + if segs[0] == "assets" and os.path.splitext(segs[-1])[1].lower() in ALLOWED_EXT: + return "/".join(segs) + return None + + # ---------------- 运行时读数 ---------------- + def _status_payload(self) -> bytes: + host, port = self.server.server_address[0], self.server.server_address[1] + payload = { + "site": "Natural Memory " + SITE_VERSION, + "note": "以下为本站 Web 服务进程的读数,不是模型运行时的指标。", + "host": getattr(self.server, "display_host", host), + "bind": "%s:%d" % (host, port), + "python": "%d.%d.%d" % sys.version_info[:3], + "uptime": human_uptime(time.time() - STARTED), + "uptime_seconds": round(time.time() - STARTED, 1), + "requests": getattr(self.server, "request_count", 0), + "started": datetime.fromtimestamp(STARTED).astimezone().isoformat(timespec="seconds"), + "now": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"), + } + return json.dumps(payload, ensure_ascii=False).encode("utf-8") + + def _send_json(self, body: bytes, status: int = 200) -> None: + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + self.end_headers() + if self.command != "HEAD": + self.wfile.write(body) + + # ---------------- 路由 ---------------- + def _dispatch(self, with_body: bool) -> None: + self.server.request_count = getattr(self.server, "request_count", 0) + 1 + path = urlparse(self.path).path + + if path.rstrip("/") == "/api/status": + self._send_json(self._status_payload()) + return + + if path == "/healthz": + self._send_json(b'{"ok":true}') + return + + rel = self._safe_rel(path) + if rel is None: + # 不区分「不存在」与「不允许」,避免泄露文件是否存在 + self.send_error(404, "Not Found") + return + + self.path = "/" + rel + if with_body: + SimpleHTTPRequestHandler.do_GET(self) + else: + SimpleHTTPRequestHandler.do_HEAD(self) + + def do_GET(self): # noqa: N802 + self._dispatch(True) + + def do_HEAD(self): # noqa: N802 + self._dispatch(False) + + # ---------------- 缓存与安全响应头 ---------------- + def end_headers(self): + path = urlparse(self.path).path.lower() + if path.endswith(".html"): + # 页面本体永不缓存:模板一变立刻生效 + self.send_header("Cache-Control", "no-store") + elif path.endswith(LONG_CACHE): + # 静态资源走构建指纹(?v=hash),可长期不可变缓存 + self.send_header("Cache-Control", "public, max-age=604800, immutable") + elif path.endswith(NO_CACHE): + self.send_header("Cache-Control", "no-cache") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Referrer-Policy", "same-origin") + self.send_header("X-Frame-Options", "SAMEORIGIN") + super().end_headers() + + # ---------------- 关闭目录列表 ---------------- + def list_directory(self, path): + self.send_error(404, "Not Found") + return None + + def log_message(self, fmt, *args): + sys.stderr.write("[%s] %s %s\n" % ( + datetime.now().strftime("%H:%M:%S"), self.address_string(), fmt % args, + )) + + +class Server(ThreadingHTTPServer): + """ThreadingHTTPServer 已含 ThreadingMixIn,这里只补充运行时读数字段。""" + daemon_threads = True + allow_reuse_address = True + request_count = 0 + display_host = "127.0.0.1" + + +def main() -> int: + ap = argparse.ArgumentParser(description="Natural Memory v2 site server") + ap.add_argument("--host", default=os.environ.get("NM_HOST", "127.0.0.1"), + help="监听地址(默认 127.0.0.1,仅本机可达)") + ap.add_argument("--port", type=int, default=int(os.environ.get("NM_PORT", "7443")), + help="监听端口(默认 7443)") + args = ap.parse_args() + + if not os.path.isfile(os.path.join(ROOT, "index.html")): + sys.stderr.write("index.html 不在 %s,请从站点根目录启动。\n" % ROOT) + return 2 + + httpd = Server((args.host, args.port), lambda *a, **kw: Handler(*a, directory=ROOT, **kw)) + httpd.display_host = args.host + + print("Natural Memory %s — site server" % SITE_VERSION) + print(" 根目录 : %s" % ROOT) + print(" 监听 : http://%s:%d/" % (args.host, args.port)) + print(" 读数 : http://%s:%d/api/status" % (args.host, args.port)) + if args.host in ("127.0.0.1", "localhost"): + print(" 提示 : 当前仅本机可达;需要对外请用反向代理,或显式 --host 0.0.0.0") + print(" Ctrl+C 退出") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n已停止。") + finally: + httpd.server_close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/pages/capacity.body.html b/src/pages/capacity.body.html new file mode 100644 index 0000000..7f58a4e --- /dev/null +++ b/src/pages/capacity.body.html @@ -0,0 +1,26 @@ +

    + 「一百万记录容量」是地址空间和分页结构的容量,不是本次已经装入了一百万条语义记忆, + 也不是一百万条记录已经通过端到端测试。这一节把容量、实际用量与存储方式分开列清楚。 +

    +
    +
    +

    PAGE MAP · 32768 PAGES × 32 SLOTS

    + +

    高亮部分为本次运行实际使用的页面(批次 A:23 页 / 723 条 active records;批次 B:24 页 / 738 条)。其余为地址空间余量。

    +
    +
    +
    +
    +
    +
    +
    +

    PERSISTENCE

    +

    +

    +
    +
    +

    TRADE-OFF

    +

    取舍

    +

    +
    +
    \ No newline at end of file diff --git a/src/pages/cost.body.html b/src/pages/cost.body.html new file mode 100644 index 0000000..6fa0067 --- /dev/null +++ b/src/pages/cost.body.html @@ -0,0 +1,15 @@ +

    +
    +
    +
    +

    +
    +
    +

    VRAM · 12 GiB CARD

    +
    +

    + allocated 与 reserved 均为每次请求结束后的快照,不是 CUDA high-water mark; + reserved 还包含 PyTorch 分配器保留的缓存。因此这里不能当作"模型峰值显存"来引用。 +

    +
    +
    \ No newline at end of file diff --git a/src/pages/division.body.html b/src/pages/division.body.html new file mode 100644 index 0000000..de6bf58 --- /dev/null +++ b/src/pages/division.body.html @@ -0,0 +1,126 @@ +

    + 这一节回答一个问题:既然模型已经有上下文窗口,为什么还要单独做一层记忆? + 答案不是「窗口不够长」,而是把长期事实塞进上下文,会以线性成本换来极低的复用率。 + 下面把「哪些东西留在 Qwen 的热 KV 里、哪些写进记忆记录」这条分界线逐层讲清楚。 +

    + +

    先看没有这层记忆时会发生什么

    +

    + 同一套题、同一协议下,原版 Qwen3.5-4B 在 246 道可回答题上只答对 3 道(1.22%)。 + 它的典型回答不是答错,而是明确表示拿不到信息: +

    +
    + 作为一个人工智能模型,我无法访问您的个人历史对话记录、系统配置或您之前设定的具体规则……每次对话对我来说都是全新的开始。 + 原版 Qwen3.5-4B · 评测样本 user-009 原始回答 +
    +

    + 这句话在事实上完全正确——模型确实没有这些信息。问题出在:这些信息本来就应该被保存下来, + 而它们不属于「当前这轮对话」,所以每轮都重新粘一遍历史是最笨的保存方式。 +

    + +

    四层分工:谁存什么、为什么

    +

    + 这是整份设计里最核心的一张表。判断标准只有一条:这份信息是「这一轮才有意义」还是「跨轮次可复用」。 +

    +
    +
    +
    层级存放什么由谁负责为什么放在这一层
    +
    + 最近对话 + 这一轮前后的原话与顺序 + Qwen 热 KV + 指代、省略、语气、"你刚才说的那句"都依赖严格的先后顺序。这类信息复用价值低、时效性强,放 KV 最自然。 +
    +
    + 长期个人 / 项目事实 + 事实、版本、来源、置信度、短文本证据 + Natural Memory 记录 + 跨会话反复被问到,且需要保留"哪个版本才是当前有效"的关系。它们不依赖顺序,只依赖地址与内容。 +
    +
    + 当前问题 + 由问题生成的有界读取 + 路由 + Top-K + 不是所有记忆都该被读。当前问题只与少量页面和记录交互,避免"记忆越多、噪声越大"。 +
    +
    + 原始持久化状态 + 记忆快照与索引 + 嵌入式 memory safetensors + 随模型包一起加载,运行时不需要额外数据库进程,也避免了磁盘分页带来的延迟抖动。 +
    +
    +
    + +
    +

    关键判断

    +

    + Memory Slot 不试图逐 token 模拟完整 KV。它接管的是 KV 里最昂贵、最适合长期保存、 + 也最容易重复利用的那一部分;而当前对话的顺序关系,仍然由 Qwen 的原生上下文负责。 +

    +
    + +

    分工落到数字上是什么样

    +

    + 下面这组对照最能说明「不是把历史灌进去,而是把命中的少量证据灌进去」。两组数字分别来自批次 A 与批次 B(同一题集、同一协议)。 +

    +
    +
    + 无记忆时的平均输入 + 30.75token + 批次 A 基线:只有问题本身,没有历史 +
    +
    + 有记忆时的平均输入 + 441.19token + 批次 B:命中的证据前缀(批次 A 为 641.42) +
    +
    + 前缀命中率 + 96.97% + 批次 B 实际注入证据的题目占比(批次 A 为 90.91%) +
    +
    + GPU cache 实际用量 + 17,990token + 有界上限 131,072 token,用到约 13.7% +
    +
    +

    + 注意最后一项:上限是 131,072 token,实际只用了 17,990。 + 这说明读取路径被"有界"约束住了——不会因为记忆库变大就把显存吃满。 +

    + +

    这条分工线换来了什么、付出了什么

    +
    +
    +

    换来的

    +
    +
    可回答正确率1.22% → 82.52%
    +
    目标记忆召回0% → 92.68%
    +
    符号定位(192 题)0/192 → 181/192
    +
    跨会话问题1/6 → 5/6
    +
    +
    +
    +

    付出的

    +
    +
    平均输入前缀30.75 → 441.19 token
    +
    平均总延迟(批次 B)2854.9 → 2878.3 ms
    +
    平均解码速度(批次 B)22.34 → 20.68 tok/s
    +
    reserved 显存峰值3.234 → 5.180 GiB
    +
    +
    +
    + +

    这条线不能推到哪里

    +
    +
    ✕Natural Memory 已经等价于百万 token 的完整 KVUNSUPPORTED
    +
    ✕长上下文限制已被解除(实测 8K 可跑,16K / 32K OOM)UNSUPPORTED
    +
    ✕设计容量 1,048,576 条记录等于已经装入并验证了这么多UNSUPPORTED
    +
    +

    + 第三条尤其要注意:1,048,576 是地址空间与分页结构的容量, + 本次两个批次实际装载的是 723 / 738 条 active records,分别是 23 / 24 个页面。 + 容量上限与已装入量是两个概念,我们不在页面上把它们混为一谈。 +

    diff --git a/src/pages/examples.body.html b/src/pages/examples.body.html new file mode 100644 index 0000000..d7053b9 --- /dev/null +++ b/src/pages/examples.body.html @@ -0,0 +1,3 @@ +

    +
    +

    \ No newline at end of file diff --git a/src/pages/index.body.html b/src/pages/index.body.html new file mode 100644 index 0000000..77c5ec0 --- /dev/null +++ b/src/pages/index.body.html @@ -0,0 +1,401 @@ + +
    +
    + +
    +
    + Natural Memory · NM2.1 + 1,048,576 槽位 · 738 条活跃记录 · 730 个索引桶 +
    + +
    +

    模型重启之后,
    它仍然记得。

    + +

    + + 1.22% + + 82.52% + + 同一 264 题题集的可回答正确率:无记忆基线 → 批次 B。 + 批次 A 为 62.60%,两批配置不同、不可合并。 +

    +
    + +
    +
    +

    + +
    +
    + 01 / 08 + 地址空间 + 1,048,576 槽位 · 738 条活跃记录 + +
    +
    +
    +

    向下滚动 · 走一遍读取路径

    +
    +
    + + +
    + +
    + + +
    +
    +

    Evidence · 重启实验

    +

    把历史扔掉,它还记得吗?

    +

    + 下面不是示意动画,是工程里两次真实运行的逐字回放。同一条事实、同一个问题、同一份协议; + 两次之间唯一的差别,是写入有没有真的落库。 +

    + +
    +
    +
    + + +
    + + +
    + +
    +
    +

    写入的事实

    +

    +

    重启之后问

    +

    + +
    +
    +
    记忆库记录0
    +
    +
    +
    +
    重启时传入的历史false
    +
    +
    +
    +
    注入模型内部的前缀0 tok
    +
    +

    +
    +
    +
    + +
      +
      + +
      +
      + 模型输出 · 逐字 + +
      +

      +
      +
      +
      + +

      +
      +
      + + +
      +
      +
      + {{include:partials/readpath.svg}} +
      + 图 1 · 读取路径 + 一次问答里记忆层做了什么。用户问题只与少量页面和记录交互,全程没有对全量记忆做注意力; + 命中的短证据作为模型内部前缀注入 Qwen。图为示意,环节顺序与实现一致。 +
      +
      +
      +
      + + +
      +
      +

      Abstract

      +

      这是什么东西

      +

      + 它接在本地 Qwen3.5-4B 上,是一个模型内的记忆层:把适合长期复用的个人事实、项目事实和短对话片段 + 写进带地址的记忆记录;当前问题只读取少量相关记录,再把这些记录作为模型内部前缀交给 Qwen 生成答案。 +

      +

      + 它要解决的具体问题是:模型重启后不携带历史聊天记录,仍能访问已经保存的个人或项目事实, + 同时不把全部历史转换成 GPU 上的长 KV Cache。 +

      +

      + 当前状态:研究原型到工程验证之间。同一 264 题题集上,可回答正确率从 1.22% 提升到 62.60%(批次 A) + 与 82.52%(批次 B);代价是输入前缀变长、解码速度下降约 10%。我们不在页面上把它说成通用智能, + 也不把实验室结果表述成线上服务承诺。 +

      +
      +
      + + +
      +
      +

      What's in this release

      +
      +
      +

      记忆层与索引

      +

      738 active records

      +

      批次 B 实际装载;24 个页面,页容量 32 条。地址空间设计容量 1,048,576。

      +
      +
      +

      记忆路由器

      +

      2,037,774 参数

      +

      16 张量 / float32 / 512 B 地址;结构 dim=128 · heads=8 · hidden=2560 · max_hops=3。

      +
      +
      +

      有界 GPU cache

      +

      256 records

      +

      上限 131,072 token;批次 B 实际用到 17,990 token,fallback 与分配失败均为 0。

      +
      +
      +

      评测题集

      +

      264 题

      +

      246 可回答 / 18 未知;真实仓库 220 题 + 项目对话事实 44 题。

      +
      +
      +

      可复核性

      +

      逐字原文

      +

      样例页的问答全部取自评测原始结果、未经润色,并包含两条我们判错的样例。

      +
      +
      +

      复现

      +

      一条命令

      +

      正式脏数据迁移测试的完整命令、协议参数与产物路径见复现页。

      +

      去复现 →

      +
      +
      +
      +
      + + +
      +
      +

      Headline result

      +
      +
      +
      +
      + 可回答正确率 + 同一 264 题题集 · 246 可回答 +
      +
      原版 Qwen + + 1.22%3/246
      +
      批次 A + + 62.60%154/246
      +
      批次 B + + 82.52%203/246
      +
      +
      + 原版 Qwen3.5-4B + 批次 A + 批次 B +
      +

      + 两个批次同一题集、同一协议,但配置不同,分数不可合并,只能并列读。 + 目标记忆召回:批次 A 67.48%、批次 B 92.68%——检索召回率不等于回答正确率。 +

      +
      +
      +
      +
      总体正确率7.58% → 64.39% → 82.95%
      +
      符号定位0/192 → 129/192 → 181/192
      +
      未知拒答17/18 → 16/18 → 16/18
      +
      读取额外耗时— → 61.10 → 26.35 ms
      +
      解码速度24.06 → 21.68 → 20.68 tok/s
      +
      +

      全部测量结果 →

      +
      +
      +
      +
      + + +
      +
      +

      After this release · NM2.1

      +

      发布之后又做了什么

      +

      + 上面所有数字都来自 v2。之后的 NM2.1 换掉了记忆路由器、修掉写入路径上一个会把事实互相销毁的缺陷, + 并把「问库里没有的属性时照样编答案」这个 75% 的失败修到 0%。 +

      + + +
      +

      未知拒答率 · 24 条同形候选

      +

      + 100.00% + 从 0.00% 起 —— 修好之前,模型 75% 的情况下照样编一个答案 +

      +

      + 这是整个项目最该被引用的一个数字。它回答的不是「能不能检索到」,而是 + 「不知道的时候会不会不懂装懂」。 +

      +
      + +
      +
      +

      未知拒答率 · 同形候选

      +

      0.00% →100.00%

      +

      问库里不存在的属性时,正确说「不知道」的比例。修好之前,模型 75% 的情况下照样编一个答案。

      +
      +
      +

      写 20 条不同属性后存活

      +

      12/20 →20/20

      +

      v2 的写入路径会把 8 条无关事实互相 retract,让端到端正确率存在 50% 的硬上限——任何排序器都救不回一条已被删掉的记录。

      +
      +
      +

      零字面重叠改写

      +

      43.75% →68.75%

      +

      查询与事实之间没有任何独特字重叠时的回答正确率;24 条同句式候选的随机水平是 4.17%。

      +
      +
      + +
      +
      +

      + 代价几乎为零:参数量 2,037,774 不变、每条记录地址 512 字节不变、模型包 8.88 GB 不变; + 交错基准(7 轮,消除顺序效应)单查询延迟 1.4203 → 1.4242 ms,差 +0.28%, + 小于同一轮里原版自身 4.85% 的离散度。 +

      +

      + 失败也一并写出来:那套「先判属性、再查库」的机制在离线实验里是 100.00% 拒答 / 0.00% 误拒, + 但接上运行时连续失败两次——第一次把可回答正确率从 100.00% 压到 6.25%; + 第二次修好了目标域(泄漏真的到 0.00%、可回答无损)却让另一个域崩到 0.00%。 + 最终该项已回退,原因也定位到了。 +

      +

      + 完整技术论证 → + 108 条真实台账 → +

      +
      +
      +

      路由器工件本身的提升

      +
      +
      Top-1 正确率41.12% → 94.14%
      +
      Recall@346.73% → 96.48%
      +
      MRR47.69% → 95.77%
      +
      hop 正确率73.23% → 100.00%
      +
      hop 欠预测率4.68% → 0.00%
      +
      替换兼容性DROP-IN OK(16/16 键)
      +
      单元测试52 项通过
      +
      +

      + 这些是路由器工件在冻结评测集上的增益。它不会自动传递到最终答案 —— + 原因见技术论证页的第十节。 +

      +
      +
      +
      +
      + + +
      + +
      + + +
      +
      +

      Boundaries · 我们不宣称的内容

      +
      +
      ✕已经等价于百万 token 的完整 KV
      +
      ✕已经击败生产级 embedding + reranker RAG
      +
      ✕任何自然语言表达都能稳定召回正确记忆
      +
      ✕记忆写入永远不会出错
      +
      ✕一百万条记录已经完成端到端验证
      +
      ✕当前 4B 结果可以直接外推到 14B、32B 或更大模型
      +
      +

      + 完整的八条与五项主要局限见 边界页。 + 把它们放在发布页正面,是因为它们和上面的数字一样重要。 +

      +
      +
      + + +
      +
      +

      Citation

      +
      +
      + BibTeX + +
      +
      @misc{naturalmemoryv2,
      +  title  = {Natural Memory v2: an in-model memory layer for local LLMs},
      +  author = {wpy},
      +  year   = {2026},
      +  note   = {Research prototype on Qwen3.5-4B. 264-question dirty-corpus
      +            transfer test; two independent batches.},
      +  url    = {https://wpyw.site}
      +}
      +
      +

      + 引用时请一并注明「批次 A 与批次 B 配置不同、分数不可合并」,否则单个数字会被误读。 +

      +
      +
      diff --git a/src/pages/ledger.body.html b/src/pages/ledger.body.html new file mode 100644 index 0000000..c7b56cf --- /dev/null +++ b/src/pages/ledger.body.html @@ -0,0 +1,25 @@ +

      + 这一页不是挑出来的样例,是全部 108 条真实评测 episode 的逐条台账。 + 每条都带模型的逐字回复、判定结果、停止原因、选中记录数与分数。 +

      +

      + +
      +
      +
      +
      + +
      + + +
      + +
      + +

      + 数据来自 runtime_score_bands.json 与 nm2_1_final_runtime_e2e.json, + 由 tools/make_ledger.py 生成 assets/js/ledger.js, + 字段逐字复制,未做改写、润色或换算。模型输出里的错字、冗余和编造的内容都按原样保留。 + 期望锚点是评测里的随机码(形如 VAL-XXXXXXX),它只出现在唯一一条候选事实里, + 因此可以用来检测「证据混用」。 +

      diff --git a/src/pages/limits.body.html b/src/pages/limits.body.html new file mode 100644 index 0000000..685de9c --- /dev/null +++ b/src/pages/limits.body.html @@ -0,0 +1,6 @@ +

      +
      +

      五项主要局限

      +
      +

      来自工程文档的原文

      +
      \ No newline at end of file diff --git a/src/pages/mechanism.body.html b/src/pages/mechanism.body.html new file mode 100644 index 0000000..df8f534 --- /dev/null +++ b/src/pages/mechanism.body.html @@ -0,0 +1,114 @@ +

      + 这一节把「一次读取」和「一次写入」拆开讲透:读取路径有哪八个环节、每一步的输入输出是什么、 + 一条记录里到底存了什么、四个状态如何演化,以及我们为这些决定付出了什么代价。 +

      + +

      一、读取路径:从提问到证据注入

      +
      + {{include:partials/readpath.svg}} +
      + +

      + 点左侧任一环节查看它在做什么,也可以依次点一遍——注意整条链路上没有一步是「和全部记忆做注意力」, + 这是显存可控的根本原因。 +

      + +
      +
      +
      +

      STAGE 01 / 08

      +

      +

      +

      +
      +
      读取上限2 records
      +
      候选页上限4 pages
      +
      页容量32 / page
      +
      全量注意力否
      +
      +
      +
      + +

      每一步的输入、输出与约束

      +

      + 括号里的数字是实测值,不是设计目标。 +

      +
      +
      +
      环节输入 → 输出约束或实测
      +
      01 用户问题普通自然语言不要求任何显式指令,不需要输入 /remember 之类的标记。
      +
      02 Qwen hidden state问题 → 紧凑查询地址地址由模型隐状态生成,不依赖外部 embedding 服务;语义地址编码用单条 batch 执行,避免编码阶段出现显存瞬时峰值。
      +
      03 LSH / 地址粗索引查询地址 → 候选页精确桶 + Hamming-1/2 探针;批次 A 的记忆状态里粗索引有 730 个桶、最近一次粗筛返回 24 个候选。
      +
      04 候选页候选页 → 少量页面页容量 32 条记录,默认最多读取 4 页;本次 active records 723 条落在 23 个页面上。
      +
      05 页内记录重排页内记录 → 有序候选记录级重排由独立的 text_retriever 完成(6 张量、1,573,377 参数)。它的 Top-1 命中率本身只有 23.20%,是当前的主要瓶颈之一。
      +
      06 Top-K 记录有序候选 → 少量证据正式测试中的读取上限为 2 条记录。目标记忆召回:批次 A 166/246,批次 B 228/246。
      +
      07 证据前缀注入证据 → 模型内部前缀命中的短证据被提升到 GPU 作为前缀;批次 B 有 96.97% 的题目实际注入了前缀,平均 410.34 token(批次 A 为 610.67)。
      +
      08 普通生成前缀 + 问题 → 回答记忆主体始终留在进程内存,只有热点记录进入有界 GPU cache。读取路径额外耗时:批次 A 61.10 ms → 批次 B 26.35 ms。
      +
      +
      + +

      二、一条记录里到底存了什么

      +

      + 记录不是一段文本,而是一条带地址、带版本关系、带状态的结构化数据。下面左侧是全部字段, + 右侧是一条真实记录(逐字取自评测结果里的命中记录)。 +

      +
      +
      +
      +
      +
      +
      +
      +
      + +

      三、状态机:旧的答案不会凭空消失

      +

      + 同一实体和属性出现新值时,旧值被标记为 superseded,而不是被覆盖或删除。 + 这是「时间冲突」类问题能答对的前提:既要给出当前有效值,也要能说清旧值去哪了。 +

      +
      + +
      +

      写入路径的教训:检索救不回不存在的记录

      +

      + 早期版本的退役授权完全依赖一个学习打分(阈值 ≥0.95),没有任何结构校验。 + 结果是新写入把已有记录误判为旧值并退役:一次 20 条记录的测试里 active 只剩 12 条、retract 高达 16 次、目标记录只存活 8/16。 + 修好之后:active 12/20 → 20/20,retract 16 → 0,目标记录存活 8/16 → 16/16,端到端 37.50% → 68.75%。 + 这件事的结论很直白——任何排序器都救不回一条已经不存在的记录。 +

      +
      + +

      四、四个设计决定与它们的代价

      +
      +
      +

      决定 1 · 两段式读取

      +

      粗索引筛页 → 页内重排,而不是对全量记录做注意力。

      +

      好处:单次读取的候选规模被压到固定上限(≤4 页 / ≤2 条),成本与记忆总量解耦。 + 代价:两段都可能出错,且错误会叠加——粗索引错了就再没有机会。

      +
      +
      +

      决定 2 · 用地址索引而非向量库

      +

      语义地址 + LSH 桶放在模型包内,不引入外部检索服务。

      +

      好处:部署时没有额外进程与网络依赖,读取耗时稳定(批次 B 26.35 ms)。 + 代价:召回质量受地址质量限制,零字面重叠的改写问法曾只有 18.40% 的 Top-1。

      +
      +
      +

      决定 3 · 有界 GPU cache

      +

      热点记录进 GPU,上限 256 条 / 131,072 token,另有动态保留。

      +

      好处:显存占用可预测,两个批次的实际用量分别是 15,885 / 17,990 token,fallback 与分配失败均为 0。 + 代价:cold page 会带来额外取数路径——不过本次两个批次都是 embedded / process-RAM 模式,cold page 为 0。

      +
      +
      +

      决定 4 · 嵌入式 weight-shard 持久化

      +

      记忆快照写进模型包的 memory safetensors 切片,默认不用 SQLite、不用磁盘分页。

      +

      好处:运行时依赖最少,重启只需加载权重切片、不回放历史聊天。 + 代价:模型包会随记忆增长变大,且固化更新需要重新保存权重切片——这是明确的工程负担,不是白拿的。

      +
      +
      + +

      五、这套机制仍然做不到的事

      +
      +
      ✕任何自然语言表达都能稳定召回正确记忆UNSUPPORTED
      +
      ✕记忆写入永远不会出错UNSUPPORTED
      +
      ✕检索召回率等于回答正确率(批次 B 召回 92.68% / 回答 82.52%,差 10.16pp)UNSUPPORTED
      +
      diff --git a/src/pages/nm21.body.html b/src/pages/nm21.body.html new file mode 100644 index 0000000..ae1bc53 --- /dev/null +++ b/src/pages/nm21.body.html @@ -0,0 +1,546 @@ +

      + 发布页上的数字来自 v2。这一页讲**之后又做了什么**:三处改动各自凭什么、一个把端到端正确率 + 锁死在 50% 的写入缺陷怎么定位的、以及一个 75% 泄漏的失败怎么修到 0%——又怎么在接上运行时之后失败两次。 + 所有数字都来自磁盘上的报告文件,每张表都标了出处;**没有一条是估算或外推**。 +

      + +
      +

      先读这一句

      +

      + 这一页里**离线实验最好的数字(未知拒答 100.00%)不等于运行时可用**。它接上运行时之后 + 连续失败两次,最终被回退。把失败过程一起写出来,是因为它比成功更能说明这套机制到底卡在哪。 +

      +
      + + +

      净变化一览

      +

      出处 · V2_dpskw\NM2_VS_NM2_1.md(该文件自述:所有数字均从磁盘上的评分卡 / JSON 读取,不是手工抄录)

      + +
      + + + + + + + + + + + + +
      轴v2(原版)NM2.1净变化
      路由器 Top-141.12%94.14%+53.02 pp
      路由器 Recall@346.73%96.48%+49.75 pp
      未知拒答率0.00%100.00%+100.00 pp
      未知问题泄漏(同形候选)75.00%0.00%−75.00 pp
      一次写 20 条不同属性后存活12 / 2020 / 20+8 条
      零字面重叠改写正确率43.75%68.75%+25.00 pp
      hop 欠预测率4.68%0.00%−4.68 pp
      参数量 / 每条记录地址字节2,037,774 / 5122,037,774 / 512不变
      +
      +

      + 「未知拒答率」与「未知泄漏」是同一件事的两面:前者是问库里没有的属性时**正确说不知道**的比例, + 后者是**照样编一个答案**的比例。v2 在这两轴上是 0% 和 75%,也就是几乎必然硬答。 +

      + + +

      一 · 路由器工件本身的检索与排序

      +

      出处 · router_scorecard_final.json(判定见 router_verdict_final.json)· 冻结 v6 评测集 21,920 条 / 10 类别

      + +
      + + + + + + + + + + + + + + +
      指标v2 · V2-128 deployed(v3)NM2.1 · REPLAY-128 v7 final
      Top-1 正确率41.12%94.14%
      Recall@137.03%88.58%
      Recall@346.73%96.48%
      Recall@548.91%97.15%
      MRR47.69%95.77%
      nDCG@344.22%95.37%
      多跳证据全中(Top-3)43.43%96.22%
      多跳证据全中(仅多正例)37.15%95.45%
      hop 正确率73.23%100.00%
      hop 欠预测率4.68%0.00%
      +
      + + +

      二 · 拒答与仲裁策略轴

      +

      出处 · router_scorecard_final.json(门槛 0.50)+ threshold_sweep_check.json(0.30–0.80 全门槛扫描)

      + +
      + + + + + + + + + + + +
      指标v2NM2.1
      need F189.58%100.00%
      need 召回99.82%100.00%
      need 精确率81.24%100.00%
      未知拒答率0.00%100.00%
      已知问题被误拒率0.18%0.00%
      未知问题被误读率100.00%0.00%
      仲裁准确率81.12%100.00%
      +
      +

      + 门槛扫描是这里真正的证据。0.30 / 0.40 / 0.50 / 0.60 / 0.70 / 0.80 六个门槛逐一复核: + NM2.1 **全门槛通过**;而原版在**每一个**门槛上未知拒答率都是 0.00%,且门槛越高误拒越差 + (0.00% → 1.32%)。一个能在整条门槛轴上都成立的结论,和一个只在某个阈值上成立的点,可信度不是一回事。 +

      + + +

      三 · 端到端整体记忆能力(同一份运行时,只有模型包不同)

      +

      出处 · nm2_battery_comparison_final.json · A 110 用例 / B 16 / C 48 / D 重启持久化

      + +
      + + + + + + + + + + + + + + + + +
      指标原版 NM2NM2.1(仅换路由器)NM2.1 最终
      A 总体正确率89.09%88.18%88.18%
      A 可回答正确率100.00%100.00%100.00%
      A 未知拒答率60.00%56.67%56.67%
      A 已知问题被误拒率0.00%0.00%0.00%
      B 零字面重叠改写回答正确率43.75%68.75%68.75%
      B 答成别的属性18.75%18.75%18.75%
      B 触发读取56.25%93.75%93.75%
      C 可回答正确率(24 同形候选)65.00%65.00%70.00%
      C 答成别的属性35.00%35.00%27.50%
      C 未知泄漏率75.00%75.00%0.00%
      C 活跃记录 min / max23 / 2423 / 2423 / 24
      D 重启后召回 / 作答 / 清理通过通过通过
      +
      +

      + 运行间波动必须自己讲出来:B 段只有 16 个用例,1 个用例 = 6.25 pp。所以 B 的 68.75% + 与另一次测得的 75.00% 属于同一水平的运行间波动,**不作为增益主张**。A / C / D 的结论在多次运行中一致。 +

      + + +

      四 · 零字面重叠改写的泛化

      +

      出处 · replay_check_zov.json(路由器级)与上表 B / C 段(端到端)· 数据集 300 条、24 个未见过的问法

      + +
      +
      +
      + + + + + + + + + +
      指标v2 权重NM2.1 权重
      路由器 Top-1(250 条可回答)18.40%59.60%
      路由器 Recall@336.00%83.20%
      路由器 MRR35.07%73.06%
      端到端改写正确率(B 段)43.75%68.75%
      端到端未知泄漏(C 段)75.00%0.00%
      +
      +
      +
      +

      + 零重叠评测集有 24 条**同句式、不同属性**的候选,随机猜中的概率是 4.17%。 + 所以 59.60% 大约是随机水平的 14 倍。 +

      +
      +
      数据集规模300 条评测 / 24 个未见问法
      +
      训练 / 留出划分前 2 个改写训练(48)/ 第 3 个留出(24)
      +
      两个划分共享的查询字符串0
      +
      查询与自身目标的重叠0
      +
      含「陷阱」候选的 episode54.53%(生成器统计)
      +
      +

      + 关键设计判断:改写与**无关**属性的字符重叠被刻意保留(命中错误候选只会得到错误答案, + 让任务更难,不提供捷径);真正必须为 0 的是与**自身**目标的重叠。最初把二者混为一谈时, + 24 个属性里有 7 个因为「起床时间」的「时」、「办公城市」的「公」这类高频字被判为「无可用的改写」。 +

      +
      +
      + + +

      五 · 代价:几乎没有

      +

      出处 · router_scorecard_final.json、router_latency_bench_prod.json(7 轮交错取中位数)

      + +
      + + + + + + + + + + +
      指标v2NM2.1说明
      参数量2,037,7742,037,774未增加
      每条记录地址字节512512存储几何未变
      单查询延迟中位数(GPU)0.9549 ms0.9169 ms略快
      批量 QPS(batch=64)66,03769,378+5.1%
      批量 QPS(batch=256)207,287242,759+17.1%
      模型包大小8.88 GB / 22 文件8.88 GB / 23 文件多出属性头 ~0.25 MB
      +
      +

      + 交错基准(7 轮,消除顺序效应):单查询中位数 1.4203 ms → 1.4242 ms,差 +0.28%。 + 同一轮里原版自身离散度就有 4.85% —— 也就是说这个差值小于测量噪声。 + 零重叠那一轮里 batch=64 的读数低 9.2%,但该轮离散度是 17.4%,且同架构同参数同 FLOPs 下 batch=256 反而更快, + 方向不一致,判为测量噪声而非回归;这条也写在这里,因为把噪声当结论是这个领域最常见的错误。 +

      + + +

      六 · 工程鲁棒性

      +
      + + + + + + + + + +
      项目v2NM2.1
      一次写 20 条不同属性事实后存活12 / 20(8 条查询前被误删)20 / 20
      端到端(写入修复前后,16 用例)37.50%68.75%
      替换兼容性基线DROP-IN OK(16 / 16 键)
      单元测试—52 项通过
      未知问题泄漏(同形候选)75.00%0.00%
      +
      + + +

      七 · 三处改动,每处都有测量依据

      + +

      改动 1 · 记录排序混合权重取 0.5

      +

      + memory_record_router_blend 控制「打包的词面重排器」与「学习出的路由器」各占多少。 + 调这个之前先测了剂量曲线 —— 而且**此前的同进程测量因顺序污染已作废**,下面这版是独立进程测的。 +

      +
      + +

      改动 2 · 把加法先验参数化,然后测出「不该动」

      +

      + 假设是「加法先验压过了学习打分」。把它暴露成可调再测,结果**调小只会更差**: +

      +
      +

      + 这是一个有价值的负面结论:它否定了原来的假设。项目里这类结论和增益一样被保留下来, + 因为它们决定了后面不再往哪个方向投入。 +

      + +

      改动 3 · 覆盖门 —— 以及为了让它真正生效必须一起修的两处

      +

      + 覆盖门本身很简单:先判断「这个提问在问哪个属性」,再查库里有没有这个属性。但单独加门是不生效的, + 有两处**必须同时修**,否则门会被静默绕过: +

      +
      +
      01
      +

      _build_text_prefix 短路

      +

      门拒绝之后如果继续往下回落,会走**旧版 16 槽注入路径**,把记忆又塞回去。 + 实测:不修这一处,泄漏只从 75.00% 降到 62.50%。

      +
      02
      +

      闭包要动态解析 bank

      +

      reset_memory() 每次都会新建 memory_os_v2;闭包捕获了旧引用的话, + 门会永远看到一个空 bank,于是**静默旁路** —— 实测诊断输出 applicable:0 / bypassed:1。

      +
      03
      +

      门必须自门控

      +

      只有当库的属性集合填充了头部词表的 ≥ 90% 时才让门生效。 + 这条阈值是必需的:放宽成「子集即可」会让门在通用套件上误判, + 把 A 段可回答正确率从 100.00% 压到 92.50%、已知误拒升到 2.50%;收紧到 90% 后 A 段完全恢复, + C 段泄漏仍为 0.00%。

      +
      + + +

      八 · 写入路径:为什么 20 条事实会互相销毁

      +

      出处 · V2_dpskw\WRITE_PATH_FIX.md(根因由栈追踪确证,非推断)

      + +
      +

      症状

      +

      + 一次写入 20 条不同属性的事实后,库里有 20 条记录但只剩 12 条 active, + 8 条在查询前就被 retract。16 个关键零重叠用例里有 8 个的目标记录已经不存在 —— + 因此端到端正确率存在 50% 的硬上限。**任何排序器都救不回一条已经被删掉的记录。** +

      +
      + +

      16 次 retract 的调用栈完全一致:

      +
      +
      call stack
      +
      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」这一条: +

      + +
      + + + + + + + + + +
      写入序号事实learned_best结果
      2出生城市0.998959confirmed_update = True
      3办公城市0.992638confirmed_update = True
      9工位楼层0.964987confirmed_update = True
      12手机尾号0.998516confirmed_update = True
      15办公楼层0.997487confirmed_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 条。 +

      + +
      +
      +

      辅助事实 1

      +

      0 / 20

      +

      exact_slots.numel() > 0(token 完全相同)一次都没命中 —— 精确重复路径与此无关。

      +
      +
      +

      辅助事实 2

      +

      {2,3,5,6,7}

      +

      shared >= 2 毫无区分力:19 个候选的 shared 全落在这个区间,因为每条事实都含「我的 / 是」模板词元。

      +
      +
      +

      辅助事实 3

      +

      0.9985

      +

      190 个不相关属性对里有 10 个 ≥ 0.95,最高 0.9985(「常住城市」vs「出生城市」)。调阈值不可行:假阳性高达 0.9989,没有任何标量切点能分开「不相关属性」与「真更新」。

      +
      +
      + +

      + 根因:retire 一条已存在记录的授权完全来自学习打分加一个固定 0.95 阈值, + 没有任何「新文本与在位记录共享 (entity, attribute)」的结构性校验。 +

      + +

      修复(两处,都是结构性的)

      +
      +
      01
      +

      confirmed_update 的两条打分类分支改为结构优先

      +

      仅当候选文本的 entity::attribute 已经在库的 + active_by_conflict 账本里存在(即确实是同一条事实的更新)才允许由打分授权; + exact_slots 这条结构安全的分支保持不变,以保留「重复写入幂等」的既有语义。

      +
      02
      +

      retract 循环加冲突键守卫

      +

      if record.conflict_key() != candidate_conflict_key: continue —— + 循环只能退掉描述同一属性的记录,不再因「共享热槽」或「词面重叠高」销毁无关事实。

      +
      +

      + 语义不受损:真·同属性更新本来就已经由 PagedMemoryBankV2.write 通过 + active_by_conflict 做版本化(旧版本保留为 superseded、version+1), + 修复只是移除了那条额外的、无监督的销毁路径。 +

      + +

      四级验证

      +
      + + + + + + + + + + + + +
      级检验修复前修复后
      ①status_transition_summary{"active->retracted": 16}{}
      ①retraction_calls / status_counts16 次 / {retracted: 8, active: 12}[] / {active: 20}
      ②正对照:同属性改值—旧值 superseded + 新值 active,其余 19 属性不受影响,0 retract
      ③重排器缺席对照会踩死代码分支20 条全 active、0 retract
      ④target_record_active8 / 1616 / 16
      ④target_record_retracted_or_absent80
      ④active_records_min / max12 / 1220 / 20
      ④probe 口径回答正确率25.00%(4 / 16)43.75%(7 / 16)
      +
      + +

      端到端效果(官方 harness,16 个零重叠用例):

      +
      + + + + + + + +
      路由器修复前修复后答成别的属性
      deployed(原版 NM2)25.00%50.00%(+25.00 pp)18.75% → 12.50%
      V2-128-v637.50%68.75%(+31.25 pp)25.00% → 18.75%
      REPLAY-12837.50%68.75%(+31.25 pp)25.00% → 18.75%
      +
      +

      + 一个诚实的补充:这 16 个用例里 93.75% 的读取仍走旧版 16 槽路径而非 V2 库记录, + 地址命中 0 / 16。写入路径的修复解除了 50% 的硬上限,但「V2 库记录真正参与读取」这一项仍未解决。 +

      + + +

      九 · 未知拒答:从 75% 泄漏到 100% 拒答

      +

      出处 · V2_dpskw\ABSTENTION_BREAKTHROUGH.md

      + +

      + 这是整个项目最差的一个数字:问库里不存在的属性时,75% 的情况下模型照样编一个答案。 + 下面按「先排除一整类无效修法 → 再给出有效机制 → 最后如实记录接上运行时的两次失败」来写。 +

      + +

      9.1 先排除掉一整类无效修法(有测量支撑)

      +

      + 最直觉的修法是「检索分数不够高就拒答」。用只在训练集拟合、评测集算 AUC 的口径, + 测了四种打分器的分数几何(top1 / margin / top-k 均值 / 离散度 / 熵 / 近邻数,共 9 个特征): +

      +
      + + + + + + + + +
      打分器最佳单特征 AUC拟合头 AUC(评测集)
      cosine(冻结键余弦)0.6257(margin)0.6086
      打包 text_retriever0.6076(margin)0.4752
      50/50 混合0.54460.5062
      训练过的路由器0.57110.5130
      +
      +

      + 操作点同样不可用:混合打分器想标出 22% 的未知要误拒 28.40% 的可回答问题; + cosine 想标出 46% 要误拒 30.00%。 + 结论:75% 的泄漏不可能靠分数阈值或分数上的分类头修好。 + 这不是调参空间不够,而是这个信号根本不存在 —— 24 条同句式候选对任何提问都「一样像」。 +

      + +

      9.2 有效的机制:先判「问的是哪个属性」,再查库里有没有

      +

      + 换一个问题问就成立了:不是「有没有候选匹配得好」,而是「这个提问在问哪个属性,这个属性在库里吗」。 +

      +
      +
      +
      + + + + + + + + + +
      封闭词表值
      可回答 episode 中「预测属性在候选集内」100.00%(250 条)
      未知 episode 中「预测属性在候选集内」0.00%(50 条)
      未知拒答率100.00%
      已知问题被误拒率0.00%
      混淆矩阵tp=50 fn=0 fp=0 tn=250
      +
      +
      +
      +

      + 开放集:真实用户会问模型没见过的属性。只靠最大概率调阈值不够 —— + 实测没有拒绝选项时,从未见过的属性 100.00% 会被认成某个存在的属性。 + 所以改成显式训练一个 NONE 类:词表取 12 个属性,另取 6 个词表外属性的真实提问作为 NONE 的负样本, + 最后用第三组、与两者都不相交的 6 个全新属性做测试。 +

      +
      +
      词表内属性识别(133 条未见改写)100.00%
      +
      从未见过的属性被拒绝(52 条)100.00%
      +
      已知问题被误拒率0.00%
      +
      随机基线(13 类)7.69%
      +
      +

      + 运行时规则只有三步,全部可审计、无阈值调参:① 属性分类头读提问的冻结键 → 输出某个属性或 NONE; + ② 若为 NONE → 拒绝;③ 若为某属性 → 查库的活跃属性集合是否包含它,不含 → 拒绝。 +

      +
      +
      + +

      9.3 接上运行时:连续失败两次(如实记录)

      +
      + + + + + + + + + +
      指标NM2.1(无门)+ 覆盖门(第一轮)+ 门 + 短路(第二轮)
      A 可回答正确率100.00%6.25%0.00%
      A 已知问题被误拒率0.00%7.50%7.50%
      A 未知拒答率56.67%43.33%—
      C 未知泄漏率75.00%62.50%0.00%
      C 可回答正确率65.00%65.00%65.00%
      +
      +

      + 第一轮:门把几乎全部可回答问题都拒掉了(100.00% → 6.25%),而它本该修好的 C 段泄漏只从 75.00% 降到 62.50%。 + 第二轮定位到真正的机制缺口 —— read() 返回空记录后不会停止,_build_text_prefix + 会继续落到旧版 16 槽注入路径把无关记忆又塞回去。加了短路之后,C 段泄漏真的到了 0.00%、可回答正确率无损, + 但 A 段崩到 0.00%。 +

      +

      + A 段崩掉的原因已测定:段事实里 94.03% 是「这是普通对话噪声…不需要长期保存」, + 只有 5.97%(63 / 1056)能解析出属性;更关键的是 A 段的属性空间完全不在属性头训练的 24 个属性之内, + 头对 A 的提问只能输出那 24 个之一 → 必然不在库里 → 门把一切都拒掉。 + 也就是说:这个机制目前是「域内可用、跨域不可用」。 +

      +

      + 结论:已把 NM2.1 的 memory_coverage_gate 改回 false,恢复为已验证的可用状态。 + 属性头工件保留在 checkpoints/memory_attribute_head/,未随包启用。 + 正确的最终形态是开放词表的属性匹配(把提问与库里真实存在的属性名做匹配,词表随写入动态变化), + 而不是 24 类闭集分类头。 +

      + +
      +

      这一节最该记住的一句

      +

      + 离线 100.00% / 0.00% 是真实的,但它只证明了机制在「裸提问键 + 24 个可解析属性」这个条件下成立, + 并不等于接上运行时就能用。这一点必须在任何对外表述里讲清楚。 +

      +
      + + +

      十 · 关键负面结论:路由器排序不影响端到端答案

      +

      出处 · V2_dpskw\ZERO_OVERLAP_FINDINGS.md §5

      + +

      + NM2.1 的路由器在零重叠集合上 Top-1 提升了 41.20 pp,但在 + eval_router_critical_e2e.py(16 个零重叠用例)上,REPLAY-128 与 V2-128-v6 的结果 + 逐位相同(默认 Top-K:37.50% / 37.50%,答成别的属性 25.00% / 25.00%,平均选中记录 5.88 / 5.88)。 +

      +

      + 于是做了一个判决性实验:保留 need_memory / hop_controller / head_gate + 全部原权重,只把排序通路(query_projection、key_projection、pair_scorer) + 替换为同形状随机权重。结果在默认 Top-K 与 Top-K=1 下都完全不变。 +

      +
      +
      01
      +

      memory_os_v2.py:1671:路由器分数被逐位置覆盖

      +

      带 semantic_key 的记录,其 _score_candidates() 分数会被 + self.record_scorer 覆盖。实测残差与 text_retriever 匹配 18 / 18, + 与 memory_router_v2 匹配 0 / 18。

      +
      02
      +

      「部署目录没有 text_retriever.pt 所以走余弦兜底」是错的

      +

      重排器被烘焙进合并包(safetensors 内有 6 个 + dynamic_memory.text_retriever.* 张量,1,573,377 参数),_text_retriever_ready + 实测为 True,余弦兜底分支根本不执行。

      +
      03
      +

      路由器在此配置下唯一实际生效的杠杆是 need_memory 门

      +

      部署权重下门开启率 68.75%(11 / 16),而三份随机初始化的路由器只有 + 37.5%(6 / 16)。判决性探针只随机化了排序通路、保留了门控头,所以「端到端零变化」 + 是由构造保证的,不是巧合。

      +
      +

      + 结论分两层,不要混为一谈:① 路由器记分卡上 22 / 22 的统治性和 +41.20 pp 的零重叠提升, + 都是路由器工件真实的增益;② 但在这套运行时里,记录级顺序由打包的 text_retriever 决定, + 路由器分数被覆盖,且相当比例的读取根本不走 V2 库、目标记录还会被写入路径删除。 + 所以路由器增益不会传递到最终答案。 +

      + + +

      十一 · 仍未解决(不粉饰)

      +
      + + + + + + + + + + +
      短板现状说明
      跨域未知拒答未解决覆盖头是 24 类闭集,只在其词表被库填充 ≥ 90% 时生效;开放词表的属性匹配实测仅 49.20% Top-1 / AUC 0.6560(零训练),不足
      答成别的属性27.50%同形候选间排序仍不理想;已排除先验重加权(更差)与单纯替换打分器(更差)
      A 段未知拒答率56.67%未见改善
      规模验证未做仅 24 属性 / 300 条评测;生产需上千属性、上万改写问法
      V2 库记录真正参与读取未解决16 个关键用例里 93.75% 仍走旧版 16 槽路径,地址命中 0 / 16
      通用能力回归套件未跑eval_general_capability.py 依赖的 comprehensive_general.jsonl 不存在
      +
      + + +

      十二 · 复现

      +
      +
      + PowerShell · 整体电池(A / B / C / D 四段) + +
      +
      $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
      +
      + +

      + 台账式的逐条原文在 真实评测台账; + v2 的正式脏数据迁移测试在 测量结果。 +

      diff --git a/src/pages/reproduce.body.html b/src/pages/reproduce.body.html new file mode 100644 index 0000000..7ce4006 --- /dev/null +++ b/src/pages/reproduce.body.html @@ -0,0 +1,37 @@ +

      + 正式脏数据迁移测试的完整命令与产物路径。协议参数与批次之间的差异见下方说明—— + 复现同一批次才能得到同一组数字。 +

      +
      +
      +
      + PowerShell · 工作目录 + +
      +
      +
      +
      +
      +
      +

      PROTOCOL

      +
      +
      基座模型
      +
      量化4-bit NF4
      +
      解码greedy
      +
      最大新生成64 token
      +
      进程显存上限10 GiB
      +
      记忆读取上限2 records
      +
      地址编码batch size 1
      +
      +
      +
      +
      +
      +

      两个批次的差别

      +

      + 上面的命令复现的是批次 A(对外技术总结采用的口径,可答题 154/246)。 + 批次 B 来自同目录的 dirty_real_corpus_compare_4b_router_final.json, + 是接入记忆路由器之后的另一次运行(可答题 203/246)。两次运行的配置不同, + 因此它们的分数不能平均、不能合并,也不能互相替代。 +

      +
      \ No newline at end of file diff --git a/src/pages/results.body.html b/src/pages/results.body.html new file mode 100644 index 0000000..bef331b --- /dev/null +++ b/src/pages/results.body.html @@ -0,0 +1,12 @@ +

      + 三个面板对应三组不同的题集与口径,不能合并成一个总分。 + 面板一是同一题集先后跑的两个独立批次(配置不同、分数不可合并,只能并列读); + 面板二是结构化工程基准;面板三是记忆路由器工程线。每组数据都标注了出处、日期与口径。 +

      +

      + 读这些数字时请留意两件事:所有对照都使用同一 tokenizer、同一 greedy 解码、同一 64 token 输出上限; + 而检索召回率与回答正确率是两个必须分开报告的量——这一点在面板一里体现得最清楚。 +

      + +
      +
      \ No newline at end of file diff --git a/src/pages/roadmap.body.html b/src/pages/roadmap.body.html new file mode 100644 index 0000000..aec2523 --- /dev/null +++ b/src/pages/roadmap.body.html @@ -0,0 +1,8 @@ +

      + 左侧是在 12 GiB GPU 上已经跑通的闭环;右侧是按对正确率最有帮助的顺序排出的下一阶段工程任务。 + 这里刻意不写时间表——顺序比日期更可信。 +

      +

      已经解决的问题

      +
      +

      下一阶段的工程任务

      +
      \ No newline at end of file diff --git a/src/partials/readpath.svg b/src/partials/readpath.svg new file mode 100644 index 0000000..6b04eba --- /dev/null +++ b/src/partials/readpath.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + 01 + 用户问题 + 自然语言 + + + + 02 + 隐状态地址 + Qwen hidden + + + + 03 + LSH 粗索引 + 730 桶 + + + + 04 + 候选页 + ≤4 页 · 32/页 + + + + 05 + 页内重排 + text_retriever + + + + 06 + Top-K 记录 + ≤2 条 + + + + 07 + 证据前缀 + 注入 Qwen + + + + 08 + 普通生成 + 回答 + + + + 无全量注意力 + BOUNDED READ + 当前问题不与全部记忆记录做注意力,只与少量页面和记录交互 + 读取额外耗时 61.10 ms → 26.35 ms(批次 A → B) · 前缀命中率 96.97% · GPU cache 实际 17,990 / 上限 131,072 token + diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..64aec1f --- /dev/null +++ b/start.bat @@ -0,0 +1,19 @@ +@echo off +rem ============================================================ +rem Natural Memory v2 — 站点启动脚本(Windows) +rem 默认监听 127.0.0.1:7443,仅本机可达,供反向代理使用 +rem ============================================================ +setlocal +cd /d "%~dp0" + +set NM_HOST=%NM_HOST% +if "%NM_HOST%"=="" set NM_HOST=127.0.0.1 +set NM_PORT=%NM_PORT% +if "%NM_PORT%"=="" set NM_PORT=7443 + +rem 优先使用项目常用的 conda 环境,其次退回 PATH 里的 python +set PYBIN=C:\Program Files\python\python.exe +if not exist "%PYBIN%" set PYBIN=python + +echo Starting Natural Memory v2 site on http://%NM_HOST%:%NM_PORT%/ +"%PYBIN%" server.py --host %NM_HOST% --port %NM_PORT% diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..54391fd --- /dev/null +++ b/templates/base.html @@ -0,0 +1,119 @@ + + + + + +{{TITLE}} + + + + + + + + + + + + + + + + + + + +
      +
      + + + Natural Memory + v2 + + + {{NUM}} + {{ZH}} + + + + + + 读取中 + +
      +
      +
      + +{{MAIN}} + + + + + + + + + + + + + + + + diff --git a/tools/_gate.html b/tools/_gate.html new file mode 100644 index 0000000..d8b238e --- /dev/null +++ b/tools/_gate.html @@ -0,0 +1,271 @@ + + +gate + + + + + + + + diff --git a/tools/make_ledger.py b/tools/make_ledger.py new file mode 100644 index 0000000..3bd4f48 --- /dev/null +++ b/tools/make_ledger.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +从磁盘上的真实评测 JSON 生成站点用的台账数据 assets/js/ledger.js。 + +⚠️ 生成物不要手改;改数据请改上游 JSON 后重跑本脚本。 + +来源(两份都是工程里保存的正式运行结果,逐字复制,不做任何改写或换算): + A. H:\\Memory\\V2_dpskw\\runtime_score_bands.json 60 episodes + B. H:\\Memory\\V2_dpskw\\nm2_1_final_runtime_e2e.json 48 episodes + +用法: + python tools/make_ledger.py +""" +from __future__ import annotations + +import io +import json +import os + +SOURCES = [ + { + "key": "bands", + "label": "运行时评分带扫描", + "path": r"H:\Memory\V2_dpskw\runtime_score_bands.json", + "config": "0.00", + "note": "60 用例 · 24 同形候选 · prior=1.00 blend=0.00", + }, + { + "key": "e2e", + "label": "NM2.1 最终运行时", + "path": r"H:\Memory\V2_dpskw\nm2_1_final_runtime_e2e.json", + "config": "prior=1.00 blend=0.50", + "note": "48 用例 · 24 同形候选 · prior=1.00 blend=0.50", + }, +] + +OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "assets", "js", "ledger.js") + + +def rows_of(path: str, key: str): + with open(path, encoding="utf-8") as fh: + j = json.load(fh) + rows = j.get("rows") or {} + if key not in rows: + raise SystemExit("在 %s 里找不到 rows[%r]" % (path, key)) + return rows[key] + + +def main() -> int: + out, total = [], 0 + for src in SOURCES: + rows = rows_of(src["path"], src["config"]) + total += len(rows) + out.append({ + "key": src["key"], + "label": src["label"], + "note": src["note"], + "n": len(rows), + "items": [{ + "id": r.get("id"), + "attr": r.get("attribute"), + "ans": bool(r.get("answerable")), + "exp": r.get("expected"), + "reply": r.get("reply"), + "ok": bool(r.get("correct")), + "wrong": bool(r.get("wrong_attribute")), + "leak": bool(r.get("leaked")), + "stop": r.get("stop_reason"), + "sel": r.get("records_selected"), + "score": r.get("top_score"), + "active": r.get("records_active"), + "need": r.get("need_memory"), + } for r in rows], + }) + + body = io.StringIO() + body.write("/* ==================================================================\n") + body.write(" Natural Memory v2 — ledger.js\n") + body.write(" 「真实评测台账」的数据层。\n\n") + body.write(" ⚠️ 本文件由 tools/make_ledger.py 从磁盘上的原始评测 JSON 生成,\n") + body.write(" 请勿手改。下面是全部 %d 条 episode,字段逐字复制、未做改写或换算。\n\n" % total) + for s in SOURCES: + body.write(" %s\n" % os.path.basename(s["path"])) + body.write(" ================================================================== */\n") + body.write("window.NM_LEDGER = ") + body.write(json.dumps(out, ensure_ascii=False, separators=(",", ":"), indent=0) + .replace("\n", "")) + body.write(";\n") + + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, "w", encoding="utf-8", newline="\n") as fh: + fh.write(body.getvalue()) + print("写出 %s(%d 条 episode / %d 字节)" % (OUT, total, os.path.getsize(OUT))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())