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