Agent 设计最佳实践:Memory

从每日日志到向量检索,深入解析 OpenClaw 如何让 AI 真正「记住」你

目录:

想象这样一个场景:你花了半小时向 AI 助手解释你的项目架构、编码偏好和团队规范,得到了一次满意的协作体验。第二天再打开对话——它全忘了。你又得从头来一遍。这不是 AI 不够聪明的问题,而是记忆架构缺失的问题。OpenClaw 的 Memory 系统试图从根本上解决这个痛点:让 AI Agent 拥有持久、可检索、可自维护的记忆能力。

为什么 Agent 记忆这么难

LLM 的上下文窗口是有限的。即便是 200K Token 的窗口,也装不下你过去三个月的所有对话。更关键的是,上下文窗口是易失的——会话结束,一切归零。

传统的解决方案有几种,各有问题:

方案问题
把所有历史塞进 System PromptToken 爆炸,成本高,模型注意力稀释
外部数据库 + RAG架构复杂,需要额外的向量数据库服务
让模型"总结"历史信息损失不可控,关键细节可能丢失
依赖平台的 Memory 功能不透明、不可控、不可迁移

OpenClaw 的设计哲学是:记忆应该是纯文本文件,透明可审计,AI 可自维护,同时支持语义检索

两层记忆架构

OpenClaw 的记忆系统分为两层,对应人类记忆的"短期"和"长期":

workspace/
├── MEMORY.md              ← 长期记忆(精炼后的重要信息)
├── memory/
│   ├── 2026-02-26.md      ← 每日日志(原始记录)
│   ├── 2026-02-27.md
│   ├── 2026-02-28.md
│   └── heartbeat-state.json

每日日志:memory/YYYY-MM-DD.md

每日日志是最忠实的原始记录,采用仅追加(append-only)模式。Agent 在对话过程中把值得记录的信息写入当天的日志文件——决策、偏好、任务进展、关键事实,什么都可以写。

会话启动时,Agent 加载今天和昨天的日志。这个设计很巧妙:既保证了近期上下文的连续性,又不会因为历史太长撑爆上下文窗口。

长期记忆:MEMORY.md

MEMORY.md 是经过提炼的精华。它不是原始日志的堆叠,而是 Agent 定期从每日日志中抽取的持久性事实——用户偏好、项目架构、重要决策、反复出现的模式。

一个关键的安全设计:MEMORY.md 只在主要的私人会话中加载,绝不在群组上下文中加载。这防止了 Agent 在群聊中意外泄露你的个人信息。

何时写入记忆

写入规则很简洁:

  • 决策、偏好、持久性事实 → 写入 MEMORY.md
  • 日常笔记、运行上下文 → 写入 memory/YYYY-MM-DD.md
  • 用户说"记住这个" → 立刻写入文件(不要只保存在上下文中)

这个设计让记忆的写入变得直觉化:如果你觉得某件事值得长期保留,就告诉 Agent"记住这个"。

自动记忆刷新:压缩前的抢救

上下文窗口不可能无限增长。当会话接近自动压缩(compaction)阈值时,OpenClaw 会触发一个静默的 Agent Turn,提醒模型在上下文被压缩之前把重要信息写入持久记忆。

{
  "agents": {
    "defaults": {
      "compaction": {
        "reserveTokensFloor": 20000,
        "memoryFlush": {
          "enabled": true,
          "softThresholdTokens": 4000,
          "systemPrompt": "Session nearing compaction. Store durable memories now.",
          "prompt": "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store."
        }
      }
    }
  }
}
// generated by hugo's coding agent

这个机制的几个设计细节值得注意:

  • 软阈值触发:当 Token 估计超过 contextWindow - reserveTokensFloor - softThresholdTokens 时才触发,不是每次都刷新
  • 默认静默:Prompt 中包含 NO_REPLY,如果没有需要保存的内容,用户完全无感知
  • 每个压缩周期只刷新一次:避免重复写入
  • 只读工作空间跳过:如果会话在沙箱中以只读模式运行,跳过刷新

这就像人类在即将入睡前,把一天中最重要的事情记到笔记本上——不是事无巨细地记录,而是抢救性地保留关键信息。

向量记忆搜索:让记忆可检索

纯文本文件解决了记忆的存储问题,但没解决检索问题。当 MEMORY.md 和几十个每日日志积累到一定量级,Agent 不可能每次都全文阅读。这就需要语义搜索。

OpenClaw 在记忆文件上构建了一个小型向量索引,支持语义查询——即使措辞不同,也能找到相关笔记。

