乐于分享
好东西不私藏

OpenClaw 记忆系统:怎么让 Agent 搜到你的笔记

OpenClaw 记忆系统:怎么让 Agent 搜到你的笔记

OpenClaw 记忆系统的做法是给 Agent 配一套文件级的记忆体系。下面这张图是完整的搜索流程,后文会把这个流程讲清楚。

记忆从哪来

Agent 每次发起会话是空的。但系统会注册三类文件作为它的记忆来源(源码 packages/memory-host-sdk/src/host/backend-config.ts)。

第一类是 MEMORY.md,workspace 根目录下的长期记忆文件。它在每次会话启动时直接注入上下文窗口,Agent 不用搜就能查到里面的内容。真实内容长这样:

# MEMORY.md - DocPilot Agent 长期记忆

## 项目概况
- DocPilot 是文档智能体产品(全链路文档生成与诊断)
- 源码路径:xxx/文档智能体架构研究/
- 架构:Agent + Operator 双层,沙箱执行环境

## Promoted From Short-Term Memory (2026-07-13)
- 根因确认:线上 Pod 报 COOKIE_EXPIRED 不是真的过期;
  问题在 inject-cookies.cjs 的 CDP Network.setCookies 注入方式有 bug
- v3 发现 SameSite=None + secure=false 被 Chromium 拒绝:
  18/21 个 Cookie 被丢弃;修复:secure Cookie → None,non-secure → Lax
- CronJob 频率改为每 4 小时:从 0 3,11,19 * * * → 0 */4 * * *

这些内容不是人自己手写的。是 Agent 自己在之前的工作中觉得"这个结论以后还会用到",主动写进去的(和Dream机制有关,后续文章会分享)。

第二类是 memory/ 目录下按日期组织的工作日记:

# 2026-07-09 Memory

## Cookie 注入问题定位完成

### 根因确认
- 线上 Pod 报 COOKIE_EXPIRED 不是真的过期
- ConfigMap baidu-cookies(21 个 Cookie)通过 curl 测试返回 200
- CronJob refresh-bce-cookie 每 8 小时正常刷新
- 问题在 inject-cookies.cjs 的 CDP Network.setCookies 注入方式有 bug:
  - 缺少 url 字段
  - sameSite 默认值不对
  - 验证等待时间 6s 不够
  - 判断条件过宽

Agent 每次工作结束时自动把重要结论写入当天的日记。这些日记不会在启动时加载(太多了),但会被建立索引,需要时通过搜索召回。

第三类是通过 extraPaths 配置纳入的外部目录:

memorySearch:
  enabled: true
  extraPaths:
    - /Users/dapeng/资料/docpilot_debug/docpilot_debug   # Obsidian 调试笔记
    - /Users/dapeng/学习资料/文档智能体架构研究/xxx       # DocPilot 源码仓库

默认索引模式是 **/*.md(源码 resolveCustomPaths 函数:let pattern = entry.pattern?.trim() || "**/*.md")。.ts.go.json 等源码文件不进入索引——配了源码仓库的 extraPath,实际索引的是仓库里的 README 和文档。这个 pattern 可以自定义(改成 **/*.ts),但分块策略是按 Markdown 行结构设计的,对代码文件效果不佳。

光纳入索引解决的是"能搜到"。Agent 搜到一个文件后还需要理解上下文——0709/ 是什么?dot/ 目录能不能改?这通过 TOOLS.md 告诉它:

### Obsidian
- Vault: docpilot_debug(路径 /Users/dapeng/资料/docpilot_debug/docpilot_debug)
- 结构:MMDD 日期目录(0304~),每个目录下含:
  - dot/ — Graphviz .dot 源文件
  - images/ — 渲染后的 .png 流程图
  - *.md — 工作笔记(Bug 分析、架构设计等)
- 命名规范:子目录用 {主题}-{动作} 格式

### 代码仓库
- 路径:/Users/dapeng/学习资料/文档智能体架构研究/xxx
- 用途:DocPilot 源码(Agent、Operator、前端、后端)
- 技术栈:TypeScript/Node.js 为主

有了这层描述,Agent 搜到 0429/code-johari-window-deploy-env.md 就知道这是 4 月 29 日的部署环境记录,而不是一个意义不明的路径。

文件怎么变成可搜索的

三类文件注册完毕后,所有 .md 文件都会经过分块、向量化、写入存储这个流程。

分块

