Skip to content

Latest commit

 

History

History
383 lines (272 loc) · 20.8 KB

File metadata and controls

383 lines (272 loc) · 20.8 KB

MemCC — OpenClaw 混合记忆插件

DAG 上下文引擎 · 原文向量长期记忆 · 四层检索

一个为 OpenClaw 智能体同时提供会话内上下文压缩(分层摘要 DAG)与跨会话长期召回(向量存储 + 分层检索)的插件 —— 打包为单一即插即用的运行时扩展。


LoCoMo 86.9% LoCoMo temporal 80.4% LongMemEval-500 80.9% LongMemEval-100 92.1%
OpenClaw ≥2026.4.2 Node 22.x TypeScript 5.7 MIT License

🚀 快速开始🌟 概述🏗️ 架构🧠 检索管线📈 评测结果🛠️ 工具⚙️ 配置🔬 基准测试


🔥 最新动态

  • [2026-04-12] 🈯 中文支持与可选向量去重正式发布。 新增 CJK 偏好提取(12 条新正则模式,覆盖喜好、习惯、目标、传记事实和怀旧表达),基于 cl100k_base 经验校准的 CJK Token 估算(0.87 字符/Token —— 修复了中文文本约 4.6 倍的低估问题,此前会导致压缩触发严重滞后),以及写入时的可选余弦相似度向量去重(vectorDedupEnabled,默认关闭)。LongMemEval-50 上的英文基准测试数字保持完全一致 —— 这是经验证实的结果,而非仅从代码构造上推导。
  • [2026-04-11] 🚀 v1.0.0 正式发布! 时序推理关键修复随首个正式版本一同发布:在索引的会话文档中添加 [Date: …] 头部,并将 asOf 传递给 LLM 评判器,使 LoCoMo 时序得分从 42.4% → 80.4%(+38.0 pts)LongMemEval-500 从 72.7% → 80.9%(+8.2 pts)

📑 目录


🚀 快速开始

MemCC 是一个 OpenClaw 插件 —— 它通过 OpenClaw 的插件发现机制加载,并同时注册为上下文引擎和记忆能力。无需独立运行时,无需单独服务器。

# 📥 克隆并安装
git clone <repo> memcc
cd memcc
npm install

# ▶️ 将 OpenClaw 指向该插件
export OPENCLAW_PLUGINS=/path/to/memcc
# 或在 openclaw 配置中添加该包路径

# 🔑 配置嵌入 + LLM 重排序所用的 OpenAI Key
export OPENAI_API_KEY=sk-...

# 🧪 运行完整测试套件
npm test

# 📊 运行基准测试套件(详见 §基准测试)
npm run bench

加载完成后,OpenClaw 将解析上下文引擎 memcc(也别名为 default),并向智能体暴露四个工具:memory_searchmemory_getlcm_greplcm_expand_query。无需额外设置 —— 插件在首次启动时会自动迁移自身的 SQLite 数据库结构,并延迟初始化 LanceDB 存储。


🌟 概述

跨多轮次、多会话运行的 LLM 智能体面临两个本质上不同的问题:

  1. 当前对话过长,无法放入上下文窗口。 你需要在不丢失精确细节可恢复能力的前提下,压缩旧的轮次。
  2. 在上一个会话中学到的内容,智能体重启后即告消失。 你需要一个能够理解语义召回而非仅依赖文本匹配的持久化存储。

大多数记忆系统只解决其中一个问题。MemCC 在一个插件中同时解决两者,关键在于两者之间存在通信:当上下文引擎压缩一段对话时,生成的摘要会直接传递给长期记忆运行时进行嵌入。当上下文引擎在下一次组装提示词时,它会向记忆运行时请求相关的召回内容,并将其拼接回与压缩摘要共存的同一上下文中。

🗜️ 上下文引擎(DAG)

会话内分层压缩

基于 SQLite 的对话存储,实现 leaf → condensed 摘要汇总。最新的消息原文保留;较早的轮次折叠成以会话为键的摘要树。大型文件被外化并替换为指针。

