😱【OpenClaw 源码解析】你的 AI 助手每次都「失忆」?学会这一招,让它记住你所有重要决策,效率直接翻倍!
😱【OpenClaw 源码解析】你的 AI 助手每次都「失忆」?学会这一招,让它记住你所有重要决策,效率直接翻倍!
副标题:深扒 OpenClaw 源码,揭秘顶级 AI Agent 背后不为人知的「记忆黑科技」——普通人永远不知道的财富密码
🔥 引言:那个让我损失惨重的下午
那是一个普通的周四下午。
我正在用 AI 助手处理一个月开了无数次会才定下来的项目架构。规则定好了、命名约定谈妥了、踩过的坑我都手动告诉了它……
然后——
我关掉了对话窗口。
第二天打开新会话,一切归零。🫥
它不记得我们花三个小时定下的接口命名规范。
不记得我们痛苦纠结后选的那个数据库方案。
不记得我说「这个模块以后不准用全局变量」。
就像花了一个月调教出来的实习生,第二天早上进门,满脸问号地看着你:「您好,请问我是来做什么的?」

这不是 AI 笨,这是它根本没有「记忆」。
而我今天要告诉你的,就是那些高阶玩家偷偷在用的 AI 记忆系统——它是如何让 AI 真正「记住你」的。看完这篇文章,你会彻底明白为什么你的 AI 效率只有别人的三分之一。
📖 本文章节目录
💡 阅读指南:章节之间是层层递进的关系——先搞明白「记什么」,再搞清楚「怎么存」,然后是「怎么找」,最后是「什么时候自动保存」。每一章都在解决上一章留下的新问题。
┌─────────────────────────────────────────────────────────┐
│ │
│ 第一章:大脑在哪里? │
│ 💾 记忆的「物理位置」——文件、数据库、索引长什么样 │
│ │ │
│ │ ➡️ 知道存哪了,那怎么「切成合适的块」? │
│ ▼ │
│ 第二章:大脑怎么工作? │
│ 🔪 把文章切碎再「向量化」——让 AI 理解语义而不只是关键字 │
│ │ │
│ │ ➡️ 切完了,那怎么「快速找到」想要的那块? │
│ ▼ │
│ 第三章:怎么想起来的? │
│ 🔍 混合搜索黑科技——向量 + 关键词,双管齐下 │
│ │ │
│ │ ➡️ 能找了,但什么时候「自动触发保存」? │
│ ▼ │
│ 第四章:什么时候自动记住的? │
│ 🧠 临界点自动 Flush——快撑不住时的「遗嘱写入」机制 │
│ │
│ 终章:我能用上吗? │
│ 🚀 普通人怎么配置这套系统,开始让 AI 真正认识你 │
│ │
└─────────────────────────────────────────────────────────┘
第一章:大脑在哪里?💾
AI 的「记忆宫殿」长这样
「记忆」这个词听起来很玄,但其实就是几个文件夹和数据库。
想象你有个超级助理,他的工位上放着:
| 东西 | 对应什么 | 放在哪 |
|---|---|---|
| 📓 日记本 | MEMORY.md |
~/.openclaw/workspace/<你的ID>/ |
| 📁 专题笔记夹 | memory/*.md |
同上,按日期/主题分文件 |
| 🎙️ 会议录音稿 | Session JSONL | ~/.openclaw/sessions/ |
| 🗂️ 智能索引卡片盒 | SQLite 数据库 | ~/.openclaw/memory/<你的ID>/index.db |
重点来了 👇
前三个是「原始材料」——人类可读的文本。最后那个 SQLite 数据库才是真正的魔法所在。整体架构长这样:
🔬 chunks 表长什么样?(src/memory/memory-schema.ts)
CREATE TABLE chunks (
id TEXT PRIMARY KEY,
path TEXT NOT NULL, -- 来自哪个文件
source TEXT NOT NULL DEFAULT 'memory', -- 'memory' | 'sessions'
start_line INTEGER NOT NULL, -- 块开始行号
end_line INTEGER NOT NULL, -- 块结束行号
hash TEXT NOT NULL, -- 内容哈希,用来检测变更
model TEXT NOT NULL, -- 用哪个 embedding 模型算的
text TEXT NOT NULL, -- 原始文本
embedding TEXT NOT NULL, -- ⚠️ JSON 序列化的向量数组!
updated_at INTEGER NOT NULL
);
注意 embedding 字段——它不是什么神秘二进制,就是一个 JSON 数组,比如 [0.023, -0.187, 0.341, ...],有多少维就有多少个数字。
还有个 files 表专门做变更追踪:
CREATE TABLE files (
path TEXT PRIMARY KEY,
source TEXT NOT NULL DEFAULT 'memory',
hash TEXT NOT NULL, -- SHA-256 文件哈希
mtime INTEGER NOT NULL, -- 修改时间
size INTEGER NOT NULL
);
每次同步时,系统先查这张表——如果文件哈希没变,直接跳过,一个 API 调用都不浪费。
TypeScript 里的核心数据结构(src/memory/types.ts)
// 一个文本块
type MemoryChunk = {
startLine: number;
endLine: number;
text: string;
hash: string; // 块内容的 SHA-256
};
// 搜索返回的一条结果
type MemorySearchResult = {
path: string;
startLine: number;
endLine: number;
score: number; // 0-1 的相关性分数
snippet: string; // 最多 700 字符的摘要
source: "memory" | "sessions";
citation?: string; // 格式:path#L15 或 path#L15-L30
};
😏 一句话总结:你写的 Markdown 笔记是「原材料」,AI 把它们切碎、编码、建索引之后,才能真正「懂得」里面说的是什么。
第二章:大脑怎么工作?🔪
第一步:把你的笔记「切碎」(src/memory/internal.ts:166-247)
不是整篇文章扔进去,而是切成小块——来看实际的函数签名:
function chunkMarkdown(
content: string,
chunking: { tokens: number; overlap: number }
) {
const maxChars = tokens * 4; // 400 tokens → ~1600 字符
const overlapChars = overlap * 4; // 80 tokens → ~320 字符重叠
// 按行累积,超过 maxChars 时切分
// 保留 overlapChars 作为下一个块的开头
}
为什么要重叠? 因为防止一句关键话刚好被切在两块的边界上,结果两块都只有「半句话」,搜索时全部失效。🤦
就像切披萨,你不会让奶酪刚好从中间断掉——重叠部分就是「多留一点边」的意思。🍕

第二步:把每个小块「向量化」(这里是真正的黑魔法)
先看 OpenClaw 定义的 Provider 接口(src/memory/embeddings.ts:21-26):
type EmbeddingProvider = {
id: string; // 'openai' | 'gemini' | 'local'
model: string; // 具体模型名
embedQuery: (text: string) => Promise<number[]>; // 单条查询
embedBatch: (texts: string[]) => Promise<number[][]>; // 批量处理
};
系统支持三种实现:
🤖 OpenAI → text-embedding-3-small(云端,要钱)
♊ Gemini → text-embedding-004(云端,免费额度大)
💻 Local → embeddinggemma-300M(本地,节点推理,零成本)
什么是「向量化」?
简单说:把一段文字变成一串数字(比如 1536 维)。语义相近的文字,对应的数字串也会很「接近」。
所以:
- 「明天下雨」和「今日有雷阵雨」→ 数字串很接近 ✅
- 「明天下雨」和「比特币暴涨」→ 数字串差很远 ✅
🧠 记忆原理:你的笔记被变成数字 → 数字存进数据库 → 下次搜索时,你的问题也被变成数字 → 找最接近的数字 → 返回对应的笔记片段

🔬 Provider 自动选择:优雅的降级链(src/memory/embeddings.ts:125-205)
auto 模式下,系统按这个顺序自动探测——你不需要手动切换,它自己找能用的:
🔬 批量 Embedding:不是一条条算,是打包发(src/memory/manager.ts:1652-1678)
// 构建批次,每批最多 8000 tokens
const BATCH_MAX_TOKENS = 8000;
function buildEmbeddingBatches(chunks: MemoryChunk[]): MemoryChunk[][] {
// 按 token 估算累积,超过 8000 就新开一批
}
// 带缓存的批量 embedding
async function embedChunksInBatches(chunks: MemoryChunk[]) {
// 1. 先查 embedding_cache 表,找已缓存的
// 2. 只对「未命中」的调用 provider.embedBatch()
// 3. 把新结果写回缓存
}
缓存的 key 结构是 (provider, model, provider_key, hash)——同一段文字不管哪次会话,只算一次。
OpenAI / Gemini 还支持异步 Batch API(src/memory/batch-openai.ts):大规模索引时可以批量提交任务,等结果回来再处理,比同步调用便宜得多。但有个自我保护机制——失败超过 2 次,自动禁用 Batch API,降级回普通调用。
🔬 各操作的超时时间表
| 操作 | 本地模型 | 远程 API |
|---|---|---|
| 单条查询 Embedding | 5 分钟 | 1 分钟 |
| 批量 Embedding | 10 分钟 | 2 分钟 |
| 向量表加载 | 30 秒 | 30 秒 |
超时后自动重试,策略是指数退避:500ms → 1s → 2s → 4s → 8s,最多 3 次。遇到 Rate Limit 错误也会自动等待重试。
第三章:怎么想起来的?🔍
混合搜索 = 向量搜索 × 0.7 + 关键词搜索 × 0.3
光有向量搜索不够——
场景:你问「上次我们决定用 PostgreSQL 还是 MySQL?」
向量搜索:能理解你在问数据库选型问题 ✅
但如果笔记里刚好写的是「选了 PG」,关键词 MySQL 根本搜不到 ❌
关键词搜索:精准匹配 "PostgreSQL" "MySQL" ✅
但如果你问「上次那个数据库决定」,啥也匹配不上 ❌
所以两个都用,然后加权合并。 来看 src/memory/hybrid.ts 里的真实函数签名:
function mergeHybridResults(params: {
vector: HybridVectorResult[];
keyword: HybridKeywordResult[];
vectorWeight: number; // 默认 0.7
textWeight: number; // 默认 0.3
}) {
// score = vectorWeight * vectorScore + textWeight * textScore
}
一行公式说清楚一切:
最终得分 = 向量分 × 0.7 + 关键词分 × 0.3
完整的搜索数据流,一图胜千言:
🔬 向量搜索:底层用的是 sqlite-vec 扩展
不是什么神秘黑箱,就是一句 SQL(src/memory/manager-search.ts):
SELECT id, embedding
FROM chunks_vec
WHERE embedding MATCH ? -- k-nearest neighbor 查询
ORDER BY distance
LIMIT ?
chunks_vec 是一张用 vec0 虚拟表引擎创建的特殊表,支持直接对向量做「最近邻搜索」,底层算余弦相似度。简单、快、不依赖外部向量数据库。
😮 很多人以为向量搜索一定要上 Pinecone、Weaviate 那种独立服务,其实 SQLite 装个扩展就够了。
🔬 关键词搜索:FTS5 + 自动构建查询
src/memory/hybrid.ts:23-34 里有个小而精的函数:
function buildFtsQuery(raw: string): string | null {
// "hello world" → '"hello" AND "world"'
const tokens = raw.match(/[A-Za-z0-9_]+/g);
return tokens.map(t => `"${t}"`).join(" AND ");
}
你输入 PostgreSQL MySQL 选型,它自动变成:
"PostgreSQL" AND "MySQL" AND "选型"
然后用 BM25 评分(就是 Google 用了很多年的那套经典算法)来排序,再归一化到 0-1 区间:
function bm25RankToScore(rank: number): number {
return 1 / (1 + rank); // 越靠前,rank 越小,分越高
}
📐 一个被低估的参数:candidateMultiplier
源码里有个默认值 candidateMultiplier: 3,意思是:
「如果你最终要返回 6 条结果,我先找出 6 × 3 = 18 条候选,再从里面合并排序、择优返回。」
为什么要这么做?因为向量搜索排第一的和关键词搜索排第一的,可能完全不是同一条记录。先各自扩大候选池,合并时才不会漏掉真正相关的结果。
所有关键参数一览:
| 参数 | 默认值 | 作用 |
|---|---|---|
vectorWeight |
0.7 | 向量搜索权重 |
textWeight |
0.3 | 关键词搜索权重 |
maxResults |
6 | 最终返回条数 |
minScore |
0.35 | 低于此分直接丢弃 |
candidateMultiplier |
3 | 候选集扩大倍数 |
第四章:什么时候自动记住的?🧠
「遗嘱写入」——快撑不住之前先把重要的存下来
每个 AI 对话都有 context window 上限(就是「脑容量」)。
接近满了会发生什么?系统会触发 Memory Flush。
触发判断函数(src/auto-reply/reply/memory-flush.ts:77-105):
function shouldRunMemoryFlush(params: {
entry?: {
totalTokens: number;
compactionCount: number;
memoryFlushCompactionCount: number; // 记录「这轮已经 flush 过了」
};
contextWindowTokens: number;
reserveTokensFloor: number;
softThresholdTokens: number; // 默认 4000
}) {
// 触发条件:
// totalTokens > (contextWindowTokens - reserveTokensFloor - softThresholdTokens)
// 且当前 compactionCount 未执行过 flush(防止重复触发)
}
用大白话说:
当前对话 token 数 > (context 上限 - 预留底线 - 4000 缓冲)
且这轮对话还没 flush 过
→ 触发!
触发之后,AI 会同时收到两条特殊指令:
系统提示(system prompt):
"Pre-compaction memory flush turn. The session is near auto-compaction; capture durable memories to disk. You may reply, but usually [NO_REPLY] is correct."
用户提示(user message):
"Pre-compaction memory flush. Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). If nothing to store, reply with [NO_REPLY]."
翻译成人话:「脑子快装满了,赶紧把重要的东西写进笔记本!没东西写就回复 [NO_REPLY] 就行。」
整个 Flush 时序是这样的:
AI 就会主动把这次对话里的关键决策、重要信息写进 memory/2024-01-15.md 这样的文件,然后下次同步时自动被切块、向量化、建索引。
🔬 彩蛋:原子化重建索引(src/memory/manager.ts:1398-1500)
这个设计超骚——索引损坏怎么办?答案是「在旁边悄悄建好新的,再瞬间换掉」:
全程旧索引都在正常服务,零停机重建。
🔬 Session 增量索引:不是每次都全量重建
会话记录也会被索引,但触发条件很保守,避免频繁 I/O:
新增字节数 > 4096 字节(约 4KB)
OR
新增消息数 > 10 条
→ 才触发增量同步
这意味着短对话根本不会触发,长对话也只在「积累足够多」之后才写索引,大幅减少数据库写入频率。
🎯 妙处:不需要你手动整理!AI 自己知道在「快撑不住」之前把重要内容持久化。这才是真正的「记忆」——不是你帮它记,是它自己知道该记什么。
终章:我能用上吗?🚀
普通用户的配置入门
最简单的起手式:
memory:
backend: "builtin" # 内置方案,不需要额外服务
citations: "auto" # 自动显示引用来源
agents:
defaults:
memorySearch:
provider: "auto" # 自动选可用的 Embedding 服务
sync:
onSessionStart: true # 每次开始自动同步
onSearch: true # 搜索时自动同步
watch: true # 文件有变化立刻同步(debounce 1500ms)
compaction:
memoryFlush:
enabled: true
softThresholdTokens: 4000 # 提前多少 token 开始 flush
只有三种要记住的 Agent 工具:
📡 memory_search("你的查询") ← 语义搜索,返回最多 6 条带行号的片段
📄 memory_get("memory/xxx.md") ← 直接读取指定文件的指定行
✍️ 直接写文件 ← 存进 memory/*.md 即可被自动索引
这套系统真正厉害在哪?
不是技术有多复杂,而是它解决了 AI 最根本的问题:
❌ 没有这套系统:AI 是个高智商失忆症患者
✅ 有了这套系统:AI 是个有完整工作记录的长期合伙人
每次对话不再从零开始。
你踩过的坑,它记得。
你定下的规范,它记得。
你的偏好和决策风格,它记得。
💡 最后一句话:
记忆,是让 AI 从「工具」变成「合伙人」的那道门槛。
而门是开着的——你只需要知道它在哪里。🚪
本文技术细节基于 OpenClaw 项目源码分析,核心文件:src/memory/manager.ts(2400行,硬核)
🔁 觉得有用?转发给你的技术群,让更多人少走弯路
顶级AI Agent背后的记忆系统,源码级别的拆解来了🔥
不是教你调参,是带你看真实工程代码怎么写的:
- 混合搜索怎么让向量+关键词互补
- Context快满了AI怎么自动「写遗嘱」
- 索引损坏怎么做到零停机重建
龙虾工程师🦞必看 ↓
浙公网安备 33010602011771号