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.

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.