Le problème
Les LLMs ont des fenêtres de contexte limitées. Un modèle de 128K tokens semble généreux jusqu’à ce que vous soustrayiez le budget de sortie, l’invite système, les descriptions d’outils et l’historique accumulé d’une conversation multi-tours. Les longues conversations, les résultats d’outils volumineux et les boucles d’agents multi-étapes poussent tous contre cette limite — souvent au cours d’une seule session. La solution naïve est la troncature : supprimer les anciens messages lorsque la fenêtre se remplit. C’est rapide et prévisible, mais cela détruit le contexte indiscriminément. L’intention originale de l’utilisateur, les décisions clés des tours précédents et les points de données critiques disparaissent tous lorsqu’une coupure de caractères brutale les atteint. L’extrême opposé — la synthèse alimentée par LLM à chaque tour — préserve le contenu sémantique mais est coûteux, lent et introduit ses propres modes de défaillance (résumés hallucés, perte de précision numérique). Le vrai défi n’est pas « tenir dans la fenêtre ». C’est : se dégrader gracieusement sans perdre les informations critiques, sans brûler de tokens sur une compaction inutile et sans ajouter de latence que l’utilisateur peut ressentir. FIM One résout ce problème avec une architecture de défense en profondeur à cinq couches. Chaque couche aborde une échelle différente du problème, et elles se composent proprement — aucune couche unique n’a besoin d’être parfaite car la suivante rattrape ce qu’elle manque.Cinq couches de défense
La gestion du contexte n’est pas un mécanisme unique. C’est une pile, où chaque couche gère une préoccupation spécifique à une granularité spécifique :| Couche | Composant | Fonction | Moment d’action |
|---|---|---|---|
| 5 | Budget Configuration | Calcule le budget de jetons d’entrée utilisable à partir des spécifications du modèle | Au démarrage / par requête |
| 4 | DbMemory | Charge l’historique persisté, compacte au chargement | Une fois par requête |
| 3 | ContextGuard | Application du budget par itération | À chaque itération ReAct |
| 2 | CompactUtils | Estimation des jetons, troncature intelligente, compaction LLM | Appelé par les couches 3 et 4 |
| 1 | Memory Implementations | Interface abstraite + stratégies concrètes | Niveau framework |
Couche 5 — Configuration du budget
Le budget est calculé à partir de trois valeurs :128 000 - 64 000 - 4 000 = 60 000 tokens.
La réserve de 4 000 tokens pour le système prompt couvre le système prompt de l’agent, les descriptions d’outils et les frais généraux de formatage. C’est une constante fixe — suffisamment généreuse pour éviter de tronquer les système prompts en pratique, assez petite pour ne pas gaspiller le budget.
Les valeurs de budget peuvent provenir de trois sources, résolues par ordre de priorité :
- ModelConfig de la base de données —
context_sizeetmax_output_tokenspar modèle définis par l’administrateur. - Variables d’environnement —
LLM_CONTEXT_SIZEetLLM_MAX_OUTPUT_TOKENS. - Valeurs par défaut codées en dur — contexte 128K, sortie 64K.
Couche 4 — DbMemory
DbMemory est l’implémentation de mémoire en production. Elle charge l’historique de conversation persisté depuis la base de données et le compacte pour tenir dans le budget de tokens avant que l’agent ne le voie.
La conception est intentionnellement en lecture seule. La persistance est gérée par chat.py — la couche API qui possède le cycle de vie complet des messages (y compris les métadonnées, le suivi d’utilisation et les pièces jointes d’images). DbMemory ne fait que lire. Ses méthodes add_message() et clear() sont des no-ops. Cette séparation prévient les écritures doubles et garde la logique de persistance au même endroit.
Au chargement, DbMemory :
- Interroge tous les messages
useretassistantpour la conversation, ordonnés par heure de création. - Supprime le dernier message utilisateur (la requête actuelle, que l’agent réajoutera).
- Reconstruit les pièces jointes d’images — les messages utilisateur qui incluaient des images stockent les métadonnées (
file_id,mime_type) dans la base de données, etDbMemoryreconstruit les data-URLs en base64 à partir du disque pour que le LLM puisse « voir » les images des tours précédents. - Compacte : si un
compact_llma été fourni, utiliseCompactUtils.llm_compact(). Sinon, revient àCompactUtils.smart_truncate().
DbMemory définit les drapeaux de suivi (was_compacted, _original_count, _compacted_count) que la couche SSE utilise pour émettre un événement compact au frontend.
Couche 3 — ContextGuard
ContextGuard est l’applicateur de budget par itération. Il est appelé au début de chaque itération ReAct — à la fois en mode ReAct autonome et à l’intérieur du sous-agent de chaque étape du DAG. C’est la dernière ligne de défense avant que les messages ne frappent l’API LLM.
L’application suit un processus en trois étapes :
-
Tronquer les messages de taille excessive. Tout message unique dépassant 50 000 caractères est tronqué brutalement avec un suffixe
[Truncated]. Cela capture les sorties d’outils qui s’échappent — un web scrape qui retourne une page entière, une lecture de fichier qui déverse un grand ensemble de données. - Estimer le total des tokens. Si le total s’inscrit dans le budget, retourner immédiatement. La plupart des itérations passent ici — la compaction est l’exception, pas la norme.
-
Compacter si dépassement du budget. Si un
compact_llmest disponible, utiliser la compaction alimentée par LLM avec un prompt spécifique à l’indice. Sinon, revenir àsmart_truncate.
| Indice | Utilisé par | Préserve | Supprime |
|---|---|---|---|
react_iteration | Boucle d’agent ReAct | Chaîne de raisonnement récente, objectif actuel, données critiques | Anciennes étapes redondantes, tentatives échouées, sorties d’outils verbeux |
planner_input | Requête enrichie du DAG | Évolution de l’intention utilisateur, décisions clés, contraintes | Détails du dialogue, salutations, mécaniques d’appel d’outils |
step_dependency | Contexte d’étape du DAG | Données clés, nombres, conclusions | Processus de raisonnement, tentatives échouées, formatage verbeux |
general | Secours par défaut | Faits clés, décisions, résultats d’outils | Salutations, remplissage, allers-retours redondants |
smart_truncate. L’agent ne voit jamais l’échec. C’est un choix de fiabilité délibéré : il est préférable de perdre un peu de contexte via une troncature heuristique que de faire planter l’itération.
Couche 2 — CompactUtils
CompactUtils est une classe utilitaire sans état — pas d’instances, pas d’état, juste des fonctions pures. Elle fournit trois capacités sur lesquelles les couches 3 et 4 s’appuient.
L’estimation de jetons convertit le texte en un nombre de jetons approximatif sans importer de bibliothèque de tokeniseur. L’heuristique :
- Caractères ASCII : ~4 caractères par jeton
- Caractères CJK / non-ASCII : ~1,5 caractères par jeton
- Images : 765 jetons par image (coût fixe)
- Surcharge par message : 4 jetons (marqueur de rôle, délimiteurs)
smart_truncate est le recours heuristique. Il conserve les messages épinglés sans condition, puis parcourt les messages non épinglés en arrière, en accumulant jusqu’à épuisement du budget. Le résultat est un suffixe de la conversation qui s’adapte. Il garantit également que le résultat ne commence jamais par un message d’assistant — un tour d’assistant orphelin sans message utilisateur précédent confond les LLM.
llm_compact est le chemin alimenté par LLM. Il divise les messages en trois groupes — messages système (toujours conservés), messages épinglés (toujours conservés) et messages compactables. Les messages compactables les plus anciens sont résumés en un seul message système [Conversation summary] ; les 4 messages les plus récents sont conservés textuellement. Si le résultat compacté est toujours trop long, il revient à smart_truncate sur la sortie compactée — double sécurité.
Couche 1 — Implémentations de mémoire
La couche de mémoire définit l’interfaceBaseMemory : add_message(), get_messages(), clear(). Trois implémentations existent :
- WindowMemory — une fenêtre glissante basée sur le nombre. Conserve les N derniers messages non-système. Simple, prévisible, aucun appel LLM. Non utilisée en production ; utile pour les tests et les scénarios sans état.
-
SummaryMemory — déclenche une résumé LLM quand le nombre de messages dépasse un seuil. Compresse les anciens messages en un message système
[Conversation summary]. Non utilisée en production ; antérieure à l’approche ContextGuard plus sophistiquée. - DbMemory — l’implémentation de production (décrite à la couche 4). Sauvegardée en base de données, en lecture seule, avec compaction LLM ou heuristique au chargement.
Comment le contexte circule dans ReAct
L’agent ReAct utilise la gestion du contexte à deux phases distinctes : le temps de chargement et le temps d’itération. Les itérations d’outils utilisentchat() sans streaming pour la rapidité ; la synthèse de réponse utilise stream_chat() avec streaming via stream_answer(). Cette division en deux phases — boucle d’outils rapide suivie d’une synthèse en streaming — optimise à la fois la latence et l’expérience utilisateur. Pour l’architecture complète du moteur ReAct incluant l’exécution en mode dual et la sélection d’outils, voir Moteur ReAct.
L’insight clé : DbMemory gère le problème du contexte historique (tours des requêtes précédentes), tandis que ContextGuard gère le problème de croissance intra-requête (résultats d’outils s’accumulant pendant une boucle d’agent). Ils opèrent à des échelles de temps différentes et détectent des modes de défaillance différents.
La requête actuelle de l’utilisateur est toujours marquée comme pinned=True. Cela garantit qu’elle survit à toute compaction — à la fois smart_truncate et llm_compact préservent les messages épinglés sans condition. Peu importe à quel point l’historique est compressé, la question réelle de l’utilisateur n’est jamais perdue.
Comment le contexte circule dans le DAG
Le mode DAG a une forme de contexte fondamentalement différente de ReAct. Au lieu d’un long fil de conversation unique, il a une arborescence : une phase de planification, plusieurs étapes d’exécution parallèles et une phase d’analyse. Chaque phase a sa propre stratégie de gestion du contexte. Phase 1 — Chargement de l’historique. DbMemory charge et compacte l’historique de conversation, comme dans ReAct. L’historique compacté est formaté en bloc de texte préfixé par"Previous conversation:".
Phase 2 — Construction de la requête enrichie. Le texte d’historique et la requête actuelle sont combinés dans une enriched_query. Si cela dépasse 16K tokens, il est résumé par LLM en utilisant le hint prompt planner_input. Le seuil de 16K est choisi parce que le planificateur doit lire l’intégralité de la requête en une seule passe — contrairement à ReAct, il n’y a pas de compaction itérative pendant la planification.
Phase 3 — Planification. Le planificateur reçoit un prompt de 2 messages : prompt système plus requête enrichie. Pas de ContextGuard ici — la requête enrichie est déjà contrôlée en taille par la vérification des 16K.
Phase 4 — Exécution des étapes. Chaque étape DAG s’exécute comme un agent ReAct indépendant avec son propre ContextGuard. De manière critique, ces sous-agents n’ont pas de mémoire — ils commencent à zéro avec seulement leur description de tâche et le contexte de dépendance. C’est intentionnel : les étapes DAG doivent être des unités de travail autonomes. Les résultats de dépendance sont injectés via _build_step_context, qui tronque les caractères à 50K (la limite max_message_chars du ContextGuard).
Phase 5 — Analyse. Les résultats des étapes sont formatés pour l’LLM analyseur avec troncature par étape à 10K caractères. Cela empêche la sortie détaillée d’une seule étape de dominer le contexte d’analyse.
Phase 6 — Replanification. Quand l’analyseur détermine que l’objectif n’a pas été atteint et que la confiance est en dessous du seuil, les résultats des étapes sont tronqués à seulement 500 caractères chacun pour le contexte de replanification. La replanification doit savoir ce qui s’est passé et ce qui s’est mal passé, pas le détail complet de la sortie de chaque étape. Cette troncature agressive garde le prompt de replan suffisamment compact pour que le planificateur le traite efficacement.
Pour l’architecture complète du pipeline DAG incluant la LLM Call Map et la logique de replanification, voir DAG Engine.
Messages épinglées
Le mécanisme d’épinglage empêche la compaction de détruire les messages qui doivent persister. Deux catégories de messages sont épinglées :- La requête utilisateur actuelle — toujours épinglée. Si l’utilisateur pose une question et l’historique est trop long, le système compresse l’historique, pas la question.
- Messages injectés en cours de flux — quand un utilisateur envoie un suivi pendant que l’agent est encore en cours d’exécution, le message injecté est marqué comme épinglé pour que l’agent le voie à l’itération suivante.
Estimation des jetons
FIM One utilise une estimation heuristique des jetons plutôt qu’un vrai tokeniseur. C’est un choix délibéré avec des compromis clairs. Pourquoi pas un vrai tokeniseur ? Trois raisons :-
Coût de dépendance.
tiktoken(le tokeniseur d’OpenAI) pèse 15 Mo de liaisons Rust compilées.sentencepiece(utilisé par certains modèles open-source) a ses propres exigences de compilation. Pour un framework qui cible plusieurs fournisseurs de LLM, il n’existe pas de tokeniseur unique correct — chaque famille de modèles en utilise un différent. - Vitesse. L’estimation heuristique est un seul passage sur la chaîne. La vrai tokenisation implique une recherche dans le vocabulaire, des opérations de fusion BPE et la gestion des jetons spéciaux. ContextGuard appelle l’estimation à chaque itération, parfois plusieurs fois — la différence de vitesse compte.
- Suffisamment bon. L’heuristique est ajustée pour le texte multilingue (la division ASCII/CJK couvre les deux cas majeurs). Elle peut être 1,5 à 2 fois décalée pour les cas limites (code fortement ponctué, Unicode inhabituel), mais la gestion du contexte est intrinsèquement approximative. Être décalé de 30 % sur un budget de 60 K laisse toujours une marge confortable.
| Type de contenu | Ratio | Justification |
|---|---|---|
| Texte ASCII | ~4 caractères/jeton | La prose anglaise et le code font en moyenne 3,5-4,5 caractères/jeton sur les tokeniseurs GPT/Claude |
| CJK / non-ASCII | ~1,5 caractères/jeton | Chaque caractère CJK représente généralement 1-2 jetons ; 1,5 est la moyenne géométrique |
| Images | 765 jetons/image | Coût approximatif d’une image codée en base64 dans l’API de vision |
| Surcharge par message | 4 jetons | Marqueur de rôle, délimiteurs, formatage |
Ce que l’utilisateur voit
La gestion du contexte est conçue pour être invisible dans les cas courants et minimalement intrusive lorsqu’elle s’active. Les signaux visibles pour l’utilisateur sont : CompactDivider. QuandDbMemory compacte l’historique au chargement, l’interface affiche un séparateur pointillé avec le texte « Earlier context (N messages) was summarized by AI. » Cela apparaît entre le résumé et les messages récents conservés, donnant à l’utilisateur un indice visuel que le contexte ancien a été compressé sans interrompre le flux de la conversation.
Affichage de l’utilisation des tokens. La carte done à la fin de chaque réponse affiche « X.Xk in / X.Xk out » — le total des tokens d’entrée et de sortie consommés. Cela inclut les tokens dépensés pour la compaction (les appels LLM rapides pour la résumé). Les utilisateurs qui surveillent la consommation de tokens peuvent voir quand la compaction ajoute une surcharge.
Gestion gracieuse des erreurs. Si la gestion du contexte échoue complètement — un scénario qui ne devrait pas se produire compte tenu de la chaîne de secours, mais pourrait théoriquement — l’erreur s’affiche sous forme de texte d’erreur d’agent dans la réponse, et non comme un plantage système. La conversation continue ; l’utilisateur peut réessayer ou reformuler.
L’objectif est que la plupart des utilisateurs ne pensent jamais à la gestion du contexte. Ils ont de longues conversations, le système gère le budget de manière transparente, et le seul artefact visible est un séparateur de compaction occasionnel. Pour les utilisateurs avancés et les opérateurs qui se soucient de l’efficacité des tokens, l’affichage de l’utilisation et les paramètres de budget configurables leur fournissent le contrôle dont ils ont besoin.