Les assistants IA s'encombrent de données inutiles avec le temps. Résultat : factures salées et réponses moins bonnes. Voici comment un outil gratuit nettoie le bazar avant même que l'IA ne voie le prompt.
UNE CONVERSATION DE 50 TOURS, UNE FACTURE DE 131 000 TOKENS
Imaginez un assistant qui se souvient de tout. Absolument tout. Pas seulement ce qui est important, mais aussi les recherches redondantes, les résultats SQL obsolètes, les préférences oubliées depuis 20 tours, et les chunks de documents presque identiques parce que l'utilisateur est revenu sur un ancien sujet. Ce scénario n'a rien d'hypothétique : il se produit dans presque tous les systèmes d'agents IA longs.
Le problème ? Chaque tour ajoute ces données inutiles au prompt envoyé au modèle. Résultat :
- Des coûts d'inférence qui explosent (chaque token coûte de l'argent)
- Des latences qui s'allongent (plus le prompt est gros, plus l'IA répond lentement)
- Des performances de raisonnement qui chutent (l'IA a du mal à trouver l'info utile dans un océan de données)
Et le pire ? Rien ne se nettoie jamais. Le prompt devient un journal append-only où chaque événement historique s'accumule, indéfiniment.
LA SOLUTION QUI NE DÉPEND PAS DE L'IA : UN NETTOYEUR DE PROMPTS DÉTERMINISTE
Plutôt que de faire confiance à une IA pour décider ce qui est important (ce qui introduirait de l'aléatoire et des hallucinations), l'auteur a construit un système de pruning déterministe. Trois passes successives analysent le prompt et suppriment uniquement ce qui est redondant ou expiré, sans jamais toucher aux dépendances critiques.
Le code est écrit avec des dataclasses, des expressions régulières et des dictionnaires Python. Pas d'appel à des modèles d'embeddings, pas de dépendances externes. Juste du code pur qui produit toujours le même résultat pour la même entrée.
PASS 1 : ÉLIMINER LE CONTEXTE EXPIRÉ (CE QUI N'A PLUS AUCUNE VALEUR)
Première étape : repérer et supprimer les messages qui ne servent plus à rien. Comment ? En utilisant des clés de rappel (toolcallkey).
Chaque message lié à un outil (comme une recherche ou une requête SQL) porte une clé unique. Si cette clé apparaît plusieurs fois dans l'historique, seules les versions les plus récentes sont conservées. Les anciennes sont marquées comme expirées et supprimées.
def pass1expiredcontextelimination(self, messages):
last_occurrence = {}
for m in messages:
if m.toolcallkey:
lastoccurrence[m.toolcall_key] = m.id
kept, removed = [], []
for m in messages:
if m.toolcallkey and lastoccurrence[m.toolcall_key] .= m.id:
removed.append(m)
else:
kept.append(m)
return kept, removed
Exemple concret : si l'utilisateur demande trois fois « quelle est la météo à Paris ? », seules les réponses des deuxième et troisième fois sont conservées. La première est supprimée.
PASS 2 : ÉLIMINER LES DUPLICATAS (CE QUI EST DÉJÀ DIT)
Deuxième étape : repérer et supprimer les messages identiques ou presque identiques. Cela arrive souvent dans les assistants RAG (Retrieval-Augmented Generation), où l'IA récupère des chunks de documents similaires.
Le système normalise le texte (supprime les espaces multiples, met en minuscules) et ne garde que la première occurrence d'un message. Les doublons suivants sont supprimés.
def pass2duplicatecontextelimination(self, messages):
seen, kept, removed = {}, [], []
for m in messages:
if m.role == ROLERETRIEVEDDOC:
norm = " ".join(m.content.lower().split())
if norm in seen:
removed.append(m)
continue
seen[norm] = m.id
kept.append(m)
return kept, removed
Exemple : si l'utilisateur revient sur un sujet déjà abordé, et que l'IA récupère les mêmes passages de documents, seuls les premiers sont conservés.
PASS 3 : RESTAURER LES DÉPENDANCES (CE QUI EST ENCORE UTILE)
Troisième étape, la plus cruciale : vérifier que rien de ce qui est nécessaire à une réponse future n'a été supprimé par erreur. C'est ici que le système prouve sa sécurité.
Chaque message peut définir une ou plusieurs clés de dépendance (defines_keys). Plus tard dans la conversation, un autre message peut référencer ces clés (references()). Si une clé est référencée mais que son message définissant a été supprimé, le système le restaure automatiquement.
def pass3dependencyrestoration(self, allmessages, keptmessages, removedmessages):
keptids = {m.id for m in keptmessages}
byid = {m.id: m for m in allmessages}
key_definer = {}
for m in all_messages:
for key in m.defines_keys:
key_definer[key] = m.id
referenced_keys = set()
for m in kept_messages:
referenced_keys.update(m.references())
restored = []
for key in referenced_keys:
definerid = keydefiner.get(key)
if definerid and definerid not in kept_ids:
restoredmsg = byid[definer_id]
keptmessages.append(restoredmsg)
keptids.add(definerid)
restored.append(restored_msg)
kept_messages.sort(key=lambda m: (m.turn, m.id))
return kept_messages, restored
Exemple : si l'utilisateur dit « Je veux les résultats en CSV » au tour 3, et que cette préférence est référencée au tour 47 pour exporter les données, le système s'assure que le message du tour 3 est conservé, même s'il a été marqué comme expiré ou dupliqué.
LE PIÈGE QUI A FAILLI TOUT FAIRE ÉCHOUER : UNE ERREUR DE CONCEPTION MAJEURE
Au début, le système ne restaurait jamais de messages. Pourquoi ? Parce que les tests synthétiques ne créaient jamais de cas où une dépendance supprimée était nécessaire. Les DEFINE n'étaient attachés qu'aux messages utilisateur, alors que les passes 1 et 2 suppriment surtout des sorties d'outils.
Résultat : la passe 3 était écrite, mais jamais testée. Il a fallu ajouter la possibilité pour les sorties d'outils de définir des dépendances pour que le système fonctionne vraiment.
Une fois corrigé, le nombre de restaurations est passé de 0 à 127 dans les conversations les plus longues. Les faits requis sont restés à 100% conservés.
TROIS CAS D'USAGE, TROIS RÉSULTATS : DE 2% À 34% DE TOKENS EN MOINS
Le système a été testé sur trois types de charges de travail, avec cinq tailles de conversation différentes (de 50 à 2000 tours). Résultat :
============================================================
SUMMARY
============================================================
Configurations run: 15 (3 workloads x 5 sizes)
Required facts preserved: 15/15
Reached fixed point (idempotent): 15/15
Workload Token reduction range Facts preserved Idempotent
Normal chat 1.9-4.1% YES YES
RAG assistant 27.0-32.5% YES YES
Tool agent 33.1-33.7% YES YES
============================================================
Normal chat (chat classique sans outils) : seulement 2 à 4% de tokens en moins. Peu de doublons ou de données expirées à supprimer.
Même à 2000 tours et 131 000 tokens, le nettoyage prend moins de 50 millisecondes. Le système est donc léger et rapide.
POURQUOI UNE COUPE POSITIONNELLE NE MARCHE PAS : L'EXEMPLE QUI TOUT DÉTRUIT
La solution la plus simple pour réduire un prompt ? Garder uniquement les N derniers messages. Problème : ça casse les dépendances sans prévenir.
Exemple :
Tour 3 : Utilisateur : « Je veux les résultats en CSV. »
Tour 4 : Assistant : « Compris. »
.
Tour 47 : Utilisateur : « Exporte les résultats. »
Si on garde seulement les 20 derniers tours, le message du tour 3 (la préférence CSV) disparaît. L'assistant ne sait plus dans quel format exporter. La coupe positionnelle ne fait pas la différence entre un message ancien inutile et un message ancien encore nécessaire.
Le système de pruning, lui, garantit que rien de nécessaire n'est supprimé. C'est une différence de sécurité fondamentale.
QUATRE PROPRIÉTÉS QUI FONT LA DIFFÉRENCE EN PRODUCTION
Le système a été conçu pour être utilisé en conditions réelles. Voici ce qui le rend fiable :
- Déterministe : la même entrée produit toujours la même sortie. Pas de hasard, pas d'hallucinations.
- Sécurisé sur les dépendances : rien de ce qui est nécessaire à une réponse future n'est supprimé.
- Idempotent : pruner(pruner(x)) == pruner(x). Pas de dérive cumulative.
- Léger : le nettoyage ne devient jamais le goulot d'étranglement.
Ces propriétés sont cruciales pour une utilisation en production, où la fiabilité prime sur tout le reste.
COMMENT LE SYSTÈME A ÉTÉ TESTÉ : UNE BENCHMARK RIGOUREUX SANS TRUCS
Le système a donc été retesté avec un modèle de charge de travail explicite :
- Taux de récupération par tour (pour le RAG)
- Taux de chevauchement de récupération (les mêmes chunks reviennent)
- Taux d'appels d'outils
- Taux de répétition d'outils
Ces paramètres étaient fixés avant les tests, pas ajustés après pour obtenir un bon résultat. Trois charges de travail ont été définies :
- Chat normal : pas de récupération, peu d'outils.
- Assistant RAG : récupération à chaque tour, chevauchement possible.
- Agent à outils : outils fréquents, haute répétition.
Chaque conversation était étiquetée avec les faits requis : quels messages étaient nécessaires pour les réponses futures. Le système devait les conserver à 100%.
- Towards Data Science
L'indépendance de CLODCO est votre garantie.
Pour que l'actualité de l'IA reste sans filtre et sans concession, votre soutien est indispensable. Votre contribution est le seul moteur de notre liberté éditoriale.
Soutenir CLODCO


