乐于分享
好东西不私藏

OpenClaw 架构拆解 #4:会话太长 Agent 会「失忆」?看它怎么给自己写记忆

OpenClaw 架构拆解 #4:会话太长 Agent 会「失忆」?看它怎么给自己写记忆

上下文窗口是 200K,但你的 Agent 聊到 50 轮之后,开始忘记你 3 小时前提的需求。问题不在模型,在上下文管理。OpenClaw 的答案叫 Compaction——让 LLM 自己把旧对话写成「记忆卡片」,还保证写不错、写不丢。


1. 引子:你的 Agent 不是变笨,是「失忆」了

你大概率遇到过这个场景:一个 Agent 任务跑得很好,但同一个会话里聊得越久,它越「蠢」——忘了你最初的约束,重复问已经答过的问题,甚至把早期的一个错误决定当成既定事实。

不是模型退化,是上下文被垃圾填满了。 每一轮对话、每一次工具调用结果都堆在上下文窗口里,真正重要的信息被稀释。窗口是有限的(OpenClaw 的上下文预算默认按 200K token 规划,实际随模型定义变化),而对话是无限增长的。

粗暴的解法是「截断」:把旧消息直接丢掉。但丢了就真的忘了——新模型接手时接不上话,用户需求、决策记录全部归零。

OpenClaw 的解法是 Compaction(上下文压缩):不是删历史,而是用 LLM 把旧对话改写成一页结构化摘要,替换掉原文,同时保留最近一段「鲜活记忆」。压缩记录本身也作为 transcript 里的一等公民 entry 存在,带着回放起点标记——下次重启、恢复、分支,都知道从哪儿接上。

本文从源码拆解这套机制:怎么切、怎么总结、怎么保证不写错、怎么兜底。所有行号基于 v2026.7.2 源码(main 最新,2026-08-03,commit 3832074cd9),可对照验证。


2. 全景:一次压缩,两套入口,一套引擎

2.1 触发:三种情形一张表

先看触发。OpenClaw 有两条触发路径,三种情形:

触发
条件
行为
自动 · overflow
上下文溢出(isContextOverflow
压缩后自动重试本轮
自动 · threshold
超阈值:tokens > window − reserve(默认 reserve 16384)
压缩后不重试,等用户继续
手动 · /compact
用户主动触发
走完整重保障管线

判定逻辑在 isContextOverflow()(packages/ai/src/utils/overflow.ts L152),三种溢出信号:

// packages/ai/src/utils/overflow.ts L152function isContextOverflow(message, contextWindow) {  // Case 1: stopReason=error 且错误信息匹配溢出模式(排除限流等已知噪音)  if (message.stopReason === "error" && message.errorMessage) {    const errorMessage = message.errorMessage;    const isNonOverflow = NON_OVERFLOW_PATTERNS.some((p) => p.test(errorMessage));    if (!isNonOverflow && OVERFLOW_PATTERNS.some((p) => p.test(errorMessage))) return true;  }  // Case 2: 正常结束但 input+cacheRead 已超窗口  if (contextWindow && message.stopReason === "stop") {    const inputTokens = resolveContextInputTokens(message);    if (inputTokens !== undefined && inputTokens > contextWindow) return true;  }  // Case 3: 输出被 length 截断且 output=0 → 输入已占满窗口  if (contextWindow && message.stopReason === "length" && message.usage.output === 0) {    const inputTokens = resolveContextInputTokens(message);    if (inputTokens !== undefined && inputTokens >= contextWindow * 0.99) return true;  }}

2.2 自动路径:每轮检查的后台保洁

自动压缩挂在 Session.checkCompaction()(src/agents/sessions/agent-session-compaction.ts L242),每轮 agent 跑完就检查一次(handlePostAgentRun,src/agents/sessions/agent-session-prompting.ts L52)。所以压缩是每轮都可能发生的后台动作,用户无感。

2.3 手动路径:/compact 的三层路由

但手动路径复杂得多。/compact 命令进入 compactEmbeddedAgentSession(src/agents/embedded-agent-runner/compact.queued.ts L239),先过一张三层路由决策树——按「谁拥有会话的上下文,谁负责压缩」逐层让位:

图2.3:/compact 三层路由决策树

第一层最容易误解:native harness 指 Codex/Copilot 这类插件式运行时——而且 OpenAI 官方 provider(api.openai.com)默认就走 Codex runtimeresolveOpenAIImplicitAgentRuntime,src/agents/openai-routing.ts L38),此时压缩由 Codex 服务端原生执行,OpenClaw 只返回 compacted:false + 专用 reason 让位。第二层交给声明 ownsCompaction: true 的 context engine 插件(带 180s 看门狗);都不归时才轮到 OpenClaw 自己的 embedded runner。

