Files
WpyQwq 643e22ecb9 Natural Memory NM2.1: 记忆路由器分叉、数据集缺陷修复与全轴评测证据
- 引入 MemoryRouterXL 与 v5/v6 流式多线程训练/编码管线
- 修复 prepare_memory_router_dataset 候选池重建缺陷(mega 家族 3568x 加速,输出逐字节相同)
- 修复 v5 被破坏的拒答与多跳标签(train 未知样本 319 -> 16319,multi_hop 平均正例 1.00 -> 2.00)
- 同存储预算下 V2-128 v6 逐轴 22/22 通过:Top-1 41.12% -> 94.62%,未知拒答 0.00% -> 100.00%
- 记录三条被实测推翻的显然优化(logits_to_keep=1 反而慢 55%、XL 容量未带来收益)
- 记忆手术跨架构可移植性 14/14,读写关闭时与原生模型逐位相同
2026-09-19 11:11:31 +08:00

36 KiB
Raw Permalink Blame History

Dynamic Memory Lab

一个在现成 Qwen3.5-4B 语言模型上进行“架构手术”的实验工程。

Natural Memory v2 已完成分页地址路由、粗索引→候选页→精排→Top-K、多跳、冲突版本、quarantine、撤回、嵌入式重启恢复和完整回归评测。请先阅读 Natural Memory v2 工程说明 与 技术论文。

本项目不重新训练整个语言模型,而是在 Transformer 的若干层之间插入一个可读、可写、可持续更新的动态记忆模块,研究下面这个问题:

能不能让一个已经训练好的 LLM,在不把所有历史对话反复塞回上下文的情况下,持续积累和使用信息?

答案是:工程中已经实现了一个生产导向的自然语言记忆核心,并完成了多事实写入、冲突更新、无历史跨重启读取、二次重启读取、未知事实拒答和 reset token 验收。它仍需要在真实业务数据上继续做压力测试,合成数据指标不能直接等同于所有场景的生产承诺。

结论先说

当前模型已经具备:

  • 在一次进程运行期间维护一组独立于 token 上下文的 memory state;
  • 对 memory 做 attention 读取;
  • 根据当前输入生成写入内容,并更新 memory;
  • 将 memory 读取结果注入 Qwen3.5-4B 的中间层;
  • 只训练记忆模块和少量门控参数,而冻结原始 Qwen 主干;
  • 对比原版 Qwen3.5-4B 与动态记忆版的 loss、困惑度、准确率、生成质量、速度和显存。

当前版本仍有边界:

  • 还没有证明对任意自然语言、多事实和长时间运行都可靠;
  • 还没有完成生产级多用户隔离、隐私策略和容量管理;
  • 自由生成在合成留出集上仍会偶尔答错值,所以不能把实验指标当作生产承诺。

因此,当前版本更准确的定位是:

一个包含可部署自然语言记忆核心、持久化 checkpoint 和验收基准的 LLM 原生动态记忆工程;生产接入前仍需补齐业务侧鉴权、加密、并发和运维策略。

项目结构

V2_dpskw/
├─ qwen_integration.py       # 动态记忆模块和 Qwen 模型适配器
├─ train_qwen_memory.py      # 记忆模块训练脚本
├─ chat_qwen_memory.py       # 多轮对话实验脚本
├─ benchmark_qwen.py         # 原版/动态版跑分脚本
├─ verify_persistent_memory.py # 重启后仅用 memory_state 验证回答
├─ make_native_memory_data.py # 生成写入/遗忘/未知/冲突数据
├─ train_native_memory.py    # 训练原生写入、遗忘和读出
├─ evaluate_native_memory.py # 留出集控制器与自由生成评估
├─ verify_native_checkpoint.py # checkpoint 重启和 reset token 验证
├─ train_natural_retriever.py # 训练自然语言记忆查询检索器
├─ train_auto_memory_policy.py # 训练自动记忆重要性策略
├─ build_production_memory_dataset.py # 真实对话导入、规范化和防泄漏切分
├─ train_production_memory_policy.py # 从规范化对话训练策略候选
├─ stress_test_natural_memory.py # 低显存连续运行/长上下文压力测试
├─ natural_memory_service.py # localhost 管理与聊天 API
├─ natural_memory_app.py    # 统一 chat/serve/train/stress 入口
├─ benchmark_natural_language_memory.py # 多事实/更新/重启/拒答验收
├─ stream_chat_qwen_memory.py # 流式聊天与随时重启测试入口
├─ make_benchmark_data.py    # 生成简单的记忆型 benchmark 数据
├─ requirements.txt          # Python 依赖
├─ memory.pt                 # 训练后的记忆模块参数,若已生成
├─ memory_config.json        # 记忆模块配置,若已生成
├─ surgery.pt                # 层手术相关参数,若已生成
└─ README.md