目标:在不损失可恢复性的前提下保持在 Token 预算之内。

🧠 长期记忆运行时

跨会话原文向量召回

LanceDB 向量存储(OpenAI text-embedding-3-small,1536 维)+ SQLite 元数据,由一个分类器将各记忆路由至 wing/room 命名空间。其上构建了合成偏好文档、知识图谱三元组和四层检索管线。

目标:在重启后依然存活的语义召回。

💡 两个子系统通过 MemoryRuntimeBridge 接口解耦 —— 上下文引擎从不直接导入记忆代码。这意味着记忆运行时可以异步初始化(嵌入客户端启动),而不会阻塞上下文引擎接收轮次。


🏗️ 架构

MemCC 架构图

三条通道 —— 左侧是 Host/用户,中间是上下文引擎(DAG),右侧是长期记忆运行时。蓝色边是主要的写入/组装流,绿色边是记忆写入,橙色是控制/引导,灰色虚线是工具接线。

核心组件

组件 文件 职责
插件壳 src/plugin/index.ts 向 OpenClaw 注册上下文引擎、记忆能力和工具;在异步初始化完成后接线 MemoryRuntimeBridge
ContextEngine src/context/engine.ts 实现 OpenClaw 的 ContextEngine 接口:ingestingestBatchassembleafterTurncompactbootstrap。拥有压缩控制权。
ConversationStore src/context/store/conversation-store.ts SQLite + 可选 FTS5,用于存储原始消息。
SummaryStore src/context/store/summary-store.ts 存储 leaf / condensed 摘要 DAG;追踪每个对话的上下文条目列表。
CompactionEngine src/context/compaction.ts 决定何时压缩(0.75 × budget),通过配置的模型生成摘要,保留 freshTailCount=4 条消息。
ContextAssembler src/context/assembler.ts 遍历 DAG + 新鲜尾部 + 记忆注入区段,生成受 Token 预算限制的消息列表。
LargeFileHandler src/context/large-files.ts 将超大 Blob 外化并在污染摘要树之前替换为指针。
Bootstrap src/context/bootstrap.ts 在首次命中时重放会话 JSONL 文件,使重启的会话不丢失 DAG。
MemoryRuntime src/memory/runtime.ts 管理记忆侧:ingestSummaryretrieveinjectIntoContextimportLegacy
VectorStore src/memory/store/vector-store.ts LanceDB 后端 —— 通过 OpenAI 客户端嵌入,并以 wing/room 元数据进行 upsert。支持写入时可选的余弦相似度去重(详见 §配置)。
MemoryStore src/memory/store/memory-store.ts SQLite 元数据存储:记忆记录、知识图谱三元组、FTS 索引。
检索层 src/memory/layers/*.ts l0-identity.tsl1-essential.tsl2-on-demand.tsl3-deep-search.ts —— 见 §检索管线。
搜索 src/memory/search/*.ts hybrid-search.ts(BM25 风格 + 时序提升)、llm-rerank.tsknowledge-graph.ts
分类 src/memory/classify.ts 基于实体/主题检测,将文本路由到 (wing, room) 命名空间。
偏好提取器 src/memory/preference-extractor.ts 从压缩摘要中挖掘用户明确偏好(27 条英文 + 12 条中文正则模式;CJK 感知的长度边界),并将其存储为合成记忆文档以支持释义召回。
Token 估算器 src/token-estimator.ts CJK 感知的 Token 计数估算,供压缩、组装、引导和层级预算使用。基于 cl100k_base 的经验比率:CJK 为 0.87 字符/Token,拉丁字符为 4.0。非 CJK 快速路径与原有的 Math.ceil(text.length / 4) 公式完全一致。

数据流 —— 单轮次端到端

  1. 写入。 OpenClaw 将每个轮次传递给 ContextEngine.ingest()。大型文件被外化;原始消息被存储,并将一个 raw 上下文条目追加到对话的条目列表中。
  2. 压缩检查。 轮次结束后,afterTurn() 询问 CompactionEngine 对话是否超过了预算的 75%。如果是,生成一个 leaf 摘要(可选地滚入 condensed 摘要)。
  3. 摘要 → 记忆。 每个摘要通过 MemoryRuntimeBridge.ingestSummary() 推送。MemoryRuntime 对其分类,用 OpenAI 进行嵌入,upsert 到 LanceDB,并运行偏好提取器。摘要文档和任何合成偏好文档都进入向量存储。
  4. 组装。 在下一个轮次,assemble() 获取最近 3 条消息作为检索查询,调用 memoryRuntime.retrieve(query),然后通过 injectIntoContext(retrieval) 生成受预算限制的 <memcc-identity><memcc-essential><memcc-context><memcc-recall> 区段。这些区段与 DAG 组装(摘要 + 新鲜尾部)交错排列。
  5. 提示。 如果存在压缩历史,则发出一个 systemPromptAddition,告知智能体可以调用 lcm_grep / lcm_expand_query 从摘要中恢复精确细节。

🧠 检索管线

MemCC 不进行单一的"Top-K 向量搜索"。每次检索会向四个独立层并行展开,每层回答不同的问题,并有各自的 Token 预算。

四层检索

查询被广播到 L0–L3 并行执行。L3 内部依次执行:向量搜索 → 混合重排序 → KG 合并 → (可选)LLM 重排序。每层在注入时写入自己的包装区段,当 contextOptimization=true 时 Token 预算收紧。

层级 数据源 用途 预算
L0 身份 agentDir/* 静态文件 该智能体是谁 —— 角色、人格、基础事实。始终原文注入。 不限
L1 必要 MemoryStore 固定记忆 顶级身份片段和高权重固定事实。按行裁剪到预算。 800 tok(优化模式下 400)
L2 按需 MemoryStore wing 过滤 用查询检测到的 wing 标记的记忆(例如代码问题对应"coding" wing)。优化模式下完全跳过。 500 tok 或放弃
L3 深度搜索 VectorStore + MemoryStore 三元组 语义召回:向量 Top-20 → BM25 + 时序重排序 → 可选 KG 合并 → 可选 LLM 重排序。 top 10(优化模式 top 3

L3 细节 —— 混合重排序

  1. 向量搜索 —— 从 LanceDB 获取 20 个候选(text-embedding-3-small)。
  2. 混合重排序hybrid-search.ts)—— BM25 风格词法评分 + 时序提升(较新的记忆在平局时优先)。
  3. KG 合并knowledge-graph.ts)—— 如果查询实体匹配已存储的 (subject, predicate, object) 三元组,则注入一个合成 KG 结果(除非已覆盖)。
  4. LLM 重排序llm-rerank.ts,由 llmRerank=true 控制,默认:gpt-4o-mini)—— 最后一公里精度的最终交叉编码器过滤。每次搜索约消耗 $0.001。

💡 层级拆分并非学术考量 —— 这正是使预算可收缩的原因。contextOptimization=true 完全放弃 L2,将 L1 减半,并将 L3 限制为 3 个命中。这就是在同一查询上"每轮注入约 2500 tok"与"每轮注入约 800 tok"的差距所在。


📈 评测结果

MemCC 通过了 LoCoMo 和 LongMemEval 上的所有目标,并在时序推理方面取得了领先成绩 —— 这历来是记忆系统最难攻克的子集。

🏆 86.9%
LoCoMo 综合(1,986 题)
🏆 80.4%
LoCoMo 时序(321 题)
🏆 88.1%
LoCoMo 情节(1,665 题)
🏆 80.9%
LongMemEval-500(498 题)
基准测试 得分 目标 状态
LoCoMo 综合 86.9% ≥ 50% ✅ 通过
LoCoMo 情节 88.1% ≥ 60% ✅ 通过
LoCoMo 时序 80.4% ≥ 50% ✅ 通过
LongMemEval-100 92.1% ≥ 88% ✅ 通过
LongMemEval-500 80.9% 领先

🛠️ 工具

MemCC 向 OpenClaw 注册四个工具。其中三个操作短期上下文(压缩后的 DAG),一个操作长期记忆。

工具 操作对象 功能
memory_search 长期(LanceDB) 跨所有已存储记忆的语义搜索。返回带分数的 L3 深度搜索层命中结果。最适合"我们上个月对 X 做了什么决定"。
memory_get 长期(LanceDB) 通过 ID 获取单条记忆 —— 当 memory_search 返回 ID 且智能体需要完整原文时使用。
lcm_grep 短期(上下文 DAG) 在当前会话的消息压缩摘要中进行全文搜索。当智能体需要从压缩历史中找到精确短语时使用。
lcm_expand_query 短期(上下文 DAG) 将压缩摘要展开回生成它的底层原始消息 —— 实际上是"撤销这部分压缩"。

assemble() 检测到压缩历史时,它会注入一条系统提示词提示,精确告知智能体该使用哪些工具:

"早期的一些对话已被压缩成摘要。如果你需要从压缩历史中获取精确细节,请使用 lcm_grep 进行搜索,然后使用 lcm_expand_query 获取完整上下文。"


⚙️ 配置

所有配置通过 openclaw.plugin.json 中定义的 OpenClaw 插件配置 Schema 流转。每个选项都是可选的 —— 默认值已经过调优。

{
  "plugins": {
    "memcc": {
      "contextOptimization": false,    // true → 收紧 L1-L3 的预算
      "compactionModel": null,          // null → 继承 OpenClaw 配置
      "embeddingModel": "text-embedding-3-small",
      "llmRerank": true,                // ~$0.001/次搜索 — 默认开启
      "rerankModel": "gpt-4o-mini",     // null → 默认使用 gpt-4o-mini
      "vectorDedupEnabled": false,      // 写入时可选的近重复抑制
      "vectorDedupThreshold": 0.95      // 余弦相似度,范围 [0,1];≥ 阈值时跳过写入
    }
  }
}
选项 默认值 效果
contextOptimization false true 时将 L1 预算减半(800→400),完全放弃 L2,将 L3 限制为 3 个命中,收紧压缩目标(leaf 1500→1000,condensed 2000→1200)。
compactionModel null(继承) 用于生成 leaf/condensed 摘要的模型。格式为 Provider/模型字符串,如 openai/gpt-4o-mini
embeddingModel text-embedding-3-small OpenAI 嵌入模型 —— 任何与 OpenAI Embeddings API 兼容的模型均可。
llmRerank true 在混合重排序后运行 LLM 交叉编码器过滤。设为 false 则仅使用向量 + BM25。
rerankModel gpt-4o-mini 用于 llmRerank 的模型。推荐使用低成本默认值。
vectorDedupEnabled false 当设为 true 时,VectorStore.add() 会计算与现有最近邻的余弦相似度,并跳过相似度 ≥ vectorDedupThreshold 的写入。仅比较已持久化的状态(非批内比较)。适用于同一事实可能被重复写入的长期运行工作负载;之所以默认关闭,是因为 LongMemEval 风格的基准测试不会触发去重路径。
vectorDedupThreshold 0.95 写入时去重的余弦相似度阈值。有效范围为 [0, 1](含端点)。超出范围或 NaN 的值会回退到默认值。阈值调紧(0.98)会抑制更少重复,调松(0.90)则抑制更多。

存储位置(自动解析)

$HOME/.openclaw/memcc/
├── context.db      # SQLite — 对话、消息、摘要
├── memory.db       # SQLite — 记忆记录、三元组、FTS
└── lance/          # LanceDB — 向量存储

硬编码的最优默认值

这些参数不在配置 Schema 中 —— 它们是通过基准测试运行调优的结果。

参数 默认值(优化模式) 说明
contextThreshold 0.75 压缩在达到 Token 预算的 75% 时触发。
freshTailCount 4 最后 4 条消息始终原文保留。
leafChunkTokens 4000 折叠成一个 leaf 的最大 Token 数。
leafTargetTokens 1500(1000) leaf 摘要的目标大小。
condensedTargetTokens 2000(1200) condensed 摘要的目标大小。
leafMinFanout 3 触发 condensed 汇总前所需的最少 leaf 数。

📦 安装

环境要求

  • 🟢 Node.js 22.x(为使用内置的 node:sqlite 模块)
  • 🔑 兼容 OpenAI 的 API(用于嵌入 + 重排序)
  • 🪶 OpenClaw ≥ 2026.4.2(对等依赖)

步骤

# 在 memCC 目录下执行
npm install

# 验证插件元数据是否被识别
cat openclaw.plugin.json

# 运行测试
npm test

# 运行基准测试(需要 OPENAI_API_KEY)
OPENAI_API_KEY=sk-... npm run bench

运行时依赖

用途
@lancedb/lancedb 向量存储后端
apache-arrow LanceDB 的列式数据传输
@sinclair/typebox 插件配置 Schema 校验
node:sqlite(内置) 上下文 + 记忆元数据存储

🔬 基准测试

基准测试套件是 test/benchmark/ 下的标准 Vitest 测试文件。它们从兄弟目录 memory-lancedb-pro/bench/ 中读取数据集(LongMemEval 和 LoCoMo)。

# 🎯 LongMemEval-50(冒烟测试,约 1 分钟)
OPENAI_API_KEY=<key> npx vitest run test/benchmark/longmemeval-50.test.ts --testTimeout 600000

# 📏 LongMemEval-100(约 3 分钟)
OPENAI_API_KEY=<key> npx vitest run test/benchmark/longmemeval-100.test.ts --testTimeout 600000

# 🏁 LongMemEval-500 完整版(约 45 分钟)
OPENAI_API_KEY=<key> npx vitest run test/benchmark/longmemeval-500.test.ts --testTimeout 3600000

# 🎢 LoCoMo 完整版(约 40-50 分钟)
OPENAI_API_KEY=<key> npx vitest run test/benchmark/locomo.test.ts --testTimeout 3600000

文件说明

  • test/benchmark/longmemeval.test.ts —— 代表性子集
  • test/benchmark/longmemeval-100.test.ts —— 100 题 + 合成偏好 + LLM 重排序
  • test/benchmark/longmemeval-500.test.ts —— 完整 500 题
  • test/benchmark/longmemeval-tail.test.ts —— 420-500 切片(超时后继续)
  • test/benchmark/locomo.test.ts —— 10 个对话,1,986 个查询

运行环境(用于复现 2026-04-11 的测试结果)

OS:       macOS 26.4 (arm64)
Node:     22.22.0
OpenClaw: 2026.4.9
MemCC:    0.1.0
嵌入器:  OpenAI text-embedding-3-small(1536 维)
评判器:  gpt-4o-mini

🗺️ 路线图

检索质量

  • 针对多会话"共有多少 X" / "列出所有 Y"查询的聚合/计数路径(LongMemEval 尾部在添加日期头部后的剩余弱点)
  • 地标缓存 —— 廉价的按文档元数据(作者、位置、实体标签,在写入时一次性提取)以解锁类似日期头部对实体中心查询的增益
  • CI 中的按子集回归门控,使任何基准子集都不会悄无声息地下降

基础设施

  • 替代嵌入后端(本地模型、Qwen3-Embedding、Voyage)
  • 可配置的向量存储后端(当前为 LanceDB;sqlite-vec 作为零依赖备用)
  • 记忆存储的导出/导入以支持智能体迁移

集成

  • 带访问控制的多智能体记忆共享(子智能体的授权已存在;扩展到对等智能体)
  • 用于从外部事件实时更新记忆的流式写入 API

欢迎贡献!


📄 许可证

MIT —— 详见 LICENSE 文件(或 package.jsonlicense 字段)。