memoLite 架构解析:SQLite、Kùzu 与向量检索如何组成 Agent 的本地记忆

从一条对话的写入与召回出发,拆解 memoLite 如何用 SQLite 保存事实源、Kùzu 维护 Episode/Derivative 关系、Embedding 与 SQLite 向量表完成检索,并通过 REST、MCP 和 OpenClaw 把本地记忆接回 Agent。

  • 编程
  • 产品

AI Agent 最直观的问题,是它只认识当前上下文。关闭会话、切换工具或超过上下文窗口后,之前说过的偏好、项目约束和历史事件就很难继续使用。

memoLite 的做法不是修改模型,而是在 Agent 外部增加一个本地记忆层:对话结束后保存原始事件并建立检索索引;下一轮开始前先找回相关内容,再把它作为上下文交给模型。

项目的简要介绍已经放在 MemoLite。这篇文章重点拆解它的框架能力和数据流。

整体架构

memoLite 是一个运行在本机的单机服务。上层可以通过 REST、MCP、Python SDK、TypeScript Client 或 OpenClaw 插件访问;中间由 MemoryOrchestrator 统一编排;底层分别使用 SQLite、Kùzu 和向量索引承担不同职责。

接入层 编排层 记忆层 存储层 REST API MCP Server Python / TS SDK OpenClaw Plugin MemoryOrchestrator Short-term Episodic Semantic SQLite 事实源 SQLite 向量表 Kùzu 关系图 Embedding / Reranker

这套架构最重要的原则是:SQLite 保存真相,Kùzu 和向量索引保存可以重建的投影。

三种记忆解决不同问题

记忆类型保存内容主要用途
Short-term当前会话的消息窗口与摘要维持眼前对话连续性
Episodic用户和 Agent 实际发生过的消息、事件、工具结果回答“以前发生过什么”
Semantic从历史中提炼出的事实、偏好、分类和标签回答“当前已知事实是什么”

Episodic 和 Semantic 不是两份互不相关的数据。SemanticFeature 可以通过 citation 指回原始 Episode,因此系统既能快速返回“用户喜欢拉面”,也能解释这条事实来自哪次对话。

SQLite:保存原文和事务状态

默认情况下,memoLite 的主要数据都位于本机的 ~/.memolite/memolite.sqlite3。SQLite 保存:

  • project 和 session 的归属信息;
  • 原始 Episode 内容、角色、顺序号和元数据;
  • SemanticFeature、配置以及 citation;
  • 待处理的 semantic ingestion 状态;
  • Short-term summary;
  • Derivative 和 SemanticFeature 对应的向量 BLOB。

一次事件写入会先提交 SQLite,再生成后续投影。这样即使进程在 Kùzu 或向量写入过程中退出,原始 Episode 仍然存在,可以重新生成派生数据。

Embedding:把文字变成可以比较的数字

Embedding 不负责保存记忆,它只负责把文字编码成固定长度的数字数组。例如:

我最喜欢吃拉面
        ↓ embedding
[0.12, -0.08, 0.31, ...]

写入时为记忆生成向量,查询时为用户问题生成向量。二者使用同一个 Embedder,才可以计算余弦相似度。

memoLite 默认使用 64 维 HashEmbedder:不需要下载模型,速度快,适合本地启动和词语重合检索。需要更真实的语义能力时,可以安装 embeddings 扩展并切换到 Sentence Transformers;向量维度由具体模型在运行时返回。检索结果还可以交给本地 CrossEncoder 重新排序。

sqlite-vec 兼容层:向量仍然在 SQLite 文件里

这个项目没有额外部署 Milvus、Qdrant 或 Pinecone。两类向量分别放在普通 SQLite 表中:

CREATE TABLE derivative_feature_vectors (
    feature_id INTEGER PRIMARY KEY,
    embedding BLOB NOT NULL
);

CREATE TABLE semantic_feature_vectors (
    feature_id INTEGER PRIMARY KEY,
    embedding BLOB NOT NULL
);

Python 的 float 数组会被打包成二进制 BLOB。表结构本身不声明维度,因此更换 Embedding 模型后必须重建向量;查询向量和存量向量维度不一致时,两者无法比较。

项目把这一层封装成 SqliteVecIndex。按当前实现,它是 sqlite-vec 的兼容层:先用 SQL 读取允许参与搜索的向量,再由 Python 计算余弦相似度并选出 Top-K。也就是说,它具备本地向量存储和检索能力,但当前并没有使用独立向量数据库,也没有把近邻计算交给原生 sqlite-vec 虚拟表。

Kùzu:把命中片段带回原始事件

长消息不适合直接作为一个检索单元。DerivativePipeline 会按句子和换行把 Episode 切成更小的 Derivative:

Episode E1
├── Derivative D1
├── Derivative D2
└── Derivative D3

每个 Derivative 都会生成向量。Kùzu 保存 Episode、Derivative 以及下面这条关系:

(Derivative D1)-[:DERIVED_FROM]->(Episode E1)

向量搜索命中的是 D1,而用户真正需要的是完整事件 E1。系统先通过 Kùzu 找到 D1 的来源,再回到 SQLite 读取完整原文。Kùzu 在这里承担的是来源关系和回溯,不是向量计算,也不是原始内容的最终存储。