一句话立住框架:谁拥有会话的上下文,谁负责压缩。

2.4 汇合:两套入口,一套引擎

关键洞察是:两套入口,一套引擎。无论自动还是手动,最终都汇合到共享核心——prepareCompaction(找切点)和 compact(生成摘要)。差异全在外围:手动路径更「郑重」——要拿 session 租约/写锁、拍 checkpoint 快照(persistCompactionCheckpoint,compact.queued.ts L712)、跑 before/after hooks(runBeforeCompactionHooks / runPostCompactionSideEffects),并走三层路由 + 队列防死锁。

图1:上下文压缩全景 —— 两套入口,一套引擎

自动压缩像后台保洁,悄悄干活;手动压缩像一次正式归档,全程留痕。这个对比贯穿全文。


3. cut point:切哪里,是门手艺

压缩的第一个问题:保多少,切哪里?

3.1 尾部保留:最近的 20K token 原样保留

findCutPoint()(packages/agent-core/src/harness/compaction/compaction.ts L422)的算法很朴素:从消息流尾部向前累加 token 数,直到攒够 keepRecentTokens(默认 20000)——这 20K 是「live tail」,原样保留,一条不压缩,因为它大概率是用户正在进行的任务上下文,压缩了反而坏事。(注:手动压缩路径的 tail 保留取决于 keepRecentTokens 是否配置。)

// agent-core compaction.ts L422:从尾部向前累加,攒够就切function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);  if (cutPoints.length === 0) {    return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };  }  let accumulatedTokens = 0;  let cutIndex = cutPoints[0];  // 从尾部向前累加,攒够 keepRecentTokens(默认 20000)就切  for (let i = endIndex - 1; i >= startIndex; i--) {    const message = getMessageFromEntryForCompaction(entries[i]);    if (!message) continue;               // 跳过 compaction/label 等元数据    accumulatedTokens += estimateTokens(message);    if (accumulatedTokens >= keepRecentTokens) {      cutIndex = cutPoints.at(-1);        // 兑底:最后一个合法切点      for (const cutPoint of cutPoints) {        if (cutPoint >= i) { cutIndex = cutPoint; break; }      }      break;    }  }  while (cutIndex > startIndex) {         // 前移到最近消息/compaction/reset 边界    const prevEntry = entries[cutIndex - 1];    if (!prevEntry || prevEntry.type === "compaction" || prevEntry.type === "reset") break;    if (getMessageFromEntryForCompaction(prevEntry)) break;    cutIndex--;  }  // isSplitTurn = 切点不是回合起点(user/bash/custom/分支摘要)且能找到回合起点  const startsTurn = isTurnStartEntry(entries[cutIndex]);  const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);  return { firstKeptEntryIndex: cutIndex, turnStartIndex, isSplitTurn: !startsTurn && turnStartIndex !== -1 };}

3.2 切点不是想切就能切

合法切点有讲究:toolResult 不能作为切点——切点必须落在 user/assistant/bashExecution/custom/分支摘要/compactionSummary 消息上(isCutPointMessage 的约束,compaction.ts L337),保证摘要边界的语义完整。更精细的规则在压缩规划层:分块时绝不拆散活跃的 toolCall→toolResult 对splitMessagesByTokenShare,src/agents/compaction-planning.ts L183,用 pendingToolCallIds 检查)——切块不能制造「孤儿工具调用」。