运行环境

推荐使用已有的 Conda 环境 LLM。

conda activate LLM
cd W:\Flash\model\V2_dpskw
pip install -r requirements.txt

如果模型路径不在默认位置,可以通过参数指定。当前工程主要面向本地 Hugging Face 格式的 Qwen3.5-4B 模型。

总体架构

原版 Qwen 的推理过程大致是:

输入 token
   │
   ▼
Embedding
   │
   ▼
Transformer Layer 0
   │
   ▼
Transformer Layer 1
   │
  ...
   │
   ▼
Transformer Layer N
   │
   ▼
LM Head
   │
   ▼
下一个 token

动态记忆版会在若干个 Transformer 层上插入 Memory Adapter:

输入 token
   │
   ▼
Qwen Transformer Layer
   │
   ├──────────────► Memory Read
   │                    ▲
   │                    │
   │              Runtime Memory State
   │              M = [B, S, D]
   │                    │
   │                    ▼
   ├──────────────► Memory Delta
   │
   ▼
Memory Layer Adapter
   │
   ▼
后续 Qwen Transformer Layers
   │
   ├──────────────► Memory Write
   │                    │
   │                    ▼
   │              M_new = Update(M, input)
   │
   ▼
LM Head

其中:

  • B 是 batch size;
  • S 是 memory slot 数量,默认 16;
  • D 是 memory embedding 维度,默认 512;
  • M 不是 token 序列,而是模型外部维护的一组连续向量;
  • memory_state 可以在不同调用之间传递,因此它能够脱离上一轮的文本上下文。

核心概念:参数、上下文和运行时记忆

理解本项目最重要的是区分三种东西。

1. 模型参数

模型参数是 Qwen 的权重以及动态记忆模块的权重,例如:

Wq, Wk, Wv, Wread, Wwrite

它们决定模型“如何读写记忆”,通常在训练阶段更新,在推理阶段保持不变。

2. 当前上下文

当前上下文是这次请求中送入模型的 token,例如:

用户:我叫小明。
助手:你好,小明。

上下文是临时的。上下文窗口结束以后,模型本身不会自动保存这些 token。

3. 运行时记忆状态

运行时记忆状态是:

M = [batch_size, memory_slots, memory_dim]

默认情况下:

M = [B, 16, 512]

它是模型运行时的一块连续状态。只要下一次调用仍然传入同一个 memory_state,模型就能继续使用之前写入的内容。

注意:

memory.pt 保存的是“记忆模块的训练参数”,不是某个用户的聊天记忆。

用户记忆应该单独保存为某种 runtime state,例如:

user_001_memory.pt
user_002_memory.pt

或者保存到数据库、对象存储、向量数据库中。

Memory Read:模型如何读取记忆

设某一层产生的隐藏状态为:

h ∈ R^D_hidden

记忆矩阵为:

M = [m₁, m₂, ..., mₛ] ∈ R^(S×D_memory)

首先把当前隐藏状态投影成 query:

q = W_q h

把每个 memory slot 投影成 key 和 value:

k_i = W_k m_i
v_i = W_v m_i

然后计算当前输入与每个 slot 的匹配程度:

score_i = q · k_i / sqrt(D_memory)

经过 softmax 得到读取权重:

α_i = softmax(score_i)

最后将各个 slot 的 value 加权求和:

r = Σ_i α_i v_i

r 就是当前输入从动态记忆中检索出来的内容。

为了避免记忆模块一开始就破坏 Qwen,代码还使用了一个 read gate:

g = sigmoid(W_gate h)

最终的记忆增量大致为:

Δh = read_scale × g × W_read(r)

然后再注入当前层:

h_new = h + Δh

这和给 Transformer 增加一个小型、可训练的外部知识通道类似。

Memory Write:模型如何写入记忆

读取解决的是“从记忆里找什么”,写入解决的是“把当前输入存什么”。

当前实现默认使用输入序列最后一个 token 的隐藏状态作为摘要:

s = h_last

也支持固定 token 偏移位置作为写入摘要。

接着通过写入投影生成候选内容:

p = W_write(s)

再根据当前输入生成写入地址和写入强度:

a = softmax(W_addr(s))
z = sigmoid(W_strength(s))

其中:

  • a_i 表示第 i 个 slot 被写入的比例;
  • z 表示本次写入总体有多强;
  • p 是候选写入向量。

对于每个 slot,更新形式近似为:

m_i_new = (1 - z × a_i) × m_i
          + (z × a_i) × p