默认行为

  • 默认启用,监视记忆文件变更(去抖动 1.5 秒)
  • 索引范围:MEMORY.md + memory/**/*.md + 可选的 extraPaths
  • 存储:每个 Agent 的 SQLite 文件(~/.openclaw/memory/<agentId>.sqlite
  • 支持多种嵌入提供商:OpenAI、Gemini、本地模型

嵌入提供商选择

OpenClaw 的嵌入提供商选择逻辑很务实:

配置了 local.modelPath 且文件存在? → 使用本地嵌入
可以解析 OpenAI 密钥?              → 使用 OpenAI
可以解析 Gemini 密钥?              → 使用 Gemini
都没有?                            → 记忆搜索禁用

本地模式使用 node-llama-cpp,默认模型是 embeddinggemma-300M-Q8_0.gguf(约 0.6 GB),首次使用时自动下载。对于不想依赖远程 API 的用户,这是一个完全自主的选项。

配置示例——使用 OpenAI 嵌入:

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "provider": "openai",
        "model": "text-embedding-3-small",
        "remote": {
          "baseUrl": "https://api.example.com/v1/",
          "apiKey": "YOUR_API_KEY"
        }
      }
    }
  }
}
// generated by hugo's coding agent

记忆工具

Agent 通过两个工具与记忆系统交互:

  • memory_search — 语义搜索 Markdown 块(目标约 400 Token,80 Token 重叠),返回片段文本、文件路径、行范围和相关性分数
  • memory_get — 按路径读取特定记忆文件内容,可指定起始行和行数

这两个工具只在 memorySearch.enabledtrue 时启用。

混合搜索:BM25 + 向量

纯向量搜索擅长语义匹配(“Mac Studio gateway host” vs “运行 gateway 的机器”),但在精确 Token 上较弱(ID、代码符号、错误字符串)。纯关键词搜索(BM25)正好相反。OpenClaw 选择了务实的中间路线:混合搜索

┌─────────────────────────────────────────────────┐
│               memory_search 查询                  │
└──────────────────────┬──────────────────────────┘
           ┌───────────┴───────────┐
           ▼                       ▼
    ┌──────────────┐       ┌──────────────┐
    │  向量相似度    │       │  BM25 关键词   │
    │ (语义匹配)   │       │  (精确匹配)   │
    └──────┬───────┘       └──────┬───────┘
           │                      │
           │   candidateMultiplier = 4
           │   各取 maxResults × 4 个候选
           │                      │
           └──────────┬───────────┘
            ┌──────────────────┐
            │   加权分数合并     │
            │ final = 0.7×vec  │
            │       + 0.3×text │
            └────────┬─────────┘
              Top K 结果返回

工作流程:

  1. 向量检索:按余弦相似度取前 maxResults × candidateMultiplier 个候选
  2. BM25 检索:按 FTS5 排名取同等数量的候选
  3. 分数转换:将 BM25 排名转换为 0-1 分数:textScore = 1 / (1 + max(0, bm25Rank))
  4. 加权合并finalScore = vectorWeight × vectorScore + textWeight × textScore

默认权重是向量 0.7、文本 0.3,可配置:

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "query": {
          "hybrid": {
            "enabled": true,
            "vectorWeight": 0.7,
            "textWeight": 0.3,
            "candidateMultiplier": 4
          }
        }
      }
    }
  }
}
// generated by hugo's coding agent

如果 sqlite-vec 扩展不可用,自动回退到纯向量搜索;如果嵌入提供商不可用,仍然运行 BM25 返回关键词匹配。不会硬失败——这是工程上的务实选择。

嵌入缓存与性能优化

频繁更新记忆文件(尤其是每日日志)会触发大量嵌入计算。OpenClaw 在 SQLite 中缓存了块嵌入——只要文本块没变,就不会重新嵌入。

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "cache": {
          "enabled": true,
          "maxEntries": 50000
        }
      }
    }
  }
}
// generated by hugo's coding agent

此外,OpenAI 和 Gemini 的嵌入支持批量模式——把大量嵌入请求打包成一个批处理作业异步执行,不仅更快,OpenAI 的 Batch API 还提供折扣定价。对于大规模回填场景(比如首次索引几百个记忆文件),这是一个显著的成本优化。

SQLite 向量加速

sqlite-vec 扩展可用时,嵌入直接存储在 SQLite 虚拟表(vec0)中,向量距离查询在数据库内执行——不需要把每个嵌入加载到 JS 内存中。

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "store": {
          "vector": {
            "enabled": true,
            "extensionPath": "/path/to/sqlite-vec"
          }
        }
      }
    }
  }
}
// generated by hugo's coding agent

sqlite-vec 缺失时自动回退到进程内余弦相似度计算。这种"有加速就用,没有也不崩"的设计让系统对部署环境的要求降到最低。

