Skip to content

Evolution der Kontextkompression (compaction)

源码版本rust-v0.145.0

codex behandelt die Konversationshistorie (conversation history) als eine begrenzte Ressource. Sobald die akkumulierten Tokens nahe an das Model Context Window heranreichen, wird die alte Historie zu einer Zusammenfassung (Summary) gefaltet, um Platz für den nächsten Turn zu schaffen. Dieser Kompressions-Pfad (compaction) wurde in core/src/compact*.rs mehrfach umgeschrieben: lokal erzeugt das Modell selbst die Zusammenfassung, später kam eine serverseitige Remote-Kompression hinzu, und schließlich evolvete es zu v2. Alle drei Implementierungen teilen sich dieselben Hook- und Analytics-Lifecycles, aber die Quelle der „Zusammenfassung" ist jeweils verschieden.

Verantwortlichkeiten

  1. Triggerauslöser bestimmen: auto (Token-Schwelle getroffen), manual (Benutzer tippt /compact) oder CompactionReason (Modell-Downshift, comp-hash-Änderung usw.) — drei Quellen, die von run_inline_auto_compact_task und run_compact_task verteilt werden.
  2. Summary-Turn laufen lassen: Die lokale Variante ruft selbst die Responses API auf, um das Ergebnis von SUMMARIZATION_PROMPT zu erhalten; die Remote-Variante lässt den Server direkt ein ResponseItem::Compaction liefern; v2 verlangt zusätzlich strikt genau ein einzelnes Compaction-Item vom Server.
  3. Ersatz-Historie aufbauen: Aus Summary + den letzten Nutzer-Nachrichten wird eine neue Historie gebaut, die alte Historie komplett verworfen; build_compacted_history_with_limit kürzt die Nutzer-Nachrichten auf ein 20k-Token-Limit.
  4. Kontextfenster (context window) pflegen: Jede Kompression öffnet ein neues Fenster; über advance_auto_compact_window / start_new_context_window werden window_id, previous_window_id aktualisiert und stehen der späteren Token-Zählung und dem Rollout-Trace zur Verfügung.
  5. Hook und Analytics exponieren: PreCompactHookOutcome / PostCompactHookOutcome erlauben Plugins, die Kompression abzufangen; CompactionAnalyticsAttempt meldet für jeden Versuch trigger, reason, implementation und phase.

Entwurfsbeweggründe

Die ursprüngliche lokale Kompression (compact.rs) ist „Client schickt erneut einen Request an das Modell, um eine Zusammenfassung zu erstellen". Ihr Problem: Die Zusammenfassung selbst kostet Tokens, und ihre Qualität hängt davon ab, wie gut das Modell SUMMARIZATION_PROMPT ausführt. Als die Modell-Provider anfingen, serverseitige Kompression nativ zu unterstützen, entstand compact_remote.rs: Der Server liefert innerhalb eines Responses-Requests das gefaltete Compaction-Item direkt zurück; der Client ist an der Erzeugung der Zusammenfassung nicht beteiligt.

Aber v1-Remote schickt die gesamte Historie an den Server, dieser gibt nur die Zusammenfassung zurück, und der Client muss die Function-Call-Ausgaben selbst nachschneiden. v2 (compact_remote_v2.rs) zieht diesen Ablauf straffer zusammen: Der Server liefert nicht nur die Zusammenfassung, sondern entscheidet auch, welche Nachrichten behalten werden (retained messages); der Client sammelt einfach das einzelne Compaction-Ergebnis, bei Fehler fatal. Zudem bringt v2 das Limit RETAINED_MESSAGE_TOKEN_BUDGET = 64_000, damit die behaltenen Nachrichten das Kontextfenster nicht selbst sprengen.

compact_token_budget.rs ist ein Seitenzweig: Es überspringt die Zusammenfassungserzeugung und öffnet direkt ein neues Fenster. Dieser Pfad bleibt für Szenarien wie „Benutzer will Kontext leeren", in denen kein zusätzlicher Modellaufruf für eine Zusammenfassung gemacht werden soll. Er durchläuft dennoch die vollständige Hook- und ContextCompaction-Turn-Item-Lifecycle; er ruft nur das Modell nicht auf.

Wichtige Dateien

codex-rs/core/src/compact.rs:150-219run_compact_task_inner, Haupteinstieg der lokalen Kompression mit pre/post-Hook und Analytics-Ummantelung.codex-rs/core/src/compact.rs:221-378run_compact_task_inner_impl: Modell-Stream, Retry, abschließend replace_compacted_history.codex-rs/core/src/compact.rs:602-663build_compacted_history_with_limit, füllt Nutzer-Nachrichten von hinten nach vorn, bis das 20k-Token-Limit erreicht ist.codex-rs/core/src/compact_remote.rs:76-107run_remote_compact_task, Remote-v1-Einstieg, markiert als ResponsesCompact.codex-rs/core/src/compact_remote_v2.rs:85-115run_remote_compact_task in v2, markiert als ResponsesCompactionV2.codex-rs/core/src/compact_remote_v2.rs:385-443collect_compaction_output, erzwingt, dass der Server genau ein Compaction-Item liefert, sonst fatal.codex-rs/core/src/compact_remote_v2_attempt.rs:32-142run_remote_compact_v2_attempt: erst trim_function_call_history_to_fit_context_window, dann Request senden.codex-rs/core/src/compact_model_fallback.rs:8-19should_retry_with_current_model, definiert, welche Fehler einen Modellwechsel für den Retry wert sind.codex-rs/core/src/compact_token_budget.rs:64-90 — Token-Budget-Kompression, springt die Zusammenfassung an und öffnet direkt ein neues Fenster.