这是一种可微分的软写入。它不会使用不可导的“直接选中某个 slot”操作,所以可以通过反向传播学习:

  • 什么输入值得写入;
  • 应该写入哪些 slot;
  • 写入幅度应该多大;
  • 如何从隐藏状态中压缩信息。

代码还支持 broadcast_write,让候选内容广播写入所有 slot。这个模式更适合做架构实验,但可能降低 slot 的分工能力。

Qwen 接入方式

qwen_integration.py 会加载原版 Qwen,并替换指定层为带记忆能力的适配层。

默认会选择若干中间层;也可以通过 layer_indices 手动指定层号。工程中同时保留了一个轻量的自定义线性注意力记忆层,便于做对照实验。

动态模型默认冻结 Qwen 主干:

Qwen 原始参数:冻结
Memory Read/Write:训练
层融合 gate:训练

这样做的好处是:

  • 显存和训练成本更低;
  • 不容易破坏原模型能力;
  • 更容易判断提升来自记忆机制还是来自主干重新学习;
  • 适合在单卡环境中快速迭代架构。

三种层融合模式

动态记忆读出后,需要决定如何注入 Qwen 的隐藏状态。当前支持三种模式。

residual

h_new = h + memory_delta

这是默认模式。它保留原始隐藏状态,并把记忆当作额外残差通道。

特点:

  • 最稳定;
  • 对原模型干扰小;
  • 适合第一版训练和 benchmark。

blend

h_new = h_residual + (1 - α) × original_token_mixer + α × memory_delta

其中 α 是可训练的融合系数。

特点:

  • 模型可以学习记忆通道应该占多大比例;
  • 适合研究“原始表示”和“记忆表示”的权衡;
  • 如果初始化或训练不稳定,可能导致原模型信息被过早削弱。

replace

h_new = memory_delta

完全使用记忆分支输出。

特点:

  • 适合验证记忆分支的独立表达能力;
  • 风险最高;
  • 通常不建议作为默认生产方案。

direct_logit_scale:直接影响输出概率

除了修改 Transformer 中间层,代码还支持把最后一次 memory readout 经过投影后直接加到 logits:

logits_new = logits_qwen + scale × projection(memory_readout)

这个选项可以直接研究:

  • memory 是否能记住某些目标答案;
  • memory 是否能把目标 token 的概率推高;
  • 记忆模块对最终预测的直接影响。

但它也更容易过拟合简单 benchmark,因此应该同时观察泛化测试和正常生成质量。

raw token pointer:精确 token 记忆实验

项目还提供一个有意“开后门”的架构消融:memory pass 不仅写入连续向量,还可以保存某个 token 在 Qwen 输出投影矩阵中的行;查询生成的第一个 token 会读取这行向量。

这个实验用于回答一个非常具体的问题:

如果记忆里已经存在目标 token,当前 Qwen 接口能不能把它可靠地送进自由生成?

示例配置:

--write-token-offset 4 --broadcast-write \
--raw-token-write --raw-logit-scale 30

注意:write-token-offset 按“从有效序列末尾数起”计算,1 是最后一个 token。当前 benchmark 的答案位于 H / 。 / <|im_end|> / 换行 中的倒数第 4 个位置,所以使用 4。raw-token-write 是 pointer ablation,不是通用自然语言记忆方案;它直接保存 token id 对应的输出投影行,不能把它的 100% 结果等同于普通 learned memory 的能力。

raw pointer 只作用于第一个生成 token,后续 token 回到 Qwen 原本的生成分布,避免把同一个答案 token 重复写满整段输出。

训练原理

当前训练脚本采用“两阶段记忆训练”。

阶段一:写入阶段

输入一段 memory text:

Memory: user=alice; favorite_color=blue

此阶段关闭 memory read,打开 memory write:

outputs = model(
    memory_text,
    memory_state=memory_state,
    read_memory=False,
    update_memory=True,
    return_memory=True,
)
memory_state = outputs.memory_state

目标是让模型把关键信息写入 memory state。

阶段二:查询阶段

再输入查询:

Question: What is alice's favorite color?

此阶段使用刚刚更新后的 memory state,并开启 memory read:

outputs = model(
    query_text,
    memory_state=memory_state,
    read_memory=True,
    update_memory=False,
    labels=labels,
)

通过语言模型 loss 训练记忆模块,让查询阶段能够根据 memory state 输出正确答案。

训练时通常只更新:

Memory Read parameters
Memory Write parameters
Memory Layer Adapter parameters
Blend/Gate parameters

而 Qwen 主干保持冻结。

数据格式

训练和 benchmark 数据使用 JSONL。当前脚本要求每行包含 memory 和 query 两个消息列表;最后一个 assistant 消息是监督目标:

{"memory":[{"role":"user","content":"记住对象 A 的代号是 H。"},{"role":"assistant","content":"好的,已记住。"}],"query":[{"role":"user","content":"对象 A 的代号是什么?"},{"role":"assistant","content":"H"}]}

额外的 id、subject、attribute、answer 字段只用于 benchmark 统计。训练和评估数据应避免把答案直接重复到 query 的 user 内容中。

数据设计时要注意:

  1. 写入文本中出现的信息,应该在查询文本中尽量不重复;
  2. 如果查询中直接包含答案,模型可能只是在复制上下文,而不是读取 memory;
  3. 训练集和评估集中的实体、属性、表述方式应尽量分离;
  4. 要加入冲突样本,测试新记忆是否能覆盖旧记忆;
  5. 要加入多条事实,测试不同 slot 是否会互相污染;
  6. 要加入无关信息,测试模型能否避免把所有内容都写进去。

make_benchmark_data.py 可以生成一个简单的单字符映射任务,用来快速检查“写入—读取—回答”链路是否工作。它适合做冒烟测试,不足以证明模型拥有通用长期记忆。

常用命令

以下命令均在工程目录执行。

训练动态记忆模块

conda activate LLM
cd W:\Flash\model
python -m V2_dpskw.train_qwen_memory --model-path "W:\Flash\model" --data V2_dpskw\data\benchmark_train.jsonl --output-dir V2_dpskw\qwen_memory_adapter --steps 100 --batch-size 1 --lr 1e-4 --max-length 128

如果要复现实验中的精确 token pointer:

python -m V2_dpskw.train_qwen_memory --model-path "W:\Flash\model" --data V2_dpskw\data\benchmark_train.jsonl --output-dir V2_dpskw\qwen_memory_adapter_pointer --steps 100 --surgery-mode blend --blend-init 0.1 --write-token-offset 4 --broadcast-write --raw-token-write --raw-logit-scale 30

如果实际模型目录不同,请替换 --model-path。

训练原生记忆控制器

原生模式会训练:摘要池化、写入决策、slot 地址、候选值读出,以及依赖旧记忆的遗忘门。训练数据中显式包含闲聊噪声、未知查询和冲突覆盖样本:

python -m V2_dpskw.make_native_memory_data --output-dir V2_dpskw\data\native_memory --train-count 512 --eval-count 128 --seed 20260904
python -m V2_dpskw.train_native_memory --model-path "W:\Flash\model" --data V2_dpskw\data\native_memory\train.jsonl --output-dir V2_dpskw\qwen_memory_adapter_native_v3 --steps 2500 --lr 8e-5 --max-length 192 --save-every 250 --direct-logit-scale 12 --write-loss-weight 0.5 --forget-loss-weight 0.75 --forget-positive-weight 6 --value-loss-weight 1

评估:

python -m V2_dpskw.evaluate_native_memory --model-path "W:\Flash\model" --adapter-dir V2_dpskw\qwen_memory_adapter_native_v3 --data V2_dpskw\data\native_memory\eval.jsonl --max-length 192 --max-new-tokens 8

生成 benchmark 数据

python -m V2_dpskw.make_benchmark_data --output-dir V2_dpskw\data

对比原版和动态版

python -m V2_dpskw.benchmark_qwen --model-path "W:\Flash\model" --data V2_dpskw\data\benchmark_eval.jsonl --adapter V2_dpskw\qwen_memory_adapter_pointer --output V2_dpskw\benchmark_qwen_pointer_eval.json --max-length 128 --max-new-tokens 4 --repeats 1 --warmup 0

benchmark 通常会报告:

  • validation loss;
  • perplexity;
  • token accuracy;
  • first target token accuracy;
  • exact sequence accuracy;
  • generation quality;
  • tokens per second;
  • 峰值显存。

最终报告必须同时关注效果和代价。一个模型如果只是在极小任务上准确率更高,却明显降低通用生成能力或推理速度,不能简单视为架构成功。

本机已验证结果

在 data/benchmark_train.jsonl 的 128 条训练样本和 data/benchmark_eval.jsonl 的 64 条 held-out 随机映射上,答案字符不是由实体名称推导出来的。当前已保存的结果文件是:

版本 PPL token accuracy 首目标 token 自由生成 prefix 速度
原版 Qwen3.5-4B 210.20 42.71% 0% 0% 19.45 tok/s
普通 learned blend 4.50 66.67% 0% 0% 12.99 tok/s
blend + raw token pointer 1.11 96.88% 100% 100% 14.56 tok/s
native learned memory v3 1.24 91.67% 75% 75% 13.33 tok/s

