分享如何节省OpenClaw 80%的Token消耗(二)

2 阅读11分钟

上一篇聊了 Skills 和 Project Context 的 Token 节省。这篇深入长记忆系统——它是整个上下文工程里最复杂、最值得调优的部分。

但它的写作难度很大,主要是怎么能同时让技术同学和普通用户理解一些事,所以有些地方写得会很啰嗦,见谅。

配置先放在最前面了,下面的部分都是论述过程,理解过程对于调试自己的OpenClaw很有帮助:

{
  "agents": {
    "defaults": {
      "blockStreamingDefault": "on",
      "compaction": {
        "mode": "safeguard", // default是一次性总结整个历史,safeguard是分块总结 → 再总结(更安全)
        "model": "ollama/llama3.1:8b", // 我用的是本地模型来进行压缩汇总
        "reserveTokensFloor": 12000, // 压缩后至少保留多少 token 的最近对话
        "maxHistoryShare": 0.4, // 压缩后的历史在上下文中最多能占多少比例
        "recentTurnsPreserve": 6, // 最近3段对话不要压缩,只压缩前面的
        "memoryFlush": {
          "enabled": true,
          "softThresholdTokens": 12000 //接近多少进行压缩
        }
      },
      "memorySearch": {
        "enabled": true,
        "provider": "gemini", // google embedding可以多模态
        "model": "gemini-embedding-2-preview",
        "fallback": "openai",
        "chunking": {
          "tokens": 400, // 每个 memory chunk 的最大 token 数, 文档会被拆分成多个 chunk 用于向量索引
          "overlap": 80
        },
        "query": {
          "maxResults": 8,  // 最多返回多少个 memory chunk
          "minScore": 0.4, // 最低相似度阈值
          "hybrid": {
            "enabled": true, // 启用 hybrid search(向量搜索 + 关键词搜索)
            "vectorWeight": 0.7, // 向量相似度权重
            "textWeight": 0.3, // 关键词匹配权重
            "mmr": {
              "enabled": true, // 启用 MMR(Maximal Marginal Relevance),用于减少返回结果之间的语义重复
              "lambda": 0.7 // 越高越偏向相关性
            },
            "temporalDecay": {
              "enabled": true, // 时间衰减
              "halfLifeDays": 30 // 每 30 天 relevance 下降 50%
            }
          }
        }
      }
    }
  },
}

环境变量文件可以放在.openclaw,或者workspace下都可以读到

OPENAI_API_KEY="sk-你的GPT_Key"
GEMINI_API_KEY="AIzaSy-你的Gemini_Key"

下面是理解这个的一些过程。

一、长记忆的大致流程

先把整个系统的流程画清楚,后面说的所有 Tips 都基于这个:

