Skip to content

ContextManager: Abschneide-Strategie für das Kontextfenster

源码版本v4.0.10

Verantwortung

ContextManager ist Clines Abschneider und Optimierer für den Fall, dass «das Kontext (context) Fenster kurz vor dem Überlaufen steht». Wenn die Gesamt-Token einer LLM-Anfrage sich dem Modelllimit nähert, rufen der Haupt-Task oder SubagentRunner seine Methoden auf, um einen älteren Abschnitt der Konversationsnachrichten nach Strategie zu löschen, dabei aber die letzten paar Runden, alle Werkzeugdefinitionen und die als «wichtig» markierten früheren Nachrichten zu behalten. Er speichert keine rohen Nachrichten — die bleiben im apiConversationHistory-Array — sondern pflegt einen Bereich conversationHistoryDeletedRange: [start, end], der dem Aufrufer mitteilt, «dieser Indexbereich ist ungültig», zusätzlich zu einer In-memory-Map contextHistoryUpdates für Überschreibungen von Nachrichteninhalten.

Seine Position in der Cline-Architektur liegt zwischen «Nachrichtenverlauf» und dem «an die API gesendeten Request-Body». Vor jeder Anfrage nutzt der Haupt-Task getTruncatedMessages (getTruncatedMessages:344), um das History-Array auf die tatsächlich zu sendende Version zuzuschneiden; nach der Anfrage, wenn die Token-Zahl kritisch wird, entscheidet getNewContextMessagesAndMetadata (getNewContextMessagesAndMetadata:227), ob weiter gekürzt wird. Also ist ContextManager ein kleinines Modul mit rein funktionalem Charakter und internem Zustand, das von der Haupt-Agent-Loop aufgerufen wird, um zu entscheiden, «was die nächste Runde gesendet wird».

Entwurfsmotivation

  • Nur Bereich löschen, Array nicht verändern: Pflegen von [start, end] statt wirklich das Array zu splicen, macht Rollback-Kosten für Checkpoints O(1) (getNextTruncationRange:299).
  • Immer die ersten zwei behalten: Index 0 und 1 sind das «zentrale user/assistant-Paar», nach dem Löschen muss die nächste Nachricht weiterhin eine assistant-Nachricht sein, um die user-assistant-Wechselstruktur zu erhalten (preserve pairing:331).
  • Vier Stufen Behaltens-Strategie: none / lastTwo / half / quarter, entsprechend wie viel gelöscht wird. Je mehr Token überlaufen, desto weniger wird behalten (keep strategies:309).
  • Dynamische Schwelle: Wenn totalTokens / 2 bereits größer als maxAllowedSize ist, geht es direkt über quarter, was den Szenario abdeckt, von einem 200k-Modell auf ein 64k-Modell zu wechseln (quarter fallback:252).
  • file read-Optierung vor dem Abschneiden: Erst die großen tool_result-Blöcke des DateileSENS durch «Markieren + späteres bedarfsgerechtes Wiederherstellen» komprimieren; wenn mehr als 30 % eingespart wird, entfällt das Abschneiden (attemptFileReadOptimizationCore:626).
  • Zeitstempel-Sortierung für Rollback: contextHistoryUpdates wird als Array aus [timestamp, updateType, update] gespeichert; von rechts nach links nach dem Cutoff gesucht, kann man Änderungen nach einem Zeitpunkt präzise rückgängig machen (truncate by timestamp:573).
  • Neuer auto-condense-Pfad: Next-Gen-Modelle nutzen auto-condende mit 0,75-Schwelle, alte Modelle bleiben bei maxAllowedSize (thresholdPercentage:163).

Schlüsseldateien

Datenfluss

Im Haupt-Task-Fluss ist getNewContextMessagesAndMetadata der entscheidende Punkt. Beim Eintritt wird zuerst die Token-Zahl der letzten API-Anfrage gelesen, mit maxAllowedSize verglichen, und nur bei Überschreitung gehandelt:

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) {
      // Beim Modellwechsel reicht half möglicherweise nicht, deshalb bei token/2 > maxAllowedSize direkt über 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,
)

Dieses Stück befindet sich nahe main truncation logic:240. Nachdem der Abschneidebereich berechnet wurde, wendet getAndAlterTruncatedMessages auf die History ab deletedRange[1] + 1 die Funktion applyContextHistoryUpdates an (applyContextHistoryUpdates:362), um die Überschreibungsänderungen wie file read-Optimierungen / Standard-Abschneidehinweise anzuwenden. ensureToolResultsFollowToolUse führt dann eine weitere Paarungskorrektur durch, um sicherzustellen, dass jedem tool_use sofort sein tool_result folgt.

Grenzen und Fehler

  • Nachrichten <= 1 werden nicht behandelt: getAndAlterTruncatedMessages gibt direkt das Original-Array zurück (early return:358).
  • Gleicher deletedRange wird nicht neu berechnet: compactConversationForContextWindow erkennt, wenn der neue Bereich dem alten vollständig entspricht, und beendet frühzeitig (range equal early return:929), um unnötiges Umschreiben zu vermeiden.
  • Bereichsende muss assistant sein: Nach dem Löschen eines Abschnitts muss die nächste Nachricht eine assistant-Nachricht sein, sonst wird rangeEndIndex -= 1 um eins zurückgesetzt (assistant pairing:333).
  • file read-Optimierung mit < 30 % Ersparnis muss trotzdem abschneiden: Bei percentSaved < 0.3 wird needToTruncate: true gesetzt (30 percent threshold:654).
  • Fehlschlag von saveContextHistory wird nur geloggt: try/catch umschließt fs.writeFile, wirft nicht an den Hauptfluss zurück (save error handling:142).
  • Verschiedene Modelle behalten unterschiedliche Puffer: contextWindow <= 64k nutzt die 80 %-Formel, um zu verhindern, dass der Puffer bei kleinen Modellen zu knapp wird (deepseek buffer:31).
  • Checkpoint-Rollback über Zeitstempel: truncateContextHistory ändert nicht den deletedRange, sondern löscht nur Updates nach dem angegebenen Zeitstempel, sodass die History zu einem beliebigen Checkpoint zurückgesetzt werden kann (truncateContextHistory:552).

Zusammenfassung

ContextManager ist die Stütze, die Cline vor dem Absturz in langen Konversationen bewahrt. Um zu sehen, wie es vom SubagentRunner genutzt wird, lies subagent; um zu sehen, wie der Haupt-Task es innerhalb der Rekursion aufruft, um die History der nächsten Runde zu entscheiden, lies agent-loop/task-class; um mehr über das Task-Verzeichnis und Persistierung zu erfahren, lies storage/state-manager.

Siehe offizielle Dokumentation: Cline-Dokumentation · README