Initial commit: 自然记忆站:12 页纯标准库 Python 静态站点生成器,含真实评测台账(108 条)与全站客户端检索

This commit is contained in:
WpyQwq
2026-09-19 11:21:45 +08:00
commit 1da4b14c19
44 changed files with 9624 additions and 0 deletions
+1448
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Natural Memory 记忆地址点阵标记:暗格为地址空间,亮格为命中记录 -->
<rect width="32" height="32" fill="#08090A"/>
<g fill="#3A4045">
<rect x="3" y="3" width="6" height="6"/>
<rect x="13" y="3" width="6" height="6"/>
<rect x="23" y="3" width="6" height="6"/>
<rect x="3" y="13" width="6" height="6"/>
<rect x="23" y="13" width="6" height="6"/>
<rect x="3" y="23" width="6" height="6"/>
<rect x="13" y="23" width="6" height="6"/>
</g>
<rect x="13" y="13" width="6" height="6" fill="#D6F24B"/>
<rect x="23" y="23" width="6" height="6" fill="#D6F24B"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.
Binary file not shown.
+334
View File
@@ -0,0 +1,334 @@
/* ------------------------------------------------------------------
Natural Memory v2 — 站点数据层
所有数字均直接摘自工程内的正式报告,src 字段标注出处文件。
本文件不包含任何推算、外推或未测量的数字。
------------------------------------------------------------------ */
window.NM_DATA = {
meta: {
name: "Natural Memory v2",
version: "v2",
base: "Qwen3.5-4B",
date: "2026-09",
repo: "H:\\Memory",
disclaimer:
"本文只描述当前实现已经完成并实际测量的内容。它不把系统描述成通用智能,也不把实验室结果表述成线上服务承诺。"
},
/* ---------- 一句话定位 ---------- */
positioning: {
headline: "模型重启之后,它仍然记得。",
latin: "A memory layer inside the model.",
body:
"Natural Memory v2 是接在本地 Qwen3.5-4B 上的一个模型内记忆层:把适合长期复用的个人事实、项目事实和短对话片段写进带地址的记忆记录," +
"当前问题只读取少量相关记录,再把这些记录作为模型内部前缀交给 Qwen 生成答案。",
problem:
"要解决的具体问题:模型重启后不携带历史聊天记录,仍能访问已经保存的个人或项目事实,同时不把全部历史转换成 GPU 上的长 KV Cache。"
},
/* ---------- 记忆与 KV 的分工 ---------- */
division: [
{ k: "最近对话", v: "Qwen 热 KV", note: "顺序关系仍由普通上下文负责" },
{ k: "长期个人/项目事实", v: "Natural Memory 记录", note: "事实、版本、来源、短证据、语义地址" },
{ k: "当前问题", v: "有界路由与 Top-K 读取", note: "不做全量 slot 注意力" },
{ k: "原始持久化状态", v: "嵌入式 memory safetensors", note: "无 SQLite、无磁盘分页" }
],
/* ---------- 读取路径(可交互的分步) ---------- */
readPath: [
{ id: "q", label: "用户问题", detail: "普通自然语言,不要求 /remember 之类的显式指令。", io: "输入 · 一句口语化的问题" },
{ id: "hs", label: "Qwen hidden state", detail: "由模型隐状态生成紧凑查询地址,而不是独立的外部 embedding 服务。", io: "问题 → 紧凑查询地址(单条 batch 编码,避免显存瞬时峰值)" },
{ id: "lsh", label: "LSH / 地址粗索引", detail: "精确桶 + Hamming-1/2 探针,把候选收敛到少量页面。", io: "查询地址 → 候选页(批次 A 粗索引 730 个桶,最近一次粗筛返回 24 个候选)" },
{ id: "pg", label: "候选页", detail: "页容量 32 条记录;默认最多读取 4 页。", io: "候选页 → 少量页面(本次 723 条 active records 分布在 23 个页面上)" },
{ id: "rr", label: "页内记录重排", detail: "记录级重排由独立的 text_retriever 完成(6 张量、1,573,377 参数)。", io: "页内记录 → 有序候选(text_retriever 自身 Top-1 只有 23.20%,是当前主要瓶颈)" },
{ id: "tk", label: "Top-K 记录", detail: "正式脏数据测试中的读取上限为 2 条。", io: "有序候选 → 少量证据(目标召回:批次 A 166/246,批次 B 228/246)" },
{ id: "inj", label: "证据前缀注入 Qwen", detail: "命中的短证据被提升到 GPU,作为模型内部前缀。", io: "证据 → 模型内部前缀(批次 B 96.97% 的题实际注入,平均 410.34 token)" },
{ id: "gen", label: "普通生成", detail: "记忆主体留在进程内存,只有热点记录进入有界 GPU cache。", io: "前缀 + 问题 → 回答(读取额外耗时:批次 A 61.10 ms → 批次 B 26.35 ms)" }
],
readPathCost: { value: 61.1, unit: "ms", label: "读取路径额外耗时", src: "对外技术总结 §4" },
/* ---------- 记忆记录解剖 ---------- */
recordFields: [
{ f: "raw_text", d: "原始短文本" },
{ f: "semantic_addr", d: "语义地址" },
{ f: "entity", d: "实体" },
{ f: "attribute", d: "属性" },
{ f: "value", d: "值" },
{ f: "timestamp", d: "时间" },
{ f: "source", d: "来源" },
{ f: "confidence", d: "置信度" },
{ f: "importance", d: "重要性" },
{ f: "access_count", d: "访问次数" },
{ f: "status", d: "状态" },
{ f: "supersedes", d: "版本关系" },
{ f: "related_ids", d: "关联记录" },
{ f: "audit", d: "审计信息" }
],
states: [
{ s: "active", d: "当前有效事实,参与正常读取", tone: "ok" },
{ s: "superseded", d: "同一实体+属性出现新值,旧值退役", tone: "mute" },
{ s: "quarantined", d: "低置信度写入,需显式批准", tone: "warn" },
{ s: "retracted", d: "已撤回,但不从审计中消失", tone: "bad" }
],
/* ---------- 主结果:正式脏数据迁移测试 ---------- */
dirty: {
title: "正式脏数据迁移测试",
src: "对外技术总结 §3",
subtitle: "真实工程源码 / 文档 + 本项目对话中用户明确说过的短事实",
corpus: [
{ k: "真实源码与文档", v: "65 个文件 · 1,110,524 字节 · 620 个分片" },
{ k: "用户对话事实", v: "15 条记录" },
{ k: "由记录派生的问题", v: "44 题(38 可回答 / 6 未知)" },
{ k: "来自源码与文档", v: "220 题" },
{ k: "题目总数", v: "264 题 · 246 可回答 · 18 未知" }
],
protocol: "Qwen3.5-4B · 4-bit NF4 · greedy 解码 · 最大新生成 64 token · 进程显存上限 10 GiB · 读取上限 2 条记录 · 同一 tokenizer",
batchNote: "同一题集(264 题 / 246 可回答 / 18 未知)、同一协议、同一仓库,先后跑了两个独立批次。批次配置不同,因此两个批次的分数不可合并、不可平均,只能并列读。",
headline: [
{ metric: "可回答正确率", base: 1.22, a: 62.60, b: 82.52, baseRaw: "3/246", aRaw: "154/246", bRaw: "203/246" },
{ metric: "目标记忆召回率", base: 0.00, a: 67.48, b: 92.68, baseRaw: "0/246", aRaw: "166/246", bRaw: "228/246" },
{ metric: "总体正确率", base: 7.58, a: 64.39, b: 82.95, baseRaw: "20/264", aRaw: "170/264", bRaw: "219/264" },
{ metric: "未知拒答正确率", base: 94.44, a: 88.89, b: 88.89, baseRaw: "17/18", aRaw: "16/18", bRaw: "16/18" }
],
batches: [
{
id: "A", label: "批次 A", date: "2026-09-05 23:22", file: "dirty_real_corpus_compare_4b.json",
tag: "对外技术总结采用",
reader: 61.10, promptTokens: 641.42, prefixRate: 90.91, prefixTokens: 610.67,
latencyBase: 2650.4, latencyNm: 2718.9, decodeBase: 24.06, decodeNm: 21.68,
allocNm: 3.134, resvNm: 5.021, records: 723, pages: 23, cacheTokens: 15885
},
{
id: "B", label: "批次 B", date: "2026-09-06 11:36", file: "dirty_real_corpus_compare_4b_router_final.json",
tag: "接入记忆路由器后",
reader: 26.35, promptTokens: 441.19, prefixRate: 96.97, prefixTokens: 410.34,
latencyBase: 2854.9, latencyNm: 2878.3, decodeBase: 22.34, decodeNm: 20.68,
allocNm: 3.134, resvNm: 5.180, records: 738, pages: 24, cacheTokens: 17990
}
],
categories: [
{ k: "symbol_navigation", label: "符号定位(函数在哪个文件)", n: 192, base: "0/192", a: "129/192", b: "181/192" },
{ k: "unknown_refusal", label: "未知拒答", n: 18, base: "17/18", a: "16/18", b: "16/18" },
{ k: "constraint", label: "约束类问题", n: 15, base: "0/15", a: "5/15", b: "6/15" },
{ k: "paraphrase", label: "自然改写", n: 9, base: "0/9", a: "5/9", b: "5/9" },
{ k: "cross_session", label: "跨会话", n: 6, base: "1/6", a: "5/6", b: "5/6" },
{ k: "temporal_conflict", label: "时间冲突目录查询", n: 4, base: "0/4", a: "4/4", b: "4/4" },
{ k: "reasoning", label: "推理", n: 4, base: "0/4", a: "2/4", b: "0/4" },
{ k: "write_safety", label: "写入安全解释", n: 2, base: "0/2", a: "2/2", b: "0/2" },
{ k: "baseline_limitation", label: "基线局限说明", n: 2, base: "2/2", a: "2/2", b: "2/2" },
{ k: "operational_architecture", label: "操作性架构", n: 2, base: "0/2", a: "0/2", b: "0/2" },
{ k: "architecture_tradeoff", label: "架构取舍", n: 2, base: "0/2", a: "0/2", b: "0/2" },
{ k: "memory_tiers", label: "记忆分层", n: 2, base: "0/2", a: "0/2", b: "0/2" },
{ k: "benchmark_protocol", label: "基准协议", n: 2, base: "0/2", a: "0/2", b: "0/2" },
{ k: "control_path", label: "控制路径", n: 2, base: "0/2", a: "0/2", b: "0/2" },
{ k: "router_path", label: "路由器路径", n: 2, base: "0/2", a: "0/2", b: "0/2" }
],
categoryNote: "类别题量差异极大:符号定位 192 题,而 11 个类别各只有 2 题——2 题的类别里错 1 题就是 50 个百分点,不构成结论。批次 B 也并非全面提升:符号定位从 129 升到 181,但写入安全从 2/2 掉到 0/2、推理从 2/4 掉到 0/4。",
domains: [
{ domain: "用户对话事实", n: 38, base: 2.63, baseRaw: "1/38", a: 55.26, aRaw: "21/38", b: 52.63, bRaw: "20/38" },
{ domain: "真实源码/文档", n: 208, base: 0.96, baseRaw: "2/208", a: 63.94, aRaw: "133/208", b: 87.98, bRaw: "183/208" }
],
gap: {
recallA: 67.48, recallARaw: "166/246",
answerA: 62.60, answerARaw: "154/246",
recallB: 92.68, recallBRaw: "228/246",
answerB: 82.52, answerBRaw: "203/246",
note: "目标记忆召回与最终回答正确必须分开报告——检索准确率不等于回答准确率。批次 A 的召回 67.48%、回答 62.60%(差 4.88pp);批次 B 召回 92.68%、回答 82.52%(差 10.16pp):路由器把该查的记忆查回来了,但证据到答案的闭环仍有丢失。"
}
},
/* ---------- 实测样例(逐字取自评测原始结果) ---------- */
examples: {
title: "实测样例",
src: "dirty_real_corpus_compare_4b_router_final.json(批次 B 逐条记录)",
subtitle: "下面五条题目与回答全部逐字取自评测原始结果,未经改写或润色。包含两条我们判错的样例。",
note:
"判分规则:可回答题要求 expected_anchors 里的锚点全部出现在回答中(去空白、转小写后做子串匹配);未知题则看模型是否拒答。" +
"输出上限为 64 token,标注「达上限截断」的回答是被硬截断的原文,不是完整句——我们保留原样,不做补全。" +
"另外:第 1 条命中的记忆记录里写的是「Natural Memory v1」,那是这条记录写入时的项目名,记录内容按原样保留、未做改写;" +
"第 5 条则是证据明明已经召回、回答却没能把锚点说全,因此判为未命中——这类「检索对、答案错」正是当前最主要的失分来源。"
},
/* ---------- Stage 5 结构化工程基准 ---------- */
stage5: {
title: "Stage 5 结构化工程基准",
src: "对外技术总结 §5",
subtitle: "与脏数据迁移集不是同一题集,不能合并成一个总分",
sets: [
{
name: "通用个人事实",
n: 107,
rows: [
{ sys: "原版 Qwen3.5-4B", acc: 3.74, raw: "4/107", lat: 1233.2, tok: 23.51, abstain: "21/21" },
{ sys: "强 RAG 基线", acc: 99.07, raw: "106/107", lat: 885.8, tok: 21.67, abstain: "20/21" },
{ sys: "Natural Memory v2", acc: 100.00, raw: "107/107", lat: 994.3, tok: 21.70, abstain: "21/21" }
]
},
{
name: "项目库",
n: 64,
rows: [
{ sys: "原版 Qwen3.5-4B", acc: 70.31, raw: "45/64", lat: 1351.5, tok: 23.70, abstain: null },
{ sys: "强 RAG 基线", acc: 100.00, raw: "64/64", lat: 1395.3, tok: 22.94, abstain: null },
{ sys: "Natural Memory v2", acc: 100.00, raw: "64/64", lat: 1463.2, tok: 21.85, abstain: null }
]
}
],
note: "强 RAG 使用本地 CPU bert-base-chinese 向量、稀疏特征和固定规则重排。它不是训练好的公开 cross-encoder,因此可以作为更强的本地基线,但不能代表所有生产 RAG 服务。"
},
/* ---------- 代价账本 ---------- */
cost: {
title: "代价账本",
src: "对外技术总结 §4",
subtitle: "以下数字来自同一次正式脏数据测试。显存字段是每次请求结束后的 allocated/reserved 快照,不是 torch.cuda.max_memory_allocated() 记录的严格峰值。",
rows: [
{ k: "平均总延迟", base: "2650.4 ms", nm2: "2718.9 ms", delta: "+2.58%", tone: "warn" },
{ k: "平均解码速度", base: "24.06 tok/s", nm2: "21.68 tok/s", delta: "-9.90%", tone: "bad" },
{ k: "平均输入 token", base: "30.8", nm2: "641.4", delta: "证据前缀增加", tone: "mute" },
{ k: "读取额外耗时", base: "无", nm2: "61.1 ms", delta: "新增路径", tone: "mute" },
{ k: "allocated 快照峰值", base: "3.084 GiB", nm2: "3.134 GiB", delta: "+0.050 GiB", tone: "ok" },
{ k: "reserved 快照峰值", base: "3.234 GiB", nm2: "5.021 GiB", delta: "+1.787 GiB", tone: "warn" }
],
vram: { card: 12, allocBase: 3.084, allocNm2: 3.134, resvBase: 3.234, resvNm2: 5.021, unit: "GiB" },
quote: "这个代价来自 Qwen 查询编码、稀疏路由、证据前缀和热点缓存,而不是免费获得的能力。"
},
/* ---------- 容量与存储 ---------- */
capacity: {
title: "容量与存储",
src: "对外技术总结 §4",
stats: [
{ k: "active records", v: "723", d: "本次运行实际装载" },
{ k: "pages", v: "23", d: "页容量 32 records/page" },
{ k: "设计容量", v: "1,048,576", d: "地址空间容量,非已装入数量" },
{ k: "GPU cache 上限", v: "256 records", d: "且 131,072 token" },
{ k: "实际 GPU cache token", v: "15,885", d: "" },
{ k: "GPU cache fallback", v: "0", d: "分配失败 0" },
{ k: "cold page", v: "0", d: "本次为 embedded / process-RAM 模式" }
],
storage: {
mode: "embedded weight-shard",
text: "记忆快照、路由器参数、页面元数据和短文本证据写入模型包的 memory safetensors 切片。启动时加载到进程内存,命中的热点记录才进入有界 GPU cache。",
tradeoff: "当前默认路径不使用 SQLite,也不使用磁盘分页。该设计减少了运行时依赖,但意味着模型包会随持久化记忆增长而变大,且需要重新保存权重切片才能固化更新。"
},
grid: { pagesTotal: 32768, perPage: 32, usedPages: 23 }
},
/* ---------- 记忆路由器工程线 ---------- */
router: {
title: "记忆路由器工程线",
src: "PRODUCTION_ROUTER.md · router_scorecard_final.md · V6_FINAL_REPORT.md · ZERO_OVERLAP_FINDINGS.md · WRITE_PATH_FIX.md",
subtitle: "128 维路由器替换原 512 维方案。以下为工程报告中的实测值。",
spec: [
{ k: "参数量", v: "2,037,774" },
{ k: "张量数", v: "16 (float32)" },
{ k: "地址字节", v: "512 B" },
{ k: "结构", v: "dim=128 · heads=8 · hidden=2560 · max_hops=3" },
{ k: "权重摘要", v: "sha256 69f8295e…d189deb · 8,156,921 B" },
{ k: "冻结评测集", v: "21,920 episode / 10 类" }
],
metrics: [
{ k: "Top-1 命中", base: "41.12%", nm2: "94.14%", note: "REPLAY-128" },
{ k: "Recall@1 / @3 / @5", base: "37.03 / 46.73 / 48.91", nm2: "88.58 / 96.48 / 97.15", note: "" },
{ k: "MRR / nDCG@3", base: "47.69 / 44.22", nm2: "95.77 / 95.37", note: "" },
{ k: "多跳证据全中 (Top-3)", base: "43.43%", nm2: "96.22%", note: "" },
{ k: "hop 正确率", base: "73.23%", nm2: "100.00%", note: "欠预测率 4.68% → 0.00%" },
{ k: "未知拒答率", base: "0.00%", nm2: "100.00%", note: "门槛 0.30–0.80 每点均为 100%" },
{ k: "单查询延迟", base: "1.0995 ms", nm2: "0.9663 ms", note: "路由 QPS 909.5 → 1034.8" },
{ k: "batch256 吞吐", base: "207,287", nm2: "242,759", note: "texts/s 口径" },
{ k: "零重叠改写 Top-1", base: "18.40%", nm2: "59.60%", note: "+41.20pp,随机基线 4.17%" },
{ k: "写入路径存活记录", base: "12/20", nm2: "20/20", note: "retract 16 → 0;e2e 37.50% → 68.75%" },
{ k: "逐轴判定", base: "—", nm2: "99 通过 / 11 未通过", note: "128 维两条线 22/22 全方位超越" }
],
caveats: [
"端到端答案不由路由器决定:93.75% 的读取仍走旧版 16 槽注入路径,地址命中 0/16,记录级排序由 text_retriever 决定(其 Top-1 仅 23.20%)。路由器 +41.20pp 不传递到最终答案。",
"512 维方案「全部指标不劣于基线」在数学上不可达(地址存储 ×4 与 batch-256 吞吐两条独立障碍);XL 架构准确率最高,但吞吐真实低 26–52%。",
"拒答机制只在域内成立:跨域 A 段可回答 0.00%,集成门已回退为 gate=false;开放词表属性匹配仅 49.20% / AUC 0.6560。",
"属性头词表固定 24 类闭集:训练后新写入的属性会被判为 NONE 而误拒。",
"同一指标在多份报告中的延迟口径不一致(1.0995→0.9663、0.9549→0.9169 等),本页只列其中一套。",
"长上下文:8K 可跑,16K / 32K OOM。不得据此宣称百万级 token 上下文。"
]
},
/* ---------- 边界:不能宣称的内容 ---------- */
limits: {
title: "我们不宣称的内容",
subtitle: "以下说法目前没有足够证据支持。把它们放在这里,是因为它们和上面的数字一样重要。",
claims: [
"Natural Memory 已经等价于百万 token 的完整 KV",
"Natural Memory 已经击败生产级 embedding + reranker RAG",
"任何自然语言表达都能稳定召回正确记忆",
"记忆写入永远不会出错",
"一百万条记录已经完成端到端验证",
"当前 4B 结果可以直接外推到 14B、32B 或更大模型",
"reserved VRAM 就是实际模型峰值显存",
"本次用户事实测试代表大规模真实用户群体"
],
limitations: [
{ t: "训练数据仍不够自然", d: "路由器主体训练仍来自本地构造的实体—属性—值事实和 hard negatives。自然改写仍是人工编写的测试变体,不是大规模脱敏真人会话。" },
{ t: "真实仓库的语义索引覆盖有限", d: "620 个真实分片全部进库,但为控制 GPU 压力,只有 96 个符号目标和 8 个操作性事实建立了 Qwen 语义地址。这不等于整个仓库都被高质量索引。" },
{ t: "开放式解释弱于明确定位", d: "「某个函数在哪个文件」明显好于「结合多个文件解释系统为什么这样设计」。后者需要多跳证据合并,仍会出现证据已召回但回答不完整。" },
{ t: "速度仍有真实代价", d: "平均延迟高约 2.58%,解码速度低约 9.90%,输入前缀显著变长。" },
{ t: "生产级基线还不完整", d: "本次对照聚焦原版 Qwen 无记忆基线;要作服务经济学结论,还需加入强 embedding 检索器、cross-encoder、滑动窗口和分页 KV。" }
],
quotes: [
{ q: "Memory Slot 不模拟完整 KV。它只接管 KV 中最昂贵、最适合长期保存、最容易重复利用的部分。", s: "README_NATURAL_MEMORY_V2.md" },
{ q: "这些限制是设计边界,不是用一个「无限上下文」数字掩盖的未验证假设。", s: "README_NATURAL_MEMORY_V2.md" },
{ q: "任何排序器都救不回一条已经不存在的记录。", s: "WRITE_PATH_FIX.md" },
{ q: "这不是调参空间不够,而是这个信号根本不存在。", s: "ABSTENTION_BREAKTHROUGH.md" }
]
},
/* ---------- 已解决的问题 ---------- */
solved: [
"普通自然语言触发自动记忆读取,不要求用户输入 /remember",
"模型重启后不回放历史聊天,只加载已保存的 memory shard",
"旧值和新值保留版本关系,旧值不会继续作为 active 事实参与正常读取",
"记忆读取器、地址索引和证据注入位于模型适配架构内部",
"记忆主体留在进程内存,热点记录使用有界 GPU cache",
"当前问题只读取 Top-K 记录,不对全量 slot 做注意力",
"低置信度写入、撤回和审计状态有独立表示",
"在真实工程源码定位任务上,相比无记忆 Qwen 有明显的可回答性提升"
],
/* ---------- 路线图 ---------- */
roadmap: [
"使用有用户同意的脱敏对话,增加自然省略、指代、错别字、时间间隔、互相矛盾的旧说法和多轮项目协作样本",
"重新训练路由器,重点覆盖「同一事实的多种问法」和「相似但错误的记录」",
"对「检索正确但回答错误」的样本单独训练证据使用与拒答策略",
"将真实仓库的语义地址覆盖从目标记录扩展到完整索引,仍保持单条/小批次编码以保护显存",
"加入多跳证据合并和当前版本优先规则",
"将写入、冲突解决和摘要从生成热路径移到回合边界或后台线程",
"把显存测试改为记录真正的 CUDA high-water mark,并分别报告 live allocated、reserved 和 cache payload",
"在同一协议下补齐强 RAG、滑动窗口和分页 KV 的成本—质量曲线"
],
/* ---------- 复现 ---------- */
reproduce: {
cwd: "H:\\Memory",
python: "C:\\Users\\Administrator\\miniconda3\\envs\\LLM\\python.exe",
cmd: [
"& C:\\Users\\Administrator\\miniconda3\\envs\\LLM\\python.exe `",
" -m dynamic_memory_lab.benchmark_dirty_real_corpus_4b `",
" --project-records 8192 `",
" --project-targets 96 `",
" --max-new-tokens 64 `",
" --encode-batch-size 1 `",
" --skip-chunk-rag `",
" --output H:\\Memory\\dynamic_memory_lab\\dirty_real_corpus_compare_4b.json"
],
files: [
{ k: "原始结果", v: "H:\\Memory\\dynamic_memory_lab\\dirty_real_corpus_compare_4b.json" },
{ k: "评测脚本", v: "H:\\Memory\\dynamic_memory_lab\\benchmark_dirty_real_corpus_4b.py" },
{ k: "对外技术说明", v: "H:\\Memory\\dynamic_memory_lab\\Natural_Memory_v2_对外技术总结.md" }
]
},
/* ---------- 结论 ---------- */
conclusion:
"Natural Memory v2 当前已经不是只有构造数据上的演示:在真实工程源码、文档和本项目用户要求组成的脏数据迁移测试中," +
"它显著提高了窗口外事实的可回答性,并在 10 GiB GPU 进程上完成了 723 条 active records 的读取运行。" +
"同时,62.60% 的可回答正确率、88.89% 的未知拒答正确率和约 10% 的解码速度下降说明它仍处于研究原型到工程验证之间。"
};
+226
View File
@@ -0,0 +1,226 @@
/* 本文件由脚本从评测原始结果逐字提取,请勿手改。
来源:H:\Memory\dynamic_memory_lab\dirty_real_corpus_compare_4b_router_final.json
创建时间:2026-09-06T11:36:26+0800 协议:greedy / max_new_tokens=64 / top_k=2 */
window.NM_EXAMPLES = [
{
"id": "user-004",
"domain": "user_dialogue",
"category": "paraphrase",
"answerable": true,
"query": "我不想翻聊天记录,当前这个记忆模型叫什么?",
"expected": "Natural Memory v1",
"expected_anchors": [
"Natural Memory v1"
],
"nm": {
"response": "当前这个记忆模型叫 **Natural Memory v1**。",
"correct": true,
"tokens": 14,
"recall": true,
"retrieved_values": [
"Natural Memory v1",
"重要信息写入,禁止胡编乱造,并学习写入、清理和总结时机"
],
"records": [
{
"record_id": "mem_da9418d179467fba",
"entity": "user",
"attribute": "模型名称",
"value": "Natural Memory v1",
"status": "active",
"version": 0,
"source": "user_provided_dialogue",
"text_preview": "用户给当前模型定名:Natural Memory v1。"
},
{
"record_id": "mem_862617d79b828161",
"entity": "user",
"attribute": "记忆质量要求",
"value": "重要信息写入,禁止胡编乱造,并学习写入、清理和总结时机",
"status": "active",
"version": 0,
"source": "user_provided_dialogue",
"text_preview": "用户要求:重要信息都要存进去,但不能让模型胡编乱造记忆,还要学会什么时候写入和清理总结。"
}
]
},
"base": {
"response": "您提到的“当前这个记忆模型”具体指代什么,取决于您使用的平台或应用。如果您是在使用某个特定的 AI 助手或应用程序,那么该应用可能内置了某种形式的记忆功能,用于记住您的偏好、历史对话或其他相关信息。\n\n不过,从技术角度来看,并没有一个统一的“记忆",
"correct": false,
"tokens": 64
},
"note": "actual user-stated fact with a hand-written natural paraphrase"
},
{
"id": "code-symbol-010-a",
"domain": "real_repository",
"category": "symbol_navigation",
"answerable": true,
"query": "我现在不想翻完整个屎山代码库,_generate_student 这个 def 定义落在哪个文件?",
"expected": "benchmark_memory_vs_full_kv.py",
"expected_anchors": [
"benchmark_memory_vs_full_kv.py",
"_generate_student"
],
"nm": {
"response": "根据已保存的长期记忆,`_generate_student` 这个函数定义落在文件 **`benchmark_memory_vs_full_kv.py`** 中(具体在第 171 行)。",
"correct": true,
"tokens": 43,
"recall": true,
"retrieved_values": [
"benchmark_memory_vs_full_kv.py",
"benchmark_compare_baselines_4b.py",
"test_restart_memory_dialogue.py"
],
"records": [
{
"record_id": "mem_df66c88a7e2ea9da",
"entity": "benchmark_memory_vs_full_kv.py",
"attribute": "symbol:_generate_student",
"value": "benchmark_memory_vs_full_kv.py",
"status": "active",
"version": 0,
"source": "real_repository_symbol_index",
"text_preview": "真实代码库事实:文件 benchmark_memory_vs_full_kv.py 的第 171 行定义了 def _generate_student。原始代码行:def _generate_student("
},
{
"record_id": "mem_7fb124c563472b07",
"entity": "benchmark_compare_baselines_4b.py",
"attribute": "symbol:_base_generate",
"value": "benchmark_compare_baselines_4b.py",
"status": "active",
"version": 0,
"source": "real_repository_symbol_index",
"text_preview": "真实代码库事实:文件 benchmark_compare_baselines_4b.py 的第 116 行定义了 def _base_generate。原始代码行:def _base_generate("
}
]
},
"base": {
"response": "我无法直接访问您的本地文件系统或代码库,因此无法告诉您 `_generate_student` 这个函数具体落在哪个文件中。\n\n不过,您可以尝试以下几种方法来快速定位:\n\n1. **使用 IDE 的搜索功能**:在您的代码编辑器(如 VS Code、PyCharm 等)中",
"correct": false,
"tokens": 64
},
"note": "real symbol extracted from repository source"
},
{
"id": "user-unknown-004",
"domain": "user_dialogue",
"category": "unknown_refusal",
"answerable": false,
"query": "我在这次对话里有没有说过生日?",
"expected": "",
"expected_anchors": [],
"nm": {
"response": "在这次对话中,**您还没有提到过生日**。\n\n截至目前,我们的交流内容中没有任何关于您生日、年龄或庆祝生日的提及。如果您想分享,随时可以告诉我!",
"correct": true,
"tokens": 41,
"recall": false,
"retrieved_values": [],
"records": []
},
"base": {
"response": "在这次对话中,**您还没有提到过生日**。\n\n截至目前,我们的交流内容中没有任何关于您生日、年龄或庆祝生日的提及。如果您想分享,随时可以告诉我!",
"correct": true,
"tokens": 41
},
"note": "unknown or deliberately sensitive field absent from the supplied dialogue slice"
},
{
"id": "code-unknown-008",
"domain": "real_repository",
"category": "unknown_refusal",
"answerable": false,
"query": "请告诉我没有出现在仓库里的部署区域。",
"expected": "",
"expected_anchors": [],
"nm": {
"response": "根据您提供的长期记忆证据(特别是 `README_NATURAL_MEMORY_V2.md` 的内容),**该文档中并没有列出任何具体的“部署区域”(Deployment Regions)**。\n\n文档主要关注的是以下技术架构和逻辑,而非地理或云端的部署位置:\n\n1. **核心设计",
"correct": false,
"tokens": 64,
"recall": false,
"retrieved_values": [
"README_NATURAL_MEMORY_V2.md",
"README_NATURAL_MEMORY_V2.md"
],
"records": [
{
"record_id": "mem_796c6e8c86afce71",
"entity": "README_NATURAL_MEMORY_V2.md",
"attribute": "operational:1",
"value": "README_NATURAL_MEMORY_V2.md",
"status": "active",
"version": 0,
"source": "real_repository_snapshot",
"text_preview": "真实项目文件:README_NATURAL_MEMORY_V2.md\n## 核心设计\n\n### KV 与 Memory Slot 的分工\n\n```text\n最近 32K token(最多可配置到 128K) -> GPU 热 KV:保留精确顺序和局部连贯性\n长期个人事实 / 项目决策 / 纠错版本 -> Memory Slot:保存压缩、可寻址的证据\n当前问题 -> 稀疏路由:只读取少量相关记录\n```\n\nMemory Slot 不模拟完整 KV。它只接管 KV 中最昂贵、最适合长期保存、最容易重复利用的部分。这里的目标是比长 K"
},
{
"record_id": "mem_32331793e07a8a4f",
"entity": "README_NATURAL_MEMORY_V2.md",
"attribute": "operational:0",
"value": "README_NATURAL_MEMORY_V2.md",
"status": "active",
"version": 0,
"source": "real_repository_snapshot",
"text_preview": "真实项目文件:README_NATURAL_MEMORY_V2.md\n把一百万个 slot 当作一张巨大的 KV Cache,而是把历史中最适合长期保存的部分压缩成有地址的记忆记录;当前对话仍由 Qwen 的热 KV 负责连续理解。\n\n## 先看结论\n\n当前版本已经完成以下闭环:\n\n```text\n普通用户输入\n -> 自动写入策略\n -> 置信度 / 重要性 / 来源审计\n -> 紧凑地址投影\n -> 分页存储\n -> LSH 粗索引\n -> 候选页重排\n -> 候选记录精排\n -> Top-K 证据注入 Qwen\n -> 版本冲突 / "
}
]
},
"base": {
"response": "您好!由于我无法访问您的仓库系统或具体业务数据,无法直接列出“未出现在仓库里的部署区域”。\n\n为了帮您准确定位,请提供以下信息:\n1. 仓库系统名称(如 GitLab、Jenkins、阿里云容器服务等)\n2. 您关注的部署区域范围",
"correct": true,
"tokens": 64
},
"note": "target deliberately absent from the real repository snapshot"
},
{
"id": "user-009",
"domain": "user_dialogue",
"category": "constraint",
"answerable": true,
"query": "你记得我对持久化文件和分页的限制吗?",
"expected": "第三个权重文件",
"expected_anchors": [
"第三个权重文件",
"不使用 SQLite",
"不使用磁盘分页"
],
"nm": {
"response": "是的,我记住了。\n\n根据您之前的要求,关于持久化方案和分页的限制如下:\n* **持久化方案**:不使用 SQLite 或磁盘分页,所有信息完全由**第三个权重文件**储存。\n* **实体**:user\n* **属性**:记忆持久化",
"correct": false,
"tokens": 64,
"recall": true,
"retrieved_values": [
"第三个权重文件,不使用 SQLite 或磁盘分页",
"Qwen3.5 4B"
],
"records": [
{
"record_id": "mem_c3886345cac35147",
"entity": "user",
"attribute": "记忆持久化方案",
"value": "第三个权重文件,不使用 SQLite 或磁盘分页",
"status": "active",
"version": 0,
"source": "user_provided_dialogue",
"text_preview": "用户要求:不使用 SQLite 或磁盘分页,完全由第三个权重文件储存所有信息。"
},
{
"record_id": "mem_149a0c2ab04c0f56",
"entity": "user",
"attribute": "对照模型",
"value": "Qwen3.5 4B",
"status": "active",
"version": 0,
"source": "user_provided_dialogue",
"text_preview": "用户要求和原版 qwen3.5 4b 做跑分对比,并纠正为 4B。"
}
]
},
"base": {
"response": "作为一个人工智能模型,我**无法访问您的个人历史对话记录、系统配置或您之前设定的具体规则**(例如具体的持久化文件大小限制、分页数量限制等)。每次对话对我来说都是全新的开始,除非您在当前的对话中明确告知我这些限制。\n\n不过,我可以帮您**重新设定",
"correct": false,
"tokens": 64
},
"note": "actual user-stated fact with a hand-written natural paraphrase"
}
];
File diff suppressed because one or more lines are too long
+50
View File
@@ -0,0 +1,50 @@
/* ==================================================================
Natural Memory v2 — nm21.js
「v2.1 之后做了什么」页面上需要交互的那部分数据。
只放剂量曲线:它们是这一轮唯一需要「拖动/悬停看数值」的东西,
其余表格都直接写在正文里并标注了来源文件,便于逐条核对。
⚠️ 数值逐字取自工程内报告,未做改写或换算:
V2_dpskw\NM2_1_FINAL.md 第 3 节(两处改动 + 剂量曲线)
V2_dpskw\record_blend_dose_response.md / prior_scale_dose_response.md 原始产物
================================================================== */
window.NM21 = {
/* 改动一:记录排序混合权重(独立进程测量,48 用例,先验保持 1.0)
此前的同进程测量因顺序污染已作废,这里用的是独立进程那一版。 */
blend: {
title: "记录排序混合权重 · 剂量曲线",
axis: "memory_record_router_blend",
xLabel: "blend 值(0 = 仅 retriever,1 = 仅路由器)",
pick: 0.5,
metrics: [
{ key: "answer", label: "可回答正确率", unit: "%", higherBetter: true,
v: [65.00, 70.00, 62.50] },
{ key: "wrong", label: "答成别的属性", unit: "%", higherBetter: false,
v: [35.00, 27.50, 37.50] },
{ key: "leak", label: "未知泄漏率", unit: "%", higherBetter: false,
v: [75.00, 75.00, 87.50] }
],
x: [0, 0.5, 1],
note: "两个打分器互补:0.50 是可回答正确率最高、答成别的属性最低的点。" +
"blend=1.00 反而全面变差,说明这不是单调关系 —— 单看某一端会得出错的结论。"
},
/* 改动二:把加法先验暴露成参数后测出来「不该动」。
这是一个有价值的负面结论:它否定了「先验压过学习打分」这条假设。 */
prior: {
title: "记录打分先验权重 · 剂量曲线",
axis: "加法先验的缩放系数",
xLabel: "先验缩放(原值为 1.00)",
pick: 1.0,
metrics: [
{ key: "answer", label: "可回答正确率", unit: "%", higherBetter: true,
v: [65.00, 47.50, 42.50, 40.00] }
],
x: [1, 0.5, 0.25, 0],
note: "调小先验只会更差(65.00 → 47.50 → 42.50 → 40.00),所以保持 1.00 不动。" +
"这条曲线的作用是排除一个假设,而不是带来增益。"
},
srcLabel: "V2_dpskw\\NM2_1_FINAL.md §3 · 原始产物 prior1_blend_{0.0,0.5,1.0}_proc.json 与 prior_scale_dose_response.json"
};
+115
View File
@@ -0,0 +1,115 @@
/* ==================================================================
Natural Memory v2 — restart.js
「重启实验」互动装置的数据层。
⚠️ 本文件的每一个数值都逐字取自工程内的原始 JSON,未做任何改写、
换算或估算。两个来源文件:
A. dynamic_memory_lab\natural_memory_v2_restart_test_v7.json (修复前)
B. dynamic_memory_lab\natural_memory_v2_restart_test_v7_final.json (修复后)
两次运行使用同一条事实、同一个问题、同一份协议,唯一差别是
「写入是否真的落库」(write_changed: false → true)。
修改此处等于修改一个「证据」页面,请先核对原始 JSON 再改。
================================================================== */
window.NM_RESTART = {
eyebrow: "Evidence · 重启实验",
h2: "把历史扔掉,它还记得吗?",
lede: "下面不是示意动画,是工程里两次真实运行的逐字回放。同一条事实、同一个问题、同一份协议;" +
"两次之间唯一的差别,是写入有没有真的落库。",
fact: "我正在开发一个长期项目,项目内部代号是NM-V2-RESTART,使用中文。",
query: "我正在开发的长期项目内部代号是什么?只回答代号。",
expected: "NM-V2-RESTART",
historyPassedToRestart: false,
/* 读取路径的四个节拍(两次运行共用同一套节拍名) */
beats: [
{ i: "01", t: "写入", s: "automatic_write: true —— 事实作为一条带地址的记录进入记忆库" },
{ i: "02", t: "重启", s: "history_passed_to_restart: false —— 重启不携带任何对话历史" },
{ i: "03", t: "召回", s: "路由器在当前问题与有界候选之间定位证据记录" },
{ i: "04", t: "作答", s: "命中的短证据作为模型内部前缀注入 Qwen,再由普通生成产出答案" }
],
variants: [
{
key: "before",
label: "修复前",
tone: "bad",
file: "natural_memory_v2_restart_test_v7.json",
/* --- 写入 --- */
writeChanged: false,
/* --- 重启后模型看到的内部状态 --- */
prefixLength: 0,
prefixUsed: false,
/* --- 路由决策 --- */
needMemory: false,
pageIds: [],
recordIds: [],
pageScores: [],
recordScores: [],
hopCount: 1,
confidence: 0.5,
stopReason: "below_read_threshold",
/* --- 生成结果 --- */
response: "Project Phoenix",
recalled: false,
correct: false,
/* --- 存储快照(before_save) --- */
store: {
records: 0, active_records: 0, pages: 0, resident_records: 0,
coarse_index_buckets: 0, gpu_cache_records: 0, gpu_cache_tokens: 0,
gpu_cache_bytes: 0
}
},
{
key: "after",
label: "修复后",
tone: "ok",
file: "natural_memory_v2_restart_test_v7_final.json",
writeChanged: true,
prefixLength: 45,
prefixUsed: true,
needMemory: true,
pageIds: ["page_00000001"],
recordIds: ["mem_3f5823eb77810b49"],
pageScores: [0.48235471503517796],
recordScores: [0.5923714575763567],
hopCount: 1,
confidence: 0.7961857287881784,
stopReason: "evidence_found",
response: "NM-V2-RESTART",
recalled: true,
correct: true,
store: {
records: 1, active_records: 1, pages: 1, resident_records: 1,
coarse_index_buckets: 1, gpu_cache_records: 1, gpu_cache_tokens: 45,
gpu_cache_bytes: 1429
}
}
],
/* 常量:出自同一份 JSON 的存储与预算字段(两次运行相同) */
limits: {
maxPages: 32768,
pageCapacity: 32,
capacityRecords: 1048576,
kvBudgetTokens: 32768,
kvTriggerTokens: 29491,
gpuCacheLimitRecords: 256,
gpuCacheLimitTokens: 131072,
gpuCacheReserveMB: 2048,
gpuCacheDevice: "cuda:0"
},
/* 同一个实验的另外两次运行,用于说明「不是每次都成功」 */
otherRuns: [
{ file: "natural_memory_v2_restart_test.json", date: "2026-09-04 23:16",
writeChanged: true, prefixLength: 36, recalled: true, quarantined: 2 },
{ file: "restart_memory_natural_v7.json", date: "2026-09-04 09:15",
note: "另一条事实(工作地点代号 R7)。复位 token 后 memory_norm 归零,模型随即答不出——" +
"这条记录的是「清掉记忆会怎样」的对照组。" }
],
note: "两次运行的判定字段(router_recalled_after_restart / generated_contains_expected)分别为 " +
"false / false 与 true / true。我们没有为发布页润色过任何一条模型输出,包括修复前那次编造出来的答案。"
};
File diff suppressed because one or more lines are too long
+1188
View File
File diff suppressed because it is too large Load Diff
+256
View File
@@ -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(); /* 循环常转:静止时目标点的呼吸也要活着 */
}
})();