沿用上一篇的 Alice。她和 Agent 已经聊了两周,transcript 也一直存在。今天她问“照上周的决定继续”,Agent 却回答得像第一次听说。最直觉的判断是“历史丢了”,但也可能是历史还在、只是没有进入当前模型窗口;还可能是关键决定只留在长 transcript 里,没有写入长期 memory;或者 compaction 已经把旧对话概括成一份没有保留该细节的摘要。

这些故障看起来都叫“忘记”,修法却完全不同。扩大窗口不能修复错误的 session route,给 prompt 加一句“请记住”也不能把数据变成持久状态,把全部 transcript 每轮塞进模型更会很快撞上成本和上限。要准确排查,必须先把 record、memory 和 runtime view 拆成三个不同的 owner。

阅读契约。读完后你应该能回答:哪些 workspace 文件默认进入 Project Context;system prompt 为什么每次运行重建;工具为什么有两份 token 成本;ContextEngine 的 ingest、assemble、compact、afterTurn 分别掌管什么;pruning 与 compaction 哪个改 transcript;以及 MEMORY.md 为什么不能与 context window 画等号。

证据边界。本文固定在提交 e6b4264,以该快照中的 workspace/bootstrap、ContextEngine、内建运行时与 SQLite 会话实现为准。默认客户端摘要、provider checkpoint 与自定义 engine 分别有自己的行为;下面明确区分,不把一种压缩路径当作所有运行时的合同。

一、一次模型请求由哪些材料组成

1.1 “上下文”包含本次请求的全部输入

在 OpenClaw 的语义里,context 不是“最近几条聊天”,而是这一次请求真正交给模型的全部输入:system prompt、conversation messages、tool calls/results、附件和工具定义。模型的 context window 是它们共同争用的容量,不存在一块只给聊天历史保留、永远不受工具 schema 影响的独立空间。

这条定义解释了一个常见现象:你没有多聊几轮,只装了几个描述很长的工具,剩给历史的预算就变少了。界面看到的消息长度也不是唯一指标;图片、结构化 tool result、JSON schema 和动态注入都可能占用窗口。

model context = system prompt
              + assembled conversation history
              + tool schemas
              + tool results and attachments

memory != model context
transcript != model context

这里两个“不等于”比公式本身更重要。transcript 是可持久化的会话记录;memory 是跨轮次甚至跨会话保留的精选事实;model context 是一次运行临时形成的视野。前两者可以成为第三者的来源,但都不会自动、完整地等于第三者。

1.2 workspace 提供工作目录,sandbox 负责隔离

每个 Agent 有一个 workspace,默认是工具相对路径和 bootstrap 文件的根目录。workspace.ts 负责建立目录和初始文件;运行时再读取这些文件,作为 Project Context 注入 system prompt。

“工作目录”不能被误读成“文件系统边界”。如果工具没有 sandbox 约束,绝对路径仍可能访问 workspace 外部;真正的文件、网络和进程隔离属于第六篇的 sandbox/tool policy。workspace 解决的是默认归属与协作材料,不独自承担安全性。

还要把 workspace 与 OpenClaw 自己的状态目录分开。前者保存 Agent 可编辑的说明、技能和记忆文件,后者保存配置、凭据、session store 等 runtime 状态。把 session 数据库放进 workspace,或把用户项目塞进 runtime state 目录,都会让备份、权限和迁移边界混乱。

1.3 Project Context 只加载约定的 bootstrap 文件

canonical bootstrap 集合有明确名字:AGENTS.md 约定工作方式,SOUL.md 描述人格与原则,IDENTITY.md 描述身份,USER.md 保存用户画像,首次初始化时还会加入 BOOTSTRAP.md;根 MEMORY.md 只有在已经存在且当前不是 shared group/channel 等受限会话时才进入 Project Context。runtime 不会递归读取 workspace,也不会因为文件“看起来重要”就自动注入。

文件默认角色容易混淆之处
AGENTS.md仓库/工作流约定与行为边界。不是任意命令执行器,仍受上层 policy 约束。
SOUL.md角色、语气、原则。人格文字不能代替访问控制。
IDENTITY.mdAgent 自我描述。与 provider/model 身份是不同层。
USER.md稳定的用户偏好与背景。有独立长度上限,避免画像吞掉窗口。
BOOTSTRAP.md首次启动引导。初始化完成后不应继续当永久上下文膨胀源。
MEMORY.md已存在时提供长期事实与决定。shared session 会移除根 memory;它不是无条件公开的全局 prompt。