3.3 ⭐ split turn:切在回合中间也不丢上下文

最独特的机制在这里。如果切点正好落在一个超大回合的中间(比如用户发了一条超长消息,assistant 正在长篇处理),简单截断会让新模型接不上话——「用户到底让我做什么、我做到哪一步了」全丢了。

findCutPoint 的处理:先把切点前移到最近的消息边界(跳过 label/custom 等纯元数据 entry);若落点不是回合起点消息(user/bashExecution/custom/分支摘要/compactionSummary,isTurnStartEntry 判定)——即切在回合中间——标记 isSplitTurn: true,并让 turnStartIndex 回退到该回合的起点。

然后 compact()(agent-core compaction.ts L873)会生成两份摘要: - 摘要 A:切点之前的旧历史 - 摘要 B:回合前缀TURN_PREFIX_SUMMARIZATION_PROMPT,compaction.ts L855,预算 0.5×reserve)——标注 "Turn Context (split turn)"

版本演进:v6.6 里两份摘要是 Promise.all 并行生成;v7.2 改为串行 awaitcompact() L906 起),先出历史摘要、再出回合前缀——避免两个并发 LLM 调用同时打 API,也让错误处理更简单(任一失败直接 return err)。

// agent-core compaction.ts L906:切在回合中间 → 先历史摘要,再回合前缀摘要if (isSplitTurn && turnPrefixMessages.length > 0) {  const historyResult =    messagesToSummarize.length > 0      ? await generateSummary(messagesToSummarize, model, settings.reserveTokens, ...) // 摘要 A      : ok("No prior history.");  if (!historyResult.ok) return err(historyResult.error);  const turnPrefixResult = await generateTurnPrefixSummary(    turnPrefixMessages, model, settings.reserveTokens, ...);  // 摘要 B:回合前缀  if (!turnPrefixResult.ok) return err(turnPrefixResult.error);  // 拼接:旧历史 + Turn Context 标记 + 回合前缀  summary = `${historyResult.value}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value}`;}

合并后,新模型看到的是:旧历史摘要 + Turn Context 前缀摘要 + 回合剩余部分用户说了什么、做到哪一步,一条都不丢。

图2:cut point 切割与 split turn 双摘要

这是「保精度压缩」和粗暴截断的分水岭:宁可多花一次 LLM 调用,也不让进行中的任务断片。


4. 摘要生成:LLM 怎么给自己写记忆

切好了,接下来是核心:怎么写摘要才不失忆?

4.1 结构化模板:记忆不是散文,是档案

generateSummary()(src/agents/sessions/compaction/compaction.ts L66 兼容桥,核心 generateSummary 在 packages/agent-core/src/harness/compaction/compaction.ts L658)使用固定结构模板,要求摘要包含:

## Goal                          ← 目标## Constraints & Preferences     ← 约束与偏好## Progress (Done|In Progress|Blocked)  ← 进度三态## Key Decisions                 ← 关键决策## Next Steps                    ← 下一步## Critical Context              ← 关键上下文

结构化是为了可检索、可增量、可审计——散文式摘要没法判断「这条决策丢了没有」。

4.2 增量更新:第二次压缩不是从零开始

如果这次压缩时已经存在上一份摘要(previousSummary),则使用 UPDATE_SUMMARIZATION_PROMPT(agent-core compaction.ts L530)——旧摘要 + 新消息一起 re-distill,而不是重读全部历史。这是记忆系统的关键:每次压缩都是「旧记忆 + 新经历 → 新记忆」,信息层层沉淀,而不是每次推倒重来。

4.3 新管线:分块、合并、兜底

embedded runner 路径用的是更重的 summarizeInStages()(src/agents/compaction.ts L291):

4.3.1 分块规划

