资讯动态

深入解析Openclaw Pre-compaction Memory:Agent上下文压缩前的记忆预整理机制

发布时间:2026/9/17 17:27:34 来源:尧图企业网站定制
如果你最近在折腾 Openclaw应该会和我一样对它内部那套记忆机制产生好奇。跑过几次长任务之后你会发现真正决定一个 Agent 是“越用越聪明”还是“聊着聊着就失忆”的往往不是模型本身而是它在上下文塞满之前做了什么。我今天想聊的 Pre-compaction memory就是这个环节里最容易被忽略、却最值得抠代码的部分。先说清楚一件事Pre-compaction memory 不是某个独立功能模块而是 Openclaw 在真正执行 context compaction上下文压缩之前对历史对话、工具调用结果、临时记忆做的一套“预整理”机制。它决定了哪些消息被留进压缩摘要、哪些被打进冷存储、哪些在压缩完成后还能被捞回来。如果你直接跳过这块去调 compaction 参数大概率会踩到“压缩完变傻”“连续任务丢状态”“token 倒是省了但工具调用链断了”这些坑。这篇东西适合两类人看一类是在本地部署 Openclaw、想把长会话跑稳的玩家另一类是打算基于 Openclaw 改记忆策略、甚至自己写 Agent 内存管理模块的开发者。我会从设计思路、核心数据结构、关键代码逻辑、触发链路一直讲到排查经验尽量做到看完就能对着源码定位问题。1. 先搞懂 Openclaw 的 Pre-compaction memory 是什么1.1 从一次“对话突然变笨”的现场说起我最早注意到 Pre-compaction memory是因为一次很典型的翻车现场。当时用 Openclaw 跑一个连续抓取网页、整理资料、再输出报告的长流程前面一个小时都很正常突然某个节点开始忽略我之前明确说过的约束甚至把早前已经完成的步骤又重复执行了一遍。事后翻日志发现系统在前一个节点触发了 context compaction而它压缩时把我中途强调过的一条关键指令给“优化”掉了。那会儿 Openclaw 的行为还是偏粗暴超过阈值就把头部消息直接截走只留最近几轮。后来我顺着代码往深处翻才看到新版本里其实已经有一套更细的预压缩设计也就是 Pre-compaction memory 机制。简单说它是在压缩发生之前先把有价值的记忆做标记、快照、分层再决定哪些进摘要哪些进持久化存储。从这里开始我就把注意力从“怎么调大窗口”转移到了“压缩前到底发生了什么”。因为窗口再大也有尽头真正影响长任务稳定性的是压缩前如何对待已经产生的记忆。1.2 为什么叫“压缩前”它和 token 窗口有什么关系要理解 Pre-compaction memory得先理解 Openclaw 对上下文窗口的管理模型。正常运行时所有消息会按顺序堆积在一个 context buffer 里包含 system prompt、用户消息、Agent 的思考过程、工具调用和工具返回结果。LLM 每次请求都会把这堆东西整体发出去所以 token 消耗和上下文窗口占用是同步增长的。窗口是有限的所以 Openclaw 会设定一个“警戒水位”。当已用 token 超过某个比例比如 maxTokens 的 75%就进入 pre-compaction 阶段。这时候系统不会立刻压缩而是先把当前内存里的消息做一次“盘点”哪些消息是最近必须原样保留的哪些中间轮次可以被摘要替代哪些历史记录可以序列化到本地存储以后按需召回。这个“盘点”动作就是 compact 之前的预备状态也就是名字里 pre- 的由来。它实际上是给真正的压缩函数提供输入数据一个经过筛选和标记的 MemoryUnit 列表、一个填充好的 CompactionContext以及一组压缩策略参数。没有这一步后面的 compact() 只能无差别处理根本谈不上“智能压缩”。1.3 这套机制解决的核心痛点我在本地试过几种不同的 Agent 框架也手写过简单的 memory manager最后总结出 Openclaw 做 pre-compaction 想解决的三个核心痛点无差别截断导致关键信息丢失。旧做法是“掐头去尾”但 Agent 场景里用户最早提出的目标、中途纠正过的指令往往比最近几句闲聊更重要。工具调用链断裂。很多任务是一连串 tool_call 和 tool_result 配对的如果压缩时只留结果、不留中间状态后续步骤就不知道上一个调用返回的变量是什么。资源浪费。不做预整理就压缩经常会把已经在摘要里出现过的重复内容再次压进去白白消耗 LLM 的摘要请求。Pre-compaction memory 把“要不要留”“留多少”“留原样还是留摘要”这些决策从压缩那一刻提前到了消息进入内存时就持续跟踪。它等于在压缩器前面加了一个调度台让压缩不再是粗暴清理而是有依据的“记忆归档”。2. 核心代码骨架想读懂先认人2.1 相关模块与数据流总览Openclaw 的代码里和 Pre-compaction memory 相关的模块并不集中在一个文件里而是分散在 memory、context、llm 这几个目录下。如果你打开源码找会看到几个高频出现的名字MemoryManager负责整个记忆生命周期的管理包括消息写入、重要性打分、快照、压缩触发。ContextBuilder负责把 MemoryUnit 列表拼装成最终发送给模型的 prompt。CompactionService执行真正的压缩逻辑但它的输入是 pre-compaction 阶段产出的 CompactionContext。MemoryStore负责持久化快照、摘要和冷数据支持按 id 或关键词召回。整个数据流大致是这样每一条新消息产生后会先经过 MemoryManager 登记ContextBuilder 时刻统计 token 占用当 token 占用超过阈值MemoryManager 进入 pre-compaction 流程它调用内部方法筛选消息、生成快照、构建压缩上下文最后交给 CompactionService 完成摘要和存储。这几步在代码里是清晰分开的所以调试的时候比较容易定位问题。我一开始以为 pre-compaction 只是 compact() 函数开头几行后来才发现它其实是 MemoryManager 里一个独立的状态阶段有专门的日志标识和异常处理分支。2.2 核心数据结构MemoryUnit 与 CompactionContext看懂代码前先看数据。Openclaw 在 pre-compaction 里最核心的两个数据结构一个是代表单条消息的 MemoryUnit另一个是承载整个压缩任务的 CompactionContext。我用代码注释的方式把关键字段整理了一下// 每条消息在内存中的表示 interface MemoryUnit { id: string; role: system | user | assistant | tool; content: string; createdAt: number; tokenCount: number; importanceScore: number; // 重要性打分越高越值得保留 tags: string[]; // 例如 [goal, correction, tool_result] relatedToolCallId?: string; // 如果是 tool_result关联对应的 tool_call metadata: Recordstring, unknown; } // 预压缩阶段产出的上下文 interface CompactionContext { snapshot: MemoryUnit[]; // 当前内存全量快照 hotWindow: MemoryUnit[]; // 最近 N 条原样保留的消息 candidates: MemoryUnit[]; // 可以被摘要替代的中间消息 coldStorageKeys: string[]; // 已序列化到冷存储的 key retentionPolicy: { maxContextTokens: number; preCompactionRatio: number; hotWindowSize: number; summaryTargetTokens: number; }; summaryModel: string; // 生成摘要用的模型 }我第一次看到 CompactionContext 的时候最受启发的是它把“全量快照”和“候选摘要列表”分开了。这意味着压缩前内存里所有信息都不会被立即销毁而是有一个 snapshot 兜底。即使摘要生成失败也还能从快照里恢复原样而不是直接丢消息。2.3 核心方法snapshot、compact、restore在 MemoryManager 里有三个方法是我认为最值得细读的它们的调用顺序构成了 pre-compaction 的主流程class MemoryManager { async preCompactIfNeeded(): PromiseCompactionContext | null { const usage await this.estimateTokenUsage(); const threshold this.config.maxContextTokens * this.config.preCompactionRatio; if (usage threshold) { return null; // 未到阈值不进入预压缩 } // 第一步生成全量快照保证可恢复 const snapshot await this.snapshotMemory(); // 第二步划分热窗口和候选压缩区 const { hotWindow, candidates } this.splitMemory(snapshot); // 第三步筛选需要持久化的冷数据 const coldStorageKeys await this.archiveColdData(candidates); return { snapshot, hotWindow, candidates, coldStorageKeys, retentionPolicy: this.config, summaryModel: this.config.summaryModel, }; } async compact(ctx: CompactionContext): PromiseMemoryUnit[] { // 1. 对 candidates 生成摘要 const summary await this.summarize(ctx.candidates, ctx.summaryTargetTokens); // 2. 重组消息列表system 摘要 hotWindow const compacted [ ctx.snapshot.find((m) m.role system), { role: assistant, content: summary, tags: [summary] }, ...ctx.hotWindow, ]; // 3. 将 coldStorageKeys 写入 store await this.memoryStore.save(ctx.coldStorageKeys, ctx.snapshot); return compacted; } async restoreMemory(sessionId: string): Promisevoid { // 从 MemoryStore 读取快照按需恢复 } }这段代码是把源码里分散的逻辑做了一个线性化整理实际实现里还会夹带很多边界处理但主线就是这么三条先快照、再分区、最后摘要替换。记住这三个动作后面所有参数调优都是围绕它们展开的。3. 关键实现细节触发链路、快照选择与压缩策略3.1 触发条件什么时候进入压缩前状态我在实际运行中观察Openclaw 并不是等到上下文快满才开始处理而是有一个“软阈值”和“硬阈值”的双层设计。软阈值对应 pre-compaction 的启动点硬阈值对应真正收缩窗口的底线。在默认配置下软阈值通常是 maxContextTokens 的 0.75硬阈值是 0.9。也就是说假设你给 Openclaw 配了 128K 上下文那么用到 96K 左右时系统就会开始跑快照和候选筛选而到 115K 时即便摘要还没生成完也必须强制压缩否则请求就会报超窗错误。这个双层设计很实用。软阈值给了 Pre-compaction memory 足够的时间做“精细活”硬阈值则保证系统永远不会把窗口用完。端口实现里还有一个细节触发前会预留一段 headroom专门给当前正在执行的工具调用返回值。因为很多任务卡在最后一步工具返回超长内容如果一点缓冲都不留会直接挤爆窗口。所以你在日志里看到类似 pre-compaction triggered, usage98000/131072 这样的信息不用慌说明软阈值生效了系统正在准备压缩。真正要担心的是日志里直接出现 force compact那说明前一轮预压缩没来得及完成已经到硬阈值了。3.2 快照保留策略哪些内容值得留下Pre-compaction 阶段最核心的决策是给每条消息打上“去留标签”。我在源码里看到一套基于规则重要性打分的策略总结下来大概是这么几类用户最新指令和关键约束。这类消息通常带 correction 或 goal 标签无论多早出现都不能被摘要替代应该原样保留或放在摘要最前面。我实际测试过如果这类消息被压成一句话模型后续大概率会“忘记”约束。工具调用链中的关键状态。判断标准是一串 tool_call 是否被后续代码引用。Openclaw 会维护一个变量引用表如果某次 tool_result 的字段在后面被读取它就会被标记为 high importance。可重复或可推导的中间内容。比如网页抓取的原始 HTML、超长的 API 返回 JSON这些通常是 candidate 首选适合被压缩成结论摘要。纯粹的寒暄和重复尝试。比如 Agent 多次调用同一个失败接口、用户反复表达同一句话这类内容重要性最低基本会被直接归档到冷存储。为了让这个决策可解释Openclaw 还给每条 MemoryUnit 写了一个 importanceScore0 到 1。pre-compaction 时会先按分数降序排再结合 token 预算决定保留边界。我测试时发现importanceScore 的默认计算方式偏向“最近消息”和“系统消息”所以如果你有特殊需求比如希望“用户第一句话永远保留”最好在记忆模块里自定义打分规则而不是依赖默认值。3.3 压缩算法选择滑动窗口、摘要提取与分层衰减真正执行压缩时Openclaw 并不是简单地把 candidates 丢给 LLM 写一段摘要完事。它在 CompactionService 里做了三个策略热窗口原样保留。默认保留最近 8 到 10 条消息具体数量由 hotWindowSize 控制。这些消息不参与摘要原封不动留在 context 里保证模型对当前任务状态有完整感知。对候选区做分段摘要。如果候选区太长超过 single-pass 摘要能力Openclaw 会把消息按时间切块每块生成一个局部摘要再合并成全局摘要。这招在处理超长工具日志时特别有效不会因为输入太长把摘要请求本身搞超时。冷数据带衰减权重归档。对于一些已经失去即时价值、但未来可能用到的历史记录Openclaw 会把它们序列化到 MemoryStore同时在摘要里只保留一个“可召回索引”比如“关于竞品价格的数据见 cold://order/123”。这样既节省 token又保留了后续深挖的可能。这套组合策略本质上就是“热数据留原样、温数据出摘要、冷数据进仓库”的分层思想。我后来在自己项目里也是照着这个思路实现的一套简化版效果比直接截断稳定得多。4. 实操验证从部署到观测 Pre-compaction memory4.1 本地部署与最小复现环境想真正读懂 Pre-compaction memory光看代码不够跑起来看日志才有感觉。Openclaw 的部署方式很简单从 GitHub main 分支拉源码按官方文档安装依赖就行。本地建议用 Node 18 以上版本装好 pnpm 后执行安装脚本再配置好模型 API key 基础环境变量即可。我自己的复现环境是这样搭的git clone https://github.com/openclaw/openclaw.git cd openclaw pnpm install cp .env.example .env # 编辑 .env 填入模型 API key、memory 相关参数 pnpm dev跑起来后我会故意构造一个长任务来触发 pre-compaction比如让 Agent 连续查询十几次天气、每次返回超长 JSON同时中途追加一条指令“把最后结果整理成表格”。这样既能快速堆高 token又能测试中途指令是否会在压缩后保留。4.2 打开相关日志和指标Openclaw 默认日志级别可能不够细需要在环境变量里加LOG_LEVELdebug MEMORY_DEBUGtrue这样运行时会在控制台看到类似下面的输出[memory] pre-compaction: usage98765/131072, ratio0.75 [memory] snapshot created, units124, tokens98765 [memory] hotWindow8, candidates96, coldStorage20 [memory] summary generated, tokens2150/4000 [memory] compacted context tokens18420这些输出就是判断 Pre-compaction memory 是否正常工作的第一手证据。我会重点关注两个指标一个是 candidates 和 hotWindow 的比例另一个是摘要生成后的 token 总数。如果发现 hotWindow 被压缩没了或者摘要 token 直接打满说明参数设置有问题。另外如果部署时启用了 Prometheus 指标导出Openclaw 还会暴露 memory_pre_compaction_triggered_total、memory_compaction_duration_ms 这类指标方便做更长时间的观测。不过本地调试用 DEBUG 日志就够了。4.3 关键参数调整与效果对比我把自己在项目里实际调过的几个参数整理成一张表方便你照着实验参数名默认值作用我的建议MEMORY_MAX_CONTEXT_TOKENS128000上下文硬上限根据模型实际窗口留 10% 缓冲MEMORY_PRE_COMPACTION_RATIO0.75软阈值比例长任务场景可以降到 0.7留更多处理时间MEMORY_HOT_WINDOW_SIZE8最近原样保留消息数工具密集型任务调到 12否则 6 也够MEMORY_SUMMARY_TARGET_TOKENS4000摘要目标长度不要超过总窗口 5%否则压缩无意义MEMORY_SNAPSHOT_DIR./memory_snapshots快照持久化目录设置成独立磁盘避免和日志混在一起我最常做的一个对比测试是在同样一个 30 轮任务里把 preCompactionRatio 从 0.75 改成 0.85观察任务末段是否出现“理解偏移”。实测下来0.75 时模型能记住中途追加的约束0.85 时偶尔会漏。原因不难理解触发晚留给快照和摘要的时间不够系统只能仓促压缩质量自然会下降。另外hotWindowSize 也不是越大越好。调大它确实能保留更多近期细节但也会挤压摘要区导致更早的历史记忆被压缩得更狠。所以如果你发现“最近几轮很聪明但整体任务目标丢失”大概率是 hotWindow 太大、摘要太短。5. 常见问题与排查技巧实录5.1 明明还有 token为什么提前触发压缩有段时间我在日志里看到 pre-compaction 在 token 占用刚到 50% 时就触发了第一反应是配置写错了。后来查代码才发现Openclaw 的 token 估算不是简单数消息字符它会额外计算每个 MemoryUnit 的 metadata 和 tool schema 长度。某些工具的入参 schema 很长虽然消息体不大但估算值时被放大了。另外还有一个隐藏因素每条 tool_result 会保留一份对应的 tool_call 内容用于配对检查。如果某个工具返回了超长 JSON即使你只用到其中两个字段估算时也会把完整 JSON 算进去。解决办法是两类一是缩短工具返回在调用工具前对 result 做截断或字段过滤二是在配置里调整 token 估算器的系数让它更接近真实模型 tokenizer 的计数。我个人更推荐前者因为截断工具返回还能降低模型误读长文本的概率。5.2 压缩后回答质量下降怎么定位是不是摘要的锅压缩后质量下降是最常见的反馈但不一定是 Pre-compaction memory 本身的 bug。我在定位时一般分三步第一步关闭 pre-compaction把同一个任务跑一遍如果质量恢复说明问题出在压缩阶段。第二步打开 debug 日志把压缩后的实际 context 内容导出来人工检查摘要里是否丢了关键信息。第三步如果发现摘要确实漏了就去检查 importanceScore 的规则看看是不是把某些关键消息错误地打成了低分。我还遇到过一种情况摘要本身没问题但 hotWindow 里的最新消息和摘要之间存在“语义断层”。比如摘要里最后一句话是“用户希望输出表格”但 hotWindow 中第一条消息是“继续”模型就不知道继续什么。这种时候需要调整摘要生成提示词强制要求在摘要末尾保留“当前任务状态”这一项。5.3 重启后上下文丢失快照没恢复Pre-compaction 阶段生成的冷存储数据是持久化的但默认并不会在每次启动时自动恢复。如果你重启 Openclaw 后之前对话的“记忆”没了先检查 memory_snapshots 目录下有没有生成最新的快照文件。有文件但没恢复大概率是启动参数里少了恢复标志。我踩过的坑是快照目录权限不对导致序列化失败但日志里只出现一行 warn不仔细看根本发现不了。建议在部署脚本里把 MEMORY_SNAPSHOT_DIR 指向一个明确存在的目录并定期检查磁盘剩余空间否则快照写一半失败恢复时只能拿到残缺数据。5.4 内存占用越来越高疑似泄漏如果你长时间跑 Openclaw发现进程内存只涨不降除了 Node.js 本身的 GC 问题最可能与 MemoryStore 里的冷数据缓存有关。Pre-compaction 会把快照和摘要同时缓存在内存里目的是加速后续召回。但当任务量很大时这个缓存可能越积越多。我的排查思路是先看 MemoryStore 的缓存上限配置通常有一个 MAX_CACHED_SNAPSHOTS 之类的参数。调低它可以让旧快照尽快被清理。然后再看是不是有某个超大 MemoryUnit 一直被 hotWindow 或 system prompt 引用导致无法回收。针对这种“钉子户”我会在业务层增加 message 合并逻辑把连续同类型的 tool_result 直接合并成一条。6. 最后分享一点个人实践建议我在本地把 Openclaw 的 Pre-compaction memory 翻来覆去调了几周最大的体会是不要把它当成一个“默认配置就够用”的黑盒。它更像是 Agent 的长期记忆中枢值得你根据实际任务类型去做针对性调整。如果你的任务偏短、工具调用少默认参数完全够用但如果像我一样经常跑长流程、批量处理多步骤任务我强烈建议你花半天时间把跑出来的快照文件打开看一看。那份快照比任何文档都能说明问题因为它真实记录了系统眼里“什么值得记住”。另外一个小技巧把 Pre-compaction 触发前后的完整 context 分别导出放在一起对比。这样你能很快建立直觉知道摘要到底丢掉了哪些细节、hotWindow 保留了哪些近期信号。我后来设计自己项目的记忆模块时就是靠这份对比记录总结出了一套非常实用的分级策略。这也是我认为 Openclaw 这个设计最值得抄作业的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价