OpenClaw 记忆系统深度解析:向量库、Memory与Session的流转机制
声明:本文档基于源码分析编写,所有结论均以源码为准。
在基于 Pi 开源框架开发的智能体应用 OpenClaw 中,记忆系统通过向量库、Memory 文件、Session 三种核心介质实现信息的分层存储与高效流转。本文将从源码角度详细解析这三个组件的定义、信息流转逻辑以及关键机制。
一、三种核心介质:定位与作用
1. Session:磁盘上的"会话记录"
Session 在 OpenClaw 中主要是 transcript 文件(JSONL 格式),存储在 ~/.openclaw/sessions/ 目录,而不是内存中的"运行时缓存"。消息实时写入磁盘(同步 append),运行时通过 before_prompt_build hook 从文件系统加载历史消息。
源码路径:src/gateway/session-transcript-files.fs.ts
// src/gateway/session-transcript-files.fs.ts:64
export function resolveSessionTranscriptCandidates(
sessionId: string,
storePath: string | undefined,
sessionFile?: string,
agentId?: string,
): string[] {
const candidates: string[] = [];
const sessionFileState = classifySessionTranscriptCandidate(sessionId, sessionFile);
// ... 解析会话文件路径
}
Session 的核心特点:
- 存储形式:
*.jsonl格式的 transcript 文件 - 位置:
~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl - 消息同步追加写入磁盘(同步操作)
- 运行时通过
before_prompt_buildhook 加载历史消息
2. Memory:持久化的"结构化记忆库"
源码路径:extensions/memory-core/src/flush-plan.ts
// extensions/memory-core/src/flush-plan.ts:13-41
const MEMORY_FLUSH_TARGET_HINT =
"Store durable memories only in memory/YYYY-MM-DD.md (create memory/ if needed).";
// 中文:将持久记忆存储到 memory/YYYY-MM-DD.md(必要时创建 memory/ 目录)
const MEMORY_FLUSH_APPEND_ONLY_HINT =
"If memory/YYYY-MM-DD.md already exists, APPEND new content only and do not overwrite existing entries.";
// 中文:如果 memory/YYYY-MM-DD.md 已存在,仅追加新内容,不要覆盖现有条目
const MEMORY_FLUSH_READ_ONLY_HINT =
"Treat workspace bootstrap/reference files such as MEMORY.md, SOUL.md, TOOLS.md, and AGENTS.md as read-only during this flush; never overwrite, replace, or edit them.";
// 中文:在 flush 期间,将工作区引导/参考文件(如 MEMORY.md、SOUL.md、TOOLS.md、AGENTS.md)视为只读;永不覆盖、替换或编辑它们
export const DEFAULT_MEMORY_FLUSH_PROMPT = [
"Pre-compaction memory flush.", // 压缩前内存 flush
MEMORY_FLUSH_TARGET_HINT,
MEMORY_FLUSH_READ_ONLY_HINT,
MEMORY_FLUSH_APPEND_ONLY_HINT,
"Do NOT create timestamped variant files (e.g., YYYY-MM-DD-HHMM.md); always use the canonical YYYY-MM-DD.md filename.",
// 中文:不要创建带时间戳的变体文件,始终使用规范的 YYYY-MM-DD.md 文件名
`If nothing to store, reply with ${SILENT_REPLY_TOKEN}.`, // 如果没有需要存储的内容,回复 NO_REPLY
].join(" ");
export const DEFAULT_MEMORY_FLUSH_SYSTEM_PROMPT = [
"Pre-compaction memory flush turn.", // 压缩前内存 flush 轮次
"The session is near auto-compaction; capture durable memories to disk.",
// 中文:会话即将自动压缩;将持久记忆捕获到磁盘
MEMORY_FLUSH_TARGET_HINT,
MEMORY_FLUSH_READ_ONLY_HINT,
MEMORY_FLUSH_APPEND_ONLY_HINT,
`You may reply, but usually ${SILENT_REPLY_TOKEN} is correct.`, // 你可以回复,但通常 NO_REPLY 是正确的
].join(" ");
Memory 的核心特点:
- 存储形式:Markdown 文件(
memory/YYYY-MM-DD.md和MEMORY.md) - 位置:workspace 目录下
- 内容来源:
- 压缩前Flush:通过
before_compactionhook 触发 - 压缩后Flush:通过
after_compactionhook 触发 - 外部文件同步:QMD 文件、
session目录 - 插件主动写入:通过 memory-plugin API
- 压缩前Flush:通过
3. 向量库:Memory的"高效检索引擎"
源码路径:extensions/memory-lancedb/index.ts
// extensions/memory-lancedb/index.ts:28-40
type MemoryEntry = {
id: string;
text: string; // 原始文本 - 向量库实际上存储了完整内容
vector: number[];
importance: number;
category: MemoryCategory;
createdAt: number;
};
向量库的核心特点:
- 存储形式:LanceDB(SQLite + 向量)
- 位置:
~/.openclaw/memory/<agentId>.sqlite - 向量库同时存储了完整的 MemoryEntry(text + vector),而非仅索引
- 检索方式:混合搜索(BM25 + 向量)
二、信息流转:从Session到Memory再到向量库
整体流程图
┌─────────────────────────────────────────────────────────────────────────────┐
│ OpenClaw 记忆系统信息流转 │
└─────────────────────────────────────────────────────────────────────────────┘
用户消息 ──▶ Session Transcript (*.jsonl) ──▶ before_prompt_build hook
│ │
│ ┌───────────────────────────────┘
│ ▼
│ ┌───────────────┐
│ │ 加载历史消息 │
│ │ 构建 Context │
│ └───────────────┘
│ │
│ ▼
│ ┌──────────────────────────────────────────┐
│ │ Compaction 触发条件 │
│ │ (tokenCount >= contextWindow - reserve │
│ │ - softThresholdTokens) │
│ └──────────────────────────────────────────┘
│ │
│ ▼
│ ┌──────────────────────────────────────────┐
│ │ 异步执行 Hooks (顺序执行) │
│ │ ┌─────────────────┐ ┌────────────────┐ │
│ │ │ before_compaction│ -> │ after_compaction│ │
│ │ │ (异步) │ │ (异步) │ │
│ │ └─────────────────┘ └────────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌─────────────────────────────────────┐ │
│ │ │ Memory Flush (写入Memory) │ │
│ │ │ - 写入 memory/YYYY-MM-DD.md │ │
│ │ │ - 写入 MEMORY.md (长期记忆) │ │
│ │ └─────────────────────────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────────────────────────┐ │
│ │ │ Session 压缩 │ │
│ │ │ - 生成 Summary 替换原始消息 │ │
│ │ │ - 写入 transcript │ │
│ │ └─────────────────────────────────────┘ │
│ └──────────────────────────────────────────┘
│ │
│ ▼
│ ┌──────────────────────────────────────────┐
│ │ 向量库同步 (异步) │
│ │ - Memory 新内容 -> 向量 │
│ │ - 更新 LanceDB 索引 │
│ └──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 记忆检索流程 │
└─────────────────────────────────────────────────────────────────────────────┘
用户新问题 ──▶ Memory Search (向量检索)
│ │
│ ▼
│ ┌─────────────────┐
│ │ 1. 转向量 │
│ │ 2. 混合搜索 │
│ │ (BM25 + 向量) │
│ └─────────────────┘
│ │
│ ▼
│ ┌─────────────────┐
│ │ 3. 返回摘要 │
│ │ 4. 注入 Prompt │
│ └─────────────────┘
│ │
└─────────────▶▶ 模型响应
步骤1:会话消息的写入
源码路径:src/plugins/hooks.ts
消息写入 transcript 是同步操作:
// src/plugins/hooks.ts:728
// session transcripts are appended synchronously.
每条消息通过 before_message_write hook 写入磁盘。
步骤2:压缩触发的条件判断
源码路径:src/auto-reply/reply/memory-flush.ts
// src/auto-reply/reply/memory-flush.ts:53-78
export function shouldRunMemoryFlush(params: {
entry?: Pick<
SessionEntry,
"totalTokens" | "totalTokensFresh" | "compactionCount" | "memoryFlushCompactionCount"
>;
tokenCount?: number;
contextWindowTokens: number;
reserveTokensFloor: number;
softThresholdTokens: number;
}): boolean {
const state = resolveMemoryFlushGateState(params);
if (!state || state.totalTokens < state.threshold) {
return false; // 未达到阈值,不触发
}
if (hasAlreadyFlushedForCurrentCompaction(state.entry)) {
return false; // 当前压缩周期已执行过 flush
}
return true;
}
触发公式:
totalTokens >= contextWindowTokens - reserveTokensFloor - softThresholdTokens
公式中文解释
| 参数 | 含义 |
|---|---|
totalTokens | 当前会话已使用的 token 数量 |
contextWindowTokens | 模型上下文窗口上限(如 128k、200k tokens) |
reserveTokensFloor | 保留 tokens 下限(默认 20000,确保压缩后仍有足够空间) |
softThresholdTokens | 软阈值(默认 4000) |
触发逻辑:
可用的 token 空间 = contextWindowTokens - reserveTokensFloor - softThresholdTokens
当 totalTokens >= 可用空间时,触发 flush
默认值计算(假设 128k 上下文):
128000 - 20000 - 4000 = 104000 tokens
即当会话使用超过 104,000 tokens 时,触发 Memory Flush,提醒模型将持久记忆写入磁盘。
配置位置:
src/config/schema.help.ts:839
步骤3:Compaction Hooks 的执行(异步)
源码路径:src/agents/pi-embedded-runner/compact.ts
// src/agents/pi-embedded-runner/compact.ts:423-476
async function runBeforeCompactionHooks(params: {
hookRunner?: CompactionHookRunner | null;
sessionId: string;
sessionKey?: string;
sessionAgentId: string;
workspaceDir: string;
messageProvider?: string;
metrics: ReturnType<typeof buildBeforeCompactionHookMetrics>;
}) {
// ... 前置处理
// 调用 before_compaction hook - 异步执行
if (params.hookRunner?.hasHooks?.("before_compaction")) {
try {
await params.hookRunner.runBeforeCompaction?.(
{
messageCount: params.metrics.messageCountBefore,
tokenCount: params.metrics.tokenCountBefore,
},
// ...
);
} catch (err) {
log.warn("before_compaction hook failed", { /* ... */ });
}
}
}
// src/agents/pi-embedded-runner/compact.ts:505-564
async function runAfterCompactionHooks(params: {
// ...
}) {
// 调用 after_compaction hook - 异步执行
if (params.hookRunner?.hasHooks?.("after_compaction")) {
try {
await params.hookRunner.runAfterCompaction?.(
// ...
);
} catch (err) {
log.warn("after_compaction hook failed", { /* ... */ });
}
}
}
关键点:
before_compaction和after_compaction都是异步执行(使用await)- 两者是顺序执行:先执行
before_compaction,等待完成后执行after_compaction - 都可能触发 Memory Flush(通过防重复机制避免重复)
防止重复 Flush 的机制
虽然 before_compaction 和 after_compaction 都可能触发 Memory Flush,但不会重复。源码有防重复机制:
源码路径:src/auto-reply/reply/memory-flush.ts
// src/auto-reply/reply/memory-flush.ts:96-107
/**
* Returns true when a memory flush has already been performed for the current
* compaction cycle. This prevents repeated flush runs within the same cycle —
* important for both the token-based and transcript-size–based trigger paths.
*/
export function hasAlreadyFlushedForCurrentCompaction(
entry: Pick<SessionEntry, "compactionCount" | "memoryFlushCompactionCount">,
): boolean {
const compactionCount = entry.compactionCount ?? 0;
const lastFlushAt = entry.memoryFlushCompactionCount;
return typeof lastFlushAt === "number" && lastFlushAt === compactionCount; // 相等则跳过
}
防重复机制说明:
- 记录次数:每次压缩完成后,更新
memoryFlushCompactionCount = compactionCount - 检查机制:触发 flush 前,检查
memoryFlushCompactionCount是否等于当前compactionCount - 相同则跳过:如果相等,说明本轮已经 flush 过,直接返回
false跳过
// src/auto-reply/reply/memory-flush.ts:73
if (hasAlreadyFlushedForCurrentCompaction(state.entry)) {
return false; // 当前压缩周期已执行过 flush,跳过
}
因此,同一个 compaction 周期内(无论 before_compaction 还是 after_compaction hook 触发),Memory Flush 只会执行一次。
步骤4:Compaction 摘要生成的提示词(含中文对照)
备注:此处的 Compaction 指的是 Session Transcript 压缩(生成摘要替换原始消息),而非 Memory Flush。提示词用于指导 LLM 如何生成高质量的会话摘要。
源码路径:src/agents/compaction.ts
4.1 合并多部分摘要的提示词
// src/agents/compaction.ts:17-30
const MERGE_SUMMARIES_INSTRUCTIONS = [
"Merge these partial summaries into a single cohesive summary.", // 将这些部分摘要合并为单个连贯的摘要
"",
"MUST PRESERVE:",
"- Active tasks and their current status (in-progress, blocked, pending)", // 必须保留:当前进行中的任务及其状态(进行中、阻塞、待处理)
"- Batch operation progress (e.g., '5/17 items completed')", // 批量操作进度(例如:'已完成 5/17 项')
"- The last thing the user requested and what was being done about it", // 用户最后请求的内容及正在处理的事项
"- Decisions made and their rationale", // 作出的决策及其理由
"- TODOs, open questions, and constraints", // 待办事项、悬而未决的问题和约束条件
"- Any commitments or follow-ups promised", // 承诺或待跟进事项
"",
"PRIORITIZE recent context over older history. The agent needs to know", // 优先近期上下文胜过旧历史。智能体需要知道
"what it was doing, not just what was discussed.", // 它正在做什么,而不仅仅讨论了什么
].join("\n");
4.2 标识符保留提示词
// src/agents/compaction.ts:31-33
const IDENTIFIER_PRESERVATION_INSTRUCTIONS =
"Preserve all opaque identifiers exactly as written (no shortening or reconstruction), " + // 准确保留所有不透明标识符(不缩短或重构)
"including UUIDs, hashes, IDs, tokens, API keys, hostnames, IPs, ports, URLs, and file names."; // 包括 UUID、哈希、ID、令牌、API密钥、主机名、IP、端口、URL 和文件名
4.3 构建摘要指令的函数
// src/agents/compaction.ts:54-70
export function buildCompactionSummarizationInstructions(
customInstructions?: string,
instructions?: CompactionSummarizationInstructions,
): string | undefined {
const custom = customInstructions?.trim();
const identifierPreservation = resolveIdentifierPreservationInstructions(instructions);
if (!identifierPreservation && !custom) {
return undefined;
}
if (!custom) {
return identifierPreservation;
}
if (!identifierPreservation) {
return `Additional focus:\n${custom}`;
}
return `${identifierPreservation}\n\nAdditional focus:\n${custom}`;
}
4.4 生成摘要的核心函数
// src/agents/compaction.ts:237-258
async function summarizeChunks(params: {
messages: AgentMessage[];
model: NonNullable<ExtensionContext["model"]>;
apiKey: string;
signal: AbortSignal;
reserveTokens: number;
maxChunkTokens: number;
customInstructions?: string;
summarizationInstructions?: CompactionSummarizationInstructions;
previousSummary?: string;
}): Promise<string> {
// 使用 generateSummary 生成摘要
summary = await retryAsync(
() =>
generateSummary(
chunk,
params.model,
params.reserveTokens,
params.apiKey,
params.signal,
effectiveInstructions,
summary, // 传入上一次的摘要用于合并
),
// ...
);
}
4.5 自定义压缩扩展机制(Compaction Safeguard)
备注:Pi 官方文档说明无法自定义压缩提示词,但 OpenClaw 通过 Pi SDK 的 Extension API 实现了自定义扩展。
OpenClaw 利用 Pi SDK 的 session_before_compact 事件,在压缩流程中注入自定义提示词逻辑。
源码路径:src/agents/pi-extensions/compaction-safeguard.ts
// src/agents/pi-extensions/compaction-safeguard.ts:780-781
export default function compactionSafeguardExtension(api: ExtensionAPI): void {
api.on("session_before_compact", async (event, ctx) => {
// 拦截压缩事件,返回自定义压缩结果
});
}
4.5.1 扩展架构
用户配置 customInstructions
↓
配置文件 (agents.defaults.compaction.customInstructions)
↓
compaction-instructions.ts 解析提示词
↓
compaction-safeguard.ts 扩展拦截 session_before_compact
↓
调用 summarizeInStages 传入自定义提示词
↓
Pi SDK 的 generateSummary 生成摘要
4.5.2 核心处理流程
以下为简化版代码,展示核心逻辑:
// src/agents/pi-extensions/compaction-safeguard.ts:781
api.on("session_before_compact", async (event, ctx) => {
// event 来自 Pi SDK,包含待压缩的消息和现有摘要
const { preparation, customInstructions: eventInstructions, signal } = event;
// 从运行时配置获取自定义提示词
const runtime = getCompactionSafeguardRuntime(ctx.sessionManager);
// 解析自定义提示词(优先级:事件提示词 > 配置提示词 > 默认提示词)
const customInstructions = resolveCompactionInstructions(
eventInstructions, // 事件级提示词
runtime?.customInstructions, // 配置级提示词
);
const summarizationInstructions = {
identifierPolicy: runtime?.identifierPolicy,
identifierInstructions: runtime?.identifierInstructions,
};
// 待压缩的消息来自 preparation
const messagesToSummarize = preparation.messagesToSummarize;
// 调用 summarizeInStages,传入自定义提示词
const summary = await summarizeInStages({
messages: messagesToSummarize,
model,
apiKey,
signal,
customInstructions, // 关键:注入自定义提示词
summarizationInstructions,
previousSummary: preparation.previousSummary,
});
// 返回自定义压缩结果
return {
compaction: {
summary,
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
},
};
});
4.5.3 提示词解析逻辑
源码路径:src/agents/pi-extensions/compaction-instructions.ts
// src/agents/pi-extensions/compaction-instructions.ts:13-17
export const DEFAULT_COMPACTION_INSTRUCTIONS =
"Write the summary body in the primary language used in the conversation.\n" +
"Focus on factual content: what was discussed, decisions made, and current state.\n" +
"Keep the required summary structure and section headers unchanged.\n" +
"Do not translate or alter code, file paths, identifiers, or error messages.";
// 优先级:事件提示词 > 运行时配置 > 默认值
export function resolveCompactionInstructions(
eventInstructions: string | undefined,
runtimeInstructions: string | undefined,
): string {
const resolved =
normalize(eventInstructions) ??
normalize(runtimeInstructions) ??
DEFAULT_COMPACTION_INSTRUCTIONS;
return truncateUnicodeSafe(resolved, MAX_INSTRUCTION_LENGTH);
}
4.5.4 配置项
| 配置项 | 路径 | 说明 |
|---|---|---|
customInstructions | agents.defaults.compaction.customInstructions | 自定义压缩提示词 |
identifierPolicy | agents.defaults.compaction.identifierPolicy | 标识符保留策略 (strict/custom/off) |
identifierInstructions | agents.defaults.compaction.identifierInstructions | 自定义标识符保留提示词 |
mode | agents.defaults.compaction.mode | 压缩模式 (default/safeguard) |
使用示例:
agents:
defaults:
compaction:
mode: safeguard # 启用 safeguard 扩展
customInstructions: |
用中文摘要,保留所有代码细节和文件名
identifierPolicy: strict
recentTurnsPreserve: 3 # 保留最近 3 轮对话
三、关键机制:避免冲突与冗余的核心设计
1. 两种"摘要提取"的区别
| 类型 | 用途 | 存储位置 | 生命周期 |
|---|---|---|---|
| Session 压缩摘要 | 缩短当前 Session 长度 | Session 内部(transcript) | 临时,对话结束可能失效 |
| Memory 摘要 | 长期记忆存储 | Memory 文件(磁盘) | 持久化,跨对话生效 |
源码路径:src/agents/compaction.ts
// src/agents/compaction.ts:237-258
async function summarizeChunks(params: {
messages: AgentMessage[];
model: NonNullable<ExtensionContext["model"]>;
apiKey: string;
signal: AbortSignal;
reserveTokens: number;
maxChunkTokens: number;
customInstructions?: string;
summarizationInstructions?: CompactionSummarizationInstructions;
previousSummary?: string;
}): Promise<string> {
// 使用 generateSummary 生成摘要
summary = await retryAsync(
() =>
generateSummary(
chunk,
params.model,
params.reserveTokens,
params.apiKey,
params.signal,
effectiveInstructions,
summary, // 传入上一次的摘要用于合并
),
// ...
);
}
步骤5:向量库的同步配置(含中文对照)
源码路径:src/agents/pi-embedded-runner/compact.ts
// src/agents/pi-embedded-runner/compact.ts:306
log.warn(`memory sync skipped (post-compaction): ${String(err)}`);
配置项:src/config/schema.help.ts
// src/config/schema.help.ts:1055
// Controls post-compaction session memory reindex mode: "off", "async", or "await" (default: "async").
// postCompactionForce: "await" - 等待完成后再继续; "async" - 不等待; "off" - 禁用
同步模式说明:
"await":等待向量库同步完成后再继续(最强一致性)"async":不等待向量库同步(降低压缩延迟)"off":禁用压缩后的向量库同步
2. 向量库与Memory的关系
源码实际情况:
向量库( LanceDB)同时存储了完整的 MemoryEntry,而非仅作为索引:
// extensions/memory-lancedb/index.ts:92-105
async store(entry: Omit<MemoryEntry, "id" | "createdAt">): Promise<MemoryEntry> {
await this.ensureInitialized();
const fullEntry: MemoryEntry = {
...entry,
id: randomUUID(),
createdAt: Date.now(),
};
// 存储完整的 text 和 vector
}
结论:向量库是 Memory 的主要存储 + 检索引擎,而非仅索引。
3. Plugin Hooks 汇总
源码路径:src/plugins/types.ts
// src/plugins/types.ts:1464-1490
export type PluginHookName =
| "before_model_resolve"
| "before_prompt_build"
| "before_agent_start"
| "llm_input"
| "llm_output"
| "agent_end"
| "before_compaction" // 压缩前 - 异步
| "after_compaction" // 压缩后 - 异步
| "before_reset"
| "inbound_claim"
| "message_received"
| "message_sending"
| "message_sent"
| "before_tool_call"
| "after_tool_call"
| "tool_result_persist"
| "before_message_write"
| "session_start"
| "session_end"
// ...
四、总结:记忆系统的核心逻辑
OpenClaw 通过"Session-Memory-向量库"三层介质,实现了"实时对话理解-长期记忆存储-高效记忆检索"的闭环:
| 组件 | 职责 | 存储形式 | 同步/异步 |
|---|---|---|---|
| Session | 当下对话 | JSONL transcript 文件 | 同步写入 |
| Memory | 过去记忆 | Markdown 文件 | 异步(hook 触发) |
| 向量库 | 连接过去与现在 | LanceDB (SQLite + 向量) | 异步 |
关键 Hook 执行顺序
用户消息
│
▼
Message Write (同步)
│
▼
before_prompt_build (同步加载历史)
│
▼
[Token 阈值触发]
│
▼
before_compaction (异步) ──▶ Memory Flush
│
▼
Compaction (生成 Summary)
│
▼
after_compaction (异步) ──▶ Memory Flush (防重复)
│
▼
向量库同步 (异步)
更多推荐



所有评论(0)