ARTICLE · 991949
OpenClaw内核解剖(一):上下文管理机制源码拆解
OpenClaw 上下文管理机制源码拆解
源码版本:2026.5.28(上一版基于 2026.4.20,2026-08-29 已按 5.28 源码复核修订)
源码根目录:/Users/jason/Desktop/code/openclaw-2026.5.28/src
作者:杰少
最后更新:2026-08-29系列文章导航:
[一] 《OpenClaw内核解剖(一):上下文管理机制源码拆解》- 本文:压缩/裁剪/上下文窗口 [二] 《OpenClaw内核解剖(二):会话与记忆系统详解》- 会话/记忆系统 [三] 《OpenClaw内核解剖(三):压缩与记忆刷新时序设计缺陷》- 压缩/记忆刷新 本文专注:上下文管理的完整技术实现,包括Context Engine、压缩、裁剪、上下文窗口、Token预算等核心机制。
一、引言:从用户现象到技术实现
1.0 从"AI失忆"到上下文管理
所有用过 ChatGPT 的人都有过这样的体验:聊着聊着,AI 就"忘了"前面说过的话。这不是 AI 在偷懒,而是它有一个硬性限制——上下文窗口(Context Window)。
你可以把上下文窗口理解成 AI 的"短期记忆容量":LLM 一次能处理的 token 数量有上限——小模型 16K,中等模型 32K/64K,大模型 128K/200K,少数甚至到 1M。一旦对话长度超过窗口,最早的内容就会被"挤出去",AI 就真的不记得了。
问题在于,人和 AI 的对话是"无限"的——今天聊完明天接着聊,一个项目持续几周,AI 需要记住之前的决策。无限对话 vs 有限窗口,这就是根本矛盾。
更反直觉的是:即使模型窗口够大、配置合理,AI 依然会"失忆"——明明模型支持 32K 窗口,实际使用中感觉连 8K 的记忆都保不住。这背后不是模型能力的问题,而是 OpenClaw 的上下文管理机制在默默工作:compaction(压缩)、pruning(裁剪)、memory 刷新……它们决定了 AI 的"短期记忆"到底能撑多久。
1.1 用户视角:上下文窗口的实际容量
把上下文窗口想象成AI的"短期记忆":
为什么32K窗口感觉像只有8K?
因为在实际使用中,上下文包含:
系统提示(身份、规则、记忆文件内容):~4K tokens 对话历史(你+AI的每一轮对话):~24K tokens 工具调用返回结果(文件内容、API数据):~8K tokens
总计:36K tokens → 超过32K → 触发压缩
一个具体的计算例子:
假设你让AI分析一个CSV文件并生成报告:
对话历史(15轮 × 1000 tokens):15000 tokens 工具调用返回(CSV文件内容):8000 tokens 系统提示(启动文件+规则):4000 tokens 小计:27000 tokens
如果模型窗口是32K,还没到压缩阈值。但随着对话持续,很快会触达。
注:压缩算法内部确实有一个
SAFETY_MARGIN=1.2(源码src/agents/compaction.ts),但它不是全局的"预算安全边距",而是统一用于补偿estimateTokens()估算偏差的安全系数——作用于压缩分块估算(6.4)、超大消息判定(6.4)、发送 prompt 前的抢占式预判(6.6.1)、压缩后 history-share 预算检查(9.1)等处。
1.2 OpenClaw的三种"应急"手段
面对"对话太长"这个问题,OpenClaw 有三种递进的处理方式:
1. 轻量裁剪(Pruning)
对话快满但还没超?先把不重要的工具返回结果截短,图片去掉。就像收拾行李箱——先把占地方的冬装拿出来,塞更轻便的。
具体做法:
保留工具返回的头部和尾部,中间用"..."代替 图片替换为"[image removed during context pruning]" 标注 "[Tool result trimmed: kept first X chars and last Y chars of Z chars.]"
2. 记忆刷新(Memory Flush)
对话确实要溢出了?在压缩之前,先把重要的东西写到"笔记本"(记忆文件)里。就像考试前先把公式抄在草稿纸上,免得脑子一空全忘了。
具体做法:
触发条件: totalTokens ≥ contextWindow - reserveTokensFloor - softThresholdTokens(5.28 判定值含"预判",实际更早触发,详见第七章与第三篇)写入位置: memory/YYYY-MM-DD.md防重复:同一压缩周期只刷新一次( memoryFlushCompactionCount追踪,5.28 已无内容哈希去重)
3. 对话压缩(Compaction)
对话真的太长了?把早期对话"摘要"成一段精简总结,保留关键信息,丢掉细节。就像把一本厚书浓缩成读书笔记。
具体做法:
分块摘要:把历史分成几块,每块单独摘要 合并摘要:把多个块的摘要合并成一个 三级回退:全量摘要 → 跳过超大消息 → 返回"Summary unavailable" 压缩后重建:重新注入行为规则(AGENTS.md 的 "Session Startup" 和 "Red Lines")
二、架构总览:Context Engine 抽象层
基于源码
src/context-engine/types.ts、src/context-engine/registry.ts、src/context-engine/legacy.ts
一句话:Context Engine 是 OpenClaw 上下文管理的插件化抽象层——把"上下文怎么注入、怎么组装、怎么压缩"封装成统一接口,方便第三方接入自己的上下文策略。
2.1 核心概念:ownsCompaction
不同模型/服务商对上下文压缩的处理方式不同:
有些提供原生压缩能力(如 Codex runtime,或第三方上下文引擎插件),压缩质量更高、延迟更低 有些依赖 OpenClaw 的通用压缩
ownsCompaction 就是用来区分"谁来管压缩"的标志:
true→ 引擎自己负责压缩,OpenClaw 不介入 未定义 / false→ 由 OpenClaw 内置逻辑处理
2.2 默认行为:Legacy 引擎
绝大多数用户(包括 Ollama 本地部署)用的都是Legacy 引擎。
它的设计哲学:所有核心上下文管理逻辑——压缩、裁剪、记忆刷新的触发——全部由 OpenClaw 运行时掌控。Legacy 只管做两件事:
把消息透传给 OpenClaw 的内部处理管线 压缩请求代理到内置的 compactEmbeddedAgentSessionDirect(4.20 中叫compactEmbeddedPiSessionDirect,5.28 更名)
用户配置层面完全无感,也无需额外配置。
2.3 生命周期一览(摘要)
会话创建 ──→ 初始化│↓每轮:注入消息 ──→ 组装上下文 ──→ 发模型 ──→ 回合后处理│├── 可选压缩│↓转录维护(如需)
日常用到的机制(压缩、裁剪、记忆刷新)都挂在这个生命周期上,后续章节逐一拆解。
小结:理解 Context Engine,你只需要知道三件事——它是抽象层、
ownsCompaction决定谁管压缩、默认 Legacy 引擎所有处理都走 OpenClaw 内置逻辑。后面各章讨论的压缩阈值、裁剪策略、记忆刷新触发条件,才是真正和你openclaw.json配置相关的内容。
三、上下文构成与组装:模型"看到"了什么
基于官方文档
docs/concepts/context.md和源码src/agents/system-prompt.ts、src/agents/bootstrap-files.ts、src/config/types.openclaw.ts的深度分析
要理解OpenClaw如何管理上下文,先要看清上下文到底由哪些部分组成、怎么装进去的。
3.1 上下文的三个核心组成部分
根据官方文档 docs/concepts/context.md:
"Context is everything OpenClaw sends to the model for a run"
三个组成部分,从稳定到动态:
3.2 系统提示词:上下文的核心骨架
系统提示词是 OpenClaw 自动构建的,包含:
行为规则 — 来自 AGENTS.md 的红线和启动流程 工具列表 — 名称+描述 技能列表 — 名称+描述+位置(元数据,非完整 SKILL.md) 注入的工作空间文件 — AGENTS.md → SOUL.md → ... → MEMORY.md 时间/运行时信息 — UTC 时间、主机/OS/模型
关键设计:系统提示词由 OpenClaw 构建和维护,每次运行重建,稳定部分可缓存复用。
3.3 上下文文件注入:优先级与顺序
OpenClaw 按优先级排序注入工作空间文件(源码 src/agents/system-prompt.ts 第59行 CONTEXT_FILE_ORDER,5.28 行号;4.20 为第44行):
HEARTBEAT.md属于动态上下文文件(源码第69行 DYNAMIC_CONTEXT_FILE_BASENAMES),每次心跳重新读取。
3.4 注入限制与截断机制
agents.defaults.bootstrapMaxChars | |||
agents.defaults.bootstrapTotalMaxChars | |||
agents.defaults.bootstrapPromptTruncationWarning | always | always,4.20 为 once) |
/context list 会显示每个文件原始大小 vs 注入大小,以及是否截断。
3.5 Bootstrap 上下文:首屏 vs 续屏
第一轮和后续轮次的注入策略不同:
首屏:注入完整的 Bootstrap 上下文,读取所有文件 —— AI 需要完整引导 续屏(continuation-skip):跳过 Bootstrap 注入 —— 信息已存在于对话历史
agents.defaults.contextInjection 控制:
always | ||
continuation-skip | ||
never |
3.6 技能与工具的上下文成本
技能和工具对上下文有两类成本(基于官方文档和源码 src/skills/loading/workspace.ts,4.20 为 pi-embedded-runner/skills.ts):
技能:
技能列表文本(默认注入):名称+描述+位置,每个约 30-50 tokens(官方示例:12 个技能 ≈546 tokens) 完整 SKILL.md(按需加载):仅模型调用时才 read,不预注入
控制机制:skills.limits.maxSkillsPromptChars(默认 18000 chars)限制技能列表 token 预算。
工具:
工具列表文本(可见):系统提示中的"Tooling"部分 工具模式 JSON(不可见但计入):发送给模型以便调用
/context detail 的输出会显示最大工具模式,帮助识别上下文开销来源。
3.7 上下文检查与诊断
/status | ||
/context list | ||
/context detail | ||
/usage tokens | ||
/compact |
3.8 上下文与内存的官方区分
官方文档强调:Context isnot the same thingas "memory"
本章总结:上下文是系统提示词 + 对话历史 + 工具结果三部分的组合。其中系统提示词由 OpenClaw 按文件优先级自动组装,受 Bootstrap 策略、截断限制和 Cache Boundary 管控。理解这个构成,是后续分析压缩、裁剪等优化机制的基础。
排查参考:Token消耗异常偏高
可能原因:工具调用返回过大(日志文件、大 JSON)、附件累积过多、系统提示过长。
排查步骤:
/context detail查看各部分 token 消耗 → 检查工具调用是否返回过大内容 → 查看是否有未清理的附件。
四、上下文窗口:Token 预算的发现与守卫
基于源码
src/agents/context.ts和src/agents/context-window-guard.ts的深度分析
上下文管理的第一步,是回答一个根本问题:当前模型有多少 token 可用?这个问题看似简单,实则涉及复杂的优先级解析、缓存机制和守卫保护。
4.1 上下文窗口的四级解析链与缓存机制
OpenClaw 从四个来源解析上下文窗口,优先级从高到低:
contextTokens | "agents": { "defaults": { "contextTokens": 64000 } } | ||
models.providers | "models": { "providers": { "openai": { "models": [{ "id": "gpt-4o", "contextWindow": 128000 }] } } } | ||
这些来源的数据最终汇集到一个运行时缓存MODEL_CONTEXT_TOKEN_CACHE 中,由 lookupContextTokens() 统一读取:
// 上下文Token缓存export const MODEL_CONTEXT_TOKEN_CACHE = new Map();// Provider 配置 → 缓存(启动时同步写入)function applyConfiguredContextWindows(cache, modelsConfig) {for (const provider of modelsConfig?.providers ?? []) {for (const model of provider.models ?? []) {if (model?.id && model?.contextWindow > 0) {cache.set(model.id, model.contextWindow);}}}}// 模型 API 上报 → 缓存(运行时异步刷新)function applyDiscoveredContextWindows(cache, models) {for (const model of models) {if (model?.id && model?.contextWindow > 0) {const existing = cache.get(model.id);if (existing === undefined || model.contextWindow < existing) {cache.set(model.id, model.contextWindow); // 保守:只取更小值}}}}
缓存策略:
启动时同步加载:Provider 配置的值立即写入缓存 运行时异步发现:模型 API 上报的值异步更新 保守原则:只有更小的值才覆盖已有缓存,保证缓存是最保守的已知窗口 不阻塞请求: lookupContextTokens快速查缓存,未命中时回退到其他来源
4.2 三种场景的优先级链
OpenClaw 有多个地方需要知道"模型上下文窗口有多大"——预检压缩、记忆刷新、实际压缩、上下文裁剪、状态显示等。虽然外观上有三个解析函数,但底层决定窗口大小的核心参数只有一组。真正有差异的,是不同场景用不同的优先级逻辑。
决定上下文窗口大小的逻辑在不同的使用场景中采用了不同的优先级链:
场景 A:记忆刷新 & 预检压缩
解析函数:resolveMemoryFlushContextWindowTokens(5.28 中调用 resolveContextTokensForModel)
contextTokensOverride(agentCfgContextTokens,最高优先级)→ Anthropic 1M 特例 → Provider 配置直扫 → 缓存(qualified/bare)→ 200K5.28 重大变化: agents.defaults.contextTokens(agentCfgContextTokens)作为contextTokensOverride传入,优先级最高——只要配置了contextTokens,直接返回它,Provider 配置/缓存根本不会走到只有未配置 contextTokens时,才依次查 Provider 配置(模型级contextTokens ?? contextWindow,再到 provider 级)→ 缓存(lookupContextTokens)200K 是硬编码的最后防线( DEFAULT_CONTEXT_TOKENS)
⚠️ 4.20 的结论已反转:4.20 中
contextTokens是后备方案(缓存优先);5.28 中它是最高优先级 override。如果同时配了contextTokens和 Provider 窗口,场景 A 用contextTokens,场景 B 用 Provider 窗口,两者仍然可能不一致(见 4.2.2)。
场景 B:实际压缩执行 & 上下文裁剪
解析函数:resolveContextWindowInfo
Provider配置(contextTokens ?? contextWindow)→ 模型原生窗口(modelContextTokens ?? modelContextWindow)→ 默认值 200K最后:agents.defaults.contextTokens 作为 cap(只压低不抬高)
先看 Provider 配置(5.28 同时支持 contextTokens和contextWindow两个字段,contextTokens优先),再看模型原生窗口(provider 级回退只存在于场景 A/C 的resolveConfiguredProviderContextTokens,场景 B 只查模型级)agents.defaults.contextTokens只用作上限帽(cap) 如果 Provider 窗口=32K 而 contextTokens=128K→ 取 32K(不被contextTokens放大)如果 Provider 窗口=96K 而 contextTokens=64K→ 取 64K(cap 生效)兜底默认值是 200K( DEFAULT_CONTEXT_TOKENS),没有/1.2除法(4.20 的"默认值/1.2"逻辑已移除)
这个逻辑更保守、Provider 感知,适合 Pi 内部执行时使用。
场景 C:状态显示 & 用量跟踪
解析函数:resolveContextTokensForModel
contextTokensOverride(运行时参数,非常规场景用)→ Anthropic 1M 特例 → Provider配置直扫 → 缓存(qualified/bare)→ fallback(各调用方自定义)contextTokensOverride是运行时参数,由调用方显式传入(如运行态切换了 provider 时的替补值),注意 5.28 中记忆刷新场景(A)也复用了这个参数(传的是 agent.defaults.contextTokens)之后依次查 Provider 配置 → 缓存 → 调用方自带的 fallback 不直接引用 agent.defaults.contextTokens(除非调用方显式传入)
这是信息最丰富的解析方式,适合做展示。
将三个场景放在一起对比:
contextTokens | ||
contextTokens 只做cap)→ 200K | ||
场景一:Provider 窗口配置正确(且未配contextTokens)
models.providers[].models[].contextWindow=32K,未配置 contextTokens
→ 只要认真配置了 Provider 窗口且不配contextTokens,所有场景都正确。没有设计矛盾。(若同时配了 contextTokens,场景 A 会优先取 contextTokens,见下方风险段)
场景二:只配了contextTokens,没有 Provider 窗口
contextTokens=128K,无 Provider 配置,API 上报也还没到
→ A 和 B 都拿到 128K,行为一致;但若模型真实窗口更小(如 64K),两条链都高估 → 压缩和刷新都偏晚,可能撞上真实窗口溢出。缓存发现真实窗口后 B 会修正(取更小值),但 A 因 override 仍锁死在 128K。(4.20 中 B 是"默认值/1.2≈173K",5.28 已改为 200K。)
真正的风险在哪?
真正值得关注的问题是场景 A 和 B 使用了不同的窗口值时可能出现的情况。典型的场景:
contextTokens=128K且 Provider 窗口=96K(或任何 < 128K 的 Provider 窗口)→ A 走 override 取 128K(无视 Provider),守卫在 104K 开始触发但 B(Pi 内部)走 Provider 配置取 96K,在 96K - 20K = 76K 就自行压缩
→ 记忆刷新在压缩周期内从未触发,因为 A 的刷新阈值(104K)远高于 Pi 实际运行的窗口(96K)
不过这个问题可以靠认真配置 Provider 窗口、不配contextTokens来规避。它的严重程度不如记忆刷新/压缩时序那个——后者是源码级的信息孤岛所致,这个是配置不完整导致。
4.3 配置建议与最佳实践
基于前面的分析,最干净的配置方式是不配 contextTokens,只配 Provider 窗口:
models.providers = {”ollama”: {”models”: [{ ”id”: ”qwen2.5:7b”, ”contextWindow”: 32768 }]}}
所有场景的行为统一为:直接从Provider 配置/缓存取值,contextTokens 的三种角色差异全部消失。
推荐理由:
消除误解:5.28 中 contextTokens在场景 A 是最高优先级 override、在场景 B 只是 cap——配了它反而制造不一致行为一致:记忆刷新、预检压缩、实际压缩拿到的窗口值完全一致 风险最低:不会出现场景 A 和 B 窗口值不一致的问题
只有一个场景需要配置 contextTokens:你想限制 Agent 的上下文窗口,但 Provider 的模型窗口更大(如模型支持 128K,但你只想用 64K)。
此时 contextTokens 作为 cap 生效,确保场景 B 的实际压缩窗口不超过 contextTokens。
{”models”: {”providers”: {”ollama”: {”models”: [{ ”id”: ”qwen3.5:9b”, ”contextWindow”: 32768 }]}}},”agents”: {”defaults”: {”compaction”: {”reserveTokensFloor”: 20000,”reserveTokens”: 20000, // 示例取与 floor 一致(默认 16384;保持一致可避开时序缺陷,见第三篇 8.4)”memoryFlush”: {”enabled”: true,”softThresholdTokens”: 4000}}}}}
注意:
contextTokens不在这个配置里——按 4.3.1 的推荐,不需要它。
❌错误1:在小模型上使用大预留值
表现:模型窗口小(如16K),但 reserveTokensFloor设为20000后果:无限压缩循环,模型永远无法正常对话 解决:依赖自动保护机制,或手动调小 reserveTokensFloor
❌错误2:忽略守卫警告
表现: /status或日志中提示窗口 < 8K(或 < 窗口的 20%)(5.28 警告线,4.20 为 32K)后果:频繁压缩,体验差 解决:升级到 ≥ 8K(且 ≥ 窗口 20%)的模型
# 查看当前模型的上下文窗口openclaw doctor | grep -i context# 查看压缩统计openclaw status | grep -i compact# 查看用量快照 / 会话内监控 tokenopenclaw status --usage# CLI 用法/usage tokens# 会话内斜杠命令
排查参考:压缩频繁触发
可能原因:
reserveTokensFloor设置过高、小模型窗口但配了大预留值、softThresholdTokens过低。排查步骤:
/status查看压缩次数和触发原因 → 检查contextTokens和实际模型窗口是否匹配 → 调整softThresholdTokens延迟触发。
4.4 安全保护机制
src/agents/context-window-guard.ts 定义了两道安全红线。⚠️ 5.28 重大变化:阈值从 16K/32K 降到4K/8K,并新增比例守卫(窗口的 10%/20%):
max(4000, 窗口×10%) | |||
max(8000, 窗口×20%) |
4.20 为固定 16K 阻断 / 32K 警告;5.28 改为 4K/8K 固定值 + 10%/20% 比例取较大者。例如 128K 窗口:阻断线 = max(4000, 12800) = 12.8K,警告线 = max(8000, 25600) = 25.6K;200K 窗口:阻断线 20K,警告线 40K。
// src/agents/context-window-guard.ts(5.28)export const CONTEXT_WINDOW_HARD_MIN_TOKENS = 4_000;export const CONTEXT_WINDOW_WARN_BELOW_TOKENS = 8_000;const CONTEXT_WINDOW_HARD_MIN_RATIO = 0.1; // 窗口的10%const CONTEXT_WINDOW_WARN_BELOW_RATIO = 0.2; // 窗口的20%export function resolveContextWindowGuardThresholds(contextWindowTokens: number,): { hardMinTokens: number; warnBelowTokens: number } {const hardMinTokens = Math.max(CONTEXT_WINDOW_HARD_MIN_TOKENS,Math.floor(contextWindowTokens * CONTEXT_WINDOW_HARD_MIN_RATIO),);const warnBelowTokens = Math.max(CONTEXT_WINDOW_WARN_BELOW_TOKENS,Math.floor(contextWindowTokens * CONTEXT_WINDOW_WARN_BELOW_RATIO),);return { hardMinTokens, warnBelowTokens };}export function evaluateContextWindowGuard(params: {...}): ContextWindowGuardResult {// 低于 hardMinTokens → 阻断(抛错)// 低于 warnBelowTokens → 警告}
守卫逻辑(5.28):4K 是硬底线(阻断)、8K 是软建议(警告),两者均与窗口的 10%/20% 比例取较大值,且均为严格小于判断。守卫函数从 4.20 的 enforceContextWindowGuard() 改为 5.28 的 evaluateContextWindowGuard() + resolveContextWindowGuardThresholds()。
这是OpenClaw中一个精巧的保护机制,解决"20K预留值压垮16K模型"的问题。
默认 reserveTokensFloor = 2000016K模型总共只有16000 tokens 逻辑矛盾:预留20000 > 总窗口16000
源码:
src/agents/agent-settings.ts→applyAgentCompactionSettingsFromConfig()(4.20 为pi-settings.ts,5.28 更名,逻辑一致)
// 以下为示意伪代码:真实逻辑内联于 applyAgentCompactionSettingsFromConfig()// (src/agents/agent-settings.ts 第68-108行),且比例下限带 Math.max(1, ...) 守卫// 计算最小Prompt预算const MIN_PROMPT_BUDGET_TOKENS = 8000; // 绝对下限const MIN_PROMPT_BUDGET_RATIO = 0.5; // 比例下限function computeSafeReserveTokensFloor(contextWindow: number): number {// 1. 计算最少需要留给prompt的token(真实代码为 Math.min(8000, Math.max(1, floor(窗口×0.5))))const minPromptBudget = Math.min(MIN_PROMPT_BUDGET_TOKENS, // 8000绝对下限contextWindow * MIN_PROMPT_BUDGET_RATIO // 50%比例下限);// 2. 计算最多能预留多少const maxReserve = contextWindow - minPromptBudget;// 3. 取较小值:不高于20000,不高于maxReservereturn Math.min(DEFAULT_RESERVE_TOKENS_FLOOR, maxReserve);}
16K模型:
minPromptBudget = min(8000, 16000×0.5) = min(8000, 8000) = 8000 maxReserve = 16000 - 8000 = 8000 reserveTokensFloor = min(20000, 8000) = 8000 ✅
32K模型:
minPromptBudget = min(8000, 32000×0.5) = min(8000, 16000) = 8000 maxReserve = 32000 - 8000 = 24000 reserveTokensFloor = min(20000, 24000) = 20000 ✅
128K模型:
minPromptBudget = min(8000, 128000×0.5) = min(8000, 64000) = 8000 maxReserve = 128000 - 8000 = 120000 reserveTokensFloor = min(20000, 120000) = 20000 ✅
五、上下文裁剪:工具结果的空间回收
基于源码
src/agents/agent-hooks/context-pruning/pruner.ts、src/agents/agent-hooks/context-pruning/extension.ts、src/agents/agent-hooks/context-pruning/settings.ts(4.20 为pi-hooks/)的深度分析
裁剪(Pruning)与压缩不同——它不做摘要,而是直接移除或截断工具结果中占用空间的部分。它是 Pi 扩展机制的一部分,通过监听 "context" 事件在消息发送给模型前执行。
5.1 两种裁剪方式
源码 pruner.ts 的 pruneContextMessages() 函数实现了两种操作:
maxChars=4000, headChars=1500, tailChars=1500 | ||
placeholder="[Old tool result content cleared]" |
软剪的输出格式(源码 pruner.ts 第282行):
[Tool result trimmed: kept first 1500 chars and last 1500 chars of 12000 chars.]图片处理:当工具结果包含图片时,软剪将其替换为 [image removed during context pruning](源码第10行常量 PRUNED_CONTEXT_IMAGE_MARKER)。
5.2 触发机制
裁剪以 cache-ttl 模式运行(默认值),原理:
每次 Pi 上下文事件触发时检查缓存时间 若距上次裁剪超过 ttl(默认 5 分钟),允许裁剪否则跳过(同一轮对话中不重复裁剪) 裁剪后更新缓存时间戳
触发条件以"窗口占比"为主门槛,"工具结果字符数"为附加门槛:
softTrimRatio:默认 0.3(当消息总字符超过上下文窗口(tokens×4)的 30% 时,裁剪才启动,先软剪) hardClearRatio:默认 0.5(软剪后总字符占比仍 ≥ 50% 时,升级为硬清) minPrunableToolChars:默认 50,000 字符(硬清前还需可剪工具字符 ≥ 此值,否则保持软剪结果)
安全保护:启动阶段(第一条用户消息之前)的消息不会被裁剪(源码 pruner.ts 第316行,4.20 为第288行)。
5.3 配置方法
{”agents”: {”defaults”: {”contextPruning”: {”mode”: ”cache-ttl”, // 默认值,唯一有效的运行模式”ttl”: ”5m”, // 裁剪冷却时间,默认5分钟”keepLastAssistants”: 3, // 保留最近 N 条助手回复”softTrimRatio”: 0.3, // 消息占窗口比例超过此值触发软剪”hardClearRatio”: 0.5, // 超过此值触发硬清”minPrunableToolChars”: 50000 // 工具结果少于这值不裁剪}}}}
软剪精度参数(一般情况下无需调整):
{”agents”: {”defaults”: {”contextPruning”: {”softTrim”: {”maxChars”: 4000, // 截断后保留的最大字符数”headChars”: 1500, // 保留开头字符数”tailChars”: 1500 // 保留结尾字符数},”hardClear”: {”enabled”: true,”placeholder”: ”[Old tool result content cleared]”}}}}}
5.4 裁剪与压缩的差异对比
softTrimRatio=0.3hardClearRatio=0.5 | CW - PiReserveTokens | |
六、Compaction 机制:对话压缩的艺术
基于官方文档
docs/concepts/compaction.md、docs/reference/session-management-compaction.md和源码src/agents/compaction.ts、src/agents/agent-hooks/compaction-safeguard.ts(4.20 为pi-hooks/)的深度分析
对话历史太长怎么办?压缩。根据官方文档 docs/concepts/compaction.md 的定义:
"Every model has a context window -- the maximum number of tokens it can process. When a conversation approaches that limit, OpenClaw compacts older messages into a summary so the chat can continue."(每个模型都有上下文窗口——它能处理的token最大数量。当对话接近这个限制时,OpenClaw压缩较早的消息为摘要,以便聊天可以继续。)
但压缩不是简单的截断——OpenClaw 设计了分块摘要 + 三级回退 + 安全保护的完整方案。
6.1 压缩算法核心常量
根据 src/agents/compaction.ts 源码,压缩算法的核心常量:
export const BASE_CHUNK_RATIO = 0.4; // 基础分块比例:每块占历史的40%export const MIN_CHUNK_RATIO = 0.15; // 最小分块比例:避免单块过大export const SAFETY_MARGIN = 1.2; // 安全边距:20%缓冲,防止token估算不准const DEFAULT_SUMMARY_FALLBACK = ”No prior history.”;const DEFAULT_PARTS = 2; // 默认分块数
6.2 官方压缩工作原理与区分
根据 docs/concepts/compaction.md 官方文档,压缩的工作原理是:
保留关联性:当OpenClaw将历史分割成压缩块时,它会保持助手工具调用与其匹配的 toolResult条目配对。如果分割点落在一个工具块内,OpenClaw会移动边界,使配对保持在一起,并保留当前未摘要的尾部。磁盘持久性:完整的对话历史保持在磁盘上。压缩只改变模型在下一轮看到的内容。 自动压缩:默认开启。当会话接近上下文限制时触发,或者当模型返回上下文溢出错误时触发(在这种情况下OpenClaw压缩并重试)。 压缩前记忆保存:在自动压缩发生之前,运行一个静默轮次,将持久状态写入磁盘(例如智能体工作区中的 memory/YYYY-MM-DD.md),确保重要信息被保存,防止上下文丢失。
官方示例的溢出签名:
request_too_largecontext length exceededinput exceeds the maximum number of tokensinput token count exceeds the maximum number of input tokensinput is too long for the modelollama error: context length exceeded
根据 docs/concepts/compaction.md,压缩与裁剪(Pruning)有明确的区分:
裁剪是轻量级的补充,修剪工具输出而不进行摘要。
6.3 预留空间与标识符保留
压缩预留空间的核心是两个容易混淆的参数:
reserveTokensFloor | agents.defaults.compaction.reserveTokensFloor | ||
reserveTokens | agents.defaults.compaction.reserveTokens | ||
max(reserveTokens, reserveTokensFloor) |
源码佐证(src/agents/agent-settings.ts第100行,5.28 行号;4.20 为 pi-settings.ts第97行):
const targetReserveTokens = Math.max(configuredReserveTokens ?? currentReserveTokens,reserveTokensFloor,);
为什么有两个?
reserveTokensFloor:安全底线,防止配置太低导致新对话放不下 reserveTokens:用户可配置更高的值(如40K),在更大的模型上预留更多空间 最终取最大值:既不低于底线,也尊重用户意愿
根据源码,压缩时必须保留所有不透明标识符:
const IDENTIFIER_PRESERVATION_INSTRUCTIONS =”Preserve all opaque identifiers exactly as written (no shortening or reconstruction), ” +”including UUIDs, hashes, IDs, hostnames, IPs, ports, URLs, and file names.”;
标识符保留的三种策略:
strict(严格):默认策略,保留所有标识符 off(关闭):不特殊保留标识符 custom(自定义):使用用户自定义的保留指令
为什么标识符如此重要?
UUID、URL、IP、端口、文件名是操作的基础 标识符被摘要改写 → Agent找不到文件/资源 → 后续命令无法执行 这是压缩质量的底线要求
6.4 压缩算法:分块摘要 + 三级回退
压缩不是"把所有历史变成一句话",而是分块摘要 → 合并 → 三级回退。
源码入口:src/agents/compaction.ts
压缩的核心挑战是"一次性处理整个历史上下文放不下"。解决方案是将历史按比例分块,每块单独摘要。
分块比例的确定依据单条消息的平均 token 数,而非对话总条数(computeAdaptiveChunkRatio(), 第283行,5.28 行号;4.20 为第262行):
export function computeAdaptiveChunkRatio(messages: AgentMessage[], contextWindow: number): number {const totalTokens = estimateMessagesTokens(messages);const avgTokens = totalTokens / messages.length;const safeAvgTokens = avgTokens * SAFETY_MARGIN;const avgRatio = safeAvgTokens / contextWindow;// 当单条消息平均超过窗口10%时,缩小分块比例if (avgRatio > 0.1) {const reduction = Math.min(avgRatio * 2, BASE_CHUNK_RATIO - MIN_CHUNK_RATIO);return Math.max(MIN_CHUNK_RATIO, BASE_CHUNK_RATIO - reduction);}return BASE_CHUNK_RATIO;}
逻辑:
先估算所有消息的总 token,计算平均每条消息占上下文窗口的比例 如果单条平均超过窗口的 10%(说明消息很大),缩减分块比例 缩幅最多到 BASE_CHUNK_RATIO - MIN_CHUNK_RATIO = 0.25,下限 0.15不是按消息条数分层,是按消息大小动态调整
多块摘要合并成一个,源码 MERGE_SUMMARIES_INSTRUCTIONS(compaction.ts 常量):
”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')””- 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.”
核心要求:保留操作状态、决策理由,优先近期上下文。
如果摘要过程中出错,源码 summarizeWithFallback()(第429行,5.28 行号;4.20 为第377行)逐级降级:
✅ 全量摘要( summarizeChunks)→ 成功就完事⚠️ 全量失败 → 跳过超大消息,标注 [Large X (~XK tokens) omitted from summary],只摘要剩余小消息⚠️ 部分摘要也失败 → 返回 "Context contained X messages (Y oversized). Summary unavailable due to size limits."
设计要点:
超大消息的判断依据 isOversizedForSummary():单条消息的 token 估算 ×1.2 安全边距 > 窗口的 50%,视为无法安全摘要失败只记录 warn 日志,不抛出异常,保证系统不因摘要失败而崩溃
回退机制的意义:
避免因单条消息过大导致整个压缩失败 至少保留"压缩失败"的提示,而不是崩溃 确保系统在极端情况下的可用性
6.5 压缩安全保护机制
根据 compaction-safeguard.ts 源码,OpenClaw 实现了完整的压缩安全保护:
源码 src/agents/agent-hooks/compaction-safeguard-quality.ts 定义了质量审计函数 auditSummaryQuality()(第221行,5.28 行号;4.20 为 pi-hooks/第219行):
const REQUIRED_SUMMARY_SECTIONS = [”## Decisions”,”## Open TODOs”,”## Constraints/Rules”,”## Pending user asks”,”## Exact identifiers”,] as const;export function auditSummaryQuality(params: {summary: string;identifiers: string[];latestAsk: string | null;identifierPolicy?: ”strict” | ”off” | ”custom”;}): { ok: boolean; reasons: string[] } {const reasons: string[] = [];// 检查必选的章节标题是否存在for (const section of REQUIRED_SUMMARY_SECTIONS) {if (!lines.has(section)) reasons.push(`missing_section:${section}`);}// strict模式下检查标识符是否保留if (enforceIdentifiers) {// 检查关键标识符是否在摘要中出现}// 检查最近的用户提问是否被反映if (!hasAskOverlap(summary, latestAsk))reasons.push(”latest_user_ask_not_reflected”);return { ok: reasons.length === 0, reasons };}
重试策略:compaction-safeguard.ts 定义 DEFAULT_QUALITY_GUARD_MAX_RETRIES = 1 和 MAX_QUALITY_GUARD_MAX_RETRIES = 3。质量不达标时最多重试 3 次。
审计维度:
摘要是否包含 Decisions、## Open TODOs等五个必选章节标题strict 模式下,原始上下文中的关键标识符是否被保留在摘要中 最新的用户提问是否在摘要中得到反映
源码 compaction-safeguard.ts 第60-61行:
const MAX_TOOL_FAILURES = 8; // 最多保留8条工具失败记录const MAX_TOOL_FAILURE_CHARS = 240; // 单条失败信息最多240字符
目的:限制工具调用失败信息的保留数量和长度,防止失败日志占用过多压缩预算。
源码 compaction-safeguard.ts 第62行(截断标记常量在第65行):
const MAX_COMPACTION_SUMMARY_CHARS = 16_000;const SUMMARY_TRUNCATED_MARKER = ”\n\n[Compaction summary truncated to fit budget]”;
摘要本身可能很长(尤其是长对话),防止它占用过多上下文空间。
// 摘要截断function truncateSummaryIfNeeded(summary: string): string {if (summary.length > MAX_COMPACTION_SUMMARY_CHARS) {const truncated = summary.substring(0, MAX_COMPACTION_SUMMARY_CHARS);return truncated + SUMMARY_TRUNCATED_MARKER;}return summary;}
源码解读:
[Compaction summary truncated to fit budget],让Agent知道摘要被截断了 | |
6.6 压缩触发条件
压缩的触发源分为两个层面——**Gateway 预检层(Pi 运行前)**和Pi 运行时层(运行中):
totalTokens ≥ ctxWindow - reserveTokensFloor - softThreshold | shouldRunPreflightCompaction | ||
/compact 命令 | handleCompactCommand |
shouldPreemptivelyCompactBeforePrompt() | embedded-agent-runner/run/preemptive-compaction.ts,详见 6.6.1) | ||
totalTokens > ctxWindow - PiReserveTokensmax(reserveTokens, reserveTokensFloor),5.28 经 agent-settings.ts 自动抬高;严格大于,见官方 docs session-management-compaction.md;4.20 文档为 ≥) | |||
embedded-agent-runner/run.overflow-compaction.harness.ts) |
两层机制对比:
用户可以通过/compact命令强制触发压缩。支持添加指令来引导摘要方向:
/compact Focus on the API design decisions/compact Preserve all code examples/compact Remember the key decisions about database schema
使用场景:
对话过长需要快速清理上下文 需要聚焦某个特定主题进行摘要 压缩质量不理想,需要手动引导
注意:配置了 keepRecentTokens(默认 20000)时,手动压缩会保留最近轮次和后缀上下文,确保对话连续性;未配置时按硬检查点处理(docs concepts/compaction.md)。
源码 src/agents/embedded-agent-runner/run/preemptive-compaction.ts(4.20 为 pi-embedded-runner/)实现了抢占式压缩的入口函数 shouldPreemptivelyCompactBeforePrompt():
export function shouldPreemptivelyCompactBeforePrompt(params: {messages: AgentMessage[];systemPrompt?: string;prompt: string;contextTokenBudget: number;reserveTokens: number;toolResultMaxChars?: number;}): {route: PreemptiveCompactionRoute;shouldCompact: boolean;estimatedPromptTokens: number;overflowTokens: number;} {// 估算新prompt+历史的总token(含1.2倍安全边距)const estimatedPromptTokens = estimatePrePromptTokens(params);const minPromptBudget = Math.min(MIN_PROMPT_BUDGET_TOKENS,Math.max(1, contextTokenBudget * MIN_PROMPT_BUDGET_RATIO));const effectiveReserveTokens = Math.min(reserveTokens,Math.max(0, contextTokenBudget - minPromptBudget));const promptBudget = contextTokenBudget - effectiveReserveTokens;const overflowTokens = Math.max(0, estimatedPromptTokens - promptBudget);// 判断工具结果裁剪能回收多少空间const toolResultPotential = estimateToolResultReductionPotential({messages, contextWindowTokens: contextTokenBudget, ...});// 路由决策if (overflowTokens <= 0) return { route: ”fits”, shouldCompact: false, ... };if (toolResultPotential.maxReducibleChars <= 0)return { route: ”compact_only”, shouldCompact: true, ... };if (toolResultPotential.maxReducibleChars >= threshold)return { route: ”truncate_tool_results_only”, shouldCompact: false, ... };return { route: ”compact_then_truncate”, shouldCompact: true, ... };}
路由枚举(preemptive-compaction.types.ts):
fits→ 放得下,不处理 compact_only→ 需要压缩(工具结果不可裁或裁了也不够) truncate_tool_results_only→ 只裁剪工具结果就够 compact_then_truncate→ 先压缩后裁剪,双管齐下
设计要点:
先算 overflowTokens(超出的部分),再决定方案minPromptBudget确保 Prompt 至少有 MIN_PROMPT_BUDGET_TOKENS=8000或 50% 窗口的空间effectiveReserveTokens会被 minPromptBudget约束,不会超过contextWindow - minPromptBudget不是层层检查能否放下,而是基于工具结果的可裁剪量做路由
抢占式压缩的意义:
在发送 prompt 前预判是否会超窗口,提前介入 避免模型返回 overflow 错误后重试的延迟
budget 触发的预检压缩和记忆刷新使用同一个阈值公式(ctxWindow - reserveTokensFloor - softThresholdTokens),但两者的判定值不同——刷新用预判值(旧值+上次输出+prompt 估算),预检压缩用持久化 totalTokens,所以刷新总是早一轮触发;4K 软阈值缓冲和时序差是第二、三层保证(5.28 已无 totalTokensFresh 跳过守卫)。详见专题文章:
📖 OpenClaw内核解剖(三):压缩与记忆刷新时序设计缺陷
6.7 压缩后上下文重建
压缩完成后,对话历史被替换成了摘要。但摘要里丢失了行为规则(如"应该先读SOUL.md"、"不能泄露隐私")。
OpenClaw 的做法是从 AGENTS.md 中提取关键段落,注入回上下文:
// 以下为示意伪代码:真实实现为 post-compaction-context.ts 的 extractSections()// 默认提取的章节const DEFAULT_POST_COMPACTION_SECTIONS = [”Session Startup”, ”Red Lines”];// 上下文重建逻辑function rebuildPostCompactionContext(): string {return `[Post-compaction context refresh]Session was just compacted. The conversation summary above is a hint,NOT a substitute for your startup sequence. Run your Session Startupsequence - read the required files before responding to the user.Critical rules from AGENTS.md:`;}
重建的必要性:
防止压缩后Agent"失忆"行为规则 确保压缩后仍能正确执行启动流程 保持安全约束的有效性
6.8 压缩提供者插件
OpenClaw 支持压缩提供者插件,允许第三方实现自定义压缩算法:
// 压缩提供者接口interface CompactionProvider {id: string;summarize(params: {messages: unknown[];signal?: AbortSignal;customInstructions?: string;}): Promise;}// 尝试使用提供者压缩async function tryProviderSummarize(provider: CompactionProvider,params: {...}): Promise {try {const result = await provider.summarize(params);if (typeof result === ”string” && result.trim()) {return result; // 使用提供者的压缩结果}return undefined; // 回退到内置LLM压缩} catch (err) {// 处理错误,回退到内置压缩return undefined;}}
插件化架构的优势(源码位置:src/agents/agent-hooks/compaction-safeguard.ts L186 tryProviderSummarize,4.20 中该逻辑相关于 compaction.ts):
支持第三方实现更先进的压缩算法 保持向后兼容,插件失败时回退到内置压缩 为技术演进提供扩展点
6.9 关键配置
通过 compaction.model 指定专用的摘要模型(5.28 实现见 src/agents/embedded-agent-runner/compact.ts resolveCompactionProviderStream;4.20 引用的编译产物 auth-profiles-B5ypC5S-.js 已不存在,不再引用编译文件行号):
{”agents”: {”defaults”: {”compaction”: {”model”: ”ollama/llama3.1:8b”}}}}
小模型(如本地 8B 模型)用于压缩摘要,主模型用于日常对话,可节省成本。
插件注册后,通过 compaction.provider 将压缩委托给第三方实现(源文件 compaction-provider.ts):
{”agents”: {”defaults”: {”compaction”: {”provider”: ”my-custom-provider”}}}}
设置 provider 后自动进入 safeguard 模式,提供者失败时回退到内置 LLM 摘要。
设置 compaction.notifyUser: true 可在压缩完成时显示简短状态消息(源文件 agent-runner-execution.ts 第1529行)。
压缩机制是OpenClaw上下文管理的核心技术,通过分块摘要、安全保护、插件化设计,在保证可用性的同时,最大限度地减少上下文丢失。理解这些机制,有助于更好地配置和优化OpenClaw的使用体验。
七、记忆刷新:压缩前的上下文保护
本文专注于上下文管理视角。记忆刷新的完整实现(触发算法、计划生成、执行流程、等待机制)详见《OpenClaw内核解剖(二):会话与记忆系统详解》第八章;时序保证与缺陷分析详见《OpenClaw内核解剖(三):压缩与记忆刷新时序设计缺陷》。
7.0 概念与执行路径
目标(来自 docs/reference/session-management-compaction.md):
"Before auto-compaction happens, run a silent agentic turn that writes durable state to disk (e.g.
memory/YYYY-MM-DD.mdin the agent workspace) so compaction can't erase critical context."
压缩的本质是摘要——它将早期对话提炼为压缩摘要,但摘要必然丢失具体细节。记忆刷新的价值就是在压缩执行前,让 Agent 把上下文中的关键信息(决策、承诺、进度、重要事实)持久化写入 memory/YYYY-MM-DD.md,从而绕过压缩的擦除。
执行路径(官方"预阈值刷新" Approach):
上下文使用量监控 ──→ 跨过软阈值 ──→ 运行静默 Agent 轮次 ──→ 写入记忆文件│↓Agent 回复 NO_REPLY│↓Gateway 拦截输出│↓用户无感知
监控(Monitor):Gateway 持续追踪当前会话的 Token 用量 预阈值(Pre-threshold):当 Token 用量达到 contextWindow - reserveTokensFloor - softThresholdTokens(即比 Pi 压缩阈值低 4K)时,判定需要刷新执行静默轮次(Silent Agentic Turn):以当前模型运行一轮特殊消息,指示 Agent 将重要信息写入记忆文件 静默 Token(NO_REPLY):Agent 回复必须以 NO_REPLY/no_reply开头,Gateway 在交付层拦截和压制,用户完全无感知
流式场景下,从 2026.1.10 开始 OpenClaw 也会压制以 NO_REPLY 开头的流式传输中间块,彻底防止静默操作泄露部分输出。
不触发刷新的场景(来自官方文档):
❌ CLI 模式的后端 ❌ 工作区只读模式( workspaceAccess: "ro"或"none")❌ 同一压缩周期内已刷新过( sessions.json的memoryFlushCompactionCount追踪)
7.1 阈值速览
记忆刷新与压缩共享同一个 Token 预算模型,但阈值不同:
刷新阈值 G = contextWindow - reserveTokensFloor - softThresholdTokens压缩阈值 P = contextWindow - PiReserveTokens其中 PiReserveTokens = max(configuredReserveTokens, reserveTokensFloor)
CW - floor - soft | |||
CW - max(reserveTokens, floor) |
正常路径下 softThresholdTokens(默认4K)就是两者之间的缓冲。触发条件的完整分析(三个守卫、时序保证条件、边界场景)请见:
📖 OpenClaw内核解剖(三):压缩与记忆刷新时序设计缺陷
7.2 配置要点
{”agents”: {”defaults”: {”compaction”: {”reserveTokens”: 16384,”reserveTokensFloor”: 20000,”memoryFlush”: {”enabled”: true, // 开启刷新(默认)”softThresholdTokens”: 4000, // 预阈值缓冲,默认4K”forceFlushTranscriptBytes”: ”2MB”, // 会话文件超过此大小强制刷新”model”: ”ollama/qwen3:8b”, // 可选:刷新轮次使用不同模型”prompt”: ”自定义刷新提示”, // 可选:替换默认刷新指令”systemPrompt”: ”额外的系统提示” // 可选:在系统提示后追加额外上下文}}}}}
⚠️ 5.28 变更:
memoryFlush的合法字段只有enabled / model / softThresholdTokens / forceFlushTranscriptBytes / prompt / systemPrompt(schema 为 strict,多写字段会校验失败)。4.20 文档中的idleTimeoutSeconds字段在 5.28 不存在——等待 Agent 空闲的 30 秒是硬编码常量DEFAULT_WAIT_FOR_IDLE_TIMEOUT_MS(src/agents/embedded-agent-runner/wait-for-idle-before-flush.tsL10),不可配置。
各字段说明:
enabled | true | |
softThresholdTokens | 4000 | |
forceFlushTranscriptBytes | 2MB | |
model | ||
prompt | ||
systemPrompt |
(idleTimeoutSeconds 已从配置表中删除:5.28 中不存在此字段,等待空闲超时固定 30 秒。)
关键配置关系:当 reserveTokens < reserveTokensFloor + softThresholdTokens 时,刷新保证在压缩前触发;等于 24000(= floor + soft)时两阈值重合,靠预判值机制兜底(非严格保证);超过 24000(如 25000)则 Pi 压缩先触发,建议同步调高 reserveTokensFloor(与 reserveTokens 保持一致)。详见专题文章8.4 短期实践建议。
7.3 相关文章导航
docs/concepts/compaction.md | |
docs/reference/session-management-compaction.md | |
八、子 Agent 与上下文传递
基于源码
src/agents/subagent-spawn.ts、src/agents/subagent-system-prompt.ts、src/agents/subagent-announce.ts的深度分析
源码根目录:/Users/jason/Desktop/code/openclaw-2026.5.28/src
子 Agent(subagent)是 OpenClaw 处理复杂/并行任务的核心机制。从上下文管理视角看,关键问题有两个:子 Agent 继承了什么上下文?完成的结果如何回到父会话?
8.1 子 Agent 的上下文构成
子 Agent不继承父 Agent 的对话历史。它的上下文由以下三部分构成:
① 任务消息(childTaskMessage)启动时作为用户消息注入:
[Subagent Task]:这是子 Agent 收到的唯一"用户输入",也是它理解工作目标的入口。
② System Prompt(buildSubagentSystemPrompt())源码 src/agents/subagent-system-prompt.ts 构造完整的子 Agent 角色说明:
// 生成的 System Prompt 核心结构- Subagent Context(声明角色)- Your Role(任务绑定)- Rules(7条行为规则:专注、完成即自动回传、不主动、短暂性、信任推送式完成、把子输出当证据、工具截断恢复)- Output Format(最终回复格式)- What You DON'T Do(禁止行为)- Session Context(会话键、频道、label 信息)
关键设计:子 Agent 的 System Prompt 包含完整的角色认知——它知道自己是被父 Agent 创建的、"不要自作主张"、完成即自动报告。
③ 工作空间(resolveSpawnedWorkspaceInheritance())默认继承父 Agent 的工作空间目录(agentWorkspaceDir),使子 Agent 能访问共享文件、记忆文件等。
④ 启动上下文模式(bootstrapContextMode)
const bootstrapContextMode: BootstrapContextMode | undefined = params.lightContext? ”lightweight”: undefined;undefined | |
"lightweight" | lightContext=true 时,sessions_spawn 可选参数) |
8.2 结果回传机制:自动报告
子 Agent 完成后的结果回传不靠主动读取,而是通过推送机制(源码 src/agents/subagent-announce.ts)。
流程:
子 Agent 完成 ──→ captureSubagentCompletionReply()│↓构建 announce(父会话键 + 子会话键 + 结果文本)│↓发送到父 Agent 作为用户消息│↓父 Agent 处理 announce,有两种类型:├── ”subagent task” → 子 Agent 任务结果└── ”cron job” → 定时任务结果
父 Agent 收到的是审核提示:先核实结果,再决定继续任务或向用户交付;内容重复或无更新时回 SILENT_REPLY_TOKEN。关键设计:
子 Agent 的最终回复文本不会直接出现在父会话中——它被包装成 announce 消息 如果子 Agent 回复了 NO_REPLY(表示无内容可报告),父 Agent 也收不到 announce子 Agent 的 System Prompt 明确告知:"Your final message will be automatically reported to the parent agent"
8.3 上下文隔离性
与文章前半部分讨论的压缩、裁剪、记忆刷新不同,子 Agent 的上下文管理独立于父会话:
✅ 子 Agent 有自己的 sessions.json条目✅ 子 Agent 独立运行压缩和记忆刷新 ✅ 子 Agent 的 Token 预算不占用父会话窗口 ❌ 子 Agent 不感知父会话的对话历史
这意味着:子 Agent 不是父会话的"延长线",而是一个带有任务上下文的独立会话。
九、全流程串联:一条消息的上下文之旅
现在把所有机制串联起来,看一个真实的上下文膨胀案例——当对话从几句话涨到接近窗口上限时(96K 窗口,默认配置),每个阶段触发了什么、为什么、效果如何。
9.0 整体流程
用户发送消息│▼1. 上下文窗口守卫(检查解析出的窗口大小,非当前用量)├── 窗口 < max(4K, 窗口×10%) → 报错阻断(5.28;4.20 为 16K)├── 窗口 < max(8K, 窗口×20%) → 警告(不影响运行)└── 窗口正常 → 继续│▼2. 预检阶段(Gateway 层,Pi 运行前)├── 预检压缩检查(用上一轮 totalTokens;5.28 已无 totalTokensFresh 跳过守卫)├── 记忆刷新检查(用预判值:旧值+输出+prompt 估算,领先压缩判定值一轮,先触发)│ ├── tokens ≥ 72K → 执行静默刷新,写入 memory/YYYY-MM-DD.md│ └── tokens < 72K → 跳过│▼3. Pi 运行(含抢占式预判)├── fits → 直接发送模型├── truncate_tool_results_only → 只裁剪工具结果├── compact_only → 执行压缩└── compact_then_truncate → 先压缩再裁剪│▼4. Pi 生成中/结束后(5.28 多触发源:preemptive/阈值维护/溢出/超时)├── tokens < 压缩阈值 → 跳过└── tokens ≥ 压缩阈值 → 自动压缩│▼5. 压缩后重建├── 注入行为规则(AGENTS.md 的 Session Startup + Red Lines)└── 更新会话状态
9.1 案例:代码分析项目的上下文膨胀
背景:模型窗口 96K,默认配置
reserveTokensFloor = 20000 (保底预留)softThresholdTokens = 4000 (刷新软阈值)maxHistoryShare = 0.5 (历史摘要最多占比)memoryFlush 阈值:96K - 20K - 4K = 72Kcompaction 阈值:96K - 20K = 76K
Token 增长曲线:
Token96K │ ┌── Subagent 独立会话│ │76K │ (Pi 运行时压缩阈值,单轮暴涨时兜底)│72K │ ┌── Stage 1: MemoryFlush(预判值越线)│ ├── Stage 2: Compaction(持久化值晚一轮越线)│ │70K │ ┌─────┤ ← 工具结果裁剪(抢占式)│ │50K │ ┌─────┤ ← 持续深入分析│ │30K │ ┌─────┤ ← 读取项目代码│ │10K │ ┌──────┬─────┤│ 初始化 1-10轮 11-20轮 21-30轮 31+轮 → 时间└───────────────────────────────────────────────────
用户让 AI 分析 GitHub 项目代码质量。AI 读取 README.md、src/main.py、package.json 等文件,工具结果累计。
触发检查:
预检压缩:上一轮 totalTokens=30K < 72K → 跳过(5.28 无 fresh 守卫,用旧值判断) 记忆刷新:预判值 30K+输出+prompt < 72K → 跳过 ✅ Pi 抢占式预判: fits→ 直接发送 ✅
AI 陆续读取了 20+ 个文件,工具结果膨胀,对话历史持续增长。
触发检查:
预检压缩:上一轮 totalTokens=70K < 72K → 跳过 记忆刷新:预判值 70K+输出+prompt ≈ 71.5K < 72K → 未达阈值,跳过(临界,下一轮才跨过) Pi 抢占式预判:工具结果占比过高 → truncate_tool_results_only(裁剪长代码块)
本次触发的是软剪——把长代码文件保留头部和尾部,中间用 [... N characters truncated] 标注。参考源码 src/agents/agent-hooks/context-pruning/pruner.ts(4.20 为 pi-hooks/)。
深入讨论架构设计问题,对话历史达到 74K tokens。
触发检查(本节开始时持久化值是 70K):
预检压缩:上一轮 totalTokens=70K < 72K → 跳过(压缩判定值还差一轮) 记忆刷新:预判值 70K+输出+prompt ≈ 74K ≥ 72K → Stage 1:MemoryFlush 触发
Stage 1 执行:
条件满足: 预判值 = 上一轮 totalTokens(70K) + 上次输出 + 本轮 prompt 估算 ≈ 74K≥ 窗口(96K) - floor(20K) - soft(4K) = 72K动作: Gateway 运行静默 Agent 轮次→ Agent 将重要架构决策写入 memory/YYYY-MM-DD.md→ Agent 回复 NO_REPLY → Gateway 拦截 → 用户无感知去重: compactionCount 记录 → 同周期不重复刷新
刷新完成后,Pi 运行,继续对话。因为 74K 仍 < 76K(Pi 压缩阈值),Pi 内部的压缩不触发。但记忆文件里已经保住了关键信息。
注:4.20 中此轮预检压缩因
totalTokensFresh=true直接跳过;5.28 已移除该守卫,改为判定值分工——本轮压缩判定值(持久化 70K)未达 72K 跳过,刷新预判值先越 G 触发;压缩要等下一轮持久化值跨过 72K 才执行(刷新比压缩早一轮,详见第三篇 6.2)。
第 30 轮结束时持久化 totalTokens=74K。若继续对话,token 将增长到 80K,但第 31 轮预检阶段压缩就触发了。
触发检查(本节开始时持久化值是 74K):
预检压缩:上一轮 totalTokens=74K ≥ 72K → Stage 2:Compaction 触发(压缩判定值滞后一轮,此时才跨过 G;5.28 无 fresh 跳过守卫) 记忆刷新:预判值 ≥ 72K → 但同一压缩周期已刷新过 → 去重跳过 Pi 压缩:预检压缩已在 Pi 运行前完成,Pi 内部不触发(76K 阈值只在单轮暴涨跨过时兜底)
Stage 2 执行(参考源码 src/agents/compaction.ts):
条件满足: tokens(74K) ≥ 窗口(96K) - floor(20K) - soft(4K) = 72K(预检压缩 budget 触发)动作:1. 自适应分块(BASE_CHUNK_RATIO=0.4,SAFETY_MARGIN=1.2)2. 每块单独摘要3. 合并摘要4. maxHistoryShare=0.5 → 历史摘要控制在 48K 以内5. 保留最近对话(keepRecentTokens + recentTurnsPreserve=3)压缩后状态:[历史摘要] ~20K(上限受 maxHistoryShare=0.5 约束:≤ 48K)[保留的近期对话] ~ 12K[系统提示等] ~ 16K合计: ~48K → 回到阈值(72K)以下 ✅
质量守卫(compaction-safeguard.ts):压缩后检查 newContentTokens > contextWindow × maxHistoryShare × SAFETY_MARGIN(1.2)。如果超过此值(96K × 0.5 × 1.2 = 57.6K),触发 pruneHistoryForContextShare() 再清理一轮。
AI 记住了架构决策(得益于记忆刷新) 对话历史被摘要压缩,释放了空间 安全标识符(UUID、URL、文件名)被保留 行为规则(Session Startup + Red Lines)被重新注入
9.2 关键决策点总结
< max(4K, 窗口×10%) | ||||
< max(8K, 窗口×20%) | ||||
CW - floor - soft | memory/YYYY-MM-DD.md | |||
CW - floor - soft | ||||
CW - PiReserveTokens | ||||
new > CW × share × 1.2 | pruneHistoryForContextShare() |
十、总结
核心发现
1. 上下文窗口解析:至少三条路径,全配 Provider 最稳
OpenClaw 有多个参数参与窗口决定(contextTokens、Provider 窗口、模型原生窗口、运行时缓存),不同场景使用不同的优先级链。我们至少识别出三个场景的差异(详见第四章)。核心结论:配好 Provider 窗口(models.providers[].models[].contextWindow)是所有场景都稳定的最简方案。
2. 压缩与记忆刷新的时序:靠预判值而非顺序保证
代码顺序是"压缩在前,刷新在后",5.28 中 totalTokensFresh 守卫已移除,改为:刷新判定值(旧值+输出+prompt 估算)恒大于预检压缩判定值(旧值),保证刷新先触发;Pi 内部又没有记忆刷新。真正保证"刷新在压缩前"的是:预判值机制(4.20 为totalTokensFresh守卫)+ 4K 软阈值缓冲 + 时序差。详见专题文章(《OpenClaw内核解剖(三)》)。
3. 小模型保护:自动防饿死
reserveTokensFloor 受 MIN_PROMPT_BUDGET_RATIO(0.5)和 MIN_PROMPT_BUDGET_TOKENS(8K)双重约束,小模型(如 16K 窗口)不会被无限压缩饿死。
4. 标识符保留:压缩的安全底线
UUID、URL、IP、端口、文件名等操作依赖的标识符被严格保留(strict/off/custom 三种策略),防止压缩后系统操作失效。
配置建议
Provider 窗口: models.providers[].models[].contextWindow是最推荐的窗口配置方式reserveTokens:保持默认(16384)或与 reserveTokensFloor一致;若调高 ≥ 24000(= floor + soft 之和),同步调高reserveTokensFloor(24000 时阈值重合,超过则 Pi 压缩先触发)softThresholdTokens:保持默认 4000;若需减少刷新频率可调大 maxHistoryShare:默认 0.5(上下文窗口的一半留作历史摘要),无需调整 压缩模型:可选小模型做压缩摘要,降低延迟
延伸阅读
附录 A:openclaw.json 压缩配置完整示例
推荐做法:在
models.providers[].models[].contextWindow中配置 Provider 窗口,而非设置contextTokens。详见第四章。
{”agents”: {”defaults”: {”compaction”: {”reserveTokensFloor”: 20000,”reserveTokens”: 16384, // 默认值(5.28 为 16384,4.20 文档误写 20000)”keepRecentTokens”: 20000,”maxHistoryShare”: 0.5,”recentTurnsPreserve”: 3, // 默认 3(可配 0-12)”model”: ”gpt-4o-mini”,”timeoutSeconds”: 900,”postCompactionSections”: [”Session Startup”, ”Red Lines”],”memoryFlush”: {”enabled”: true,”softThresholdTokens”: 4000,”forceFlushTranscriptBytes”: ”2MB”}}}}}
附录 B:核心源码文件索引(32 个)
context-engine/types.ts | ||
context-engine/legacy.ts | ||
context-engine/delegate.ts | ||
context-engine/registry.ts | ||
config/types.openclaw.ts | ||
config/types.agent-defaults.ts | ||
config/types.agents.ts | ||
agents/context.tsagents/context-cache.ts) | ||
agents/context-window-guard.ts | ||
agents/system-prompt.ts | ||
agents/embedded-agent-runner/system-prompt.tspi-embedded-runner/) | ||
skills/loading/workspace.tspi-embedded-runner/skills.ts) | ||
plugin-sdk/skills-runtime.tspi-embedded-runner/skills-runtime.js) | ||
agents/embedded-agent-runner/run/attempt.context-engine-helpers.tspi-embedded-runner/) | ||
agents/agent-settings.tspi-settings.ts) | ||
agents/agent-compaction-constants.tspi-compaction-constants.ts) | ||
agents/compaction.ts | ||
agents/embedded-agent-runner/compact.tspi-embedded-runner/) | ||
agents/embedded-agent-runner/run/preemptive-compaction.tspi-embedded-runner/) | ||
agents/agent-hooks/compaction-safeguard.tspi-hooks/) | ||
gateway/session-compaction-checkpoints.ts | ||
agents/agent-hooks/context-pruning/pruner.tspi-hooks/) | ||
auto-reply/reply/post-compaction-context.ts | ||
auto-reply/reply/inbound-context.ts | ||
plugins/memory-state.ts | ||
plugins/memory-state.ts | resolveMemoryFlushPlan) | |
auto-reply/reply/memory-flush.ts | ||
auto-reply/reply/agent-runner-memory.ts | ||
auto-reply/reply/agent-runner.ts | ||
agents/embedded-agent-runner/wait-for-idle-before-flush.tspi-embedded-runner/) | ||
agents/spawned-context.ts | ||
config/context-visibility.ts |
附录 C:关键配置参数速查表
agents.defaults.contextTokens | |||
agents.defaults.contextInjection | always | ||
agents.defaults.compaction.reserveTokensFloor | |||
agents.defaults.compaction.reserveTokens | |||
agents.defaults.compaction.keepRecentTokens | |||
agents.defaults.compaction.maxHistoryShare | |||
agents.defaults.compaction.recentTurnsPreserve | |||
agents.defaults.compaction.model | |||
agents.defaults.compaction.timeoutSeconds | |||
agents.defaults.compaction.postCompactionSections | ["Session Startup", "Red Lines"] | ||
agents.defaults.compaction.memoryFlush.softThresholdTokens | |||
agents.defaults.compaction.memoryFlush.forceFlushTranscriptBytes | |||
MAX_COMPACTION_CHECKPOINTS_PER_SESSION) |
下一篇预告
上下文管理解决了"对话装不下"的问题,但压缩后的 AI 还是会"失忆"——丢了细节,只剩骨架。OpenClaw 的记忆系统给出了完整的答案:三层记忆架构、记忆刷新、语义检索。
下一篇《OpenClaw内核解剖(二):会话与记忆系统详解》,我们深入 OpenClaw 的会话路由、存储架构和三层记忆体系,把 AI 的"长期记忆"机制拆个清楚。
敬请期待 👀