Skip to content

Kontext-Kompression und MoA ​

源码版本v2026.7.20

Verantwortung ​

Zwei Dinge: (1) Wenn die Historie sich der Kontextobergrenze (context upper bound) des Providers nähert, werden alte Nachrichten zusammengefasst/abgeschnitten, um Budget (budget) freizugeben (context_compressor); (2) optional aggregiert der Mixture-of-Agents-Pfad die Ausgaben mehrerer Modelle (moa_loop). Beide werden innerhalb der Hauptschleife aufgerufen und sind die Stützen, die «lange Konversationen am Laufen halten» und «Antworten stabiler machen».

Schlüsseldateien ​

Datenfluss ​

  1. Die Hauptschleife prüft jeden Turn (turn), ob Kompression (compression) nötig ist (siehe _should_run_preflight_estimate:213).
  2. Wird die Schwelle getroffen, ruft der context_compressor auf:
  3. Der durch Kompression «neu geschaffene» Raum wird über iteration_budget.refund() (agent/iteration_budget.py) teilweise zurückgegeben, sodass die Hauptschleife zusätzliche Turns drehen darf.
  4. Wenn MoA aktiviert ist, ruft moa_loop in diesem Turn parallel mehrere Provider (provider) auf und aggregiert sie als Verstärkung oder Schiedsspruch der Hauptantwort.

Die Budget-Schätzung ist die Grundlage der Kompressionsentscheidung. Hier der Kern aus agent/context_compressor.py:419-437:

python
def _estimate_msg_budget_tokens(msg: dict) -> int:
    """Counts the full ``tool_call`` envelope, not just ``function.arguments``
    — counting only the arguments undercounted parallel-tool turns by 2-15x
    (#28053). Also counts provider replay fields (``codex_reasoning_items``)."""
    content_len = _content_length_for_budget(msg.get("content") or "")
    tokens = content_len // _CHARS_PER_TOKEN + 10  # +10 for role/key overhead
    for tc in msg.get("tool_calls") or []:
        if isinstance(tc, dict):
            tokens += len(str(tc)) // _CHARS_PER_TOKEN
    for key in _REPLAY_BUDGET_KEYS:
        tokens += _serialized_length_for_budget(msg.get(key)) // _CHARS_PER_TOKEN
    return tokens

Zwei schon durchlaufene Fallen stehen im Kommentar: (1) Nur die function.arguments-Zeichenkette zu zählen unter-zählt um das 2- bis 15-fache (Szenario paralleler Werkzeugaufrufe); (2) Replay-Felder wie codex_reasoning_items nicht zu zählen, lässt «klein wirkende, aber tatsächlich große» Assistant-Nachrichten schützen — nach der Kompression bleibt die Historie weiter knapp unter der Obergrenze und wird dann ständig neu komprimiert (#55572).

Bereinigung und Persistenz-Marker-Strip siehe agent/context_compressor.py:136-160:

python
def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]:
    """Shallow .copy() propagates ``_db_persisted``; new child session then
    skips every row (#57491). Strip at every copy site."""
    fresh = msg.copy()
    fresh.pop(_DB_PERSISTED_MARKER, None)
    return fresh


def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None:
    """Terminal sweep — invariant holds regardless of intermediate copy sites."""
    for msg in messages:
        if isinstance(msg, dict):
            msg.pop(_DB_PERSISTED_MARKER, None)

«Zwei Verteidigungslinien»: _fresh_compaction_message_copy streift den Marker an jedem Copy-Punkt lokal, _strip_persistence_markers macht am Ende von compress() noch einen abschließenden Sweep. Erstere ist absichtsklar, letztere ist strukturelle Auffangwehr. Die Bild-Beschneidung (siehe _strip_image_parts_from_parts in agent/context_compressor.py:497-515) arbeitet ähnlich — image-/image_url-/input_image-Parts werden durch Text-Platzhalter ersetzt; bei fehlendem Bild wird None zurückgegeben, damit der Aufrufer das Ersetzen überspringt und nicht für jede Nachricht eine sinnlose neue Liste entsteht.

