--- title: "拆解 OpenClaw 架构(四):70% 向量 + 30% 关键词,一套生产级记忆检索引擎" source: wechat url: https://mp.weixin.qq.com/s/t4OzcK0zHh3Toxs5V6EQQg ingest_date: 2026-07-04 vxc: 64 stars: 4 sha256: acd03bf03f2f9d94764af336a1144e79b8d1b918bfb4b1d7751799e9bc7a344d --- # 拆解 OpenClaw 架构(四):70% 向量 + 30% 关键词,一套生产级记忆检索引擎 **来源**: 科技充电站 **发布日期**: 2026-03-01 **原文链接**: https://mp.weixin.qq.com/s/t4OzcK0zHh3Toxs5V6EQQg --- AI 时代,有两种行为: 一种,活在别人的评测里,把模型的强当自己的强,痴人说梦; 另一种,活在真实的实战里,用最顶级的 AI,武装自己。 前者在噪音里坐享"技术平权",后者在 疼痛中完成"自我进化"。 朋友们好,我是行小招。 这是 OpenClaw 深度技术解析系列的第四篇。前三篇我们拆了消息流水线、人格系统和 Agent Runner,今天聊 Memory,也就是 OpenClaw 的记忆系统。 上篇结尾我提到,OpenClaw 的记忆不是传统 RAG,而是一套完整的 IR 工程。写这篇之前我其实有点犹豫,因为"记忆"这个词太容易让人想到向量数据库、Embedding、语义检索这套标准叙事。 但读完 OpenClaw 的记忆模块源码后,我发现这套系统最有意思的地方根本不在检索算法,而在一个更底层的设计哲学。 先说结论, OpenClaw 的记忆系统不用数据库做主存储 。 所有记忆都是 Markdown 文件,明文存放在 Agent 的工作区目录里。 SQLite?只是一个派生的加速层。你把 SQLite 文件删了,系统会从 Markdown 文件自动重建索引。数据不会丢一个字。 这个设计选择在数据库出身的工程师看来简直是"开历史倒车",但在 AI Agent 的场景下,它有自己的道理。 传统 RAG 把数据藏在不透明的向量数据库里。你知道数据"在里面",但想看看具体存了什么?要写查询;想修改一条记录?要写代码;想追踪某条记忆是什么时候、因为什么事件被存进来的?祝你好运。 OpenClaw 的 Markdown 文件呢?人类可读, cat 一下就能看;Git 可版本化,谁改了什么一目了然; grep 可搜索,不用等索引构建;任何编辑器可编辑,vim、VS Code、甚至 iPhone 备忘录都行。 官方把这叫 "Memory as Documentation" ,跟传统的 "Memory as Database" 形成鲜明对比。 我觉得这个选择背后的逻辑是: 对于 AI Agent 的记忆,可审计性比查询性能重要得多 。 你想想,一个 Agent 替你管日程、写文档、回消息,时间长了它积累的"记忆"里会有你的偏好、你的决策模式、你跟谁聊了什么。这些信息如果藏在一个二进制的向量数据库里,你连看都看不到,更别说审计和修改。但如果它就是一堆 Markdown 文件,放在你能直接访问的目录下,安全感就完全不同了。 Milvus/Zilliz 团队后来把这套记忆架构提取成了一个独立库叫 memsearch ,让任何 Agent 都能用 OpenClaw 风格的记忆系统。一个向量数据库公司主动把"不以向量数据库为中心"的架构做成通用库推广,这本身就说明这个设计思路得到了业内认可。 ## 工作区文件层级:8 个文件各司其职 OpenClaw 的记忆不是一个单一的"记忆库",而是一套层级分明的文件体系。每个文件有明确的职责和加载规则: 文件 用途 加载时机 SOUL.md Agent 人格、语气、价值观、边界 每次会话 AGENTS.md 操作指令、启动序列 每次会话 USER.md 用户档案:名字、时区、偏好 每次会话 MEMORY.md 精选的长期记忆,约 100 行 仅私聊,群组不加载 memory/YYYY-MM-DD.md 每日日志,只追加不修改 启动时加载今天+昨天 IDENTITY.md 显示名称、主题、emoji 每次会话 TOOLS.md 本地环境备注 每次会话 HEARTBEAT.md 定期检查计划 仅心跳运行时 这里有个细节容易被忽略: MEMORY.md 在群聊中不会被加载。 这不是 bug,是一个刻意的安全决策。MEMORY.md 里存的是长期积累的用户敏感信息,比如你的工作习惯、常用联系人、个人偏好,如果 Agent 被拉进一个群聊,这些信息不应该出现在群聊的上下文里,群里的其他用户(或者通过群聊注入的 prompt attack)不应该有途径触及你的私人记忆。 每日日志 memory/YYYY-MM-DD.md 的设计也很讲究:只追加、不修改,这意味着记忆的时间线是完整的,你可以精确地知道 Agent 在哪一天"学到"了什么。启动时只加载今天和昨天的日志,既保证了近期上下文的连贯性,又不会把几个月的日志全塞进上下文窗口。 把这 8 个文件的 token 开销加起来,每次会话启动大约要消耗 4000 到 10000 tokens 的"阅读作业"。这是"记得住"要付的代价。以 Claude Opus 4.6 的定价来算,每次会话启动光是"回忆"这一步就要花几美分。如果你的 Agent 一天被唤醒几十次,一个月下来这笔"记忆税"并不便宜。 但跟"Agent 忘记了关键信息"导致的错误决策相比,这点 token 开销完全值得。 ## 两个记忆工具:搜索与读取 Agent 跟记忆系统的交互只通过两个工具。 memory_search 做语义搜索。它把 Markdown 文件切成约 400 token 的块,块之间有 80 token 的重叠(避免关键信息正好被切断),返回结果包括片段文本(上限 700 字符)、文件路径、行范围和相关性分数。 这个工具的描述里有一条强制性要求,我觉得特别值得关注: "在回答任何关于之前的工作、决策、日期、人物、偏好或待办事项的问题之前,必须先搜索记忆。" 注意措辞,"必须先搜索",不是"建议搜索"。这意味着 OpenClaw 从工具层面就约束了模型的行为,你问 Agent "上次我们讨论的那个方案进展怎么样了",它不能靠"幻觉"编一个答案,必须先去记忆里查。 memory_get 做精确读取,给定文件路径和可选的行范围,返回原始内容。一个小但精妙的设计:当文件不存在时,它不会抛出 ENOENT 错误,而是返回 { text: "", path } 。 为什么这么做? 因为"还没有记录"跟"出错了"是两种完全不同的状态,如果文件不存在就报错,Agent 还得加一套错误处理逻辑来区分"真的出错了"和"只是还没记过这件事"。返回空文本,Agent 自然就知道"哦,还没有相关记忆",然后该怎么回答怎么回答。 这种优雅降级的思路在整个 OpenClaw 的设计中反复出现,前面几篇我们已经见过很多次了。 ## 混合搜索:一个做过搜索系统的人一看就懂的设计 记忆系统的检索引擎是这篇文章的重头戏。代码在 src/memory/hybrid.ts ,实现了一套经典的混合检索方案。 核心公式: 70% 的权重给向量语义相似度,30% 给 BM25 关键词相关性(基于 SQLite FTS5)。 做过搜索系统的人一看就知道为什么需要混合:纯向量搜索"语义好但精确差",你搜"张三的生日",向量搜索可能返回"李四的纪念日",因为语义空间里这两个概念很近;纯关键词搜索"精确好但语义差":你搜"项目进展",关键词搜索找不到"方案推进情况",因为没有词重叠。 混合检索是两全其美的方案,但魔鬼在细节里。OpenClaw 的实现有几个值得细看的点。 用 union 不是 intersection, 这是一个关键决策,union 意味着向量搜索找到的结果和关键词搜索找到的结果都会进入最终排名,哪怕某条记忆只被一种搜索方式命中,它依然有机会出现在结果里。如果用 intersection,就只有两种搜索都命中的结果才能进入最终排名,召回率会大幅下降。 对于记忆搜索这个场景,漏掉一条相关记忆的代价远大于多返回几条不太相关的结果。所以 union 是正确的选择。 BM25 rank 转 score 的公式:, 排名第 0 的结果得分 1.0,排名第 9 的得分 0.1。这个转换把排名位置变成了一个 0 到 1 之间的分数,让它能跟向量相似度(也是 0 到 1)直接加权融合。 candidateMultiplier: 4,融合前获取 4 倍候选, 也就是说,如果最终要返回 10 条结果,向量搜索和关键词搜索各自先拿 40 条候选,然后在 80 条候选里做融合排序。多拿候选是为了让融合阶段有足够的"选材"空间。 为什么选加权求和而非 RRF(Reciprocal Rank Fusion)? 这是一个信息检索领域的经典争论。RRF 的问题在于它会抹平分数量级差异:cosine 相似度 0.98 和 0.71 的结果,到了 RRF 里只是排名第 1 和排名第 5 的区别,0.98 那个"几乎完全匹配"的信号被严重稀释了。 加权求和保留了原始分数的量级信息,而且权重是显式可配的。你觉得语义搜索对你的 Agent 更重要?把向量权重调到 0.8。你的使用场景里精确匹配更关键?把 BM25 权重调高。RRF 做不到这种灵活性,因为它把所有排名器视为平等。 这整套搜索管线,从分块策略到混合融合到权重配置,放在任何一个正经的搜索系统里都是及格水平以上的设计。不是随便拼凑的 demo,是经过思考的 IR 工程。 ## 嵌入 Provider 自动选择:6 级降级链 记忆系统要做向量搜索,首先得有嵌入向量。OpenClaw 在这里又做了一套降级链,代码在 src/memory/embeddings.ts:135-167 : Local → OpenAI → Gemini → Voyage → Mistral → BM25-only fallback 优先用本地模型。 node-llama-cpp 首次使用时会自动下载一个 GGUF 格式的嵌入模型( embeddinggemma-300m-qat-q8_0 ),之后就不需要网络了。本地推理没有 API 成本,延迟也低。 本地不可用的话,依次尝试 OpenAI 的 text-embedding-3-small 、Google 的 gemini-embedding-001 ,然后是 Voyage 和 Mistral。 如果所有嵌入 provider 都不可用呢?系统降级到纯 BM25 关键词搜索,语义能力没了,但至少搜索功能还在。记忆不会彻底失效。 这种"怎么都能跑"的设计跟 Agent Runner 的 6 级压缩级联如出一辙。OpenClaw 的 Peter 对"永远不要完全崩溃"这件事有一种执念。 还有两个细节值得提一下。OpenAI 的 Batch API 集成让批量索引的成本降低了约 50%。SHA-256 哈希去重防止对未修改的文本块重复做嵌入计算,文件没改过就不重新向量化,节省 API 调用。 ## 后处理:时间衰减和 MMR 去重 混合搜索返回结果后,还有两个可选的后处理步骤。 时间衰减。 公式: decayedScore = score × e^(-λ × ageInDays) ,30 天半衰期。 换成人话:今天的内容分数保持 100%,7 天前的内容打 84 折,30 天前的打 50 折,90 天前的只剩 12.5%。 但这里有个关键例外: 长期类文件不衰减。 MEMORY.md 和非日期命名的笔记文件,不管存了多久都保持原始分数。这很合理,"我喜欢用 vim" 这种偏好不会因为记了 90 天就变得不重要。 默认情况下时间衰减是关闭的,你得手动开启。我猜这是因为衰减的效果高度依赖使用场景,对于日常助理型 Agent 可能很有用(最近的对话更重要),但对于知识库型 Agent 可能会适得其反(一年前的技术文档可能比昨天的闲聊更有价值)。 MMR(Maximal Marginal Relevance)去重。 用 Jaccard 文本相似度消除近重复片段,默认 lambda 0.7。 举个例子,如果你在 3 月 1 号和 3 月 5 号各记了一次"项目 X 的进度更新",内容高度相似,MMR 会只保留一条,避免搜索结果里塞满重复信息。lambda 0.7 意味着 70% 权重给相关性、30% 给多样性。 这两个后处理步骤默认都关着,但它们的存在说明 OpenClaw 团队在记忆检索的质量上想得很远。不是简单地"搜到就行",而是考虑了时间维度和结果多样性。 ## 压缩前记忆冲刷:上篇的伏笔在这里展开 上一篇讲 Agent Runner 时,我花了不少篇幅聊压缩级联和"临终遗言"机制。那时候是从运行引擎的角度讲的,这里从记忆系统的角度补充几个细节。 当 session 接近上下文限制, softThresholdTokens: 4000 被触发时,OpenClaw 在真正执行压缩之前,偷偷插入一个静默 turn。系统给模型发一条隐藏指令:"Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store." 流式传输被抑制,用户完全看不到这个过程。模型把对话中的关键信息写到当天的日志文件里,然后压缩才正式开始。 这个机制可以通过 agents.defaults.compaction.memoryFlush 配置开关。如果工作区是只读的(比如某些沙箱环境),记忆冲刷会自动跳过,因为写不了文件。 从记忆系统的角度看,这个设计的意义在于: 它在压缩的有损操作和记忆的持久化之间建了一座桥 。 压缩必然丢信息,但至少关键信息在丢失之前已经被"固化"到了文件系统里。下次会话启动时,这些信息会通过 memory_search 被重新召回。 信息的生命周期变成了:对话上下文 → 压缩前冲刷到文件 → 文件被索引 → 下次搜索时召回 → 重新进入上下文。一个闭环。 ## SQLite 存储:派生层的 5 张表 虽然文件是真相源,但搜索性能需要索引。每个 Agent 有一个独立的 SQLite 数据库,路径在 ~/.openclaw/memory/{agentId}.sqlite ,五张表: files 表 记录文件路径、内容哈希和时间戳。哈希用来判断文件有没有改过,没改就不重新索引。 chunks 表 存切块结果:块 ID、源文件路径、起止行号、内容哈希、嵌入模型名、原文、嵌入向量。 embedding_cache 表 做跨文件的向量去重。同样的文本内容出现在不同文件里,只计算一次嵌入。 chunks_fts 是 SQLite FTS5 虚拟表,给 BM25 关键词搜索用的。 vec_chunks 是 sqlite-vec 虚拟表,给向量搜索做加速。sqlite-vec 可用时走原生向量索引,不可用时回退到 JavaScript 实现的暴力搜索。 几个关键常量: SNIPPET_MAX_CHARS = 700 (返回片段最大长度)、 EMBEDDING_BATCH_MAX_TOKENS = 8000 (批量嵌入的 token 上限)、 EMBEDDING_INDEX_CONCURRENCY = 4 (并行索引的并发数)、 VECTOR_LOAD_TIMEOUT_MS = 30,000 (向量加载超时 30 秒)。 这些常量看起来平淡无奇,但每一个背后都是工程权衡:SNIPPET_MAX_CHARS = 700 是在"返回足够上下文让模型理解"和"不要一次性塞太多文本到 tool result"之间找的平衡点,EMBEDDING_INDEX_CONCURRENCY = 4 是在"索引速度"和"不要把用户的 CPU 打满"之间做的妥协。 ## QMD 后端:一个有趣的实验方向 除了默认的 SQLite 后端,OpenClaw 还有一个实验性的替代方案叫 QMD,通过 memory.backend = "qmd" 启用。 QMD 是一个本地 sidecar 进程,用 Bun + node-llama-cpp 运行,实现 BM25 + 向量搜索 + 重排序。GGUF 模型自动下载,完全本地运行,不需要任何外部 API。 如果 QMD 启动失败,系统自动回退到内置的 SQLite 管理器。又是一层降级保护。 QMD 还支持一个有意思的功能:按聊天类型限制记忆访问的作用域规则。比如你可以配置"在 Discord 频道中阻止记忆搜索,但在私信中允许"。这比 MEMORY.md 的"群聊不加载"更细粒度,可以按具体的聊天渠道做访问控制。 ## 插件架构:薄薄一层适配器 memory-core 插件的代码量很小,但很有教育意义。 它做的事情只有两件:注册 memory_search 和 memory_get 两个工具(调用 api.runtime.tools.createMemorySearchTool() 和 createMemoryGetTool() ),然后安装一个 memory CLI 命令。 插件本身只是一个适配器,所有的重活(索引、搜索、融合、后处理)都在运行时工具实现中完成。这种"插件只做胶水"的设计让记忆系统的核心逻辑跟插件框架解耦,改插件不影响搜索质量,改搜索算法不需要动插件接口。 不过社区反馈里有一些围绕插件的可用性问题:在容器化部署场景中,memory-core 插件的持久化和可用性出过故障,有人报告更新后记忆数据丢失,有人发现配置校验在 memory-core 不在构建产物中时会直接失败。 这些问题不是设计上的缺陷,更多是"插件是一个可插拔的 slot,发布时确保 slot 里有东西"这个运维环节的疏忽。但它们提醒我们: 越是解耦的架构,越需要严格的集成测试 。 ## 一些个人观察 写到这里,分享几个更深层的思考。 关于 "Memory as Documentation" 的 tradeoff, 用 Markdown 文件做记忆存储,可读性、可审计性、可版本控制这些优势是真实的。 但代价也是真实的:没有 at-rest 加密(明文文件在 ~/.openclaw/memory/ 这个可预测路径下),没有跨文件关系映射(文件 A 提到的"项目 X"和文件 B 提到的"项目 X"之间没有显式关联),没有事务保证(中途断电可能导致写入不完整)。 任何有文件系统访问权限的程序都能读到 Agent 的全部记忆。在本地使用的场景下这不是大问题,但如果 Agent 部署在共享服务器上,这就是一个严重的安全隐患。这也呼应了我们后面要聊的安全主题。 关于混合搜索的 IR 工程, 作为一个写过搜索系统的人,看到 OpenClaw 的混合检索设计时有一种"同行相认"的感觉。union not intersection、加权融合而非 RRF、时间衰减、MMR 去重,每一个都是信息检索领域的经典技术,放在这里不是炫技,而是每一个都解决了一个具体的问题。 这套设计不像是一个 AI Agent 框架的工程师随手写的,更像是有 IR 背景的人参与了设计。后来 Milvus/Zilliz 团队把它提取成 memsearch,也印证了这个判断。 关于 4000-10000 tokens 的"阅读作业", 每次会话启动都要花这么多 token 让 Agent "回忆",这笔开销在模型便宜的时候不算什么,但在成本敏感的部署场景下是一个实打实的痛点。 更值得思考的问题是:当 Agent 积累了几年的记忆,几百个 memory/YYYY-MM-DD.md 文件,几十万行 Markdown,索引的规模会膨胀到什么程度?SQLite 能不能 hold 住?搜索延迟会不会飙升? 目前来看,对于个人助手级别的使用量,这套架构完全够用。但如果有人想用 OpenClaw 做企业级的 Agent(上百个 Agent 同时运行、每个 Agent 几年的记忆积累),文件系统 + SQLite 的方案可能需要重新审视。 关于 memsearch 的信号意义, Milvus/Zilliz 是做向量数据库的。他们的核心产品就是帮人把数据存进向量库、做相似度搜索。但他们看完 OpenClaw 的记忆架构后,不是说"你们应该用 Milvus 来做这个",而是把这套"文件优先、数据库只是加速层"的架构提取成了独立库。 这说明两件事:一是这套设计确实有普适性,不只是 OpenClaw 特定场景下的权宜之计;二是即使是向量数据库公司,也认可"不是所有场景都需要把数据扔进向量库"这个观点。 下一篇,我们聊工具链与 Shell 安全。OpenClaw 的核心工具原语只有四个:Read、Write、Edit、Bash。用整个 Unix 生态作为工具层,shell 命令的成本是 LLM 推理链的千分之一。但这也意味着 Agent 手里握着一把能打开几乎所有门的钥匙,安全边界怎么划? 你的系统里是用数据库还是文件系统做持久化的?在什么场景下你会优先选择可读性而非查询性能?欢迎留言聊聊。 我是行小招,持续探索 AI 在个人生活和企业落地中的应用场景,欢迎一起聊聊,也欢迎你把这篇文章分享给身边做技术、做产品的朋友。 当 90% 的内容都在沦为噪音,真正稀缺的是:深度阅读,独立思考,持续实战。 交给 AI 的是任务,留给自己的是思考。 脑子不停转,持续定义问题,决定解决什么问题,这才是 AI 时代的底层逻辑。 👇 长按关注,一起用 AI 武装自己