假设我们还在让 OpenClaw 修改那份项目清单。
起初,事情很简单:读文件,整理几项任务,调整措辞。后来又看了测试输出,讨论了两个方案,撤回其中一个,还补了一句:“先别动数据库,今天只改文档。”几个小时过去,对话里已经塞进了大片日志、代码和来回讨论。
这时,界面提示正在压缩上下文。
我们当然希望它继续工作。可比起“对话变短了”,更在意的是另一件事:刚才那句“别动数据库”,它还记不记得?已经放弃的方案,会不会又被拿出来执行?
上下文压缩真正要交接的,是一项尚未完成的工作。沿着 OpenClaw 的实现往下看,会发现这件事至少有三个环节:决定什么时候整理,划出哪些原文继续保留,再把整理后的状态接回执行循环。任意一处边界含糊,下一轮都可能接错话。
本文基于 OpenClaw v2026.5.22 的固定提交,以及它使用的 Pi 0.75.4。下面的清单任务是解释机制的假设场景;局部实验使用构造消息,不是真实模型对话。
桌面满了,但不能等到纸掉下去
可以把模型的上下文窗口想成一张有限大小的工作桌。用户要求、工具返回、旧回答都摊在上面,接下来的生成也需要空间。
Pi 的基础判定很短:启用压缩时,如果已用上下文超过“窗口大小减去预留量”,就达到压缩条件。注意这里是严格大于。假设窗口只有 1,000 token、预留 200,800 不触发,801 才触发。这些数字只是方便看边界的实验参数。判定实现
这里有两个很容易混淆的预算。reserveTokens 影响什么时候开始腾空间;keepRecentTokens 决定划分历史时,希望留下多少近期原文。调大前者会更早触发,调大后者会倾向于保留更多近期内容。它们不是同一个“压缩强度”旋钮,也不能推出摘要一定有多少字。
输入大小也不总是精确值。代码优先利用有效的模型 usage;若后面又追加了消息,再估算这段尾巴。按消息估算的路径用了字符数除以四一类的启发式,并对部分内容类型特殊处理。这不是通用 tokenizer,更不能保证中文、代码和图片都估得一样准。OpenClaw 另有预算与保护逻辑,因此 Pi 的基础公式不能当成整套系统唯一的触发条件。usage 与估算