源码 packages/memory-host-sdk/src/host/internal.ts:387中的chunkMarkdown` 函数,一个长文件不能整体做 embedding,因为文本太长时向量会丢失细节。所以逐行读取,按字符权重累加,到 1600 字符(= 400 token × 4)时 flush 一块,同时保留末尾 320 字符(= 80 token × 4)作为下一块开头。

CJK 字符每个计为 4(源码 src/utils/cjk-chars.ts,常量 CHARS_PER_TOKEN_ESTIMATE = 4)——因为 1 个汉字约等于 1 个 token,而 4 个拉丁字母才约等于 1 个 token。不做加权的话,400 个汉字和 400 个英文字母会被当作同样大的块,但前者实际消耗的 token 是后者的 4 倍。加权后不管中英文,每块稳定在约 400 token。

核心逻辑伪代码:

maxChars = 400 × 4 = 1600
overlapChars = 80 × 4 = 320

逐行读取:
  累加 estimateStringChars(line)  // 拉丁 1:1,CJK 1:4
  超过 maxChars → flush 当前块 {startLine, endLine, text, hash}
  保留末尾 overlapChars → 下一块开头

单行超长:按 maxChars 切片;CJK 仍超则再按 400 字符细切

每个 chunk 产出 {startLine, endLine, text, hash}——记住它来自原文件的哪几行,后面召回时要用。

向量化

每个分块独立调用 OpenAI text-embedding-3-small(1536 维),生成一条 embedding,源码中常量 DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai"memory-search.ts:133。embedding 是一组浮点数,表示这段文本在语义空间中的位置。

存储

存储引擎是本地 SQLite,通过加载 sqlite-vec 扩展获得向量搜索能力(sqlite-vec 是 SQLite 的 loadable extension,源码 packages/memory-host-sdk/src/host/sqlite-vec.tssqliteVec.load(params.db))。每个 chunk 写入同一个 .sqlite 文件的三张表,用同一个 id 关联(源码 extensions/memory-core/src/memory/manager-embedding-ops.ts:753-798):

memory_index_chunks 是主表,存 chunk 原文、embedding JSON、元数据(文件路径、起止行号、hash、模型名、时间戳),带 UPSERT 去重。memory_index_chunks_vec 是 sqlite-vec 虚拟表,存 embedding 二进制 blob,用于向量近似最近邻搜索。memory_index_chunks_fts 是 FTS5 全文索引表,存 chunk 纯文本,用于关键词检索。

一个 chunk 存三份——原文一份、向量一份、纯文本一份。三张表各自优化各自的查询路径,搜索时分别查向量表和 FTS 表,再合并结果。

Agent 怎么想起来

当 Agent 工作中需要回忆——比如用户问"上次 Cookie 问题怎么解决的"——它调用 memory_search(query)

系统同时走两条路。向量搜索对 memory_index_chunks_vec 做余弦相似度计算,权重 0.7。FTS 全文检索对 memory_index_chunks_fts 做 BM25 关键词匹配,权重 0.3。两路结果加权合并:总分 = 向量分 × 0.7 + FTS 分 × 0.3,低于 0.35 的丢弃,最多保留 6 条(源码常量 DEFAULT_MAX_RESULTS = 6DEFAULT_MIN_SCORE = 0.35memory-search.ts:122-123)。

为什么不纯用向量?语义搜索对专有名词(COOKIE_EXPIREDinject-cookies.cjs)不够精确,FTS 能补上这块。

返回的是 chunk 片段,不是全文。每条结果包含 {path, startLine, endLine, snippet, score},snippet 从主表 text 字段截取,源码manager-search.ts:224snippet: truncateUtf16Safe(row.text, params.snippetMaxChars))。Agent 拿到片段后判断信息够不够用——不够就按行号回读磁盘原文件:memory_get(path="memory/2026-07-09.md", from=15, lines=30)

相邻 chunk 之间 80 token 的重叠保证了边界信息不丢失,大多数情况下单条 chunk 自己就有足够上下文。需要更多时再回读。SQLite 里是检索副本,磁盘上的 .md 文件才是 source of truth。

记忆怎么流动

每次开新 session,MEMORY.md 直接注入上下文。工作中需要回忆旧事,Agent 调 memory_search 在所有集合中搜索。工作结束,Agent 把重要结论写入当天日记。定期提炼——系统把日记中反复被召回的高分内容 promote 到 MEMORY.md,下次启动直接可用。

日记是每天的摘要,MEMORY.md 是从每天摘要里沉淀。前者是需要工具才能搜到,后者每次启动直接加载。