按预算把消息切成 chunk(single 或 split 模式)。核心是 buildStageSplitPlan(src/agents/compaction-planning.ts L286):

// src/agents/compaction-planning.ts L286function buildStageSplitPlan(params) {  const minMessagesForSplit = Math.max(2, params.minMessagesForSplit ?? 4);  const parts = normalizeCompactionParts(params.parts ?? DEFAULT_PARTS, params.messages.length);  const totalTokens = estimateMessagesTokens(params.messages);  // 消息太少 / 超预算 → 不拆,整体一次压(single 模式)  if (parts <= 1 || params.messages.length < minMessagesForSplit      || totalTokens <= params.maxChunkTokens) {    return { mode: "single" };  }  // 否则按 token 均分到 N 个 chunk(split 模式)  const chunks = splitMessagesByTokenShare(params.messages, parts).filter(    (chunk) => chunk.length > 0,  );  return chunks.length > 1 ? { mode: "split", chunks } : { mode: "single" };}

4.3.2 逐块摘要

每个 chunk 单独调 LLM,最多尝试 3 次(500ms-5s 指数退避 + 20% jitter)。配置就在 summarizeChunks 里(src/agents/compaction.ts L148):

// src/agents/compaction.ts L148:每个 chunk 独立调 LLM,最多尝试 3 次summary = await retryAsync(  () => generateSummary(chunk, params.model, params.reserveTokens,      params.apiKey, params.headers, params.signal, effectiveInstructions, summary),  {    attempts: 3,        // 最多 3 次(含首次),不是固定重试 3 次    minDelayMs: 500,    // 指数退避起点 500ms    maxDelayMs: 5000,   // 退避上限 5s    jitter: 0.2,        // 20% 随机抖动,防多 chunk 同时重试打爆 API    shouldRetry: (err) =>      !params.signal.aborted && (isAbortError(err) || !isTimeoutError(err)),    // 调用方中止不重试;AbortError 可重试、传输超时不可重试  },);

4.3.3 合并

MERGE_SUMMARIES_INSTRUCTIONS(src/agents/compaction.ts L50)明确要求合并时优先保留:

"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."// "The agent needs to know what it was doing, not just what was discussed."

4.3.4 兜底降级

summarizeWithFallback(src/agents/compaction.ts L195)的三级兜底链:全量失败 → 排除超大消息重试 → 用 partialSummary → 最终抛错终止。超大消息判定:单条 token × 1.2 > 窗口的 50%(compaction-planning.ts L257)——被排除的消息会在摘要末尾附加备注,绝不静默丢失

// compaction-planning.ts L266:被排除的超大消息留痕oversizedNotes.push(  `[Large ${message.role} (~${Math.round(tokens / 1000)}K tokens) omitted from summary]`,);

另有个细节:摘要末尾会附加文件操作清单formatFileOperations,src/agents/agent-hooks/compaction-safeguard.ts L523,读/改了哪些文件)——Agent 干活留下的痕迹也被记进记忆。而若所有尝试全部失败,summarizeWithFallback 会抛 CompactionError("summarization_failed") 终止(compaction.ts L258)——宁可失败,也绝不静默返回损坏摘要

图3:摘要生成 —— 经典路径 vs 新管线


5. 工程细节:把「估算不准」当设计前提

这一章全是干货,讲 OpenClaw 怎么认真对待工程不确定性。

5.1 token 估算天生不准 → 安全边际 1.2

estimateTokens 用的是 char/4 启发式,图片按 4800 chars 固定折算——它知道自己估不准。所以所有预算先除以 SAFETY_MARGIN = 1.2(src/agents/compaction-planning.ts L21)再分块。同时设两道地板:reserveTokensFloor = 20000(src/agents/agent-settings.ts L49,常量 DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000 定义在 L9)和 MIN_PROMPT_BUDGET_RATIO = 0.5(src/agents/agent-compaction-constants.ts L12,上下文窗口至少留一半给 prompt)。原则:宁少勿爆——压缩本身不能把 prompt 挤没。

