声明:本文档基于源码分析编写,所有结论均以源码为准。

在基于 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_build hook 加载历史消息

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.mdMEMORY.md
  • 位置:workspace 目录下
  • 内容来源:
    1. 压缩前Flush:通过 before_compaction hook 触发
    2. 压缩后Flush:通过 after_compaction hook 触发
    3. 外部文件同步:QMD 文件、session 目录
    4. 插件主动写入:通过 memory-plugin API

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_compactionafter_compaction 都是异步执行(使用 await
  • 两者是顺序执行:先执行 before_compaction,等待完成后执行 after_compaction
  • 都可能触发 Memory Flush(通过防重复机制避免重复)
防止重复 Flush 的机制

虽然 before_compactionafter_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;  // 相等则跳过
}

防重复机制说明

  1. 记录次数:每次压缩完成后,更新 memoryFlushCompactionCount = compactionCount
  2. 检查机制:触发 flush 前,检查 memoryFlushCompactionCount 是否等于当前 compactionCount
  3. 相同则跳过:如果相等,说明本轮已经 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 配置项
配置项路径说明
customInstructionsagents.defaults.compaction.customInstructions自定义压缩提示词
identifierPolicyagents.defaults.compaction.identifierPolicy标识符保留策略 (strict/custom/off)
identifierInstructionsagents.defaults.compaction.identifierInstructions自定义标识符保留提示词
modeagents.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 (防重复)
    │
    ▼
向量库同步 (异步)
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