Skip to content

ContextManager : stratégie de troncation de la fenêtre de contexte

源码版本v4.0.10

Responsabilités

ContextManager est le troncateur/optimiseur que Cline utilise quand « la fenêtre de contexte (context window) est sur le point de déborder ». Quand le total de tokens d'une requête LLM approche de la limite du modèle, le Task principal ou le SubagentRunner appelle ses méthodes pour supprimer une tranche des anciens messages, tout en conservant les derniers échanges, toutes les définitions d'outils et les messages plus anciens marqués comme « importants ». Il ne stocke pas les messages bruts — ceux-ci restent dans le tableau apiConversationHistory — mais maintient un intervalle conversationHistoryDeletedRange: [start, end] qui indique à l'appelant « ces index sont invalidés », ainsi qu'une map en mémoire contextHistoryUpdates qui enregistre les modifications par recouvrement du contenu des messages.

Sa place dans l'architecture de Cline se situe entre « l'historique des messages » et « le corps de requête réellement envoyé à l'API ». Avant chaque requête, le Task principal utilise getTruncatedMessages (getTruncatedMessages:344) pour découper le tableau d'historique dans la version réellement envoyée ; une fois la requête terminée et le nombre de tokens critique, il appelle getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227) pour décider d'une suppression supplémentaire. ContextManager est donc un petit module à état interne et allure fonctionnelle, que la boucle principale de l'agent appelle pour décider « quoi envoyer au prochain tour ».

Motivation de conception

  • Tronquer un intervalle sans modifier le tableau d'origine : maintenir [start, end] plutôt que de vraiment splicer le tableau rend le rollback d'un checkpoint (point de contrôle) O(1) (getNextTruncationRange:299).
  • Toujours conserver les deux premiers messages : les index 0 et 1 forment « la paire user/assistant centrale », et après suppression le message suivant doit toujours être un assistant, afin de préserver la structure alternée user-assistant (preserve pairing:331).
  • Quatre stratégies de rétention : none / lastTwo / half / quarter, correspondant à la quantité à conserver. Plus le dépassement de tokens est important, moins on conserve (keep strategies:309).
  • Seuil dynamique : quand totalTokens / 2 est déjà supérieur à maxAllowedSize, on passe directement à quarter, ce qui couvre le passage d'un modèle 200k à un modèle 64k (quarter fallback:252).
  • L'optimisation des file read passe avant la troncation : on commence par compresser les grosses blocs tool_result des lectures de fichiers via « marquage + restitution à la demande » ; si on économise plus de 30 %, on évite toute troncation (attemptFileReadOptimizationCore:626).
  • Tri par timestamp pour le rollback : contextHistoryUpdates stocke des tableaux [timestamp, updateType, update] et cherche le cutoff de droite à gauche par ordre chronologique, ce qui permet d'annuler précisément les modifications postérieures à un point donné (truncate by timestamp:573).
  • Nouveau chemin auto-condense : les modèles next-gen suivent l'auto-condense à seuil 0.75, tandis que les anciens modèles restent jugés sur maxAllowedSize (thresholdPercentage:163).

Fichiers clés

Flux de données

Dans le flux du Task principal, getNewContextMessagesAndMetadata est le point de décision clé. À l'entrée, on lit le nombre de tokens de la requête API précédente, on compare à maxAllowedSize, et on n'agit que si le seuil est dépassé :

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,
)

Ce bloc se trouve près de main truncation logic:240. Une fois l'intervalle de troncation calculé, getAndAlterTruncatedMessages parcourt l'historique après deletedRange[1] + 1 et applique applyContextHistoryUpdates (applyContextHistoryUpdates:362), c'est-à-dire les modifications par recouvrement (optimisation file read, avis de troncation standard, etc.). ensureToolResultsFollowToolUse fait alors une passe de correction d'appairage pour garantir que chaque tool_use est immédiatement suivi de son tool_result.

Limites et échecs

  • Aucune action si le nombre de messages est <= 1 : getAndAlterTruncatedMessages renvoie directement le tableau d'origine (early return:358).
  • Pas de recalcul si le deletedRange est identique : compactConversationForContextWindow détecte que le nouveau range est strictement égal à l'ancien et sort tôt (range equal early return:929), évitant une réécriture inutile.
  • La fin du range doit être un assistant : après avoir supprimé une tranche, le message suivant doit être un assistant, sinon rangeEndIndex -= 1 recule d'une position (assistant pairing:333).
  • Si l'optimisation file read économise moins de 30 %, on tronque quand même : percentSaved < 0.3 force needToTruncate: true (30 percent threshold:654).
  • Échec d'écriture de saveContextHistory seulement logué : fs.writeFile est enveloppé dans un try/catch, sans remonter à la boucle principale (save error handling:142).
  • Buffer différent selon le modèle : contextWindow <= 64k applique une formule à 80 %, pour éviter un buffer trop fin sur les petits modèles (deepseek buffer:31).
  • Le rollback de checkpoint passe par timestamp : truncateContextHistory ne touche pas à deletedRange, il ne supprime que les updates postérieurs à un timestamp donné, ce qui permet de revenir à n'importe quel checkpoint (truncateContextHistory:552).

Résumé

ContextManager est le pilier qui empêche les longues conversations Cline de s'effondrer. Pour voir comment SubagentRunner l'utilise, lire subagent ; pour voir comment le Task principal l'appelle depuis la récursion pour décider de l'historique du prochain tour, lire agent-loop/task-class ; pour le répertoire task et la persistance, lire storage/state-manager.

Voir la documentation officielle : Cline docs · README.