对应文件分别是 benchmark_qwen_native_v3_eval.json、benchmark_qwen_pointer_eval.json 中的 baseline/dynamic 记录,以及 benchmark_qwen_full_eval.json 中保存的普通 blend 结果。native v3 行来自同一套 64 条 benchmark;pointer 行是精确 token 消融实验,不能替代通用 learned memory 的结论。不同运行的速度会受显存缓存和系统状态影响。

原生控制器 v3 在 128 条完全不同用户编号的留出集上得到:写入准确率 100%,遗忘准确率 99.58%,replacement 遗忘准确率 96.30%,查询 token 准确率 92.12%,自由生成事实召回 81.31%,未知查询安全拒答 100%。这些结果来自 qwen_memory_adapter_native_v3/native_eval_report.json;其中自由生成仍有少量错误值,不能宣称已经达到可靠生产级记忆。

多轮对话实验

python -m V2_dpskw.chat_qwen_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter

要让用户记忆跨进程保存,指定一个用户专属文件:

python -m V2_dpskw.chat_qwen_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter --memory-state V2_dpskw\data\users\alice.pt

首次运行可以输入:

/remember 我叫小明,喜欢蓝色

退出后再次运行同一条命令,直接询问个人事实即可;启动时只会加载 alice.pt,不会自动加载上一轮聊天文本。/reset 会把该用户的 state 重置为零并保存,/save 可以手动保存。

需要区分“记忆持久化”和“记忆能力”:qwen_memory_adapter_natural_auto_v3 加上持久化 checkpoint 已验证自然语言写入、冲突覆盖、两次无历史重启读取、未知事实拒答和 reset;真实业务上线仍需要按业务数据继续扩充评测。

不启用 --persistent-memory 时,当前 chat 脚本中的 memory 默认是进程内状态;关闭脚本后,这块状态会消失。

使用原生模式并把用户记忆写进适配器 checkpoint:

python -m V2_dpskw.chat_qwen_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_native_v3 --persistent-memory --persist-in-adapter

原生 adapter 默认使用 <|fim_prefix|> 作为 reset token;也可以通过 --reset-token 或 --reset-token-id 指定其他 tokenizer token。向模型发送该 token 会在模型内部清零 memory,不需要外部清理函数参与推理。

原生 chat 的普通用户消息会先经过自动记忆策略头判断是否值得长期保存;写入阶段不读取旧记忆,避免把回忆内容或问题句误写回去。随后生成阶段只读,不把模型自己的回答再次写回记忆。/remember 仍可用于强制写入。

当前保存机制的边界

当前代码已经提供真正的 runtime state 持久化接口:

model.save_runtime_memory("user_memories/alice.pt")
model.load_runtime_memory("user_memories/alice.pt")

保存文件包含:

  • memory_state:连续动态记忆张量;
  • raw_memory:如果启用了 token pointer,则保存对应的辅助状态;
  • hidden size、memory shape、层配置等兼容性信息。

它不包含模型权重,也不包含历史聊天文本。因此重启后的调用可以只传入加载后的 state 和新的 query。

如果希望把“记忆模块参数 + 当前用户 memory state”放到同一个紧凑适配器包中:

model.save_persistent_memory_checkpoint("V2_dpskw/qwen_memory_adapter_native_v3_persistent")

该包中的 persistent_memory.pt 会在 load_memory_adapter() 时自动加载,新的模型实例不需要再显式传入 memory_state。验证命令:

python -m V2_dpskw.verify_native_checkpoint --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_native_v3 --data V2_dpskw\data\native_memory\eval.jsonl --output-adapter V2_dpskw\qwen_memory_adapter_native_v3_persistent

可以用下面的脚本自动验证完整流程:

python -m V2_dpskw.verify_persistent_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_pointer --data V2_dpskw\data\benchmark_eval.jsonl --memory-state V2_dpskw\data\user_memory_demo.pt

该脚本会执行:写入 memory → 保存 state → 销毁模型 → 重新加载模型 → 只用 state 生成回答。

项目会保存动态记忆模块的训练参数,例如:

memory.pt
memory_config.json
surgery.pt

这些文件描述的是“模型如何使用记忆”;persistent_memory.pt 则是打包进适配器的“某个用户已经记住了什么”。

可以把它们类比成:

memory.pt        = 记忆系统的大脑结构和读写规则
user_memory.pt   = 某个用户实际写入的内容

当前实现的 runtime state 通常通过以下方式流动:

outputs = model(
    input_ids=input_ids,
    memory_state=memory_state,
    return_memory=True,
)
memory_state = outputs.memory_state

