Skip to content

Context Engine:上下文壓縮抽象

源码版本v2026.6.11

職責

Context Engine 是 agent 上下文 (context) 生命週期的抽象層:誰負責 ingest 訊息、誰負責組裝 prompt、誰負責壓縮 (compact)、誰負責維護 (maintain) transcript——這些決策都從 agent 主循環裡抽出來,變成 ContextEngine 介面的幾個方法。

OpenClaw 內建一個 LegacyContextEngine 作為預設實作,它把所有工作委託回原有路徑(SessionManager 持久化、attempt.ts 組裝、compactEmbeddedAgentSessionDirect 壓縮),保持 100% 向後相容。第三方外掛可以透過 api.registerContextEngine("my-engine", factory) 註冊自己的 engine,把摘要策略、檢索增強、向量索引這些高階特性掛進來。

設計動機

為什麼把 context 這塊抽象成 engine?因為 agent 跑得越久,transcript 越長,token 預算 (token budget) 越緊張。OpenClaw 原生的壓縮策略是「分段摘要 + 保留重要訊息 + 旋轉 session 檔案」,這套策略寫死在 compactEmbeddedAgentSessionDirect 裡。但不同模型、不同場景需要的壓縮策略不同:Claude 長上下文適合保留最近 N 條 + 遠端摘要;RAG-heavy 場景適合「壓縮時順便建立向量索引」;有些 plugin 想在 compact 時同步寫 memory。

如果把壓縮策略寫死在 agent runner 裡,plugin 想替換就得 fork 整個 runner。抽成 ContextEngine 介面後,plugin 只要實作 ingest / assemble / compact / afterTurn 幾個方法,agent 主循環只負責在合適的時機調它們——具體策略留給 engine 實作。

但完全外掛化也有風險:engine 出 bug 會直接把對話上下文搞丟。所以 registry 同時實作了「quarantine」機制——engine factory 拋錯或行為異常,自動隔離 (quarantine) 這個 engine id,回退到 default,讓 agent 能繼續跑。

關鍵檔案

資料流

attempt 循環裡,觸發壓縮有兩個入口:一是 proactively 在 prompt 前檢查 token 是否接近預算,二是 reactively 在 provider 回傳 overflow 錯誤後再壓。reactive 路徑(overflow compaction:2638)走的是 compactContextEngineWithSafetyTimeout:

typescript
overflowCompactionAttempts++;
log.warn(
  `context overflow detected (attempt ${overflowCompactionAttempts}/${MAX_OVERFLOW_COMPACTION_ATTEMPTS}); attempting auto-compaction for ${provider}/${modelId}`,
);
let compactResult: Awaited<ReturnType<typeof contextEngine.compact>>;
await runOwnsCompactionBeforeHook("overflow recovery");
try {
  const overflowCompactionRuntimeSettings = buildEmbeddedContextEngineRuntimeSettings(
    {
      tokenBudget: ctxInfo.tokens,
      degradedReason: "context_overflow",
    },
  );
  compactResult = await compactContextEngineWithSafetyTimeout(
    contextEngine,
    {
      sessionId: activeSessionId,
      sessionKey: params.sessionKey,
      sessionFile: activeSessionFile,
      tokenBudget: ctxInfo.tokens,
      ...(overflowTokenCountForCompaction !== undefined
        ? { currentTokenCount: overflowTokenCountForCompaction }
        : {}),
      force: true,
      compactionTarget: "budget",
      runtimeContext: overflowCompactionRuntimeContext,
      runtimeSettings: overflowCompactionRuntimeSettings,
    },
    resolveCompactionTimeoutMs(params.config),
    params.abortSignal,
  );

四個細節: force: true 跳過 engine 的 threshold 自檢,溢出時無腦壓; compactionTarget: "budget" 讓 engine 收斂到 token budget 而不是 threshold; safety timeout 來自 config,plugin 卡死會被中斷; abortSignal 是 run-level 的,run 被取消時 compact 也得停。這套約束在 compact() 契約裡寫得很清楚(compact 契約:402):

typescript
  /**
   * Compact context to reduce token usage.
   * May create summaries, prune old turns, etc.
   *
   * The host always bounds this call with a finite safety timeout (the same
   * one that protects native runtime compaction). Engines that run long
   * operations SHOULD additionally honor `abortSignal` so an in-flight
   * compaction can be canceled promptly on run abort or host timeout instead
   * of running to completion in the background.
   */
  compact(params: {
    sessionId: string;
    sessionKey?: string;
    sessionFile: string;
    tokenBudget?: number;
    /** Force compaction even below the default trigger threshold. */
    force?: boolean;
    /** Optional live token estimate from the caller's active context. */
    currentTokenCount?: number;
    /** Controls convergence target; defaults to budget. */
    compactionTarget?: "budget" | "threshold";
    customInstructions?: string;
    /** Optional runtime-owned context for engines that need caller state. */
    runtimeSettings?: ContextEngineRuntimeSettings;
    runtimeContext?: ContextEngineRuntimeContext;
    /**
     * Optional abort signal honored before and during compaction. The host
     * aborts it on run-level abort or when its compaction safety timeout
     * fires; engines should stop work and reject promptly when it aborts.
     */
    abortSignal?: AbortSignal;
  }): Promise<CompactResult>;

engine 怎麼註冊?plugin 透過 SDK 調 registerContextEngine(registerContextEngine:542),它內部轉給 registerContextEngineForOwner:

typescript
export function registerContextEngineForOwner(
  id: string,
  factory: ContextEngineFactory,
  owner: string,
  opts?: RegisterContextEngineForOwnerOptions,
): ContextEngineRegistrationResult {
  const normalizedOwner = requireContextEngineOwner(owner);
  const registry = getContextEngineRegistryState().engines;
  const existing = registry.get(id);
  if (
    id === defaultSlotIdForKey("contextEngine") &&
    normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER
  ) {
    // The default fallback id is core-owned; plugins can select other ids through slots.
    return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER };
  }
  if (existing && existing.owner !== normalizedOwner) {
    return { ok: false, existingOwner: existing.owner };
  }
  if (existing && opts?.allowSameOwnerRefresh !== true) {
    return { ok: false, existingOwner: existing.owner };
  }
  registry.set(id, { factory, owner: normalizedOwner });
  clearContextEngineRuntimeQuarantine(id);
  return { ok: true };
}

