Das Problem
LLMs haben endliche Kontextfenster. Ein 128K-Token-Modell klingt großzügig, bis man das Ausgabebudget, den Systemprompt, Werkzeugbeschreibungen und die angesammelte Historie einer mehrteiligen Konversation abzieht. Lange Konversationen, große Werkzeugergebnisse und mehrstufige Agentenschleifen drücken alle gegen diese Grenze – oft innerhalb einer einzigen Sitzung. Die naive Lösung ist Kürzung: Alte Nachrichten verwerfen, wenn das Fenster voll wird. Das ist schnell und vorhersehbar, aber es zerstört den Kontext wahllos. Die ursprüngliche Absicht des Benutzers, wichtige Entscheidungen aus früheren Runden und kritische Datenpunkte verschwinden alle, wenn ein stumpfer Zeichenabschnitt sie trifft. Das andere Extrem – LLM-gestützte Zusammenfassung bei jedem Durchgang – bewahrt semantischen Inhalt, ist aber teuer, langsam und führt zu eigenen Fehlermodi (halluzinierte Zusammenfassungen, verlorene numerische Genauigkeit). Die eigentliche Herausforderung ist nicht „in das Fenster passen”. Sie lautet: elegant degradieren, ohne kritische Informationen zu verlieren, ohne Token für unnötige Komprimierung zu verschwenden und ohne Latenz, die der Benutzer spürt. FIM One löst dies mit einer fünfschichtigen Defense-in-Depth-Architektur. Jede Schicht adressiert eine andere Größenordnung des Problems, und sie komponieren sich sauber – keine einzelne Schicht muss perfekt sein, weil die nächste das fängt, was sie verpasst.Fünf Verteidigungsebenen
Kontextmanagement ist nicht ein einzelner Mechanismus. Es ist ein Stapel, bei dem jede Ebene eine spezifische Aufgabe auf einer spezifischen Granularität handhabt:| Ebene | Komponente | Funktion | Zeitpunkt |
|---|---|---|---|
| 5 | Budget-Konfiguration | Berechnet nutzbares Input-Token-Budget aus Modellspezifikationen | Beim Start / pro Anfrage |
| 4 | DbMemory | Lädt persistierte Historie, komprimiert beim Laden | Einmal pro Anfrage |
| 3 | ContextGuard | Budget-Durchsetzung pro Iteration | Bei jeder ReAct-Iteration |
| 2 | CompactUtils | Token-Schätzung, intelligente Kürzung, LLM-Komprimierung | Aufgerufen von Ebenen 3 und 4 |
| 1 | Memory-Implementierungen | Abstrakte Schnittstelle + konkrete Strategien | Framework-Ebene |
Layer 5 — Budget-Konfiguration
Das Budget wird aus drei Werten berechnet:128,000 - 64,000 - 4,000 = 60,000 tokens.
Die 4.000-Token-Systemanfrage-Reserve deckt die Systemaufforderung des Agenten, Werkzeugbeschreibungen und Formatierungsaufwand ab. Dies ist eine feste Konstante — großzügig genug, um Systemanfragen in der Praxis nicht zu kürzen, klein genug, um das Budget nicht zu verschwenden.
Budget-Werte können aus drei Quellen stammen, aufgelöst in Prioritätsreihenfolge:
- Database ModelConfig — pro-Modell
context_sizeundmax_output_tokensvom Administrator festgelegt. - Umgebungsvariablen —
LLM_CONTEXT_SIZEundLLM_MAX_OUTPUT_TOKENS. - Hartcodierte Standardwerte — 128K Kontext, 64K Ausgabe.
Layer 4 — DbMemory
DbMemory ist die produktive Speicherimplementierung. Sie lädt persistierte Gesprächsverlauf aus der Datenbank und komprimiert ihn, um in das Token-Budget zu passen, bevor der Agent ihn sieht.
Das Design ist absichtlich schreibgeschützt. Persistierung wird von chat.py behandelt — der API-Schicht, die den vollständigen Nachrichtenlebenszyklus besitzt (einschließlich Metadaten, Nutzungsverfolgung und Bildanhänge). DbMemory liest nur. Seine Methoden add_message() und clear() sind No-Ops. Diese Trennung verhindert doppelte Schreibvorgänge und hält die Persistierungslogik an einem Ort.
Beim Laden führt DbMemory folgende Schritte durch:
- Fragt alle
user- undassistant-Nachrichten für das Gespräch ab, sortiert nach Erstellungszeit. - Verwirft die letzte Benutzernachricht (die aktuelle Abfrage, die der Agent erneut anfügt).
- Rekonstruiert Bildanhänge — Benutzernachrichten, die Bilder enthielten, speichern Metadaten (
file_id,mime_type) in der Datenbank, undDbMemoryerstellt die base64-Daten-URLs von der Festplatte neu, damit das LLM Bilder aus früheren Durchläufen „sehen” kann. - Komprimiert: Wenn ein
compact_llmbereitgestellt wurde, verwendetCompactUtils.llm_compact(). Andernfalls wird aufCompactUtils.smart_truncate()zurückgegriffen.
DbMemory Verfolgungsflags (was_compacted, _original_count, _compacted_count), die die SSE-Schicht verwendet, um ein compact-Ereignis an das Frontend zu senden.
Layer 3 — ContextGuard
ContextGuard ist der Pro-Iteration-Budget-Erzwinger. Er wird am Anfang jeder ReAct-Iteration aufgerufen — sowohl im eigenständigen ReAct-Modus als auch innerhalb des Sub-Agenten jedes DAG-Schritts. Dies ist die letzte Verteidigungslinie, bevor die Nachrichten die LLM-API erreichen.
Die Durchsetzung folgt einem dreistufigen Prozess:
-
Überdimensionierte einzelne Nachrichten kürzen. Jede einzelne Nachricht, die 50K Zeichen überschreitet, wird hart gekürzt mit einem
[Truncated]-Suffix. Dies erfasst unkontrollierte Tool-Ausgaben — ein Web-Scraping, das eine ganze Webseite zurückgibt, ein Datei-Lesen, das einen großen Datensatz ausgibt. - Gesamttoken schätzen. Wenn die Gesamtmenge innerhalb des Budgets passt, sofort zurückgeben. Die meisten Iterationen bestehen hier — Komprimierung ist die Ausnahme, nicht die Regel.
-
Komprimieren, wenn über Budget. Wenn ein
compact_llmverfügbar ist, verwenden Sie LLM-gestützte Komprimierung mit einem hinweisspezifischen Prompt. Andernfalls greifen Sie aufsmart_truncatezurück.
| Hinweis | Verwendet von | Erhält | Löscht |
|---|---|---|---|
react_iteration | ReAct-Agent-Schleife | Aktuelle Reasoning-Kette, aktuelles Ziel, kritische Daten | Alte redundante Schritte, fehlgeschlagene Wiederholungen, ausführliche Tool-Ausgaben |
planner_input | DAG angereicherte Abfrage | Entwicklung der Benutzerabsicht, wichtige Entscheidungen, Einschränkungen | Dialogdetails, Grüße, Tool-Call-Mechanik |
step_dependency | DAG-Schrittkontext | Wichtige Daten, Zahlen, Schlussfolgerungen | Reasoning-Prozess, fehlgeschlagene Versuche, ausführliche Formatierung |
general | Standard-Fallback | Wichtige Fakten, Entscheidungen, Tool-Ergebnisse | Grüße, Füllmaterial, redundante Hin- und Hergespräche |
smart_truncate zurück. Der Agent sieht den Fehler nie. Dies ist eine bewusste Zuverlässigkeitsentscheidung: Es ist besser, etwas Kontext durch heuristische Kürzung zu verlieren, als die Iteration zum Absturz zu bringen.
Layer 2 — CompactUtils
CompactUtils ist eine zustandslose Utility-Klasse — keine Instanzen, kein Zustand, nur reine Funktionen. Sie bietet drei Funktionen, auf denen die Layer 3 und 4 aufbauen.
Token-Schätzung konvertiert Text in eine ungefähre Token-Anzahl, ohne eine Tokenizer-Bibliothek zu importieren. Die Heuristik:
- ASCII-Zeichen: ~4 Zeichen pro Token
- CJK / Nicht-ASCII-Zeichen: ~1,5 Zeichen pro Token
- Bilder: 765 Token pro Bild (Pauschalgebühr)
- Pro-Nachricht-Overhead: 4 Token (Rollenmarkierung, Trennzeichen)
smart_truncate ist der heuristische Fallback. Es behält angeheftete Nachrichten bedingungslos bei, durchläuft dann rückwärts durch nicht angeheftete Nachrichten und sammelt diese an, bis das Budget aufgebraucht ist. Das Ergebnis ist ein Suffix des Gesprächs, das passt. Es stellt auch sicher, dass das Ergebnis nie mit einer Assistent-Nachricht beginnt — ein verwaister Assistent-Turn ohne vorhergehende Benutzer-Nachricht verwirrt LLMs.
llm_compact ist der LLM-gestützte Pfad. Es teilt Nachrichten in drei Gruppen auf — Systemnachrichten (immer beibehalten), angeheftete Nachrichten (immer beibehalten) und komprimierbare Nachrichten. Die ältesten komprimierbaren Nachrichten werden in einer einzigen [Conversation summary] Systemnachricht zusammengefasst; die 4 neuesten Nachrichten werden wörtlich beibehalten. Wenn das komprimierte Ergebnis immer noch zu lang ist, wird auf smart_truncate für die komprimierte Ausgabe zurückgegriffen — doppelt haltbar.
Layer 1 — Speicherimplementierungen
Die Speicherschicht definiert dieBaseMemory-Schnittstelle: add_message(), get_messages(), clear(). Es gibt drei Implementierungen:
- WindowMemory — ein zählbasiertes Schiebefenster. Behält die letzten N Nicht-System-Meldungen. Einfach, vorhersehbar, keine LLM-Aufrufe. Wird nicht in der Produktion verwendet; nützlich für Tests und zustandslose Szenarien.
-
SummaryMemory — löst LLM-Zusammenfassung aus, wenn die Meldungsanzahl einen Schwellenwert überschreitet. Komprimiert alte Meldungen in eine
[Conversation summary]Systemmeldung. Wird nicht in der Produktion verwendet; geht dem ausgefeilteren ContextGuard-Ansatz voraus. - DbMemory — die Produktionsimplementierung (beschrieben in Layer 4). Datenbankgestützt, schreibgeschützt, mit LLM- oder heuristischer Komprimierung beim Laden.
Wie der Kontext durch ReAct fließt
Der ReAct-Agent verwendet Kontextverwaltung in zwei unterschiedlichen Phasen: Ladezeit und Iterationszeit. Werkzeugiterationen verwenden nicht-streamendeschat() für Geschwindigkeit; die Antwontsynthese verwendet Streaming stream_chat() über stream_answer(). Diese zweigeteilte Aufteilung – schnelle Werkzeugschleife gefolgt von Streaming-Synthese – optimiert sowohl Latenz als auch Benutzererlebnis. Für die vollständige ReAct-Engine-Architektur einschließlich Dual-Mode-Ausführung und Werkzeugauswahl siehe ReAct Engine.
Die Schlüsseleinsicht: DbMemory behandelt das historische Kontextproblem (Züge aus vorherigen Anfragen), während ContextGuard das Wachstumsproblem innerhalb der Anfrage behandelt (Werkzeugergebnisse, die sich während einer Agent-Schleife ansammeln). Sie arbeiten auf unterschiedlichen Zeitskalen und erfassen unterschiedliche Fehlermodi.
Die aktuelle Abfrage des Benutzers ist immer mit pinned=True gekennzeichnet. Dies stellt sicher, dass sie alle Komprimierungen übersteht – sowohl smart_truncate als auch llm_compact bewahren angeheftete Nachrichten bedingungslos. Egal wie aggressiv die Historie komprimiert wird, die eigentliche Frage des Benutzers geht nie verloren.
Wie der Kontext durch DAG fließt
Der DAG-Modus hat eine grundlegend andere Kontextform als ReAct. Statt eines langen Gesprächsfadens hat er einen Baum: eine Planungsphase, mehrere parallele Ausführungsschritte und eine Analysephase. Jede Phase hat ihre eigene Kontextverwaltungsstrategie. Phase 1 — Verlauf laden. DbMemory lädt und komprimiert den Gesprächsverlauf, genau wie ReAct. Der komprimierte Verlauf wird in einen Textblock formatiert, dem"Previous conversation:" vorangestellt ist.
Phase 2 — Konstruktion der erweiterten Abfrage. Der Verlauftext und die aktuelle Abfrage werden in eine enriched_query kombiniert. Wenn diese 16K Token überschreitet, wird sie mit dem planner_input Hinweis-Prompt vom LLM zusammengefasst. Die 16K-Schwelle wird gewählt, weil der Planer die gesamte Abfrage in einem Durchgang lesen muss — anders als ReAct gibt es während der Planung keine iterative Komprimierung.
Phase 3 — Planung. Der Planer erhält einen 2-Nachrichten-Prompt: System-Prompt plus erweiterte Abfrage. Kein ContextGuard hier — die erweiterte Abfrage ist bereits durch die 16K-Prüfung größenkontrolliert.
Phase 4 — Schrittausführung. Jeder DAG-Schritt wird als unabhängiger Agentursystem mit eigenem ContextGuard ausgeführt. Entscheidend ist, dass diese Sub-Agenturen kein Gedächtnis haben — sie starten neu mit nur ihrer Aufgabenbeschreibung und dem Abhängigkeitskontext. Dies ist beabsichtigt: DAG-Schritte sollten eigenständige Arbeitseinheiten sein. Abhängigkeitsergebnisse werden über _build_step_context eingespritzt, das bei 50K Zeichen gekürzt wird (das max_message_chars-Limit des ContextGuard).
Phase 5 — Analyse. Schrittergebnisse werden für das Analyzer-LLM mit Pro-Schritt-Kürzung bei 10K Zeichen formatiert. Dies verhindert, dass die ausführliche Ausgabe eines einzelnen Schritts den Analyskontext dominiert.
Phase 6 — Neuplanung. Wenn der Analyzer feststellt, dass das Ziel nicht erreicht wurde und das Vertrauen unter der Schwelle liegt, werden Schrittergebnisse für den Neuplanungskontext auf nur 500 Zeichen pro Schritt gekürzt. Die Neuplanung muss wissen, was passiert ist und was schief gelaufen ist, nicht die vollständigen Details der Ausgabe jedes Schritts. Diese aggressive Kürzung hält den Neuplan-Prompt kompakt genug, damit der Planer ihn effizient verarbeiten kann.
Für die vollständige DAG-Pipeline-Architektur einschließlich der LLM Call Map und Neuplanungslogik siehe DAG Engine.
Angeheftete Nachrichten
Der Anheftungsmechanismus verhindert, dass die Komprimierung Nachrichten zerstört, die erhalten bleiben müssen. Es gibt zwei Kategorien von angehefteten Nachrichten:- Die aktuelle Benutzerabfrage — immer angeheftet. Wenn der Benutzer eine Frage stellt und der Verlauf zu lang ist, komprimiert das System den Verlauf, nicht die Frage.
- Injizierte Mid-Stream-Nachrichten — wenn ein Benutzer eine Folgefrage sendet, während der Agent noch läuft, wird die injizierte Nachricht als angeheftet markiert, damit der Agent sie in der nächsten Iteration sieht.
Token-Schätzung
FIM One verwendet heuristische Token-Schätzung statt eines echten Tokenizers. Dies ist eine bewusste Entscheidung mit klaren Kompromissen. Warum kein echter Tokenizer? Drei Gründe:-
Abhängigkeitskosten.
tiktoken(OpenAIs Tokenizer) ist 15MB kompilierte Rust-Bindings.sentencepiece(von einigen Open-Source-Modellen verwendet) hat eigene Build-Anforderungen. Für ein Framework, das mehrere LLM-Anbieter unterstützt, gibt es keinen einzigen korrekten Tokenizer — jede Modellfamilie verwendet einen anderen. - Geschwindigkeit. Heuristische Schätzung ist ein einzelner Durchgang durch den String. Echte Tokenisierung beinhaltet Vokabular-Lookup, BPE-Merge-Operationen und spezielle Token-Behandlung. ContextGuard ruft die Schätzung bei jeder Iteration auf, manchmal mehrfach — der Geschwindigkeitsunterschied ist relevant.
- Ausreichend gut. Die Heuristik ist für mehrsprachigen Text optimiert (die ASCII/CJK-Aufteilung deckt die zwei Hauptfälle ab). Sie kann bei Grenzfällen um das 1,5-2fache daneben liegen (stark punktierter Code, ungewöhnliches Unicode), aber Kontext-Management ist von Natur aus ungefähr. Um 30% daneben zu liegen bei einem 60K-Budget lässt immer noch eine komfortable Marge.
| Inhaltstyp | Verhältnis | Begründung |
|---|---|---|
| ASCII-Text | ~4 Zeichen/Token | Englische Prosa und Code durchschnittlich 3,5-4,5 Zeichen/Token über GPT/Claude-Tokenizer |
| CJK / Nicht-ASCII | ~1,5 Zeichen/Token | Jedes CJK-Zeichen ist typischerweise 1-2 Token; 1,5 ist das geometrische Mittel |
| Bilder | 765 Token/Bild | Ungefähre Kosten eines Base64-codierten Bildes in der Vision-API |
| Pro-Nachricht-Overhead | 4 Token | Rollenmarkierung, Trennzeichen, Formatierung |
Was der Benutzer sieht
Das Kontextmanagement ist so konzipiert, dass es im Normalfall unsichtbar ist und nur minimal störend wirkt, wenn es aktiviert wird. Die benutzerorientierten Signale sind: CompactDivider. WennDbMemory beim Laden die Historie komprimiert, rendert das Frontend einen gestrichelten Teiler mit dem Text „Früherer Kontext (N Nachrichten) wurde von KI zusammengefasst.” Dies erscheint zwischen der Zusammenfassung und den beibehaltenen aktuellen Nachrichten und gibt dem Benutzer einen visuellen Hinweis, dass älterer Kontext komprimiert wurde, ohne den Gesprächsfluss zu unterbrechen.
Token-Nutzungsanzeige. Die done-Karte am Ende jeder Antwort zeigt „X.Xk in / X.Xk out” — die insgesamt verbrauchten Input- und Output-Token. Dies umfasst Token, die für die Komprimierung aufgewendet wurden (die schnellen LLM-Aufrufe zur Zusammenfassung). Benutzer, die den Token-Verbrauch überwachen, können sehen, wenn die Komprimierung zusätzlichen Overhead verursacht.
Fehlerbehandlung mit Bedacht. Falls das Kontextmanagement vollständig fehlschlägt — ein Szenario, das angesichts der Fallback-Kette nicht vorkommen sollte, aber theoretisch möglich ist — wird der Fehler als Agent-Fehlertext in der Antwort angezeigt, nicht als Systemabsturz. Das Gespräch wird fortgesetzt; der Benutzer kann erneut versuchen oder die Frage umformulieren.
Das Ziel besteht darin, dass die meisten Benutzer sich nie Gedanken über das Kontextmanagement machen. Sie führen lange Gespräche, das System verwaltet das Budget transparent, und das einzige sichtbare Artefakt ist ein gelegentlicher Komprimierungs-Teiler. Für Power-User und Betreiber, denen Tokeneffizienz wichtig ist, bieten die Nutzungsanzeige und die konfigurierbaren Budget-Parameter die erforderliche Kontrolle.