乐于分享
好东西不私藏

OpenClaw 是怎么管理上下文的?从下一轮提问发出去之前开始拆解

OpenClaw 是怎么管理上下文的?从下一轮提问发出去之前开始拆解

粟风导读

《Agent 工程拆解》第 3 篇。从「下一轮提问发出去之前」拆 OpenClaw Context Engine:ingest、assemble、compact、afterTurn,以及默认挂载方式与失败降级。

开篇

假设你的 Agent 已经聊了几十轮:查过知识库、调过几次工具、还拉过一次子代理啃日志。用户又问:

那按这个结论,回滚方案怎么写?

下一轮发出去之前,旧消息谁收?

这轮模型究竟看见哪些内容?

窗口快满了、子代理那堆过程还在不在主会话里?

上一篇拆的是 Codex 上下文管理:规则、会话、子任务怎么分层。这篇看 OpenClaw 的 Context Engine(上下文引擎)——模型每次开跑前,消息怎么进场、怎么组装进窗口、窗口满了怎么压、答完怎么收尾。

一、下一轮发出去之前,Context Engine 在干什么

读 OpenClaw 配置时,会碰到 plugin slot(插件槽)这个词:框架预留几个独占位置,用来挂载不同能力,每个位置同一时刻只启用一套实现。

上下文这块的槽位名就叫 plugins.slots.contextEngine。挂在上面的那套能力,就是 Context Engine(上下文引擎)——专门管「每次模型开跑时,上下文怎么被接住、组装、压缩、收尾」。你可以把它想成进模型前的一道闸:闸后面才是真正发给模型的那包消息。配置里填哪个引擎 id,运行时就用哪套;没填时,默认用内置的 legacy(把旧行为包成一个引擎,多数项目先按老逻辑跑)。

另外还有一个 plugins.slots.memory,挂 Memory(记忆)插件,负责检索、召回。找得到旧材料,不等于自动进窗;进不进窗,仍由 Context Engine 放行。

用户那句回滚问题进来之后,到模型真正开口之前,引擎大致走这条链:

ingest(进场)   → 新消息先被接住:可存、可建索引;还没决定要不要整段塞进窗口  assemble(组装)   → 模型开跑前,挑出「这轮该看的消息」;可附带一段系统提示增量   → 贴合 token budget(上下文预算,窗口装不下就得裁)  compact(压缩)   → 窗口满了,或用户手动 /compact:把旧历史摘要掉,腾出空间  afterTurn(回合后收尾)   → 模型答完还能做:落盘、后台再压一点、更新索引  (可选)子代理钩子   → 拉子代理前准备上下文;结束后清理;用来隔离,不是多开角色聊天

图1 · 下一轮发出去之前:Context Engine 主链

二、消息先被接住:ingest,不是直接糊进 Prompt

上下文不是从模型开跑那一秒才开始的。

ingest 的字面意思是「摄取 / 进场」。新消息进 session(会话)时,引擎可以先把它存起来、建索引、写进自己的数据面。默认的 legacy 引擎在这一步几乎什么都不做——会话管理器照旧落库——但接口位置已经留好了:以后你想换存储、换索引,不用去改「发给模型」那条主路。

很多自研 Agent 的写法是:messages.append,下一轮整包送给模型。进场和进窗绑死成一步,后面想换策略,只能改核心路径。

进场和进窗,不是同一步。

三、真正跑模型前:assemble 决定「看见什么」

assemble 的意思是「组装」。引擎最值钱的一步就在这里:每次要跑模型前,它返回一组排好序的消息——也就是模型这轮实际能看见的上下文。还可以附带 systemPromptAddition(系统提示增量:临时拼进系统提示的一段话),并尽量贴合 token budget。

回滚方案那一轮,模型不该再看见子代理啃过的全部日志原文,也不该把知识库检索的原始片段无差别堆满窗口。该留的是结论、约束、关键引用;噪声该在 assemble 被挡住。Memory 检索出来的材料,也是在这一步决定进不进窗、进多少——找得到,不等于自动塞进来。

谁决定这轮进模型的消息列表,要有唯一入口。

四、窗口顶上了:compact 出场

compact 就是「压缩 / 腾地方」。窗口满了,或用户手动敲 /compact,引擎要把旧历史摘要掉,给后面腾空间。

上一篇在 Codex 里看过「摘要替换大半历史」。这里多看一层工程归属:压缩到底算谁的责任?官方用 ownsCompaction(是否由引擎自己拥有压缩权)表达这件事——

1. 为 true:引擎自己管压缩和溢出恢复,运行时关掉内置的自动压缩

2. 为 false 或未设:仍可走引擎的 compact(),也可委托 runtime(运行时)自带的压缩

策略可以换,责任方不能含糊。一个挂着的引擎如果把 compact() 做成空操作,手动压缩和溢出恢复可能一起失效——这比算法不够花哨更危险。

压缩算法怎么选、保留什么丢掉什么,留给后面单点篇。这里先记住:压缩要挂在明确生命周期上,并且所有权要显式。

五、这一轮答完:afterTurn 收尾

afterTurn 就是「这一轮对话结束后再干的事」。模型答完,还可以落盘、触发后台压缩、更新索引。

上下文治理不只发生在「发送前」,回合后还有收尾。进场、组装、压缩、收尾是一整段,不是一个拼 Prompt 的函数。

六、又要拉子代理时:隔离也有钩子

如果这一轮还要再拉子代理——比如让它去核一版回滚脚本风险——OpenClaw 给了两个可选钩子:

1. prepareSubagentSpawn(子代理启动前准备):子会话开始前,准备共享或隔离的上下文;模式可以是 isolated(隔离,少带主会话包袱)或 fork(分叉,从主会话拷一份再走)

2. onSubagentEnded(子代理结束时清理):子会话结束或被回收时做清理

还有一种刻意很干净的路径:原生子代理若请求 lightContext(轻量上下文),并落到 isolated,可以跳过上面的 prepare,直接从很瘦的启动上下文起步。

我怎么理解这些钩子:它们首先服务隔离,不是多开几个角色聊天。 子代理边界写进生命周期,比写在注释里可靠。

图2 · 子代理边界:隔离也有钩子

七、回头一看:为什么是「引擎」而不只是几个函数

走完一轮再回头,做成独立引擎、再用槽位挂上去,主要是为了这三件事:

1. 组装权独占。contextEngine 槽同一时刻只启用一套引擎。不是十个插件一起改消息列表——那样调试时你说不清谁最后动了刀子。

2. 没配也不折腾。 槽位空着就用内置 legacy:组装透传原来的清理 → 校验 → 限长流水线;压缩委托内置摘要。旧行为先收成一个引擎,不用逼你立刻重写。

3. 失败可降级。 你换上的引擎校验失败、创建失败或跑的时候抛错,OpenClaw 会隔离它,并降回 legacy,避免整条回复路径哑火。策略可以换,但得留保守默认;否则插件一挂,全站一起静音。

没有统一组装入口,检索、记忆、指令、工具结果很容易糊成一锅;调试时你甚至说不清这轮模型看见了什么。

八、自己做 Agent,能借鉴什么

不必先抄 OpenClaw 的槽位配置语法。迁的是边界,不是插件名。

可以借鉴的思路:

1. 进场和进窗分开——消息落库是一回事,送进模型是另一回事

2. 组装权独占——每次 run 只从一个入口出消息列表,别多处 append

3. 检索供给和放行分开——RAG / Memory 负责找材料,组装入口决定进不进窗、进多少

4. 压缩、子任务隔离、失败降级都有明确落点——别散落在业务 if 里;策略挂了最好还能回到保守默认

可以落到自己 Agent 系统工程里的:

1. 固定一个组装入口(名字随意,比如 assembleContext):每次调用模型只从这里出消息列表,并留下可回放记录——出事能说清「这轮看见了什么」

2. 检索结果先当候选,由组装入口决定是否进窗、进多少;不要检索完直接 append 进历史

3. 子任务开工前写清上下文契约(复制、隔离、还是轻量启动);回流只要结构化结论;压缩触发写在单独入口里

什么时候再做成「可替换引擎」?当你已经需要在不改核心的前提下换策略,或者策略一挂必须能降级,或者已经说不清这轮模型看见了什么——再抽可插拔。短会话 Demo、策略不会换,先把第2篇那套分层做稳、把组装入口收拢就够。

九、面试怎么讲

别背生命周期方法名。面试官问的是你有没有把「上下文」当成工程问题。可以按下面这种对答准备:

面试官:你们 Agent 的上下文是怎么管的?听说过 Context Engine 吗?

答:Context Engine 是上下文治理那块能力:每次模型开跑前,谁决定模型看见什么——进场、组装、压缩、收尾、子代理边界。OpenClaw 把它挂在 contextEngine 这个槽上,方便切换实现;记忆检索是另一个槽,只负责找材料,不负责最终放行。

面试官:Memory 和 Context Engine 有什么区别?

答:Memory 管找得到;Engine 管这轮放不放进窗、放多少。可以协作,但不是同一个东西。混在一起,调试时最容易说不清。

面试官:为什么组装入口要独占,多个插件一起改不行吗?

答:上下文组装需要单一责任方。好几个插件同时改消息列表,最后模型看见什么就变成黑盒,出了问题不知道该回滚哪一段。

面试官:ownsCompaction 是干什么的?

答:声明压缩到底归引擎自己,还是交给运行时。所有权含糊的时候,手动压缩和窗口溢出恢复最容易一起坏掉。

面试官:引擎挂了怎么办?

答:公开机制里,非默认引擎失败会被隔离,并降回保守的 legacy,保证还能回用户。自研也一样:策略可以激进,但要留一条保守默认,别插件一挂全站静音。

面试官:如果让你从零做,最小怎么做?

答:先保证单一组装入口,并且能日志回放「这轮看见了什么」。这步稳了,再谈可插拔、可换压缩策略。

十、收束

先想清楚「这轮看见什么」由谁放行,往往比先堆插件更值钱。

下一篇把 Codex 和 OpenClaw 放在一起比:两种上下文思路服务不同约束,学的是权衡维度,不是站队。你现在的 Agent,组装上下文是一个明确入口,还是散落在业务代码里?欢迎留言。

参考文档

主要依据 OpenClaw 官方文档:

1. Context engine

https://docs2.openclaw.ai/concepts/context-engine

2. 概念页(仓库)

https://github.com/openclaw/openclaw/blob/main/docs/concepts/context-engine.md

我是粟风,5年 Java 开发,3年 AI 应用架构开发。本公众号不做泛 AI 工具推荐,也不贩卖转型焦虑。主要记录和分享后端工程师和团队在 AI 应用落地中真正会遇到的问题,围绕 RAG、企业知识库、智能客服和企业级 Agent 架构,拆解 AI 应用落地中的真实工程问题。