Files
noc/doc/nocmem.md
Fam Zheng 7000ccda0f add nocmem: auto memory recall + ingest via NuoNuo hippocampal network
- nocmem Python service (mem/): FastAPI wrapper around NuoNuo's
  Hopfield-Hebbian memory, with /recall, /ingest, /store, /stats endpoints
- NOC integration: auto recall after user message (injected as system msg),
  async ingest after LLM response (fire-and-forget)
- Recall: cosine pre-filter (threshold 0.35) + Hopfield attention (β=32),
  top_k=3, KV-cache friendly (appended after user msg, not in system prompt)
- Ingest: LLM extraction + paraphrase augmentation, heuristic fallback
- Wired into main.rs, life.rs (agent done), http.rs (api chat)
- Config: optional `nocmem.endpoint` in config.yaml
- Includes benchmarks: LongMemEval (R@5=94.0%), efficiency, noise vs scale
- Design doc: doc/nocmem.md
2026-04-11 12:24:48 +01:00

9.3 KiB
Raw Blame History

nocmem — NOC 自动记忆系统

动机

NOC 现有记忆100 个文本槽位200 字符/槽)+ 滑动窗口摘要。全部塞在 system prompt 里,每次对话都带着。

问题:

  • 没有语义检索,无关记忆浪费 token
  • 槽位容量有限,不可扩展
  • 没有联想能力A 提到 → 想起 B → 引出 C

nocmem 用 NuoNuo 的 Hopfield-Hebbian 混合记忆网络替代朴素文本槽位,实现自动召回自动存储

核心技术

NuoNuo Hippocampal Memory

生物启发的双层记忆架构(详见 ../nuonuo/doc/architecture.md

Layer 1 — Hopfield单跳噪声容忍

存储 (cue, target) embedding 对。召回时两阶段:

  1. NN 预过滤cosine similarity 找 top-K 候选K=20
  2. Hopfield settle:β-scaled softmax attention 迭代收敛3 步)

关键特性:paraphrase 容忍 — 用户换一种说法问同样的事,照样能召回。通过存储 cue variants同一条记忆的多种表述实现attention 按 memory_id 聚合。

Layer 2 — Hebbian多跳联想链

WTA pattern separation384D → 16384D 稀疏码k=50稀疏度 0.3%+ 外积权重矩阵 W。

Hopfield 找到起点后Hebbian 通过 W @ code 沿关联链前进A → B → C。

这是传统 RAG 做不到的——向量搜索只能找"相似"Hebbian 能找"相关但不相似"的东西。

性能指标

指标 数值
Paraphrase recall+augmentation, 2K bg 95-100%
Multi-hop3 hops, 500 bg 100%
Scale20K memories, no augmentation 80%
Recall 延迟 @ 20K 4ms
VRAM ~1 GB

Embedding

使用 all-MiniLM-L6-v2384 维CPU/GPU 均可。选择理由:

  • NuoNuo 实验P1验证gap metric相关与不相关的分数差比绝对相似度更重要
  • MiniLM 在 gap metric 上优于 BGE-large 等更大模型
  • 推理快GPU ~1msCPU ~10ms per query

记忆提取

对话结束后,用 LLM 从 (user_msg, assistant_msg) 中提取 (cue, target, importance) 三元组:

  • cue:什么情况下应该回忆起这条记忆(触发短语)
  • target:记忆内容本身
  • importance0-1 重要度评分

LLM 不可用时回退到 heuristic问答模式检测 + 技术关键词匹配)。

提取后LLM 为每个 cue 生成 3 个 paraphrase作为 cue_variants 存入,提升召回鲁棒性。

架构

              ┌─────────────┐
              │   Telegram   │
              │    User      │
              └──────┬───────┘
                     │ message
                     ▼
              ┌─────────────┐
              │     NOC      │
              │   (Rust)     │
              │              │
              │ 1. 收到 user │
              │    message   │
              │              │
              │ 2. HTTP POST ├──────────────────┐
              │    /recall   │                  │
              │              │                  ▼
              │              │         ┌─────────────────┐
              │              │         │     nocmem      │
              │              │         │   (Python)      │
              │              │         │                 │
              │              │         │ embed(query)    │
              │              │◄────────┤ hippocampus     │
              │   recalled   │         │   .recall()     │
              │   memories   │         │ format results  │
              │              │         └─────────────────┘
              │ 3. 构建 messages:       
              │    [...history,         
              │     user_msg,           
              │     {role:system,       
              │      recalled memories}]
              │              │
              │ 4. 调 LLM    │
              │    (stream)  │
              │              │
              │ 5. 得到      │
              │    response  │
              │              │
              │ 6. 异步 POST ├──────────────────┐
              │    /ingest   │                  │
              │              │                  ▼
              │              │         ┌─────────────────┐
              │              │         │     nocmem      │
              │              │         │                 │
              │              │         │ LLM extract     │
              │              │         │ embed + store   │
              │              │         │ save checkpoint │
              │              │         └─────────────────┘
              │ 7. 回复用户  │
              └──────────────┘

