ContextManager:上下文視窗截斷策略
職責
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)。
關鍵檔案
ContextManager class:44— 持有contextHistoryUpdatesmap,無單例。initializeContextHistory:100— 啟動時從磁碟讀歷史context_history_updates。saveContextHistory:131— 把記憶體裡的 updates 持久化回 task 目錄。shouldCompactContextWindow:150— 給主 Task 用的布林判斷,按上次請求 token 數對閾值。getNewContextMessagesAndMetadata:227— 主入口,決定是否截斷、回傳截斷後的 history 和新的 deletedRange。getNextTruncationRange:299— 給定 keep 策略,算出要刪的[start, end]區間。getTruncatedMessages:344— 公開介面,套用 history updates 後回傳最終下發的訊息陣列。ensureToolResultsFollowToolUse:375— 校正 tool_use 和 tool_result 的配對,刪一段後可能錯位。truncateContextHistory:552— 刪除給定 timestamp 之後的所有 updates,用於 checkpoint 回滾。applyContextOptimizations:606— 找並儲存 file read 最佳化點。attemptFileReadOptimizationInMemory:696— 給 SubagentRunner 用的記憶體版本,直接回傳最佳化後的 conversation。getContextWindowInfo:10— 把contextWindow和maxAllowedSize算出來,不同模型留不同 buffer。
資料流
主 Task 流程下,getNewContextMessagesAndMetadata 是關鍵決策點。進來先讀上一次 API 請求的 token 數,跟 maxAllowedSize 比,超了才動:
// 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.3時needToTruncate: 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。