InitialContextInjection ist eine scheinbar nebensächliche, aber entscheidende Enum: Sie legt fest, ob in die neue Historie nach der Kompression der anfängliche Kontext (initial context) eingefügt wird. Pre-turn / manuelle Kompression nutzt DoNotInject, damit der nächste reguläre Turn selbst wieder injiziert; Mid-Turn-Kompression muss BeforeLastUserMessage verwenden, weil das Modell darauf trainiert ist, „nach der Zusammenfassung folgt die letzte Nachricht", und der Kontext muss vor der letzten echten Nutzer-Nachricht stehen.

rust
// compact.rs:56-69 — mid-turn 与 pre-turn 压缩的上下文注入策略
#[derive(Debug)]
pub(crate) enum InitialContextInjection {
    BeforeLastUserMessage(Arc<WorldState>),
    DoNotInject,
}

Der Kern der lokalen Kompression ist, die gesamte Historie plus SUMMARIZATION_PROMPT an das Modell zu senden und nach dem Streamen die letzte Assistant-Nachricht als Summary zu nehmen. Sie verwendet die Modellausgabe nicht direkt als neue Historie, sondern hüllt sie in format!("{SUMMARY_PREFIX}\n{summary_suffix}"), damit später is_summary_message sie erkennen kann.

rust
// compact.rs:323-336 — 从模型 stream 结果里取出摘要并打标
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);

let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);

Die zentrale Auflage von v2-Remote: Der Server muss genau ein ResponseItem::Compaction zurückgeben, sonst fatal:

rust
// compact_remote_v2.rs:419-434 — server 返回多条 compaction 视为协议错误
if !saw_completed {
    return Err(CodexErr::Stream(
        "remote compaction v2 stream closed before response.completed".to_string(),
        None,
    ));
}
if compaction_count != 1 {
    return Err(CodexErr::Fatal(format!(
        "remote compaction v2 expected exactly one compaction output item, got {compaction_count} from {output_item_count} output items"
    )));
}

Datenfluss

Grenzen und Fehler

  • ContextWindowExceeded-Selbstheilung: Wenn beim lokalen Komprimieren der Prompt selbst das Fenster sprengt, wird in einer Schleife history.remove_first_item() aufgerufen und der älteste Eintrag gekappt, bis nur noch einer übrig ist; erst dann bricht er endgültig mit Fehler ab (codex-rs/core/src/compact.rs:285-300).
  • Straffere Stream-Retry-Grenze: v2-Remote clamp die Anzahl der Retries auf MAX_REMOTE_COMPACTION_V2_STREAM_RETRIES = 2, weil die Kompression selbst lange dauert und das gewöhnliche Stream-Retry-Budget eine Kompression zu lang blockieren würde (codex-rs/core/src/compact_remote_v2.rs:54-57).
  • Modell-Fallback: InvalidRequest, ContextWindowExceeded, ServerOverloaded und ähnliche Fehler lösen should_retry_with_current_model aus und wechseln zum aktuell ausgewählten Hauptmodell, damit Provider-Spezifika die Kompression nicht blockieren (codex-rs/core/src/compact_model_fallback.rs:8-19).
  • Mid-Turn-Injektionsposition: In der neuen Historie nach der Zusammenfassung darf der initial context nicht einfach ans Ende gepushed werden, sondern muss vor der letzten echten Nutzer-Nachricht eingespleißt werden; gibt es keine echte Nutzer-Nachricht, wird er vor der Summary eingefügt, sodass die Summary stets die letzte ist (codex-rs/core/src/compact.rs:542-587).
  • Token-Budget-Kompression ohne Summary: Über start_new_context_window wird direkt das Fenster gewechselt, ohne das Modell aufzurufen; dennoch wird das ContextCompaction-Turn-Item emittiert, damit Hook und UI über die unterschiedlichen Pfade hinweg kein Ereignis verpassen (codex-rs/core/src/compact_token_budget.rs:76-82).

Zusammenfassung

Die Komplexität dieses Codestücks stammt im Wesentlichen aus „drei koexistierenden Implementierungen": lokaler Stream, Remote v1, Remote v2 mit jeweils eigenem entry und inner_impl, aber geteiltem Gerüst aus CompactionAnalyticsAttempt, Hook und InitialContextInjection. Bei neuen Features sollte zuerst festgestellt werden, welchen Pfad der Provider nimmt — should_use_remote_compact_task ist die Verzweigung —, bevor der entsprechende Datei gelesen wird. Wie der Modellaufruf selbst hinausgeht, siehe Client und Responses API; wie das Kompressionsergebnis in die Historie geschrieben wird, siehe Sitzungsthread und Nachrichten-Historie.