OpenClaw / 05
Context Engine:OpenClaw 的记忆架构
Agent 的”记忆”不是一个简单的消息列表。OpenClaw 的记忆系统分三层:
flowchart TD
A["持久化记忆 MEMORY.md<br>跨 Session 存活,人类可编辑"]
B["Context Engine<br>每轮动态组装上下文,token 预算内最大化信息量"]
C["Session 管理<br>会话生命周期、transcript 持久化"]
A --> B --> C
这三层各自解决不同的问题,组合起来才是一个完整的生产级记忆系统。
第一层:MEMORY.md — 文件级持久化
OpenClaw 使用 Markdown 文件作为跨 Session 记忆的载体:
// src/memory/root-memory-files.ts
export const MEMORY_FILE_NAMES = ['MEMORY.md', 'memory.md'] // canonical + legacy
export function findMemoryFile(workspacePath: string): string | null {
for (const name of MEMORY_FILE_NAMES) {
const fullPath = path.join(workspacePath, name)
if (fs.existsSync(fullPath)) return fullPath
}
return null
}
MEMORY.md 的设计哲学
为什么是 Markdown 文件而不是数据库?
- 人类可读可编辑:用户可以直接打开文件查看、修改、删除记忆
- Git 友好:可以版本控制、diff、回滚
- 零依赖:不需要额外服务(Redis、Postgres),文件系统就够了
- LLM 原生格式:Markdown 是 LLM 最擅长读写的格式
MEMORY.md 的结构
- [用户偏好](user_preferences.md) — 喜欢简洁回复,代码注释用英文
- [项目架构](project_arch.md) — monorepo,pnpm workspace,TypeScript
- [禁止操作](forbidden.md) — 不允许 git push,不删除 .env
每条记忆是一个独立的 .md 文件,MEMORY.md 是索引。这样:
- 单条记忆可以独立更新/删除
- 索引文件保持简短,不超出上下文预算
- Agent 按需读取具体记忆文件
记忆的写入时机
// Agent 在以下时机写入记忆:
// 1. 用户显式要求 "记住这个"
// 2. 用户纠正了 Agent 的行为(feedback 类型)
// 3. 发现了非显而易见的项目规则
// 4. 用户角色/偏好信息
// 记忆不应该存储的:
// - 代码结构(读文件就能得到)
// - Git 历史(git log 就能查到)
// - 临时任务状态(用 task list 跟踪)
第二层:Context Engine — 可插拔的上下文管理
这是 OpenClaw 记忆系统的核心。Context Engine 负责在每次 LLM 调用前,在 token 预算内组装最优的上下文。
接口定义
// src/context-engine/index.ts
export interface ContextEngine {
// 初始化:加载 MEMORY.md、系统指令等
bootstrap(config: EngineConfig): Promise<void>
// 摄入新内容(用户消息、工具结果)
ingest(event: ContextEvent): Promise<void>
// 核心方法:组装发送给 LLM 的完整 messages
assemble(budget: TokenBudget): Promise<Message[]>
// 压缩:当上下文接近预算上限时触发
compact(): Promise<void>
// 每轮结束后的维护(更新摘要、清理过期内容)
maintain(): Promise<void>
// 生命周期钩子
afterTurn(turnResult: TurnResult): Promise<void>
}
assemble():上下文组装的核心
assemble() 是整个记忆系统最关键的方法。它要在有限的 token 预算内,决定哪些信息进入上下文:
async assemble(budget: TokenBudget): Promise<Message[]> {
const messages: Message[] = []
let tokensUsed = 0
// 1. 系统指令(最高优先级,必须包含)
const systemPrompt = this.buildSystemPrompt()
messages.push({ role: 'system', content: systemPrompt })
tokensUsed += countTokens(systemPrompt)
// 2. MEMORY.md 索引(持久化记忆)
const memoryContent = await this.loadMemoryIndex()
if (memoryContent) {
messages[0].content += `\n\n# Memory\n${memoryContent}`
tokensUsed += countTokens(memoryContent)
}
// 3. 早期对话的压缩摘要(如果有)
if (this.compressedSummary) {
messages.push({ role: 'assistant', content: `[Earlier context]\n${this.compressedSummary}` })
tokensUsed += countTokens(this.compressedSummary)
}
// 4. 近期对话(从最新往前填充,直到预算用完)
const recentMessages = this.transcript.slice().reverse()
const fittingMessages: Message[] = []
for (const msg of recentMessages) {
const msgTokens = countTokens(msg.content)
if (tokensUsed + msgTokens > budget.maxTokens) break
fittingMessages.unshift(msg)
tokensUsed += msgTokens
}
messages.push(...fittingMessages)
return messages
}
优先级从高到低:系统指令 > 持久化记忆 > 压缩摘要 > 近期对话。预算不够时,从低优先级开始裁剪。
compact():上下文压缩
当对话历史接近 token 预算时触发压缩:
async compact(): Promise<void> {
// 取出需要压缩的早期消息
const cutoff = this.transcript.length - this.keepRecentCount
const toCompress = this.transcript.slice(0, cutoff)
// 用 LLM 生成摘要
const summary = await this.llm.chat([
{ role: 'system', content: 'Summarize the key decisions, findings, and context from this conversation. Preserve actionable information.' },
...toCompress
])
// 替换为摘要
this.compressedSummary = summary.content
this.transcript = this.transcript.slice(cutoff)
}
面试关键区分:压缩 vs 截断
| 方案 | 做法 | 问题 |
|---|---|---|
| 截断 | 直接丢弃最早的消息 | 可能丢失关键决策和约束 |
| 压缩 | 用 LLM 生成摘要替代原始消息 | 保留语义,代价是一次 LLM 调用 |
| OpenClaw 方案 | 压缩 + 持久化关键信息到 MEMORY.md | 重要信息永不丢失 |
可插拔架构
// src/context-engine/registry.ts
// 进程全局单例注册表,支持切换不同的 Context Engine 实现
class ContextEngineRegistry {
private engines: Map<string, ContextEngineFactory> = new Map()
private activeEngine: string = 'default'
register(name: string, factory: ContextEngineFactory): void {
this.engines.set(name, factory)
}
getActive(): ContextEngine {
return this.engines.get(this.activeEngine)!.create()
}
}
// 配置文件中选择策略
// config.plugins.slots.contextEngine = "legacy" | "semantic" | "custom"
为什么做成可插拔的?不同场景需要不同的上下文策略:
- 短对话场景:不需要压缩,直接全量传入
- 长任务场景:激进压缩,只保留最近几轮 + 任务计划
- RAG 场景:每轮动态注入检索结果,压缩早期检索内容
Dreaming 系统:记忆的自动整理
OpenClaw 独有的 Dreaming 机制——受人类睡眠中记忆巩固的启发,通过定时任务自动整理和丰富长期记忆。
三阶段合成
| 阶段 | 对应睡眠 | 做什么 |
|---|---|---|
| Light Sleep | 浅睡眠 | 扫描当日对话,提取候选记忆片段 |
| Deep Sleep | 深睡眠 | 合并相似主题、修复引用、消除矛盾 |
| REM | 快速眼动 | 跨主题关联、生成新的 Compiled Truth |
记忆评分公式
每条候选记忆通过加权评分决定是否被”提升”为长期记忆:
Score = frequency(0.24) + relevance(0.30) + query_diversity(0.15)
+ recency(0.15) + consolidation(0.10) + conceptual_richness(0.06)
| 维度 | 权重 | 含义 |
|---|---|---|
| relevance | 0.30 | 与用户核心工作的相关度 |
| frequency | 0.24 | 被提及/使用的频率 |
| query_diversity | 0.15 | 在不同类型查询中出现 |
| recency | 0.15 | 最近的信息权重更高 |
| consolidation | 0.10 | 已经被其他记忆引用的次数 |
| conceptual_richness | 0.06 | 包含的概念复杂度 |
只有有据可查的记忆片段(grounded snippets)才有资格被提升——防止幻觉记忆进入长期存储。
运行机制
// 默认 cron 配置:每天凌晨 3 点运行
dreaming:
schedule: "0 3 * * *"
phases: [light, deep, rem]
maxDuration: 1800 // 最多运行 30 分钟
输出写入 DREAMS.md——Agent 的”巩固日记”:
<!-- DREAMS.md -->
## 2026-05-09 Consolidation
### Promoted to long-term
- 用户偏好 TypeScript monorepo 结构 (score: 0.87)
- 项目禁止 git push 到 remote (score: 0.82)
### Merged
- "API 架构偏好" + "微服务选型" → 合并为 "后端架构偏好"
### Retired
- "调试 ESLint 配置" — 问题已解决,不再相关
面试价值
Dreaming 系统体现了一个关键认知:记忆不是只写不删的日志,而是需要主动维护的知识库。面试中提到这个机制,能展示你对”记忆质量 > 记忆数量”这个生产级认知的理解。
Compaction:标识符保留策略
OpenClaw 的 Compaction 不是简单的”用 LLM 总结”——它有一套精确的规则确保压缩后的上下文仍然可用:
// 压缩时的标识符保留策略
compaction:
identifierPreservation: "strict" // 默认严格模式
// strict: 文件路径、函数名、变量名、行号必须原样保留
// relaxed: 只保留文件路径和函数名
// none: 不做特殊保留(不推荐)
为什么需要标识符保留?
// 不保留标识符的压缩结果:
"之前分析了认证模块,发现了几个安全问题。"
→ Agent 无法继续工作(哪个文件?哪个函数?哪一行?)
// 保留标识符的压缩结果:
"分析了 src/auth/login.ts:42-78 的 validateToken() 函数,
发现 JWT 验证缺少 exp 字段检查(第 56 行)。"
→ Agent 可以直接定位并继续操作
Compaction 还支持双模式:
- 自动模式:token 使用率超过阈值时自动触发
- 手动模式:用户显式请求(如 Claude Code 的
/compact命令)
压缩前会执行 auto-flush——把关键信息写入 MEMORY.md,确保压缩不会导致知识丢失。
记忆后端:可插拔存储
OpenClaw 支持多种记忆后端:
| 后端 | 适用场景 | 特点 |
|---|---|---|
| SQLite(默认) | 个人使用、本地部署 | 零配置、单文件、够用 |
| LanceDB | 需要向量检索 | 嵌入式向量库,无服务 |
| Honcho | 多用户、大规模 | 专为 AI Agent 设计的记忆服务 |
| GBrain | 生产级、企业级 | Postgres + pgvector,功能最全 |
第三层:Session 管理
// src/sessions/index.ts
export interface Session {
id: string // 唯一标识
createdAt: Date
lastActiveAt: Date
transcript: TranscriptEvent[] // 完整事件记录
config: SessionConfig // 模型、级别等覆盖配置
}
Session 管理解决的问题:
- 多用户隔离:不同用户的会话互不干扰
- 断点续传:
agentLoopContinue()从 transcript 恢复 - 审计追踪:所有交互都有记录
GBrain:外部记忆宿主(高级)
GBrain 是 OpenClaw 的生产级外部记忆系统,解决 MEMORY.md 无法处理的大规模记忆场景:
架构
Postgres + pgvector
├── 混合检索(向量 + BM25 + RRF 融合)
├── Compiled Truth 页(当前理解,可被更新)
├── Timeline 条目(追加式证据链,不可变)
└── 知识图谱(实体引用 + 类型化链接)
Compiled Truth + Timeline 模式
传统做法:每次新信息来了就追加一条记忆。问题:记忆越来越多,检索越来越难。
GBrain 的做法:
Page: "用户的技术栈偏好"
├── Compiled Truth: "偏好 TypeScript + pnpm,讨厌 Python 类型系统"
└── Timeline:
├── 2025-03-01: 用户说 "我主要写 TypeScript"
├── 2025-03-15: 用户说 "Python 的类型注解太弱了"
└── 2025-04-02: 用户在项目中使用了 pnpm workspace
Compiled Truth 是 Agent 直接使用的结论;Timeline 是支撑这个结论的原始证据。当新信息到来时,更新 Compiled Truth。
Dream Cycle(夜间合成)
GBrain 运行定时任务进行知识整理:
- 合并相似主题的 Pages
- 检测过时信息并标记
- 从 Timeline 中提取新的 Compiled Truth
- 丰富知识图谱的链接关系
GBrain 生产数据
GBrain 在实际生产环境中的规模(Garry Tan 的个人使用):
- 17,888 个知识页面
- 4,383 个人物实体
- 723 个公司实体
- P@5: 49.1%(前 5 结果中有正确答案的概率)
- R@5: 97.9%(正确答案出现在前 5 的概率)
Minions Job Queue
GBrain 用 Postgres 原生 Job Queue(Minions)替代了不稳定的 sub-agent 方式做后台任务:
// 传统方式:spawn sub-agent 做异步任务
// 问题:进程崩溃丢失状态、无法重试、无法观测
// GBrain 方式:Postgres 持久化 Job Queue
await minions.enqueue({
type: 'consolidate_memory',
payload: { pageId: 'tech-preferences', newEvidence: '...' },
retries: 3,
timeout: 60_000
})
好处:crash-safe、可重试、可观测、有事务保证。
设计原则:确定性操作优先于 LLM 判断
GBrain 在能用确定性逻辑的地方绝不用 LLM:
- 实体抽取:正则 + 规则,不用 NER 模型
- 关系链接:模式匹配(”works_at”、”invested_in”),不用 LLM 推理
- 知识图谱连边:基于共现和明确语法结构,不用向量相似度
LLM 只在必须推理时使用(如 Compiled Truth 的更新、Deep Sleep 阶段的冲突消解)。
对比:各家 Agent 的记忆方案
| Agent | 短期记忆 | 长期记忆 | 跨 Session |
|---|---|---|---|
| ChatGPT | 消息列表 | Memory 功能(自然语言) | ✅ |
| Claude Code | 消息列表 + compact | CLAUDE.md + MEMORY.md | ✅ |
| pi-mono | 消息列表 | ❌ | ❌ |
| OpenClaw | Context Engine | MEMORY.md + GBrain | ✅ |
| LangChain | ConversationBufferMemory | 向量数据库 | 需自建 |
面试高频题
Q:Agent 的”记忆”和”上下文”有什么区别?
上下文是单次 LLM 调用时传入的 messages——有 token 上限,会话结束就消失。记忆是跨会话持久化的信息——用户偏好、项目规则、关键决策。Context Engine 的工作就是把合适的记忆加载进当前上下文。
Q:为什么 OpenClaw 用文件(MEMORY.md)而不是数据库存记忆?
三个原因:1) 人类可直接查看和编辑(透明性);2) Git 版本控制(可追溯);3) 零依赖(不需要额外服务)。对大规模记忆场景(数千条),再引入 GBrain(Postgres)。
Q:Context Engine 的 assemble() 方法为什么重要?
它解决了 Agent 的核心难题:token 预算有限,但需要最大化上下文信息量。assemble() 的优先级策略决定了 Agent “记住什么、忘记什么”——这直接影响任务完成质量。好的 assemble 策略 = 好的 Agent。
Q:压缩摘要的弊端是什么?怎么缓解?
弊端:摘要会丢失细节(具体代码行号、精确数字)。缓解方案:1) 把关键信息持久化到 MEMORY.md(不依赖摘要);2) 压缩时用 LLM 判断哪些细节必须保留;3) 保留最近 N 轮完整消息不压缩(近期信息最重要)。