只要把 memory_state 保存下来,之后重新加载并传回模型,就可以恢复对应记忆。

能不能实现跨对话、无上下文、原生记忆?

需要先把这个问题拆开。

现在能做到的版本:跨调用、无历史文本

可以。

同一个进程里:

第 1 次调用:输入事实,更新 memory_state
第 2 次调用:只输入问题,传入 memory_state
第 3 次调用:继续传入更新后的 memory_state

第 2 次调用不必把第 1 次调用的完整聊天记录重新放进 prompt。模型可以从 memory state 中读取信息。

加一个持久化层后:跨程序、跨会话

也可以实现,但它不是模型单独完成的,而是:

用户 ID
   │
   ▼
加载该用户的 runtime memory state
   │
   ▼
调用动态记忆模型
   │
   ▼
保存更新后的 runtime memory state

最小实现可以是:

from pathlib import Path
import torch

def load_user_memory(user_id, model):
    path = Path("user_memories") / f"{user_id}.pt"
    if path.exists():
        return torch.load(path, map_location="cpu")
    return model.initial_memory(batch_size=1)

def save_user_memory(user_id, memory_state):
    path = Path("user_memories") / f"{user_id}.pt"
    path.parent.mkdir(parents=True, exist_ok=True)
    torch.save(memory_state.detach().cpu(), path)

实际生产环境还需要:

  • 用户隔离;
  • 加密;
  • 并发写入保护;
  • 版本迁移;
  • memory state 校验;
  • 删除和导出接口;
  • 过期时间或记忆衰减;
  • 防止恶意 prompt 写入长期记忆。

严格意义上的“完全无上下文”不可能凭空发生

如果“无上下文”指:

不提供历史文本
不提供 memory state
不提供数据库
不提供任何外部信息

但又希望模型知道上一场对话发生了什么,那么这在信息上是不可能的。

信息必须存在某个地方:

上下文窗口
模型参数
外部数据库
向量索引
神经 memory state

本项目选择的是“神经 memory state”这条路线。它可以让历史信息不以原始文本形式出现,但不能让信息在没有任何载体的情况下存在。

为什么仍然需要生产化治理

当前自然语言记忆核心已经不只是连续状态:它有精确文本槽、冻结 Qwen 检索键、训练过的查询检索器、写入门控、冲突替换、最老槽淘汰、checkpoint 校验和 reset。但上线前仍要针对真实业务补充:

  • 用户身份隔离、加密、并发写入和版本迁移;
  • 真实语言中的事实抽取、撤回/删除语义和多事实长时间压力测试;
  • 记忆容量策略、审计日志、导出与合规删除;
  • 不同语言、不同 tokenizer、不同 batch 和服务重启方式的回归测试。

更准确的说法是:

模型已经拥有内部的“是否写入、如何检索、何时拒绝读取、如何覆盖和淘汰”的自然语言记忆路径;用户 checkpoint 仍必须存在于某种持久化介质中,这是信息保存的必要条件,而不是外部代码替模型执行读取。

为什么不直接每轮修改 Qwen 主权重

每轮对话都直接更新主模型权重,理论上可以把信息写进参数,但会产生明显问题:

  • 很容易灾难性遗忘;
  • 不同用户之间会互相污染;
  • 每次写入都需要保存或更新大模型权重;
  • 难以撤销某条记忆;
  • 难以处理隐私和权限;
  • 推理延迟和存储成本都很高。

外部 runtime memory 的好处是:

主模型参数 = 稳定的通用能力
用户 memory = 可修改、可删除、可隔离的个体状态

这是更接近实际产品需求的拆分。

当前自然语言记忆核心

这是当前推荐的生产导向路径,不再把个人事实压缩成一个容易丢失多 token 值的连续向量:

用户输入
   │
   ├─ Automatic memory policy:判断普通消息是否值得长期记忆
   ├─ Native write controller:提供写入表示和槽位地址
   ├─ Frozen Qwen key encoder:生成稳定检索键
   ├─ Learned retriever:查询与记忆槽匹配
   └─ Internal text bank:保存合法 memory prefix、原始 fact key、年龄和有效位
          │
          ▼
   相关记忆才被模型内部拼成 prefix
          │
          ▼
   原版 Qwen 生成回答