每个文件和总 bootstrap 都有容量上限;太长时会截断,并在注入内容里留下 truncation marker。缺失的常规文件会留下可观察标记,但未创建的 USER.md、MEMORY.md 会直接跳过。会话过滤还会让 subagent 只保留 AGENTS.md,cron 只保留 AGENTS.md、SOUL.md、IDENTITY.md、USER.md;两者都不加载根 memory。这些差异不能简单归因于截断。

TOOLS.md、BOOT.md、HEARTBEAT.md 各自有用途,但不能因此推断它们总在默认 Project Context 列表里。源码阅读时应追真实 bootstrap resolver,而不是只按文件名猜注入语义。

1.4 system prompt 每轮组合稳定规则与动态现场

buildAgentSystemPrompt 不是安装时生成的一张静态模板。每轮运行都会根据当前工具、技能 metadata、workspace、runtime、时间、channel capability、sandbox 状态和 bootstrap 内容组合 system prompt。bootstrap snapshot 每轮重新读取文件,内容相同才复用旧数组;因此通过会话过滤和长度限制的文件修改会在下一轮构建中生效,不需要回写旧 transcript。

这也意味着 prompt 必须分清稳定与动态。角色原则适合放 workspace 文件,当前时间、可用工具和沙箱状态应运行时生成;把所有动态信息持久化进 transcript 会制造过期事实,把所有稳定规则由 channel 每次拼接又会造成入口不一致。

斜杠 directive 也属于前处理。/think、/model、/queue 等控制信息先更新 runtime/session 设置,再从送给模型的用户正文中剥离。模型看到的是处理后的请求,而不是 Gateway 用来控制本轮的全部协议文本。

1.5 Skills 按需展开,工具 schema 随请求发送

为了控制成本,system prompt 默认只列出可用 skill 的名字、摘要和位置。模型判断需要某项能力时,才通过读取 SKILL.md 加载完整指令。若把每个 skill 的全文永久注入,安装得越多,模型真正留给任务和历史的空间反而越少。

工具的成本更隐蔽。system prompt 里有一份面向模型阅读的工具名称/简述;provider 请求里还有决定可调用参数的 JSON schema。后者不一定以普通聊天文字显示,却同样计入 context。工具描述精炼、schema 去掉无效嵌套,常常比删几句用户历史更有效。

一个实用判断:skill 是“什么时候需要去读哪份操作手册”,tool schema 是“本轮模型现在能够发出什么结构化调用”。前者适合按需展开,后者要在可调用时完整可见。不要为了省 token 把工具参数合同藏起来,也不要把所有操作手册提前塞满窗口。

二、ContextEngine 怎样选择本轮消息并维护历史

2.1 ContextEngine 组装模型输入,不自动替代 session store

默认的 legacy context engine 保留原有行为:ingest 和 afterTurn 基本不做额外工作,assemble 让现有 sanitize/validate/limit pipeline 处理消息,compact 委托内建总结。插件可以占用唯一的 plugins.slots.contextEngine,但它接管的是上下文生命周期,不自动成为 transcript 的唯一数据库。

ContextEngine 的核心生命周期可按时间读:

  1. ingest:消息进入 session 时,让 engine 保存、索引或观察它。
  2. assemble:每次模型请求前,根据 token budget 返回有序 messages,并可附加 systemPromptAddition。
  3. compact:窗口接近上限或用户执行 /compact 时,缩减旧历史。
  4. afterTurn:成功运行结束后更新索引、持久状态或安排后台维护。
  5. maintain(可选):通过受控 rewrite API 做 transcript 维护,可配置后台执行。

assemble 返回的是本轮消息序列,并可增加 system prompt 片段;实际调用给它 messages、预算、当前用户 prompt 与可用工具名集合,并不把完整 system prompt 和工具 JSON schema 都交给它重建。运行时另外组装这些材料,最终送给 provider。选择消息也不授权插件任意删改持久记录:重写 transcript 仍须走 runtime context 的安全接口。

2.2 turn fence 防止重试重复写入当前消息

一个 custom engine 可能把 transcript 同步进向量库,再按相关性组装上下文。真正危险的地方在 retry:同一个用户 turn 若第一次 provider 请求失败并重试,engine 不能把它提交两次,也不能在组装时一会儿看见当前消息、一会儿看不见。

因此持久接管 admitted turn 时,engine 要声明 current-turn fence 与 atomic-idempotent advancement,并用 advancementKey 实现原子 commitTurn。相同 key 的重试必须返回 duplicate,而不是追加第二份记录。没有完整声明时,host 会保守地让该逻辑 turn 回到 legacy 路径,避免插件创造双写历史。