消息注入策略

关键设计recalled memories 注入在 user message 之后,作为独立的 system message。

[
  {"role": "system", "content": "persona + memory_slots + ..."},   // 不变
  {"role": "user", "content": "历史消息1"},                         // 历史
  {"role": "assistant", "content": "历史回复1"},
  ...
  {"role": "user", "content": "当前用户消息"},                      // 当前轮
  {"role": "system", "content": "[相关记忆]\n- 记忆1\n- 记忆2"}    // ← nocmem 注入
]

为什么不放 system prompt 里?

KV cache 友好。System prompt 是所有对话共享的前缀,如果每条消息都改 system prompt 的内容(注入不同的 recalled memories整个 KV cache 前缀失效,前面几千 token 全部重算。

放在 user message 之后前缀system prompt + 历史消息 + 当前 user message保持稳定只有尾部的 recalled memories 是变化的KV cache 命中率最大化。

临时性。Recalled memories 不持久化到对话历史数据库。每轮对话独立召回,下一轮消息进来时重新召回当时相关的记忆。这避免了历史消息中堆积大量冗余的记忆注入。

HTTP API

POST /recall

请求:

{"text": "数据库最近是不是很慢"}

响应:

{
  "memories": "[相关记忆]\n- 上次数据库慢是因为缺少索引 (hop=1)\n- PostgreSQL 跑在 5432 端口 (hop=2)",
  "count": 2
}
  • 如果没有相关记忆,返回 {"memories": "", "count": 0}
  • NOC 检查 count > 0 才注入,避免空消息

POST /ingest

请求:

{
  "user_msg": "帮我看看数据库为什么慢",
  "assistant_msg": "检查了一下,是 users 表缺少 email 字段的索引..."
}

响应:

{"stored": 2}
  • fire-and-forgetNOC 不等响应
  • 内部流程LLM 提取 → embed → generate paraphrases → store → save checkpoint

GET /stats

{
  "num_memories": 1234,
  "num_cue_entries": 4500,
  "augmentation_ratio": 3.6,
  "vram_mb": 1024,
  "embedding_model": "all-MiniLM-L6-v2"
}

NOC 侧改动

config.yaml

nocmem:
  endpoint: "http://127.0.0.1:9820"

Rust 改动(最小化)

config.rs:加一个可选字段

#[serde(default)]
pub nocmem: Option<NocmemConfig>,

#[derive(Deserialize, Clone)]
pub struct NocmemConfig {
    pub endpoint: String,
}

main.rs(主消息处理路径):

api_messages.push(user_msg) 之后、run_openai_with_tools 之前:

// auto recall from nocmem
if let Some(ref nocmem) = config.nocmem {
    if let Ok(recalled) = nocmem_recall(&nocmem.endpoint, &prompt).await {
        if !recalled.is_empty() {
            api_messages.push(serde_json::json!({
                "role": "system",
                "content": recalled
            }));
        }
    }
}

在 LLM 回复之后(push_message 之后):

// async ingest to nocmem (fire-and-forget)
if let Some(ref nocmem) = config.nocmem {
    let endpoint = nocmem.endpoint.clone();
    let u = prompt.clone();
    let a = response.clone();
    tokio::spawn(async move {
        let _ = nocmem_ingest(&endpoint, &u, &a).await;
    });
}

nocmem_recallnocmem_ingest 是两个简单的 HTTP 调用函数。recall 设 500ms 超时(失败就跳过,不影响正常对话)。

同步覆盖的调用点

位置 场景 recall ingest
main.rs handle_message 用户聊天
life.rs AgentDone 子代理完成通知
life.rs run_timer 定时器触发
http.rs api_chat HTTP API 聊天
gitea.rs Gitea webhook

部署

nocmem 作为独立 Python 服务运行:

cd /data/src/noc/mem
uv run uvicorn server:app --host 127.0.0.1 --port 9820

可配 systemd 管理。checkpoint 持久化到 ./data/hippocampus.pt(相对于 mem 目录)。

未来方向

  • 重要度衰减:长期不被召回的记忆自动降权
  • 矛盾检测:新记忆与旧记忆冲突时自动替换
  • 记忆整合sleep consolidation:定期合并碎片记忆为更紧凑的表示
  • 和 memory slot 融合:逐步迁移 slot 内容到 nocmem最终淘汰 slot 系统