Openclaw Pre-compaction memory 核心代码分析:从触发条件到记忆落盘的完整链路 1. 从一次记忆丢失说起Openclaw Pre-compaction memory 到底在做什么如果你在本地跑过 Openclaw 的长会话大概率遇到过这种诡异现象聊到第 40 轮前面明确交代过的项目路径、命名规范、接口约定突然就忘了。你回头翻会话记录发现上下文并没有被截断但模型就是答非所问。更麻烦的是日志里既没有报错也没有明显的压缩事件只有一条不起眼的compaction事件一闪而过。这个问题的根子就在 Openclaw 的 Pre-compaction memory 机制上。它是一套会话接近自动压缩前先把重要信息落盘的防护逻辑核心代码集中在src/auto-reply/reply/memory-flush.ts编译产物对应node_modules/openclaw/dist/extensionAPI.js。它要解决的核心矛盾是上下文窗口是有限的但会话里积累的持久信息比如这个项目用 pnpm 不用 npm是无限的。如果不做落盘压缩一发生这些信息就被摘要算法当成低价值 token丢掉了。Pre-compaction memory 适合谁三类人最需要吃透它一是本地复现 Openclaw 记忆机制的开发者二是被记忆丢失/重复压缩折磨的 Agent 调试者三是想把 Openclaw 的 compaction 策略迁移到自己项目里的工程师。它不是一个黑盒开关而是一条从token 计数到阈值判定再到落盘元数据的完整链路每一环都有可观测、可配置、可排障的抓手。我试过在本地把一个 200k 上下文窗口的会话硬聊到触发点观察memoryFlushAt和memoryFlushCompactionCount两个字段的变化才真正理解为什么有些场景会重复压缩、有些场景干脆不落盘。下面按源码链路拆开讲每一步都给出可复制的配置和验证动作。2. 前置准备TaoToken 接入与 Openclaw 运行环境搭建在拆源码之前得先把运行环境跑通否则你连compaction事件都看不到。Openclaw 的模型调用走的是标准 OpenAI 兼容协议所以任何兼容端点都能接。这里用 TaoToken 作为模型接入层原因是它的 Base URL 和 Key 管理足够干净方便你在调试 memory flush 时快速切换模型做对照实验。第一步拿到 API Key。访问 https://taotoken.net/api-keys 创建密钥注意这个页面是控制台里的密钥管理入口创建后只显示一次复制到本地环境变量里。第二步确认 Base URL 是https://taotoken.net/api注意这个地址不带任何查询参数是纯 API 根路径。第三步在 Openclaw 的配置文件里把 provider 指向这个端点。这里有个容易踩的坑Openclaw 的 provider 配置和模型 ID 是分开的Base URL 填错会导致local proxy failed或者 401但报错信息不会直接告诉你URL 错了而是抛一个模型调用失败。所以配置完先别急着聊长会话用一条最小请求验证连通性。# 验证 TaoToken 端点连通性确认 Key 和 Base URL 正确 curl -s https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ | head -c 500如果返回模型列表 JSON说明接入层没问题。如果返回 401检查 Key 是否有多余空格如果返回 404检查 Base URL 是否误加了/v1之外的路径。这一步过了再进 Openclaw 的配置。Openclaw 的模型配置在openclaw.json的agents.defaults下provider 段需要写全三件套Base URL、API Key 引用、Model ID。很多人只填了 Key 和 Model忘了 Base URL结果 Openclaw 回落到默认端点触发local proxy failed。完整片段如下{ agents: { defaults: { provider: { baseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY, model: claude-sonnet-4-20250514 }, compaction: { memoryFlush: { enabled: true, softThresholdTokens: 4000 }, reserveTokensFloor: 8000 } } } }注意softThresholdTokens和reserveTokensFloor这两个值它们直接决定 memory flush 的触发时机后面会详细算。配置写完后用openclaw --print-config确认解析结果避免 JSON 语法错误导致配置被静默忽略。3. 可复制配置触发阈值、提示词与落盘路径的完整 settings 片段Pre-compaction memory 的触发逻辑全在shouldRunMemoryFlush这个函数里核心是一个减法公式。理解这个公式你就能精确控制什么时候落盘。阈值计算是这样的threshold contextWindow - reserveTokens - softThreshold。其中contextWindow是当前模型的上下文窗口 token 数reserveTokens是reserveTokensFloor默认保留给压缩后摘要的空间softThreshold是softThresholdTokens默认 4000。当会话的totalTokens超过这个 threshold且上一次 flush 的compactionCount不等于当前compactionCount才会触发。举个例子假设模型上下文窗口 200000reserveTokensFloor设 8000softThresholdTokens设 4000那么 threshold 200000 - 8000 - 4000 188000。也就是说会话累计到 188000 token 时memory flush 才会启动。如果你把softThresholdTokens调大到 20000threshold 变成 172000落盘会更早发生但代价是更频繁的额外模型调用。这里给出一个偏保守的配置适合本地调试时观察行为{ agents: { defaults: { compaction: { memoryFlush: { enabled: true, softThresholdTokens: 4000, prompt: Pre-compaction memory flush. Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). If nothing to store, reply with NO_REPLY., systemPrompt: Pre-compaction memory flush turn. The session is near auto-compaction; capture durable memories to disk. You may reply, but usually NO_REPLY is correct. }, reserveTokensFloor: 8000 } } } }prompt和systemPrompt是发给模型的指令告诉它现在把持久信息写到memory/YYYY-MM-DD.md。注意ensureNoReplyHint会给这两个提示自动追加静默回复的约定所以模型在没有信息可存时会回NO_REPLY而不是硬编一段废话。这个设计很关键它避免了 flush 本身污染上下文。落盘路径由模型自己决定约定是memory/YYYY-MM-DD.md相对于 workspace 目录。如果memory/目录不存在模型需要自己创建。这里有个权限坑如果 Openclaw 跑在沙箱里workspaceAccess必须是rw否则memoryFlushWritable判定为 false整个 flush 直接跳过而且不会报错只在 verbose 日志里留一行。所以调试时先把 verbose 打开。# 打开 verbose 日志观察 memory flush 是否被跳过 openclaw --verbose --config ./openclaw.json如果你用的是 Claude Code 做本地开发可以把上面的配置片段直接放进项目的.claude/settings.json里做对照但注意 Openclaw 的配置键名和 Claude Code 不同别混用。需要看更多接入示例的话https://taotoken.net/doc 里有完整的协议说明。4. 验证请求从 compaction 事件到 memoryFlushAt 落盘的逐步确认配置就绪后怎么确认 memory flush 真的跑了不能只看模型回复要看会话存储里的元数据。Openclaw 在 flush 成功后会更新两个字段memoryFlushAt时间戳和memoryFlushCompactionCount触发时的压缩计数。这两个字段是判断是否落盘的唯一可信依据。验证分三步。第一步制造一个接近阈值的会话。最省事的办法是把softThresholdTokens临时调小比如设成 100这样几轮对话就能触发。第二步观察compaction事件流。在runEmbeddedPiAgent的onAgentEvent回调里当evt.stream compaction且phase end且willRetry为 false 时memoryCompactionCompleted会被置为 true随后incrementCompactionCount才会执行。第三步检查会话存储文件里的memoryFlushAt。# 触发一次 flush 后检查会话存储中的落盘元数据 cat ~/.openclaw/sessions/session-key.json | jq { compactionCount, memoryFlushAt, memoryFlushCompactionCount }如果memoryFlushAt有值且memoryFlushCompactionCount等于当时的compactionCount说明落盘成功。如果memoryFlushAt为空说明 flush 被跳过了回到第 3 步检查workspaceAccess和 verbose 日志。这里有个反直觉的点memoryFlushCompactionCount的作用是防止重复 flush。shouldRunMemoryFlush里有一行if (typeof lastFlushAt number lastFlushAt compactionCount) return false;意思是如果上一次 flush 已经记录在当前压缩计数上就不再触发。这解决的是同一轮压缩被多次判定的问题。但如果你发现记忆重复落盘往往是incrementCompactionCount没被调用导致compactionCount没变而memoryFlushAt被反复更新。验证模型侧是否真的写了文件可以直接看 workspace 下的memory/目录# 确认模型是否按约定写入了记忆文件 ls -la ./workspace/memory/ cat ./workspace/memory/$(date %F).md如果目录为空但memoryFlushAt有值说明模型收到了 flush 提示但判定无信息可存回了NO_REPLY。这不算 bug但如果你明确知道有信息该存就要检查prompt是否被ensureNoReplyHint改写得太激进或者模型本身对指令的遵循度不够。换一个遵循度更高的模型做对照能快速定位是提示词问题还是模型问题。5. 本篇常见错排查401、local proxy failed 与 reading choices 的真实报错对照调试 memory flush 时报错往往不直接指向根因。下面按真实遇到的报错逐条对照。401 Unauthorized最常见的是 Key 没读到。Openclaw 用apiKeyEnv引用环境变量如果变量名拼错或没 export请求会带空 Key。检查echo $TAOTOKEN_API_KEY是否有值以及openclaw.json里的apiKeyEnv是否和实际变量名一致。另一个隐蔽原因是 Key 前后有换行从网页复制时容易带上。local proxy failed这个报错通常出现在 Base URL 配置错误时。Openclaw 会尝试把请求发到配置的端点如果端点不可达或路径不对就抛这个。确认baseUrl是https://taotoken.net/api不要写成https://taotoken.net/api/v1或带尾斜杠。如果用了本地代理做请求转发检查代理是否在运行但更推荐直接用标准端点排除变量。reading choices of undefined这个报错说明响应体结构不符合预期代码在解析response.choices[0]时拿到 undefined。根因通常是端点返回了错误 JSON比如 401 的错误体但调用方没检查状态码就直接读choices。排查时先看原始响应# 打印原始响应确认返回结构 curl -s -w \nHTTP_STATUS:%{http_code}\n https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:ping}]}如果 HTTP 状态是 401 但 body 里没有choices那就是 Key 问题如果状态 200 但choices为空检查 model ID 是否拼错。OAuth 相关报错如果你用的是需要 OAuth 的 providertoken 过期会抛 OAuth 错误。Openclaw 的runWithModelFallback会在主 provider 失败时尝试 fallback但如果 fallback 也没配最终错误会冒泡。检查resolveAgentModelFallbacksOverride的配置确保至少有一个可用的 fallback 模型。记忆重复落盘前面提过根因是compactionCount没递增。检查onAgentEvent里phase end !willRetry的条件是否满足。如果压缩事件带了willRetry: truememoryCompactionCompleted不会被置 trueincrementCompactionCount不执行但memoryFlushAt仍会被更新导致下次判定时lastFlushAt ! compactionCount再次触发。解决办法是确认压缩事件正常结束或者手动在配置里调大softThresholdTokens降低触发频率。沙箱权限导致静默跳过如果workspaceAccess不是rwmemoryFlushWritable为 false整个 flush 被跳过且无报错。这是最隐蔽的坑因为日志里只有一行 verbose。排查时先确认沙箱配置{ sandbox: { workspaceAccess: rw } }对照完这些报错你会发现大部分问题都集中在配置没生效和事件没走完两类。前者靠--print-config和 curl 验证后者靠 verbose 日志和会话存储元数据验证。6. 语义一致 CTA把 memory flush 调试沉淀成可复用的接入流程拆完这条链路你会发现 Pre-compaction memory 的设计思路其实很通用用 token 阈值做触发用压缩计数做去重用元数据做落盘确认。这套模式可以迁移到任何需要上下文压缩前抢救信息的 Agent 项目里。如果你在本地复现时卡在接入层建议先把模型端点跑通再调 memory flush否则你分不清是 flush 逻辑问题还是请求根本没发出去。模型对话调试入口在 https://taotoken.net/chat可以快速验证 Key 和模型 ID 是否匹配。需要长期跑编码类 Agent、反复触发 compaction 的场景用 Coding Plan 更划算入口在 https://taotoken.net/coding-plan。完整的接入协议和配置字段说明在 https://taotoken.net/doc遇到 401 或reading choices这类报错时对照文档里的响应结构能省不少时间。最后留一个实用技巧调试 memory flush 时把softThresholdTokens设成 100reserveTokensFloor设成 0这样几乎每轮对话都会触发你能在几分钟内观察到完整的触发-落盘-去重循环。等逻辑确认无误再改回生产值。这个办法比读十遍源码都管用。