磁盘文件(MEMORY.md + memory/*.md)
    ↓ listMemoryFiles()                   — 扫描工作区
    ↓ hashText()                          — 变更检测
    ↓ 分块(默认 400 token,80 重叠)
    ↓ embedBatch()                        — 向量化
    ↓
SQLite 索引
    ├── chunks_fts(FTS5 全文搜索)
    └── chunks_vec(sqlite-vec 向量存储)
    ↓
search(query)
    ├── searchVector()    — 余弦相似度
    ├── searchKeyword()   — BM25
    └── 混合融合:score = 0.7 × vector + 0.3 × keyword
    ↓ MMR 重排(多样性去重)
    ↓ 时间衰减(可选)
    ↓
返回前 N 条(默认 maxResults=6,minScore=0.35

这其实很抽象,但我们可以分成两个阶段去理解:

Query

当用户问问题:

User: 我之前做过哪些视频策略?

OpenClaw才会做:

search(query)

流程是:

  1. 在数据库里找相似内容
  2. 用两种方式找
    • 语义相似(vector)
    • 关键词匹配(keyword)
  3. 把两种结果混合排序
  4. 去掉重复、增加多样性
  5. 返回最相关的几条
  6. 把这些记忆片段放进 AI 的上下文。

Embedding (写入记忆、索引阶段)

当这些文件发生变化时:

MEMORY.md
memory/*.md

OpenClaw会:

  1. 扫描这些文件
  2. 切成小段
  3. 做向量化
  4. 存进本地数据库

二、记忆文件:存什么,怎么存

文件扫描范围

listMemoryFiles() 只扫描两个位置:

  1. MEMORY.md(根目录的索引文件)
  2. memory/*.md(memory 目录下的所有 Markdown 文件)

这两个位置以外的文件不会被索引,无论你在哪里创建都没用。

分块机制

每个记忆文件被读进来之后,会被切成若干个 Chunk:

  • 默认块大小:400 token
  • 块间重叠:80 token

我们可以在Openclaw中配置,配置路径:agents.defaults.memory.chunkSize / chunkOverlap

如果没有重叠,分块边界很容易把一段关键语义切成两半,导致搜索时命中不了完整信息,但区间重叠不是万能的。

这意味着什么?

如果你的记忆文件里有一段非常关键的连续逻辑(比如一套完整的判断规则),要确保它不会被切断在两个 Chunk 的边界。400 token 大约是 300 个中文或 300 个英文单词,超过这个长度的单段内容会被跨块存储,检索时可能只命中其中一块。

Tips:

  • 不要去加大区间重叠,会导致索引膨胀、搜索结果高度重复、Embedding成本增加
  • 单条记忆不要写超过 300 字,关键信息要在前半段说完整
  • 如果你的记忆文件很长,用 Markdown 标题(#####)分段,语义边界更清晰,分块时不容易截断关键信息

会话自动变成记忆

OpenClaw 有一个内置 Hook(src/hooks/bundled/session-memory/HOOK.md),在你执行 /new/reset 或上下文要爆了的时候时自动触发:

  • 提取当前会话前 N 条消息(默认 15 条)
  • AGENT.mdSOUL.MDMEMORY.md等全注入到上下文
  • MEMORY.md 作为具体的内容和采用什么格式的指导
  • 用 LLM 生成一个描述性文件名 slug
  • 保存为 memory/YYYY-MM-DD-{slug}.md

这些自动生成的会话记忆文件和你手写的记忆文件一视同仁地被索引

Tips:

  • 如果你频繁开新会话,memory/ 目录会积累大量自动生成的文件,定期清理很重要
  • 自动生成的文件质量参差不齐,重要的内容建议手动整理到正式的记忆文件里

MEMORY.md

示例

## Write Rules

Only store what survives compaction: user preferences, project decisions,
architecture choices, long-term behavioral corrections.

Never store:
- debug logs
- code snippets
- temp discussions
- git-trackable content


Entry format:

### [topic keyword]
[one-line fact, absolute dates only]

**Why:** [motivation]

**How to apply:** [when this changes AI behavior]


## Index (optional)

<!-- Maintain this after each flush. One line per file. -->
<!-- Format: - memory/YYYY-MM-DD.md: [comma-separated topic keywords] -->

为什么这么短

MEMORY.md 会在每一轮对话完整加载进上下文

这意味着:

  • 你写 1 个字,每轮对话都会 多消耗 1 个 token
  • 模型输出效果会随着上下文变大指数变差

所以它的第一优先级是短

为什么这么写

Flush 的 system prompt 已经硬编码了:

  • memory/YYYY-MM-DD.md 路径规则
  • append-only 机制
  • memory_search / memory_get 的读取方式

所以 MEMORY.md 只需要补充 系统没有管的两件事

1️⃣ 什么值得写(quality filter)
2️⃣ 怎么写(entry format)

而默认 memory chunk 参数是:

chunkSize   = 400 tokens
chunkOverlap = 80 tokens

使用:

### topic

作为结构边界有三个好处:

1️⃣ 语义边界明确
2️⃣ 降低 chunk 切断关键信息概率
3️⃣ 向量搜索更容易命中

### = chunk 的自然分隔符

Index 是否需要

Index 主要是为了减少 memory_search 成本。

文件少

memory 文件 < 10

可以不写 Index
AI 搜一遍成本不高。

文件多

memory 文件 > 20

建议保留 Index。

格式:

- memory/2026-03-20-xxx.md: openclaw-architecture, memory-design
- memory/2026-03-21-xxx.md: token-optimization

这样 AI 可以快速定位目标文件,而不是搜索整个 memory 目录。

顺手一提:最近harness engineering的概念很火,这也是我在做OpenClaw魔改的主要方向之一,大家可以了解一下,对提升效果会非常显著。

三、Embedding:向量化的那些细节

稍微补充一下,OpenClaw其实是可以不开Embedding跑的,但是会造成输出质量差(召回质量下降)、上下文 Token 浪费这两个主要的问题。

启用Embedding会省钱得多。

支持的 Embedding 提供商

来源:src/memory/embeddings.ts

提供商默认模型特点
OpenAItext-embedding-3-small默认首选
Google Geminigemini-embedding-2-preview支持多模态(图片/音频)
Voyagevoyage-4-large代码/技术文档效果好
Mistralmistral-embed欧洲合规
Ollamanomic-embed-text本地自托管
Local(node-llama-cpp)embeddinggemma-300m-qat-q8_0完全离线

Auto 模式的优先级openai → gemini → voyage → mistral

如果你没有 OpenAI Key,系统会自动尝试下一个。但这个 fallback 链是固定的,如果你想用 Voyage 但有 OpenAI Key,需要显式配置。

如果最终这个表中所有的都没有,就不会走Embeding模式,而是全部走到关键词查询匹配搜索。

嵌入缓存

SQLite 里有一张 embedding_cache 表,存的是 (provider, model, hash) → embedding

同样的文本内容,只要没变,不会重复调用 Embedding API,是基于内容哈希的缓存。

Tips:

  • 记忆文件不要频繁做微小的格式调整(比如加减空格、换行),每次变化都会触发重新 Embed,消耗 API 调用次数
  • 如果你在测试记忆内容,先把核心内容定稿再写入文件

多模态 Embedding

如果你用的是 Gemini embedding-2-preview,memory 目录里可以放图片和音频文件(.jpg.png.mp3.wav 等),也会被索引。

限制:单文件最大 10MB(默认值),来源:src/memory/multimodal.ts


四、Query:搜索是怎么工作的

混合搜索权重

这是最重要的一个参数。默认值来自源码:

score = 0.7 × 向量相似度 + 0.3 × BM25 关键词匹配

来源:src/memory/hybrid.ts,配置路径:agents.defaults.memory.hybrid.vectorWeight / textWeight

什么时候调整权重?

  • 如果你的记忆文件用词非常精确、专业(比如代码变量名、API 名),可以提高 textWeight,BM25 对精确词匹配更可靠
  • 如果你的记忆是自然语言描述的想法、经验、感受,保持默认(向量权重更高)

MMR 重排:防止结果都是同一件事

MMR(Maximal Marginal Relevance)的作用是在相关性高的候选里,过滤掉内容高度重复的条目:

MMR = λ × 相关性 - (1-λ) × 与已选结果的最大相似度

默认 lambda=0.7,也就是 70% 考虑相关性,30% 考虑多样性。

来源:src/memory/mmr.ts

实际影响:如果你的多条记忆内容高度重合(比如同一件事写了三条),MMR 会只保留其中最相关的那条。这本身是好的,但也意味着重复记忆是浪费——不仅浪费存储,搜索时也只有一条能活下来。

时间衰减

默认禁用。如果开启,使用半衰期模型:

decay_multiplier = exp(-λ × 文件创建距今天数)

默认半衰期 30 天。

关键细节(来源:src/memory/temporal-decay.ts):

  • MEMORY.md没有日期前缀的 memory/*.md 不会衰减(视为常青知识)
  • 有日期前缀的文件(比如 memory/2026-03-24-xxx.md)才会根据日期衰减

这就是会话自动保存文件的命名格式YYYY-MM-DD-slug.md)。所以自动保存的会话记忆天然会随时间衰减,而你手写的无日期记忆不会。这个设计很合理。

Tips:

  • 如果你想让某条记忆永远高权重,不要给它加日期前缀
  • 如果你在做项目记录、希望旧的决策背景自然淡出,可以开启时间衰减,并用日期前缀命名

查询扩展

在关键词搜索(BM25)之前,OpenClaw 会对你的查询做扩展(来源:src/memory/query-expansion.ts):

  • 过滤停用词(英文的冠词、代词、介词等)
  • 对中文查询有特殊处理(限制单字汉字,防止过于宽泛的匹配)
  • 把对话式的查询("那个我们讨论过的认证问题")转换成关键词(["认证", "讨论"])

对你的影响:当你的记忆文件用自然语言写的时候,不需要担心措辞和查询完全一致——系统会做关键词提取。但如果你的记忆里有很多同义词,建议在关键位置保留最常用的术语。

默认的搜索参数

来源:src/agents/memory-search.ts

参数默认值含义
maxResults6每次最多返回 6 条
minScore0.35低于 0.35 的结果直接丢弃

这两个参数可以在 Agent 配置里覆盖。如果你发现记忆召回率低,可以降低 minScore;如果结果太多太杂,可以提高。

五、Auto Memory Flush:上下文压缩前的自动保存

这是一个很多人不知道的机制,来源:src/auto-reply/reply/memory-flush.ts

触发条件:会话上下文快要触发 auto-compaction 时(默认阈值 softThresholdTokens=4000

做什么:在压缩发生前,静默地提醒模型把重要信息写入持久记忆

配置路径agents.defaults.compaction.memoryFlush

enabled: true(默认开启)
softThresholdTokens: 4000

实际意义:如果你的会话很长,快到压缩点时,系统会自动触发一次"请把重要信息写进记忆"的提示。这是避免长对话中信息丢失的保险机制。你不需要手动干预,但知道它的存在可以帮你理解为什么有时候 AI 会在某个时刻主动说"我把这个记下来了"。

六、两种后端:内置 vs QMD

来源:src/config/types.memory.ts

后端适用场景搜索能力
builtin(默认)普通用户SQLite + FTS5 + sqlite-vec
qmd(实验性)高级用户BM25 + 向量 + LLM 重排,三种搜索模式

QMD 的三种搜索模式(src/memory/qmd-manager.ts):

  • search(默认快速):BM25 + 向量
  • query(最佳召回):查询扩展 + 重排,但 CPU 密集
  • vsearch(纯向量):只用向量相似度

对于大多数用户,builtin 后端已经够用。QMD 是给需要更精细控制搜索行为的高级场景准备的。


结尾

下一篇会写缓存机制,这一块就很常规了,但同样有用。

唔,然后我在找那种稳定供应的API供应商(我是企业用,应该能满足单独号池的需求),图片/视频/文字LLM都需要。

也可以招安一个,能跑通一个全流程的同学(号池、逆向、供应商),发工资来做,海外。

这是我