Skip to content

ContextManager:上下文視窗截斷策略

源码版本v4.0.10

職責

ContextManager 是 Cline 在「上下文 (context) 視窗快要撐爆」時的截斷與最佳化器。當一輪 LLM 請求的 total token 接近模型上限,主 Task 或 SubagentRunner 會呼叫它的方法,把老的對話訊息按策略刪掉一段,同時保留最新的幾輪、所有工具定義、以及被標為「重要」的更早訊息。它不存原始訊息——那還在 apiConversationHistory 陣列裡——而是維護一個 conversationHistoryDeletedRange: [start, end] 區間,告訴呼叫方「這段索引已經無效」,外加一個 contextHistoryUpdates 的 in-memory map 記錄對訊息內容的覆蓋式修改。

它在 Cline 架構裡位置介於「訊息歷史」和「實際發往 API 的請求體」之間。每輪請求前,主 Task 用 getTruncatedMessages (getTruncatedMessages:344) 把 history 陣列切成實際下發的版本;請求完發現 token 數臨界,再用 getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227) 決定要不要進一步刪。所以 ContextManager 是個純函式 + 內部狀態的小模組,主 agent loop 呼叫它來決定「下一輪發什麼」。

設計動機

  • 只刪區間不改原陣列:維護 [start, end] 而不是真去 splice 陣列,讓 checkpoint 回滾成本 O(1) (getNextTruncationRange:299)。
  • 永遠留前兩條:index 0 和 1 是「核心 user/assistant pair」,刪完之後下一條仍必須是 assistant 訊息,保持 user-assistant 交替結構 (preserve pairing:331)。
  • 四檔保留策略:none / lastTwo / half / quarter,對應刪多少。token 超得越多保留越少 (keep strategies:309)。
  • 動態閾值:totalTokens / 2 已經大於 maxAllowedSize 時直接走 quarter,覆蓋從 200k 模型切到 64k 模型的場景 (quarter fallback:252)。
  • file read 最佳化先於截斷:先把讀檔案的大塊 tool_result 用「標記 + 後續按需還原」的方式壓縮,能省 30% 以上就免截斷 (attemptFileReadOptimizationCore:626)。
  • timestamp 排序支援回滾:contextHistoryUpdates[timestamp, updateType, update] 陣列存,按時間從右往左找 cutoff,能精確撤銷某時間點之後的修改 (truncate by timestamp:573)。
  • auto-condense 新路徑:next-gen 模型走 0.75 閾值的 auto-condense,舊模型仍按 maxAllowedSize 判斷 (thresholdPercentage:163)。

關鍵檔案

資料流

主 Task 流程下,getNewContextMessagesAndMetadata 是關鍵決策點。進來先讀上一次 API 請求的 token 數,跟 maxAllowedSize 比,超了才動:

typescript
// apps/vscode/src/core/context/context-management/ContextManager.ts
if (previousApiReqIndex >= 0) {
  const previousRequestText = clineMessages[previousApiReqIndex]?.text
  if (previousRequestText) {
    const timestamp = clineMessages[previousApiReqIndex].ts
    const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequestText)
    const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
    const { maxAllowedSize } = getContextWindowInfo(api)

    if (totalTokens >= maxAllowedSize) {
      // 切模型時 half 可能不够,所以 token/2 超 maxAllowedSize 直接走 quarter
      const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"

      let { anyContextUpdates, needToTruncate } = this.attemptFileReadOptimizationCore(
        apiConversationHistory,
        conversationHistoryDeletedRange,
        timestamp,
      )

      if (needToTruncate) {
        anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
        conversationHistoryDeletedRange = this.getNextTruncationRange(
          apiConversationHistory,
          conversationHistoryDeletedRange,
          keep,
        )
        updatedConversationHistoryDeletedRange = true
      }

      if (anyContextUpdates) {
        await this.saveContextHistory(taskDirectory)
      }
    }
  }
}

const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
  apiConversationHistory,
  conversationHistoryDeletedRange,
)

這段在 main truncation logic:240 附近。截斷區間算出來後,getAndAlterTruncatedMessages 會把 deletedRange[1] + 1 之後的 history 跑一遍 applyContextHistoryUpdates (applyContextHistoryUpdates:362),把 file read 最佳化 / 標準截斷提示等覆蓋式修改 apply 上去。ensureToolResultsFollowToolUse 再做一次配對校正,保證每個 tool_use 後面緊跟它的 tool_result。

邊界與失敗

  • 訊息少於等於 1 條不處理:getAndAlterTruncatedMessages 直接回傳原陣列 (early return:358)。
  • deletedRange 相同時不重算:compactConversationForContextWindow 檢測 new range 跟 old range 完全相等就早退 (range equal early return:929),避免無謂重寫。
  • range 結尾必須是 assistant:刪完一段後下一條必須是 assistant 訊息,否則 rangeEndIndex -= 1 回退一位 (assistant pairing:333)。
  • file read 最佳化省不夠 30% 仍要截斷:percentSaved < 0.3needToTruncate: true (30 percent threshold:654)。
  • saveContextHistory 寫盤失敗只記 log:try/catch 包住 fs.writeFile,不拋回主流程 (save error handling:142)。
  • 不同模型留不同 buffer:contextWindow <= 64k 走 80% 公式,避免小模型 buffer 太薄 (deepseek buffer:31)。
  • checkpoint 回滾走時間戳:truncateContextHistory 不改 deletedRange,只刪指定時間戳之後的 update,讓歷史能回退到任意 checkpoint (truncateContextHistory:552)。

小結

ContextManager 是 Cline 長會話不崩的支柱。想看它怎麼被 SubagentRunner 用可以讀 subagent;想看主 Task 如何在遞迴裡呼叫它決定下一輪的 history 可以讀 agent-loop/task-class;想看 task 目錄和持久化相關可以讀 storage/state-manager

對照官方資料:Cline 文件 · README