export const SAFETY_MARGIN = 1.2;            // compaction-planning.ts L21:估算不准 → 预算先打 83 折export const MIN_PROMPT_BUDGET_RATIO = 0.5;  // agent-compaction-constants.ts L12:窗口至少留一半给 promptexport const DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000;  // agent-settings.ts L9:保留预算地板

5.2 规划是重活 → worker 线程 offload

压缩规划要全量 estimateTokens 每一条消息,是 CPU 密集操作。消息数 ≥64 条时,规划被挪到 worker 线程(60s 超时,失败回退主线程)——不能因为压缩规划卡死 agent 的 event loop

// compaction-planning-worker.ts L166:<64 条直接主线程跑,不值得开 workerif (messages.length < COMPACTION_PLANNING_WORKER_MIN_MESSAGES) {  return params.fallback(params.input.messages);   // 主线程兑底}const value = await runCompactionPlanningWorker({ ... });  // ≥64 条 → worker 线程// 注:仅 worker 不可用(CompactionPlanningWorkerError code="unavailable")时回退主线程,其他错误直接抛(L188-191)

5.3 压缩后不是改内存,是变换持久化数据结构

这是最容易被忽略的设计:手动压缩路径完成后,transcript(JSONL 文件)经历三次操作:

5.3.1 boundary 加固

压缩流程内联完成 boundary 加固(v7.2 已无独立 hardenManualCompactionBoundary 函数):compact() 计算出的切点,firstKeptEntryId指向压缩后保留的第一条消息(agent-core compaction.ts L812 const firstKeptEntryId = firstKeptEntry.id;)——回放从这条消息继续,摘要 entry 成为新的边界,旧消息从此不再进上下文:

// agent-core compaction.ts L812:firstKeptEntryId = 切点后保留的第一条消息 idconst firstKeptEntryId = firstKeptEntry.id;   // 回放起点(不是 compaction entry 自己)

(写入 transcript 的调用点在 compaction-session-execution.ts L472 的 appendCompaction,透传该 id。)

5.3.2 transcript 轮转

resolveCompactionSuccessorTranscript(src/agents/embedded-agent-runner/run/compaction-runtime.ts L193):压缩后生成全新 successor 文件(新 sessionId + 父指针指向旧文件),历史冻结归档。后续 prompt 状态直接接管新会话身份(L193-209):

// run/compaction-runtime.ts L190-195:压缩后接管 successor 会话身份const successor = resolveCompactionSuccessorTranscript(compactResult);await sessionPromptState.adoptSessionTarget(  nextSessionTarget && successor.sessionId ? { ... } : nextSessionTarget,);sessionPromptState.adoptSessionId(successor.sessionId);

(注:6.6 的可配置开关 truncateAfterCompaction 在 v7.2 已退役——被移入 legacy 配置迁移,轮转改为默认行为。)

5.3.3 checkpoint 持久化

压缩前快照、压缩后落盘——压缩失败可回滚,不会弄丢会话:

// compact.queued.ts L712:压缩前快照 → 压缩成功后 checkpoint 落盘checkpointSnapshotRetained = await persistCompactionCheckpoint({  config: params.config, sessionKey: params.sessionKey,  sessionId: postCompactionSessionId, trigger: params.trigger,  snapshot: checkpointSnapshot, summary: result.result?.summary, ...});

图4:工程细节 —— 预算三件套 & 压缩后生命周期

把上下文压缩当作持久化数据结构的变换(原子写 + 分支 + 回滚点),而不是内存数组的 splice——两者的可靠性不在一个量级。


6. 安全:LLM 生成的摘要,本身就是攻击面

最后也是最容易被忽视的一层:摘要是由 LLM 生成的,而 LLM 可能被注入、可能出错、可能偷工减料。 OpenClaw 按「默认不可信」设计。

6.1 防注入:摘要 prompt 三重隔离

6.1.1 系统提示词禁止对话

摘要模型的系统提示词第一句就划清界限(agent-core compaction.ts L493):