如果估算没拦住增长,模型仍可能返回上下文超限。Pi 的会话层把这种情况和普通阈值触发分开:同模型的溢出走压缩恢复,并限制一次压缩后重试;普通阈值触发不会因此自动重跑刚结束的回答。它还避开压缩边界之前的旧 usage,防止刚整理完,又拿旧账单判断桌面已经满了。恢复与防重复检查
这是一种很实用的状态机思路:提前整理和失败恢复虽然共用压缩能力,进入它们的原因、退出后的动作却不同。
也可以反过来看两个参数的代价。很早就开始整理,会更频繁地付出摘要调用的时间和费用;保留大量近期原文,则可能让整理后的上下文仍然很大。对于一轮里反复读文件的任务,单纯增加保留预算未必解决问题,因为真正占空间的可能就是刚刚返回的内容。配置应当配合任务形态,而不是追求一个适合所有会话的固定比例。
从最后一句往回找,找到哪里才能下剪刀
假设最新一轮是这样的:用户要求检查清单;助手发起读文件;工具返回一大段内容;助手给出初步判断。现在需要保留最近一段,其余交给摘要。
最直观的做法是截取最后若干 token。但消息不是没有结构的字符串。工具结果需要能对应到工具调用;留下半截 JSON 或一个失去来源的结果,都可能让下一次模型请求难以理解。
Pi 的 findCutPoint 先找合法消息边界,再从末尾向前累计估算大小。预算达到后,选择当前位置或其后的合法切点。工具结果本身不作为切点;助手消息则可以成为切点,因为它发出的工具调用及随后结果可以一起留下。切点算法
“保留预算”因此只是近似目标,不能理解成严格留下恰好这么多 token。我们的构造实验里,预算落在一条工具结果内部,最终切点移到了后面的助手消息;增加预算后,切点又能回到带工具调用的助手消息,连同结果一起保留。算法首先尊重可用的结构边界。
还有一个更细的问题:即便没有拆坏调用与结果,也可能从一轮任务的中间开始保留。刚才的原始要求已经在切点前面,留下的助手却正在解释它。
Pi 为此记录 turnStartIndex,将待处理内容分成更早的历史、当前轮被截掉的前半段,以及保留的后半段。prepareCompaction 分别提供 messagesToSummarize 和 turnPrefixMessages。摘要不仅要交代此前做了什么,还要解释这一轮为什么走到这里。准备阶段
换成清单任务,就是不能只留下“第二种做法不合适”,却丢掉第二种做法是什么、为何被否决。这个前缀交接,比简单增加保留长度更有针对性。
从算法成本看,这里主要是几次线性扫描:收集边界、逆向累计、定位轮次起点。它没有尝试为每条消息计算语义价值,再求一个最优组合。这样的取舍让准备阶段更容易预测和测试;信息价值的判断主要交给摘要阶段承担,而摘要质量也因此成为另一项必须单独检验的能力。
摘要也要读材料,太长了怎么办
到这里还不能直接理解为“Pi 生成一段摘要,OpenClaw 原样使用”。Pi 会先发出 session_before_compact 扩展事件,允许扩展返回压缩结果或取消。OpenClaw 的 safeguard 实现就在这条接口上工作,可以处理分阶段摘要、文件操作、工具失败以及标识符保留;配置自定义压缩 provider 时也会走相应扩展路径。扩展接缝 · OpenClaw safeguard
这里会遇到一个递归般的难题:原对话已经很长,负责总结它的模型也有上下文限制。
OpenClaw 的摘要辅助层会根据消息大小和可用窗口组织分块、分阶段处理,再合并结果。以 splitMessagesByTokenShare 为例,它按估算 token 分配块大小,却额外维护尚未配齐结果的工具调用 ID 集合。只要一次工具交互还没有闭合,就不能仅因为达到平均份额,便随意把它切成两块。分块与工具配对 · 分阶段摘要
这和刚才的切点算法解决的是不同层次的问题。前者划分“哪些原文留给下一轮”,这里划分“哪些材料交给一次摘要请求”。两层都需要尊重消息结构,但不能混为一个函数。
摘要的内容目标也很具体。Pi 的提示结构会要求交代目标、约束、进展、决定和下一步;后续压缩会接收旧摘要来更新状态。OpenClaw 又补上保留精确标识符等要求。对我们的任务而言,“不要改数据库”和“文档里还有两项待检查”,显然比已经解决的长报错更值得留下。摘要生成与更新 · 标识符策略
不过,这些依旧是引导和保护,不是无损压缩证明。多阶段会增加模型调用,也增加中间摘要遗漏信息的机会。路径被要求精确保留,不代表每次都保留成功;约束写进摘要,也不等于执行器已经获得了独立的权限约束。
写完交接纸,还得把它放到下一轮桌上
摘要完成后,系统还需要一个明确的接续位置。Pi 的压缩结果里,除了 summary,还有 firstKeptEntryId:从哪个条目开始继续保留原文。
会话层先检查取消状态,再调用 appendCompaction 写入压缩条目,随后通过 buildSessionContext 重建模型视图,把它放回 agent.state.messages。重建后的主体是摘要与保留的近期内容,后续新消息继续接在后面。这让“摘要说了什么”和“原文从哪里接上”成为一份可恢复的状态,而不只是内存里临时拼接的一段提示。落盘与重新接入 · 会话视图重建

我们用原始 Pi 模块在临时目录做了局部验证:写入六条消息、追加压缩条目,再重新打开会话。新视图是摘要加指定尾部,旧的六条消息仍在文件里。这验证了“模型所见变短”与“历史记录被删除”并不等价;没有验证断电、并发写入或真实模型的摘要质量。
OpenClaw 还提供可选的 transcript rotation:成功压缩后建立后继记录文件,迁移必要状态和未摘要尾部,旧文件保留。它与每次请求前临时裁剪工具结果的 pruning 也不同,后者不是持久化摘要检查点。压缩、轮换与裁剪的区别
手动整理,为什么可能收得更干净
还有一个容易让实际体验与架构图对不上的分支:/compact。
这个版本里,OpenClaw 在手动压缩后会尝试强化边界。如果显式配置了 agents.defaults.compaction.keepRecentTokens,就尊重近期尾部;否则,在最新条目确实是压缩条目、摘要非空且保留尾部之前确有可摘要消息等条件满足时,可以把 firstKeptEntryId 改成压缩条目自身,让重建视图从摘要开始。手动边界的条件与实现 · 调用处
因此,“压缩后总会保留最近两万 token”不是正确承诺。既要区分 Pi 默认设置和 OpenClaw 的实际配置,也要区分自动整理与用户主动要求建立检查点。这里说的是原始消息尾部;摘要本身仍可能包含扩展保留下来的近期内容。
如果要自己设计类似系统,我会把触发器、边界选择、摘要生成、持久化提交分别做成可替换模块。触发器测试阈值和恢复次数;边界模块测试调用与结果、超长单轮;摘要模块用固定任务集测量约束与进度是否保留;提交模块测试取消、重开和失败。不能用“少了多少 token”这一项,代替全部验收。
这次的 26 项局部检查覆盖了预算边界、切点、轮次前缀、文件追踪和会话重建,也检验了手动边界改写的纯转换函数;完整 Gateway、模型调用和摘要质量仍未做端到端复现。
回到那份清单,理想的交接应该让下一轮知道:文件已经读过,哪个方案被否决,还有哪些工作没完成,以及为什么今天不能动数据库。能继续生成文字只是第一步,能带着正确的工作状态继续,才是上下文压缩值得花心思设计的地方。