会话记忆搜索(实验性)

除了 Markdown 记忆文件,OpenClaw 还支持索引会话记录本身——这意味着即使你没有显式"记住"某个信息,只要它出现在过去的对话中,memory_search 就有可能找到它。

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "experimental": { "sessionMemory": true },
        "sources": ["memory", "sessions"]
      }
    }
  }
}
// generated by hugo's coding agent

会话索引是选择加入的(默认关闭),更新被去抖动并异步索引。这是一个有趣的方向——从"Agent 主动记录"扩展到"所有对话都可能被记住"。

需要注意的信任边界:会话日志存储在磁盘上(~/.openclaw/agents/<agentId>/sessions/*.jsonl),任何有文件系统访问权限的进程都可以读取。对于严格隔离需求,建议在独立的操作系统用户下运行 Agent。

额外记忆路径

如果你想让 Agent 的记忆范围超出默认工作空间——比如索引团队共享文档或项目笔记——可以通过 extraPaths 扩展:

{
  "agents": {
    "defaults": {
      "memorySearch": {
        "extraPaths": ["../team-docs", "/srv/shared-notes/overview.md"]
      }
    }
  }
}
// generated by hugo's coding agent

路径支持绝对和相对(相对于工作空间),目录会递归扫描 .md 文件。只索引 Markdown,符号链接被忽略。

完整的记忆生命周期

把所有组件串起来,OpenClaw 的记忆系统形成了一个完整的生命周期:

对话中                    压缩前                    心跳周期
  │                        │                        │
  ▼                        ▼                        ▼
用户说"记住X"          memoryFlush 触发          Agent 回顾每日日志
Agent 写入文件          抢救关键信息到文件         提炼到 MEMORY.md
  │                        │                        │
  └────────────┬───────────┘────────────────────────┘
         memory/*.md + MEMORY.md
     文件变更触发重新索引(去抖动 1.5s)
     SQLite 向量索引 + FTS5 全文索引
     memory_search / memory_get 供 Agent 检索

写入路径有三个入口:对话中显式写入、压缩前自动刷新、心跳周期中的记忆维护。

读取路径有两种:会话启动时加载近期日志和长期记忆(直接读文件),以及运行时通过 memory_search 语义检索(走索引)。

与其他方案的对比

维度ChatGPT MemoryClaude ProjectsOpenClaw Memory
存储介质云端黑箱项目知识库本地 Markdown 文件
用户可见性可查看条目可上传文件完全透明,可直接编辑
语义检索不透明不透明混合搜索,权重可配置
AI 可自维护有限完全可以
版本控制Git 原生支持
隐私云端存储云端存储本地存储,完全自控
多 Agent 隔离N/AN/A每个 Agent 独立索引

OpenClaw 的优势在于透明性和可控性。你可以直接打开 MEMORY.md 看 Agent 记住了什么,删掉不准确的信息,甚至用 Git 追踪记忆的变化历史。

设计启示

OpenClaw 的 Memory 系统给 Agent 记忆设计提供了几个值得借鉴的思路:

文件即记忆,不用数据库。用 Markdown 文件作为唯一的记忆事实来源,简单到不可能出错。任何文本编辑器都能查看和修改,Git 天然提供版本控制。复杂的数据库方案往往在"怎么让用户看到和修改记忆"这个问题上陷入困境。

两层分离:日志 vs 精炼。每日日志保证了信息不丢失,长期记忆保证了上下文窗口不爆炸。这种分离让 Agent 可以自主执行"回顾-提炼-遗忘"的记忆管理循环。

混合检索比纯向量更实用。现实中的记忆查询既有语义模糊的(“之前讨论过的那个部署方案”),也有精确的(“错误码 ERR_CONNECTION_REFUSED 的处理方式”)。BM25 + 向量的组合用最小的复杂度覆盖了两种场景。

隐私边界内建于架构MEMORY.md 只在私人会话加载、每个 Agent 索引隔离、会话日志的文件系统权限——这些不是事后补丁,而是从第一天就考虑的设计约束。

优雅降级优于硬依赖。没有 sqlite-vec?回退到进程内计算。没有嵌入 API Key?回退到纯 BM25。嵌入提供商挂了?用缓存兜底。每个组件都可以独立失败而不拖垮整个系统。

记忆是 Agent 从"工具"进化为"助手"的关键能力。OpenClaw 的方案证明了一点:好的记忆系统不需要复杂的基础设施,一组 Markdown 文件 + 一个 SQLite 索引 + 一套清晰的读写协议,就已经足够让 AI 真正"记住"你。


See also