Do NOT continue the conversation. Do NOT respond to any questions inthe conversation. ONLY output the structured summary.

摘要模型禁止跟会话内容对话——历史里的任何指令、任何问题,对摘要模型都不生效。

6.1.2 自定义指令按不可信数据包裹

用户给 /compact 附加的自定义指令,被 wrapUntrustedInstructionBlock 当作不可信数据包裹(上限 4000 字符)——你给压缩器说的话,和会话里的历史,被当成两种可信度处理:

// compaction-safeguard-quality.ts L27:指令按不可信 prompt 数据块包裹,4000 字符上限// (MAX_UNTRUSTED_INSTRUCTION_CHARS 定义在 L11)export function wrapUntrustedInstructionBlock(label: string, text: string): string {  return wrapUntrustedPromptDataBlock({    label, text, maxChars: MAX_UNTRUSTED_INSTRUCTION_CHARS,  });}

6.1.3 压缩模型独立通道

(手动/网关路径)压缩模型与会话模型分离compaction.model 可独立配置(v7.2 起正在迁移到 context engine 插件的 summaryModel 配置)——摘要在一个独立模型通道里执行,原始会话历史不直接进入主模型的 prompt。

6.2 质量审计:摘要要能验证

safeguard 模式下,压缩结果过 auditSummaryQuality()(src/agents/agent-hooks/compaction-safeguard-quality.ts L199)三道检查:

6.2.1 必填章节检查

5 个必填章节,缺一个直接判失败(compaction-safeguard-quality.ts L14):

const REQUIRED_SUMMARY_SECTIONS = [  "## Decisions", "## Open TODOs", "## Constraints/Rules",  "## Pending user asks", "## Exact identifiers",] as const;// L83:按顺序扫描,缺一个就返回 false → 触发重试function hasRequiredSummarySections(summary: string): boolean {  ...  for (const heading of REQUIRED_SUMMARY_SECTIONS) {    const index = lines.findIndex((l, i) => i >= cursor && l === heading);    if (index < 0) return false;    cursor = index + 1;  }  return true;}

6.2.2 标识符保留

严格标识符保留:函数名、变量名、路径不丢失(policy: strict/off/custom)。

6.2.3 最新 ask 覆盖

用户最后一条请求必须出现在摘要里(token 重叠判定:ask 短于 3 token 要求重叠 ≥1,≥3 token 要求重叠 ≥2,MIN_ASK_OVERLAP_TOKENS_FOR_DOUBLE_MATCH = 3,compaction-safeguard-quality.ts L13/L194)。

审计失败 → 重试(默认 1 次,可配置上限 3 次)→ 仍失败 → 降级兜底。宁可摘要降级,也不让「关键决策丢失」静默发生。

6.3 三级 fallback:摘要不可用时的完整退路

6.3.1 L1 模型级

主模型失败 → runWithModelFallback(src/agents/model-fallback-runner.ts L139)换 fallback 模型(注意:auth 按 fallback 候选的 provider 重新解析,profile 是 provider 作用域的,resolveAuthProfileOrder L327):

// model-fallback-runner.ts L139:主模型失败 → 遍历 fallback 链// outcome: "exhausted" 表示全部 fallback 用完async function runWithModelFallback<T>(params: RunWithModelFallbackParams<T>): Promise<ModelFallbackRunResult<T>> {  const result = await runWithModelFallbackInternal(params, deferredSuspension);  if (result.outcome === "exhausted") { ... }

6.3.2 L2 通道级

summarizeViaLLM(src/agents/agent-hooks/compaction-safeguard.ts L288)换通道重试:safeguard 模式下摘要走独立 summarization 通道,质量重试失败时保留上次成功摘要(L1181-1184)。

6.3.3 L3 路由级

harness 绑定失败 → 回退 context engine → 再落 embedded runner。

图5:质量保障 —— 摘要审计 & 三级 fallback


7. 压缩之外:短期记忆与长期记忆的分工

7.1 两种记忆的分工