一条记忆如何写进去

以用户说“我最喜欢吃拉面”为例:

OpenClaw agent_end POST /memories SQLite 写 Episode 按句切 Derivative Embedding SQLite 写向量 Kùzu 写关系 加入 Semantic 队列 提取 favorite_food 写 Feature 与 Citation 写 Semantic 向量

最终得到三组相互关联的数据:

  • SQLite 中有完整 Episode 和结构化 SemanticFeature;
  • SQLite 向量表中有 Derivative 与 SemanticFeature 的 embedding;
  • Kùzu 中有 Derivative 到 Episode 的 DERIVED_FROM 关系。

下一轮如何把记忆找回来

当用户问“我喜欢吃什么”时,查询链路分为 Episodic 和 Semantic 两支。

Episodic 检索

  1. 将问题转换为 Query Embedding。
  2. 在 Kùzu 中按 session_id 找出允许搜索的 Derivative。
  3. 如果传入角色或事件类型,再通过 SQLite 缩小 Episode 范围。
  4. 将这些 Derivative 转成 allowed item IDs。
  5. 只对允许范围内的向量计算相似度。
  6. 通过 Kùzu 的 DERIVED_FROM 找回 Episode ID。
  7. 从 SQLite 读取完整 Episode,并按 sequence number 补充前后对话。
  8. 可选用 CrossEncoder 重排候选。

Semantic 检索

  1. 先在 SQLite 中按 set_id、category、tag 和 deleted 状态过滤事实。
  2. 得到允许参与搜索的 SemanticFeature IDs。
  3. 只在这些向量中计算相似度。
  4. 按 min_score 去掉低相关结果。
  5. 返回最多 limit 条 Feature。

因此范围限制发生在向量计算之前,而不只是搜索后再做过滤:

session_id / set_id / role / category
                ↓
        allowed_item_ids
                ↓
       cosine similarity
                ↓
       min_score + limit

Orchestrator 最后把 Short-term、Episodic 和 Semantic 结果合并。OpenClaw 插件在 before_agent_start 阶段把它们放进 <relevant-memories>,模型因此能在新一轮对话中使用过去的信息;成功运行结束后,agent_end 又会捕获新的消息,形成下一次召回的输入。

接入方式

接口用途
REST / FastAPI项目、会话、记忆、语义配置、健康检查与指标
MCP stdio / HTTP让 Codex、Claude 等 Agent 主动调用 add/search/list/delete memory
Python SDKPython 应用以类型化客户端接入 REST 服务
TypeScript ClientNode.js 和前端工具接入
OpenClaw Plugin提供自动捕获、自动召回和 memory_* 工具

本地启动后,可以直接执行一次混合搜索:

curl -X POST http://127.0.0.1:18731/memories/search \
  -H 'content-type: application/json' \
  -d '{
    "query": "我喜欢吃什么",
    "session_id": "session-a",
    "semantic_set_id": "user-1",
    "mode": "mixed",
    "limit": 5,
    "min_score": 0.5
  }'

删除、对账与重建

同时写 SQLite、Kùzu 和向量表意味着一次操作可能只完成一部分。memoLite 将 SQLite Episode 视为恢复锚点,并提供 reconcile、repair、export、import 和 rebuild 工具。

删除 Episode 时,系统先在 SQLite 中标记删除,再通过 Kùzu 找出相关 Derivative,删除对应向量和图节点,同时清理 Semantic citation;如果某个 SemanticFeature 已经没有其他来源,也可以继续清理。

修复时则反过来:读取 SQLite 中仍然有效的 Episode,重新切片、生成 embedding,并重建 Kùzu 节点、关系和 Derivative 向量。这种“主数据 + 可重建投影”的设计,比要求三个存储永远同时成功更适合本地服务。

框架的能力边界

  • 它是本地优先、单机运行的记忆基础设施,不是分布式记忆集群。
  • 默认 HashEmbedder 更偏轻量词语匹配,真实语义质量取决于选用的 Embedding 模型。
  • 当前向量层是 SQLite BLOB 加 Python 相似度计算,适合轻量数据集,不等同于大规模 ANN 向量数据库。
  • Kùzu 当前主要保存 Episode 与 Derivative 的来源关系,没有替代 SQLite 的事务数据。
  • MCP 提供记忆工具,但自动捕获和自动召回需要宿主生命周期钩子;OpenClaw 插件已经实现这层衔接。

总结

memoLite 的框架价值,不是简单地把 SQLite、Kùzu 和 Embedding 放在一起,而是明确了每一层的数据职责:

SQLite:保存事实和事务状态
Embedding:把文字转换为可比较向量
SQLite 向量表:保存向量并提供本地相似度检索
Kùzu:保存 Derivative 到 Episode 的来源关系
Orchestrator:连接写入、召回、删除和恢复
Agent 插件:在正确的生命周期自动捕获与注入

当一条消息能够被保存、索引、召回、追溯、删除并在故障后重建,本地记忆才真正形成闭环。

源码索引

  • MemoryOrchestrator
  • DerivativePipeline
  • EpisodicSearchService
  • SqliteVecIndex
  • KuzuGraphStore
  • OpenClaw Plugin
  • Migration and Repair