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 原生动态记忆工程;生产接入前仍需补齐业务侧鉴权、加密、并发和运维策略。
项目结构
dynamic_memory_lab/
├─ 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\dynamic_memory_lab
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 内容中。
数据设计时要注意:
- 写入文本中出现的信息,应该在查询文本中尽量不重复;
- 如果查询中直接包含答案,模型可能只是在复制上下文,而不是读取 memory;
- 训练集和评估集中的实体、属性、表述方式应尽量分离;
- 要加入冲突样本,测试新记忆是否能覆盖旧记忆;
- 要加入多条事实,测试不同 slot 是否会互相污染;
- 要加入无关信息,测试模型能否避免把所有内容都写进去。
make_benchmark_data.py 可以生成一个简单的单字符映射任务,用来快速检查“写入—读取—回答”链路是否工作。它适合做冒烟测试,不足以证明模型拥有通用长期记忆。
常用命令
以下命令均在工程目录执行。
训练动态记忆模块
conda activate LLM
cd W:\Flash\model
python -m dynamic_memory_lab.train_qwen_memory --model-path "W:\Flash\model" --data dynamic_memory_lab\data\benchmark_train.jsonl --output-dir dynamic_memory_lab\qwen_memory_adapter --steps 100 --batch-size 1 --lr 1e-4 --max-length 128
如果要复现实验中的精确 token pointer:
python -m dynamic_memory_lab.train_qwen_memory --model-path "W:\Flash\model" --data dynamic_memory_lab\data\benchmark_train.jsonl --output-dir dynamic_memory_lab\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 dynamic_memory_lab.make_native_memory_data --output-dir dynamic_memory_lab\data\native_memory --train-count 512 --eval-count 128 --seed 20260904
python -m dynamic_memory_lab.train_native_memory --model-path "W:\Flash\model" --data dynamic_memory_lab\data\native_memory\train.jsonl --output-dir dynamic_memory_lab\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 dynamic_memory_lab.evaluate_native_memory --model-path "W:\Flash\model" --adapter-dir dynamic_memory_lab\qwen_memory_adapter_native_v3 --data dynamic_memory_lab\data\native_memory\eval.jsonl --max-length 192 --max-new-tokens 8
生成 benchmark 数据
python -m dynamic_memory_lab.make_benchmark_data --output-dir dynamic_memory_lab\data
对比原版和动态版
python -m dynamic_memory_lab.benchmark_qwen --model-path "W:\Flash\model" --data dynamic_memory_lab\data\benchmark_eval.jsonl --adapter dynamic_memory_lab\qwen_memory_adapter_pointer --output dynamic_memory_lab\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 dynamic_memory_lab.chat_qwen_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter
要让用户记忆跨进程保存,指定一个用户专属文件:
python -m dynamic_memory_lab.chat_qwen_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter --memory-state dynamic_memory_lab\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 dynamic_memory_lab.chat_qwen_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\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("dynamic_memory_lab/qwen_memory_adapter_native_v3_persistent")
该包中的 persistent_memory.pt 会在 load_memory_adapter() 时自动加载,新的模型实例不需要再显式传入 memory_state。验证命令:
python -m dynamic_memory_lab.verify_native_checkpoint --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter_native_v3 --data dynamic_memory_lab\data\native_memory\eval.jsonl --output-adapter dynamic_memory_lab\qwen_memory_adapter_native_v3_persistent
可以用下面的脚本自动验证完整流程:
python -m dynamic_memory_lab.verify_persistent_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter_pointer --data dynamic_memory_lab\data\benchmark_eval.jsonl --memory-state dynamic_memory_lab\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 dynamic_memory_lab.train_natural_retriever --model-path "W:\Flash\model" --base-adapter dynamic_memory_lab\qwen_memory_adapter_native_v3 --output-adapter dynamic_memory_lab\qwen_memory_adapter_natural_controller_v13 --steps 3000 --pair-count 3200 --batch-size 32 --lr 2e-4
训练自动记忆策略头:
python -m dynamic_memory_lab.train_auto_memory_policy --model-path "W:\Flash\model" --base-adapter dynamic_memory_lab\qwen_memory_adapter_natural_controller_v13 --output-adapter dynamic_memory_lab\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 dynamic_memory_lab.chat_qwen_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter_natural_auto_v13 --persistent-memory --persist-in-adapter
如果要测试流式输出和“随时重启”,使用:
python -m dynamic_memory_lab.stream_chat_qwen_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter_natural_auto_v13 --memory-state dynamic_memory_lab\data\users\stream_user_memory.pt
该脚本每轮只发送当前用户消息,不发送历史;写入或清空操作会在生成前原子保存。生成过程中按 Ctrl+C 退出后,再次执行同一命令即可从最近一次保存的 memory state 继续。普通消息会自动保存高价值个人事实,/remember <事实> 用于强制写入,/reset 清空全部记忆。
运行完整验收:
python -m dynamic_memory_lab.benchmark_natural_language_memory --model-path "W:\Flash\model" --adapter dynamic_memory_lab\qwen_memory_adapter_natural_auto_v13 --output-adapter dynamic_memory_lab\qwen_memory_adapter_natural_production_auto_v13 --report dynamic_memory_lab\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 dynamic_memory_lab.merge_memory_weights `
--base-model "W:\Flash\model" `
--adapter "W:\Flash\model\dynamic_memory_lab\qwen_memory_adapter_natural_auto_v13" `
--memory-state "W:\Flash\model\dynamic_memory_lab\data\users\stream_user_memory.pt" `
--output "W:\Flash\model\dynamic_memory_lab\qwen3_5_4b_memory_merged_v13"
额外第三分片模式使用同样的参数,并增加 --add-shard;目标文件名应为 model.safetensors-00003-of-00003.safetensors:
python -m dynamic_memory_lab.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 dynamic_memory_lab.stream_chat_qwen_memory `
--model-path "W:\Flash\model\dynamic_memory_lab\qwen3_5_4b_memory_merged_v13" `
--memory-state "W:\Flash\model\dynamic_memory_lab\data\users\merged_runtime.pt"
上面的旧模式仍可使用 merged_runtime.pt 保存后续变化;真正执行动态记忆仍需要本项目的模型架构代码。
当前推荐的第三分片启动方式是:
python -m dynamic_memory_lab.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
│
├─ 记忆写入策略
├─ 记忆读取策略
└─ 连续向量状态
记忆管理器
│
├─ 事实抽取
├─ 去重
├─ 冲突处理
├─ 时间衰减
└─ 重要性评分
持久化服务
│
├─ 用户隔离
├─ 加密
├─ 版本管理
└─ 删除/导出
评测时应该回答的关键问题
不要只问“准确率有没有变高”,还要问:
- 模型是否真的使用了 memory,而不是从 query 中猜答案?
- 新记忆能否覆盖旧记忆?
- 无关内容是否会污染 memory?
- memory 容量增加后,效果是否持续提升?
- 程序重启后能否恢复?
- 不同用户之间是否完全隔离?
- memory read/write 是否降低原模型的通用能力?
- 记忆状态是否可以解释、导出和删除?
- 推理速度和显存成本是多少?
- 长时间运行后是否出现状态漂移或数值爆炸?
下一步建议
如果目标是开发一个真正有新意的架构,建议按下面顺序推进:
- 扩展到多事实、多属性和长序列连续写入;
- 增加时间衰减、记忆容量压力和可解释 slot 诊断;
- 做多用户隔离、并发读写和异常恢复测试;
- 再尝试更激进的结构,例如 fast weights、可写 KV cache、分层记忆和稀疏路由。
免责声明
本项目是研究和实验性质的代码。它不保证训练出的 adapter 在所有任务上提升,也不保证当前连续 memory state 能可靠保存所有自然语言事实。
如果使用第三方模型权重或数据集,请遵守对应的模型许可证、数据许可证和隐私要求。