Compaction 管的是「这次对话怎么不忘」。但 Agent 的记忆还有另一半——跨会话的长期记忆:MEMORY 引擎。这也是 #3 结尾预告的另一半,现在兑现。

两者的分工,一句话就清楚:

Compaction
MEMORY 引擎
生命周期
会话内
跨会话
载体
transcript 里的摘要 entry
MEMORY.md + memory/*.md + 会话转录索引
写入方式
LLM 压缩旧对话
检索后注入 System Prompt
目的
这次对话不忘
下次对话能想起

7.2 MEMORY 引擎怎么工作

工作方式(memory-search 源码):

7.2.1 分块索引

文本按 400 token 切块(80 token 重叠),会话转录增量变化(每 50 条消息或 100KB)触发重新索引。

7.2.2 混合检索

向量(embedding)+ 文本双路召回,权重 0.7/0.3,候选放大 4 倍后合并去重。

7.2.3 过滤注入

相似度阈值 0.35、最多 6 条结果,拼进 System Prompt 的 Memory 部分(就是 #3 里的 includeMemorySection)。

7.3 一句话总结

所以一个完整的 Agent 记忆系统是两层:Compaction 让 Agent「不忘」,MEMORY 引擎让 Agent「想起」——前者是会话内的数据压缩,后者是跨会话的知识检索。embedding 模型选型、MMR 去重、时间衰减这些细节,留给系列后续展开。


8. 总结:OpenClaw 的压缩哲学

如果你只带走三句话:

第一,压缩不是「删历史」,是「结构化改写」。 旧对话被改写成 Goal/Progress/Decisions 档案,split turn 保证进行中的回合不断片,增量 UPDATE 让记忆层层沉淀。

第二,上下文是持久化资产,必须有显式边界。 回放起点(firstKeptEntryId)、transcript 轮转、checkpoint 回滚——压缩是数据结构的变换,不是内存操作。

第三,LLM 写的记忆必须可审计。 必填章节、标识符保留、ask 覆盖检查、三级 fallback——摘要默认不可信,质量是设计出来的,不是祈祷出来的。

对自建 Agent 的启示很简单:你的 Agent 聊久了会「变笨」,不是模型问题,是你没给它写记忆的系统。现在你知道该抄什么了。


系列回顾#1 Gateway 架构 · #2 执行引擎 10 级自愈 · #3 System Prompt 缓存边界与动态注入 · #4 上下文压缩与记忆(本篇) 下篇预告#5 Channel Adapter——20+ 通道如何统一抽象。


作者简介 Carapace Huang,Android/Linux BSP 技术架构师,多年嵌入式系统经验。研究 AI Agent 架构并输出源码级技术拆解。

适合发布平台:公众号 / 知乎 源码版本:OpenClaw v2026.7.2 源码(main 最新,2026-08-03,commit 3832074cd9) 主要参考文件packages/agent-core/src/harness/compaction/compaction.ts(核心:findCutPoint L422 / compact L873 / 摘要 prompt)· packages/ai/src/utils/overflow.ts(溢出判定 L152)· src/agents/compaction.ts(新管线:summarizeInStages L291 / MERGE_SUMMARIES_INSTRUCTIONS L50 / summarizeWithFallback L195)· src/agents/compaction-planning.ts(分块规划 L286 / SAFETY_MARGIN L21 / 超大消息备注 L266)· src/agents/embedded-agent-runner/(compact.queued.ts 路由 L239 / compaction-checkpoint.ts / run/compaction-runtime.ts 轮转 L193)· src/agents/sessions/agent-session-compaction.ts(自动压缩 L242)· src/agents/sessions/settings-manager.ts(默认值:reserve 16384 / keepRecent 20000)· src/agents/agent-hooks/compaction-safeguard-quality.ts(摘要审计 L199)· src/agents/agent-hooks/compaction-safeguard.ts(summarizeViaLLM L288 / formatFileOperations L523)· src/agents/memory-search.ts(MEMORY 引擎)