Designmotiv ​

Warum «Schätzen» statt exaktes Token-Counting? Die Tokenizer der Provider sind nicht öffentlich (besonders die versteckten Felder von Reasoning-Modellen); tiktoken ist langsam und ungenau. Das Modul fährt mit einer char-basierten Schätzung + 10 Zeichen Overhead als feste Formel — das reicht für die Entscheidungen «komprimieren oder nicht» und «wie den Tail schützen», weil es konservativ bleibt. Es braucht keine Präzision, nur dasselbe Maß wie die Preflight-Schätzung (siehe _should_run_preflight_estimate in turn_context), sonst entsteht ein Deadlock «Preflight sagt: nicht komprimieren; Kompression sagt: geht nicht».

Warum Werkzeugaufruf-Namen (tool call names) und Pfad-Erwähnungen erhalten? Wenn ein LLM alte Nachrichten zusammenfasst, schluckt es schnell eine Schlüsseloperation wie «hat write_file(/tmp/foo.py) aufgerufen». Im nächsten Turn liest das Modell «vorher wurde über eine Datei gesprochen», weiß aber nicht, in welchen Pfad geschrieben wurde, und legt sie neu an oder liest eine falsche Stelle. _extract_tool_call_name_and_args / _collect_path_mentions kleben die harten Fakten neben die Summary — Kosten: ein paar zusätzliche Tokens; Gewinn: spätere Werkzeugaufrufe ohne Amnesie.

Dass der Refund ans iteration_budget geht statt direkt das Budget zu erhöhen, verhindert, dass max_iterations ausgehöhlt wird — bei jeder Kompression ein paar Turns drauf, dann läuft eine lange Konversation endlos. Der Refund gibt nur die ein bis zwei Werkzeugaufruf-Iterationen zurück, die die Kompression selbst eingespart hat; «die Obergrenze bleibt die Obergrenze».

Grenzen und Fehler ​

  • Kompression kommt nicht voran: _compression_made_progress verlangt, dass die Token um >5 % sinken, um als Fortschritt zu gelten. Ein Turn, der nur 2 % spart, wird als «kein Fortschritt» gewertet und verhindert endloses Kreisen; die Hauptschleife schaltet dann auf Auto-Reset und öffnet eine neue Sitzung.
  • Persistenz-Marker bleibt hängen: Trägt ein Kompressionsergebnis _db_persisted, überspringt die state.db der neuen Child-Session diese Zeile, und der kompakte Transcript geht in der DB verloren (#57491). Der Terminal-Sweep am Ende von compress() erzwingt ein nochmaliges Durchsehen als Auffangwehr.
  • Summary wird als neue Anweisung gelesen: Schwache Modelle lesen «## Active Task» in der Summary als neue User-Anfrage und antworten (#11475, #14521). _SUMMARY_END_MARKER setzt eine explizite Trennlinie; in Alternation-Grenz-Szenarien zusätzlich das Doppel-Wrapper-Paar _MERGED_PRIOR_CONTEXT_HEADER / _MERGED_SUMMARY_DELIMITER, um Mehrdeutigkeit zu vermeiden.
  • Historischer Prefix wird wiederbelebt: _HISTORICAL_SUMMARY_PREFIXES bewahrt alte Summary-Präfixe. Beim Resume in eine neue Lineage werden die alten Präfixe mit abgestreift, damit ein veralteter Directive nicht im Body weiter die Antwort kapert.

Zusammenfassung ​

Der Kompressor ist die Lebenslinie langer Konversationen. Der zentrale Trade-off ist «was wegwerfen, was behalten» – Werkzeugaufrufe und Pfad-Erwähnungen werden bevorzugt erhalten, weil ihr Verlust in folgenden Turns zu «Amnesie» führt. MoA ist eine orthogonale, ein-/ausschaltbare Verstärkung. Beide verändern die Struktur der Hauptschleife nicht, sondern klinken sich nur zum richtigen Zeitpunkt ein.