三層保護: default slot 只能 core 註冊,plugin 搶不走; 同 id 已被其他 owner 註冊時拒絕; same-owner refresh 預設關,避免 plugin 誤覆蓋自己已註冊的實例。clearContextEngineRuntimeQuarantine 在註冊成功後清掉之前的 quarantine 記錄,允許重新嘗試被隔離過的 engine。

resolve 流程和 quarantine 回退:

邊界與失敗

  • default slot 鎖定:"legacy" 這個 id 是 default engine slot,只有 CORE_CONTEXT_ENGINE_OWNER 能註冊,plugin 不能搶——保證 fallback 永遠可達。
  • quarantine 隔離:wrapContextEngineWithRuntimeQuarantine(src/context-engine/registry.ts:785)把 engine 每次方法呼叫包一層 try-catch,拋錯時把 engine id 寫進 quarantinedEngines,後續 resolve 直接跳過它。quarantine 資訊持久化到進程級儲存,clearContextEngineRuntimeQuarantine 只在重新註冊時清。
  • safety timeout:compactContextEngineWithSafetyTimeoutresolveCompactionTimeoutMs(config) 給 plugin compact 加硬上限,逾時拋錯走 catch,被當 quarantine 觸發。同一個 timeout 也保護 native runtime compaction,保證 plugin 和 native 行為一致。
  • abortSignal 必須被尊重:契約裡寫明 engine「SHOULD」在 abort 時立刻 reject,但 host 不強依賴——因為 safety timeout 兜底。但忽略 abort 的 engine 會浪費 CPU/IO 資源到 timeout 才停。
  • LegacyContextEngine 的 compact 是委託:LegacyContextEngine.compact 不自己實作演算法,直接調 delegateCompactionToRuntime(src/context-engine/delegate.ts:34),後者 lazy import compact.runtime.js 調 compactEmbeddedAgentSessionDirect。這條橋讓第三方 engine 也能複用 native 壓縮路徑——只需要實作 assembleingest 的差異化邏輯,compact 可以委託。
  • compactionTarget 在 delegate 路徑被忽略:delegateCompactionToRuntime 註釋明說 native runtime 不暴露這個 knob,所以 legacy engine 的 compact 行為不受 target 影響。需要 target-specific 壓縮的 engine 必須自己實作 compact。
  • ownsCompaction 標記:ContextEngineInfo.ownsCompaction(src/context-engine/types.ts:166-167)告訴 host 這個 engine 自己管壓縮生命週期,host 不再自動觸發 compact——用於完全自治的 RAG-style engine。
  • resolve 失敗回退 default:engine id 不在 registry 裡、factory 拋錯、或回傳的 engine 缺方法,都走 resolveDefaultContextEngine(src/context-engine/registry.ts:1013),保證 agent loop 不會因為 plugin 沒裝好就完全跑不起來。
  • afterTurn 觸發 proactive compaction:compact() 不只在 overflow 時被調,afterTurn 鉤子 (src/context-engine/types.ts:353-369) 讓 engine 在每輪結束後主動判斷要不要壓,host 透過 tokenBudget 參數告訴它當前預算,engine 可以決定現在壓還是等下一輪。
  • subagent spawn 時也調:SubagentSpawnPreparation(src/context-engine/types.ts:182-187)讓 engine 在 subagent 啟動前準備好隔離的 context,避免父子 agent 上下文汙染。

小結

Context Engine 是 agent 上下文生命週期的可插拔抽象。預設 LegacyContextEngine 把所有工作委託回原有路徑,第三方透過 registerContextEngine 注入自己的實作。registry 用 owner 校驗 + default slot 鎖定 + quarantine 三層保護 plugin engine 的失敗半徑,host 用 safety timeout + abortSignal 雙重約束 compact 時長。具體 compact 怎麼被 attempt 循環觸發看 Agent 主循環;engine 注入的 memory 怎麼落到磁碟看 記憶檔案

對照官方資料:Context Engine 文件 · README