From 1da4b14c19175fd88f5ca0172ff95f3e6c2e5162 Mon Sep 17 00:00:00 2001 From: WpyQwq <3911625973@qq.com> Date: Sat, 19 Sep 2026 11:21:45 +0800 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20=E8=87=AA=E7=84=B6=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E7=AB=99=EF=BC=9A12=20=E9=A1=B5=E7=BA=AF=E6=A0=87?= =?UTF-8?q?=E5=87=86=E5=BA=93=20Python=20=E9=9D=99=E6=80=81=E7=AB=99?= =?UTF-8?q?=E7=82=B9=E7=94=9F=E6=88=90=E5=99=A8=EF=BC=8C=E5=90=AB=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E8=AF=84=E6=B5=8B=E5=8F=B0=E8=B4=A6=EF=BC=88108=20?= =?UTF-8?q?=E6=9D=A1=EF=BC=89=E4=B8=8E=E5=85=A8=E7=AB=99=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E6=A3=80=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DESIGN.md | 230 +++ assets/css/site.css | 1448 +++++++++++++++++ assets/favicon.svg | 15 + assets/fonts/archivo-latin-wdth-normal.woff2 | Bin 0 -> 90104 bytes .../fonts/geist-mono-latin-wght-normal.woff2 | Bin 0 -> 23128 bytes assets/js/data.js | 334 ++++ assets/js/examples.js | 226 +++ assets/js/ledger.js | 11 + assets/js/nm21.js | 50 + assets/js/restart.js | 115 ++ assets/js/search.js | 2 + assets/js/site.js | 1188 ++++++++++++++ assets/js/space.js | 256 +++ build.py | 324 ++++ capacity.html | 184 +++ cost.html | 173 ++ division.html | 284 ++++ examples.html | 161 ++ index.html | 585 +++++++ ledger.html | 183 +++ limits.html | 164 ++ mechanism.html | 335 ++++ nm21.html | 704 ++++++++ reproduce.html | 195 +++ results.html | 170 ++ roadmap.html | 166 ++ server.py | 229 +++ src/pages/capacity.body.html | 26 + src/pages/cost.body.html | 15 + src/pages/division.body.html | 126 ++ src/pages/examples.body.html | 3 + src/pages/index.body.html | 401 +++++ src/pages/ledger.body.html | 25 + src/pages/limits.body.html | 6 + src/pages/mechanism.body.html | 114 ++ src/pages/nm21.body.html | 546 +++++++ src/pages/reproduce.body.html | 37 + src/pages/results.body.html | 12 + src/pages/roadmap.body.html | 8 + src/partials/readpath.svg | 64 + start.bat | 19 + templates/base.html | 119 ++ tools/_gate.html | 271 +++ tools/make_ledger.py | 100 ++ 44 files changed, 9624 insertions(+) create mode 100644 DESIGN.md create mode 100644 assets/css/site.css create mode 100644 assets/favicon.svg create mode 100644 assets/fonts/archivo-latin-wdth-normal.woff2 create mode 100644 assets/fonts/geist-mono-latin-wght-normal.woff2 create mode 100644 assets/js/data.js create mode 100644 assets/js/examples.js create mode 100644 assets/js/ledger.js create mode 100644 assets/js/nm21.js create mode 100644 assets/js/restart.js create mode 100644 assets/js/search.js create mode 100644 assets/js/site.js create mode 100644 assets/js/space.js create mode 100644 build.py create mode 100644 capacity.html create mode 100644 cost.html create mode 100644 division.html create mode 100644 examples.html create mode 100644 index.html create mode 100644 ledger.html create mode 100644 limits.html create mode 100644 mechanism.html create mode 100644 nm21.html create mode 100644 reproduce.html create mode 100644 results.html create mode 100644 roadmap.html create mode 100644 server.py create mode 100644 src/pages/capacity.body.html create mode 100644 src/pages/cost.body.html create mode 100644 src/pages/division.body.html create mode 100644 src/pages/examples.body.html create mode 100644 src/pages/index.body.html create mode 100644 src/pages/ledger.body.html create mode 100644 src/pages/limits.body.html create mode 100644 src/pages/mechanism.body.html create mode 100644 src/pages/nm21.body.html create mode 100644 src/pages/reproduce.body.html create mode 100644 src/pages/results.body.html create mode 100644 src/pages/roadmap.body.html create mode 100644 src/partials/readpath.svg create mode 100644 start.bat create mode 100644 templates/base.html create mode 100644 tools/_gate.html create mode 100644 tools/make_ledger.py 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 0000000000000000000000000000000000000000..e2448d0561ffdb0b0974d063a7cdbb712f41ad49
GIT binary patch
literal 90104
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";
+      }
+      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())