OpenClaw 3.7 ContextEngine 插件接口源码级拆解
一、AI Agent 的上下文管理为什么是'最头疼的问题'?
在与 AI 对话时,每一轮历史消息都作为'上下文'发送给模型。模型需要看到之前的对话内容才能理解当前问题。
核心问题在于:模型的上下文窗口有大小限制。
即使拥有 128K token 的窗口,聊了 200 轮加上工具调用返回值,token 也很容易超限。此时系统必须决策:
- 哪些消息保留?最近的肯定要,但边界在哪?
- 旧消息如何处理?直接截断还是压缩成摘要?
- 组装顺序如何?系统提示 + 历史 + 工具 Schema 的顺序和内容怎么组合?
这就是上下文管理(Context Management)。每个实用的 AI Agent 框架都绕不开此问题。
关键在于:这些处理逻辑一旦写死在核心代码里,日后每次调整都是高风险操作。
二、旧架构痛点分析
逆向 OpenClaw v2026.3.7 之前的源码可见,旧的上下文管理是一条完全硬编码的流水线,散布在 5+ 个文件中,没有任何抽象层:
用户消息进来 → SessionManager 写入 JSONL → 加载全部历史消息 → sanitize(清洗) → validate(校验) → limit(截断) → repair(修复) → 送给模型
2.1 Sanitize 复杂度
函数 sanitizeSessionHistory() 内部塞了大约 10 个处理步骤:
- 标注跨会话用户消息
- 清理/缩放图片(不同模型有不同图片限制)
- 删除
<thinking>块 - 规范化工具调用的名称和 ID
- 修复孤立的 tool_use → tool_result 配对
- 删除工具返回值中的冗余字段
- 清理过期的 assistant usage 快照
- 记录模型快照元数据
- 降级 OpenAI 推理格式(仅 OpenAI Responses API)
- 修复 Google/vLLM 的轮次顺序约束(仅 Google provider)
2.2 压缩逻辑重复
当 token 快要超限时,系统有个'压缩'机制——把旧对话总结成摘要,删掉原始消息。
这个逻辑在 compact.ts(约 763 行),是最大的痛点。因为压缩需要调模型来生成摘要,而调模型需要完整的上下文环境,于是 compact.ts 把 attempt.ts 里大量环境构建代码复制了一遍:
compact.ts 独立重复的逻辑:
├── resolveModel() — 模型解析
├── resolveModelAuthMode() — 认证模式
├── resolveSandboxContext() — 沙箱配置
├── resolveChannelCapabilities() — 频道能力
├── sanitize→validate→limit→repair — 完整管线
└── ...更多重复代码
改一处忘了改另一处?上线即 bug。
2.3 三种压缩触发方式全是硬编码
旧架构有三种压缩触发方式:
- SDK 自动检测:Agent 运行期间检测到即将超限,主动触发
- 用户手动
/compact:用户在对话中输入命令 - Overflow 重试:模型返回 token 超限错误,自动重试(最多 3 次)
问题不在于触发方式不够多,而在于——你无法通过插件替换这些策略。想自定义'什么时候该压缩'的判断逻辑?只能改核心代码。
2.4 痛点总结
| 痛点 | 具体表现 |
|---|---|
| 逻辑散落 | 散布在 sanitize、validate、limit、repair、compact 5+ 个文件 |
| 大量重复 | compact.ts 复制了 attempt.ts 的模型解析、沙箱配置等大量代码 |
| 无法替换 | 想换个压缩算法?必须改核心代码 |
| 策略硬编码 | 三种压缩触发方式全硬编码在核心代码 |
| 无后处理 | Agent 一轮结束后没有任何钩子做后续优化 |
| 子 Agent 隔离 | 父子 Agent 之间没有上下文共享/清理机制 |
三、ContextEngine 的核心思路:把'做什么'和'怎么做'分开
OpenClaw 3.7 的 ContextEngine 接口(PR #22201,44 个文件改动),解决方案其实很经典——定义一组接口,让核心运行时只关心'要做什么',具体'怎么做'交给可替换的引擎。
翻译成人话:以后换个上下文处理算法,像换插件一样简单。
3.1 七个生命周期钩子
ContextEngine 定义了 7 个钩子,覆盖了上下文的完整生命周期:
会话开始 → ① bootstrap() — 初始化引擎状态 → ② ingest() — 每条消息摄入 → ③ assemble() — 组装模型上下文 → 调用模型 → ④ afterTurn() — 回合后处理 → ⑤ compact() — 需要压缩时执行
子 Agent 场景: → ⑥ prepareSubagentSpawn() — 准备子 Agent 上下文 → ⑦ onSubagentEnded() — 清理子 Agent 上下文
| 从前(硬编码) | 现在(ContextEngine 钩子) | 意味着什么 |
|---|---|---|
| 消息直接写入 JSONL | ingest() — 引擎决定怎么存 | 可以存到向量库、做预处理 |
| sanitize→validate→limit→repair | assemble() — 引擎决定怎么组装 | 可以用 RAG、用检索、用任何算法 |
| 三种硬编码压缩方式 | compact() — 引擎决定怎么压 | 可以不压、可以自定义摘要策略 |
| 回合结束什么都不做 | afterTurn() — 引擎做后处理 | 可以异步优化上下文 |
| 子 Agent 完全独立 | prepareSubagentSpawn() + onSubagentEnded() | 父子 Agent 可以共享上下文 |
3.2 接口定义(精简版)
interface ContextEngine {
readonly info: ContextEngineInfo;
// 3 个必选 = 上下文管理的三件核心事
ingest(params: { sessionId, message }): Promise<IngestResult>;
assemble(params: { sessionId, messages, tokenBudget? }): Promise<AssembleResult>;
compact(params: { sessionId, sessionFile, tokenBudget?, ... }): Promise<CompactResult>;
// 4 个可选 = 增强能力
bootstrap?(params): Promise<BootstrapResult>;
afterTurn?(params): Promise<void>;
prepareSubagentSpawn?(params): Promise<SubagentSpawnPreparation | undefined>;
onSubagentEnded?(params): Promise<void>;
}
3 个必选方法覆盖了上下文管理最本质的三件事:消息怎么存、上下文怎么组、太多了怎么压。4 个可选方法提供增强能力,不实现也不影响基本运转。
四、向后兼容的艺术:LegacyContextEngine 与绞杀者模式
引入新接口最怕什么?Breaking change。
OpenClaw 用了一个经典的架构模式——Strangler Fig(绞杀者模式):在旧系统外面包一层新接口,旧逻辑原封不动跑在新接口里。
LegacyContextEngine 就是这个包装器:
| 方法 | 实际做了什么 | 效果 |
|---|---|---|
ingest() | 返回 { ingested: false },什么都不做 | SessionManager 照旧处理 |
assemble() | 原样返回 messages,不改动 | 旧管线照旧跑 |
compact() | 调用原来的压缩函数 | 压缩逻辑完全不变 |
afterTurn() | 空操作 | 没有副作用 |
结果:不配置任何 context engine 插件时,系统行为和升级前 100% 一致。零风险升级。
五、注册与解析机制:怎么做到'换插件一样简单'?
5.1 进程全局注册表
// 通过 Symbol.for() 挂在 globalThis 上
// 为什么?monorepo 构建可能把模块打包成多份拷贝
// Symbol.for() 确保不管有几份 js 文件,注册表只有一个
const REGISTRY = Symbol.for("openclaw.contextEngineRegistryState");
registerContextEngine(id, factory);
getContextEngineFactory(id);
listContextEngineIds();
5.2 Slot 机制
{
"memory": "memory-core",
"contextEngine": "legacy"
}
一个坑位只能站一个人。两个插件都声明 kind: "context-engine",只有被配置选中的那个加载。
5.3 一行配置搞定切换
{
"plugins": {
"slots": {
"contextEngine": "lossless-claw"
}
}
}
不配这一行 → 默认 "legacy" → 行为不变。
这就是'像换插件一样简单'的实现原理。
六、实战:10 分钟写一个自定义 Context Engine
这不是空谈,看代码:
export default function register(api) {
api.registerContextEngine("my-rag-context", () => ({
info: {
id: "my-rag-context",
name: "RAG Context",
ownsCompaction: true
},
async ingest({ sessionId, message }) {
// 每条消息写入向量数据库
await vectorDb.insert(sessionId, message);
return { ingested: true };
},
async assemble({ sessionId, messages, tokenBudget }) {
// 不是暴力截断,而是 RAG 检索最相关的历史消息
const relevant = await vectorDb.search(sessionId, latestQuery, tokenBudget);
return { messages: relevant, estimatedTokens: countTokens(relevant) };
},
async compact({ sessionId }) {
// RAG 模式下不需要压缩——assemble 时按需检索
return { ok: true, compacted: false };
},
}));
}
三个核心方法,加起来不到 20 行。但效果是什么?你把上下文管理从'暴力截断'变成了'语义检索',而且没有改动 OpenClaw 的任何核心代码。
七、ContextEngine ≠ 记忆系统,别搞混了
很多人会混淆这两个概念。简单区分:
| ContextEngine(上下文管理) | Memory 插件(记忆系统) | |
|---|---|---|
| 管什么 | 当前 session 内的消息流 | 跨 session 的持久化知识库 |
| 核心问题 | 哪些消息送给模型、何时压缩 | 怎么索引、搜索、存取记忆 |
| 生命周期 | 一个 session 内 | 跨越所有 session |
| 存储 | session JSONL 文件 | Markdown 文件 + SQLite/LanceDB |
它们之间的桥梁是 Memory Flush——在 ContextEngine 执行压缩之前,先触发一个静默 Agent 回合,提醒 Agent 把值得保留的对话内容写入记忆文件。
上下文管理决定'模型这次能看到什么',记忆系统决定'Agent 下周还记得什么'。两者协同但独立。
八、从 ContextEngine 学到的架构思维
不管你用不用 OpenClaw,这次的 ContextEngine 设计有几个通用的工程模式值得带走:
8.1 绞杀者模式(Strangler Fig)
新接口包装旧逻辑,渐进式替换。不一口气重写,而是让新旧共存,逐步迁移。这在任何大型系统的重构中都适用。
8.2 接口 + 注册表 + 配置驱动
定义接口 → 工厂函数注册到全局注册表 → 配置文件选择用哪个引擎 → 运行时动态解析。这套模式在 Java 的 SPI、Python 的 entry_points、Rust 的 trait 中都有对应。
8.3 Best-effort 回调
子 Agent 生命周期回调失败只记日志不阻断主流程。在分布式系统中,'让非关键路径的失败不影响主路径'是基本原则。
8.4 必选 + 可选方法分离
3 个必选方法保证核心能力,4 个可选方法扩展增强能力。实现者可以从最简开始,逐步增加功能。
九、行业背景与对比
当前开源社区对主流 AI Agent 框架的记忆系统进行了广泛分析,涵盖了多种设计哲学:
| 框架 | 语言 | 特点 |
|---|---|---|
| OpenClaw | TypeScript | 插件架构、混合搜索、优雅降级 |
| nanobot | Python | 极简主义,2 个 Markdown 文件搞定 |
| NullClaw | Zig | 10 种存储后端、9 阶段检索管线 |
| OpenFang | Rust | 知识图谱、记忆衰减、统一 SQLite 底座 |
从 2 个 Markdown 文件到 10 种存储后端、从暴力截断到 9 阶段检索管线——4 种完全不同的设计哲学,让你一次看透 AI Agent 记忆系统的全貌。
适合以下人群参考:
- 正在构建 AI Agent 的工程师:直接参考已经验证过的架构
- 想理解'记忆'到底怎么实现的研究者:源码级拆解,不是概念堆砌
- 想在 LangGraph / LangChain / 自研框架中做上下文管理的开发者:每个框架都有复刻指南
十、总结
OpenClaw 3.7 的 ContextEngine 接口,核心价值用一句话概括:
以后换个上下文处理算法,像换插件一样简单,不用再担心改一处崩全程。
它不是一个'多了个功能'的小更新,而是一次架构层面的升级——把 AI Agent 中最头疼的上下文管理问题,从'硬编码在核心代码里'变成了'可插拔的标准接口'。
7 个生命周期钩子、绞杀者模式的向后兼容、全局注册表 + Slot 机制的引擎管理——这些设计模式不仅适用于 OpenClaw,同样适用于你正在做的任何 AI Agent 系统。
不论你用什么技术栈,上下文管理的核心问题是一样的。理解它、借鉴它、在你的项目中用好它。