这条合同值得迁移到所有“外部记忆 + Agent”系统:检索相关性只是读路径;幂等提交、可重复组装和当前 turn 的可见边界才决定状态能不能相信。

2.3 Transcript、Memory 与本轮模型输入分别保存不同内容

Transcript 保存持久会话记录,Memory 按需检索,Project Context 经会话过滤后进入临时组装的本轮模型输入;保存内容不等于本轮可见内容
图示区分持久来源与临时输入,不表示自动互写。虚线只画按需检索;允许的会话也可通过 Project Context 加载根 MEMORY.md,具体过滤见 1.3。

Transcript 按顺序记录某个 sessionId 里真实发生的消息、tool call 与 result。Memory 保存经过筛选、希望长期复用的事实与决定。本轮模型输入则是 assemble 临时选出的消息序列;请求结束后,这份临时序列不需要再次写成历史。

MEMORY.md 用于 curated long-term memory,memory/YYYY-MM-DD.md 用于每日记录。main private session 可以把长期 memory 作为受信上下文或检索来源;群组与其他 session 不应无条件拿到同一份私人画像。memory retrieval 是一次有权限边界的选择性桥接,不是“把所有记忆文件 concat 进 prompt”。

上一章提到的跨会话 recall 也遵循同样边界:它可以从其他获准 transcript 中检索片段,但不会合并 session key,不会改变原 transcript owner。否则“记得相关事实”就会意外变成“继承另一个会话的全部权限”。

三、历史变长后怎样缩减模型输入

3.1 Pruning 只缩本轮输入,Compaction 写入持久摘要

OpenClaw pruning 与 compaction:pruning 只裁剪本轮输入中的较旧工具结果;compaction 追加摘要与保留起点,后续输入使用摘要和近期消息,两条路径都保留原始历史
两条路径对照不同的持久语义,不表示每轮都依次执行。裁剪只改变本轮副本;压缩追加记录,原始历史仍在。

两者都让模型输入变小,但持久语义完全不同。Pruning 在组装时丢弃旧 tool result 的大块内容,只影响 in-memory prompt;磁盘 transcript 仍保留完整结果,之后可以审计或在不同策略下重新组装。它适合处理可复现、已失去即时价值的工具输出。

默认客户端 compaction 把旧 conversation 总结成摘要,并追加 compaction 记录,其中 firstKeptEntryId 标出保留消息的起点。下次重建上下文时,运行时用最新摘要加保留的近期消息继续推理。原始历史仍然保存在磁盘;有损的是摘要代表的模型视图,而不是压缩把旧记录直接抹掉。摘要与切分点持久化,因此其影响会延续到后续请求。

机制本轮 context持久 transcript适合解决
Pruning移除较旧 tool results。不改写。工具输出占用过大,但仍需保留审计记录。
Compactionsummary + recent messages。追加 summary 与保留起点,原始历史仍在。对话本身长期增长,必须越过窗口边界。

所以“context 里没看到”不能直接推出“数据被删除”:assembly 可能没有选中,pruning 可能只裁掉本轮结果,compaction 则可能让摘要漏掉了细节。下面是简化的状态形状,省略记录 id、父节点和时间;关键是追加压缩记录,而不是覆盖 Alice 原来的决定。

持久历史:旧消息 … + 近期消息 …
追加记录:{ type: "compaction", summary: "已确定方案 …",
            firstKeptEntryId: "recent-user-entry", tokensBefore: 42000 }
下轮输入:摘要 + 从 recent-user-entry 开始的保留消息
原始历史:仍可从会话存储读取

3.2 Memory flush 与可选维护何时运行

Memory flush 是一次静默 housekeeping turn,提醒 Agent 把值得长期复用的事实追加到 memory 文件。摘要延续当前会话,memory 服务更长期的检索,两者不互相替代。但“压缩前先 flush”并不等于每次用户提问都必须同步等待一次记忆整理。

内建运行时把必要的推理前维护与可选的回复后维护分开。需要先缩减历史才能推理时,前台仍执行必要 compaction,并可在它前面尝试 flush。持续运行的 Gateway 则把额外 flush/compaction 放到回复投递已结束、前台写入者已退出之后,使用独立会话写入许可和本轮剩余时间;一次性 openclaw agent --local 不启动这种返回后的可选工作。

例如 Alice 收到回复后马上发来下一条消息,前台协调器会取消可抢占的维护,并等待它真正结束后才读取会话。取消信号不是“写入已经停止”的证明,也不会撤销已经提交的摘要。可选维护失败只记录故障,不能把已经完成的回复替换成失败。