具体保证:

  • 每个槽保存可直接参与 Qwen 对话的内部 memory prefix,并单独保存原始 fact key,因此“红富士苹果”“蓝鲸-47”等多 token 值不会被单个向量强行压缩;
  • 普通消息由训练过的自动记忆策略决定是否写入;写入阶段关闭读取,生成阶段只读,避免把模型自己的回答或已召回事实再次写回记忆;
  • 同一属性的更新由训练过的语义检索器确认,不同事实优先使用空槽,容量满时按最老槽淘汰;
  • 读取由模型内部的 learned retriever 触发。无关问题低于阈值时,不注入记忆 prefix,降低个人事实幻觉;
  • persistent_memory.pt 直接包含连续状态、文本槽、检索键、年龄和计数器。重启时只加载这个用户 checkpoint,不需要聊天历史,也不需要外部“记忆读取器”代码;
  • <|fim_prefix|>(或显式配置的 reset token)由模型包装器在生成入口内识别并清空全部记忆槽。

训练检索器:

conda activate LLM
cd W:\Flash\model
python -m V2_dpskw.train_natural_retriever --model-path "W:\Flash\model" --base-adapter V2_dpskw\qwen_memory_adapter_native_v3 --output-adapter V2_dpskw\qwen_memory_adapter_natural_controller_v13 --steps 3000 --pair-count 3200 --batch-size 32 --lr 2e-4

训练自动记忆策略头:

python -m V2_dpskw.train_auto_memory_policy --model-path "W:\Flash\model" --base-adapter V2_dpskw\qwen_memory_adapter_natural_controller_v13 --output-adapter V2_dpskw\qwen_memory_adapter_natural_auto_v13 --steps 2600 --example-count 1600 --batch-size 32 --lr 2e-4 --threshold 0.35 --text-memory-threshold 0.30

启动自然语言记忆聊天:

python -m V2_dpskw.chat_qwen_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_natural_auto_v13 --persistent-memory --persist-in-adapter

如果要测试流式输出和“随时重启”,使用:

python -m V2_dpskw.stream_chat_qwen_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_natural_auto_v13 --memory-state V2_dpskw\data\users\stream_user_memory.pt

该脚本每轮只发送当前用户消息,不发送历史;写入或清空操作会在生成前原子保存。生成过程中按 Ctrl+C 退出后,再次执行同一命令即可从最近一次保存的 memory state 继续。普通消息会自动保存高价值个人事实,/remember <事实> 用于强制写入,/reset 清空全部记忆。

运行完整验收:

python -m V2_dpskw.benchmark_natural_language_memory --model-path "W:\Flash\model" --adapter V2_dpskw\qwen_memory_adapter_natural_auto_v13 --output-adapter V2_dpskw\qwen_memory_adapter_natural_production_auto_v13 --report V2_dpskw\benchmark_natural_language_memory_auto_v13_final.json --max-new-tokens 48 --text-memory-threshold 0.30

当前验收报告为 benchmark_natural_language_memory_auto_v13_final.json:三条事实写入后保留两个独立有效槽,工作地点从 R7 更新为 K9;两条已知事实在两次模型重启后均能回答;未知血型不注入 memory prefix 并拒答;reset token 后连续状态和文本槽都清零。报告中的 production_gate_pass 为 true。

自动记忆策略的训练报告为 qwen_memory_adapter_natural_auto_v13/auto_policy_training.json:2600 步、每类 1600 条样本,评估准确率 99.22%,负样本特异度 100%,误写率 0%。这些是合成数据结果,仍需用真实用户语言继续扩充压力测试。

将记忆模块和当前用户状态合并进 safetensors

如果需要把 v13 的记忆模块参数、检索器、自动写入策略和某个用户的当前状态写进模型权重,可以生成一个新的合并目录。下面的命令是兼容的合并方式;推荐在新包上使用额外第三分片模式。

python -m V2_dpskw.merge_memory_weights `
    --base-model "W:\Flash\model" `
    --adapter "W:\Flash\model\V2_dpskw\qwen_memory_adapter_natural_auto_v13" `
    --memory-state "W:\Flash\model\V2_dpskw\data\users\stream_user_memory.pt" `
    --output "W:\Flash\model\V2_dpskw\qwen3_5_4b_memory_merged_v13"

额外第三分片模式使用同样的参数,并增加 --add-shard;目标文件名应为 model.safetensors-00003-of-00003.safetensors:

python -m V2_dpskw.merge_memory_weights `
    --base-model "<base-model>" `
    --adapter "<adapter>" `
    --memory-state "<memory-state>" `
    --output "<merged-package>" `
    --add-shard "<merged-package>\\model.safetensors-00003-of-00003.safetensors"

兼容旧合并方式生成的目录会把 dynamic_memory.* 张量放入第二个 safetensors 分片;原始 Qwen 两个分片不会被覆盖。自定义动态记忆模型可以直接从该目录加载:

python -m V2_dpskw.stream_chat_qwen_memory `
    --model-path "W:\Flash\model\V2_dpskw\qwen3_5_4b_memory_merged_v13" `
    --memory-state "W:\Flash\model\V2_dpskw\data\users\merged_runtime.pt"

上面的旧模式仍可使用 merged_runtime.pt 保存后续变化;真正执行动态记忆仍需要本项目的模型架构代码。

当前推荐的第三分片启动方式是:

python -m V2_dpskw.stream_chat_qwen_memory `
    --model-path "<merged-package>"

