Kontext-Kompression und MoA
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
context_compressor-Modul— Kompressions-Haupteinstieg (ca. 3700 Zeilen)Budget-Schätzung:190-242—_estimate_msg_budget_tokens/_serialized_length_for_budgetNachrichten-Bereinigung:136-155—_fresh_compaction_message_copy/_strip_persistence_markersExtraktion von Werkzeugaufruf und Pfad-Erwähnung:337-360—_extract_tool_call_name_and_args/_collect_path_mentionsInhalts-Beschneidung:472-515—_append_text_to_content/_strip_image_parts_from_partsmoa_loop— Mixture-of-Agents-AggregationIterationBudget— Kompressions-Refund und Budget-Kopplung
Datenfluss
- Die Hauptschleife prüft jeden Turn (turn), ob Kompression (compression) nötig ist (siehe
_should_run_preflight_estimate:213). - Wird die Schwelle getroffen, ruft der context_compressor auf:
- Token-Budget pro Nachricht schätzen (
agent/context_compressor.py:419) - Bereinigte Kopien alter Nachrichten erstellen (
agent/context_compressor.py:136), Persistenz-Marker abstreifen (agent/context_compressor.py:155) - Werkzeugaufruf-Namen/-Argumente und Pfad-Erwähnungen erhalten, damit die Zusammenfassung keine Schlüsseloperationen verliert (
agent/context_compressor.py:337) - Bild-Teile und überlange Inhalte beschneiden (
agent/context_compressor.py:472)
- Token-Budget pro Nachricht schätzen (
- 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. - Wenn MoA aktiviert ist, ruft
moa_loopin 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:
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 tokensZwei 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:
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_progressverlangt, 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 diestate.dbder neuen Child-Session diese Zeile, und der kompakte Transcript geht in der DB verloren (#57491). Der Terminal-Sweep am Ende voncompress()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_MARKERsetzt 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_PREFIXESbewahrt 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.