时刻谁执行保证与限制
模型请求前,历史必须缩减前台运行必要 compaction 成功后再推理;失败保留会话并报告。
回复投递与前台写入结束后独立维护运行有条件地 flush、compact;可能没有工作或剩余时间。
下一条消息到达前台协调器取消并等待维护结束,避免两个写入者同时改变历史。

flush 的单独模型 override 是精确选择,不继承 session fallback chain。其工具只保留读和针对指定 memory 文件的 append-only 写入,之后仍须通过正常 tool policy;若写工具最终被移除,运行时会警告无法保存。最可靠的实践仍是及时记下决定,而不是依赖窗口将满时的一次抢救。

3.3 窗口溢出时仍要保留消息与工具结果的配对

运行前会估算 prompt 是否接近 provider context window;运行中也可能从 provider 收到 overflow。legacy engine 可触发 compaction 并重试,拥有 compaction 的 custom engine 则负责自己的 compact 合同。无论哪条路径,都要保住 tool call/result 配对、当前用户 turn 和可继续的 recent history。

compaction 受有限安全超时保护;取消整个运行后,运行时不能继续启动新的恢复工作,也不能把“停止等待”当作回滚已提交摘要。工具调用尤其需要保留配对:assistant tool call 与对应 result 不能裁成孤儿。当前内建运行时还能在工具结果已经落定后遇到 provider overflow 时,基于已记录结果压缩并继续,保留当前模型、账户和原请求,避免重新执行已经完成的外部动作;待执行工具、审批等待与取消不能借此路径继续。

因此缩减上下文不只是 token 算术。系统还要修复 transcript 结构、满足 provider 约束,并保证重试不会打乱已经发生的工具副作用。

四、怎样确认 Agent 为什么“忘记”

4.1 用报告检查实际请求,而不是猜聊天页

/context list 先看总贡献,/context detail 展开最大的 system prompt 与 tool schema,/context map 用更直观的分布观察预算。报告优先使用上一次 embedded run 真正捕获的 system prompt;没有 run report 时才现场估算。

排查顺序可以固定为:先确认 route、sessionId 与 lifecycleRevision 是否正确,再确认 transcript 是否有原始事实,再看 Project Context 是否截断、memory 是否写入/检索,最后看 pruning/compaction 与工具 schema 占比。这样不会一见“忘记”就盲目扩大窗口。

/context list
/context detail
/context map
/compact Focus on decisions, constraints, and unfinished work

/compact 的 focus 提示应描述希望摘要保留的结构,而不是让它编造新事实。压缩前后最好用可验证的任务状态、文件路径、决定与未完成项做检查点。

4.2 五种“忘记”对应五个检查位置

症状可能 owner正确动作
同一聊天突然像新会话route / lifecycleRevision / sessionId检查 key 与 reset 边界:普通持久会话保留 sessionId,推进 lifecycleRevision 并追加边界;不要仅凭 sessionId 判断上下文是否延续。
规则文件后半段不生效Project Context检查 bootstrap cap、截断标记,拆短稳定规则。
工具输出仍在磁盘但模型看不到Runtime View检查 pruning 和当前 assembly;不要认定 transcript 丢失。
旧决定被摘要概括掉Compaction改进 focus/summary 质量,并把耐久决定提前写入 memory。
跨会话事实没有召回Memory/retrieval检查是否写入、索引、权限和检索命中;不要合并 transcripts。

4.3 七条可以迁移的上下文规则

  1. 持久记录与模型视野分开。保存成功不代表每轮都应完整注入。
  2. 每类信息都有明确保存位置。transcript、memory、workspace 与本轮模型输入不互相冒充。
  3. bootstrap 要显式且有界。递归吞目录既昂贵,也让注入来源无法审计。
  4. 能力有双重成本。skill 目录适合按需展开,tool schema 必须精确但应保持紧凑。
  5. assembly 与 rewrite 分权。选择本轮材料不等于获得任意改历史的权限。
  6. pruning 是视图优化,compaction 是持久迁移。两者的恢复和审计承诺不同。
  7. 有损压缩前先提炼长期事实。memory flush 是最后防线,不应是唯一写记忆时机。

下一篇不再问“模型看到了什么”,而是问“模型为什么能做这些事”。我们会把 tools、skills、plugins 与 hooks 拆成一条能力装配链,分清说明、发现、注册、策略过滤和执行分别发生在哪一层。

参考源码与文档