不传 --adapter 和 --memory-state 时,模型会从 model.safetensors-00003-of-00003.safetensors 读取内置 memory state;之后的自动写入、/save 和 /reset 也只原子重写第三分片,不再创建或依赖 merged_runtime.pt。旧的“把动态张量追加到第二分片”方式仍可通过不使用 --add-shard 保留,但每次保存会重写较大的第二分片。

检索器 v13 额外加入了短事实、无标点问法、未见过的值、登记/称呼表达、编程工具表达和“你是谁”硬负样本;训练与线上都使用 plain tokens,并按长度分组避免 Qwen 补零造成表示漂移。训练报告的标准留出、短事实留出和 raw sigmoid 严格阈值召回均为 100%;当前 Wpy 真实身份/拒读压力测试为 25/25。后两项是更接近线上行为的指标,仍应随真实用户分布持续回归。

通用能力回归报告为 comprehensive_benchmark_natural_auto_v13_final.json:54 个任务上原版 Qwen3.5-4B 与动态记忆版均为 96.30%,整体差值 0,回归门禁通过。

需要明确:模型内部已经拥有“何时读、读什么、如何拒绝无关记忆”的路径,但跨机器或跨服务保存用户 checkpoint 仍然需要持久化介质;这是信息存在的物理要求,不等于推理时依赖固定外部读取代码。

当前架构的局限

当前版本主要用于研究“记忆模块能否工作”,还不是最终架构。

记忆写入过于粗粒度

默认使用最后一个 token 的 hidden state 作为摘要。复杂文本可能包含多条事实,仅靠最后一个 token 很容易丢失信息。

后续可以加入:

  • sentence-level summarizer;
  • entity/value extractor;
  • 多 token pooling;
  • 特殊 memory token;
  • 独立的写入路由器。

slot 语义还不稳定

固定数量的连续 slot 不一定自动形成清晰分工。后续可以研究:

  • slot type;
  • key-value memory;
  • 稀疏路由;
  • memory usage regularization;
  • slot 专家化;
  • 多层级 memory。

teacher forcing 与自由生成存在差距

在训练中,模型通常知道正确答案的标签;但在真正聊天时,需要连续生成多个 token。应该分别测试:

  • 单 token 读取;
  • 多 token 事实回答;
  • 多轮连续记忆;
  • 错误回答后的恢复;
  • 新记忆覆盖旧记忆;
  • memory 容量接近上限时的退化。

还缺少完整的记忆治理

生产级系统至少需要三层:

模型内部 Memory
   │
   ├─ 记忆写入策略
   ├─ 记忆读取策略
   └─ 连续向量状态

记忆管理器
   │
   ├─ 事实抽取
   ├─ 去重
   ├─ 冲突处理
   ├─ 时间衰减
   └─ 重要性评分

持久化服务
   │
   ├─ 用户隔离
   ├─ 加密
   ├─ 版本管理
   └─ 删除/导出

评测时应该回答的关键问题

不要只问“准确率有没有变高”,还要问:

  1. 模型是否真的使用了 memory,而不是从 query 中猜答案?
  2. 新记忆能否覆盖旧记忆?
  3. 无关内容是否会污染 memory?
  4. memory 容量增加后,效果是否持续提升?
  5. 程序重启后能否恢复?
  6. 不同用户之间是否完全隔离?
  7. memory read/write 是否降低原模型的通用能力?
  8. 记忆状态是否可以解释、导出和删除?
  9. 推理速度和显存成本是多少?
  10. 长时间运行后是否出现状态漂移或数值爆炸?

下一步建议

如果目标是开发一个真正有新意的架构,建议按下面顺序推进:

  1. 扩展到多事实、多属性和长序列连续写入;
  2. 增加时间衰减、记忆容量压力和可解释 slot 诊断;
  3. 做多用户隔离、并发读写和异常恢复测试;
  4. 再尝试更激进的结构,例如 fast weights、可写 KV cache、分层记忆和稀疏路由。

免责声明

本项目是研究和实验性质的代码。它不保证训练出的 adapter 在所有任务上提升,也不保证当前连续 memory state 能可靠保存所有自然语言事实。

如果使用第三方模型权重或数据集,请遵守对应的模型许可证、数据许可证和隐私要求。