Brief IA

LLMs: Reducing Prompts to Cut Costs and Improve Quality

🔬 Research·Tom Levy·

LLMs: Reducing Prompts to Cut Costs and Improve Quality

LLMs: Reducing Prompts to Cut Costs and Improve Quality
Key Takeaways
1LLMs are penalized by overly long prompts, increasing costs and degrading quality.
2A prompt reduction layer decreases token usage without breaking dependencies.
3This solution is validated by real benchmarks and tested in production.
💡Why it mattersOptimizing LLM prompts reduces costs and improves efficiency, which is essential for their adoption.
Le brief IA que lisent les pros

Le brief IA que les pros lisent chaque soir

Les 7 actus IA du jour, décryptées en 5 min. Gratuit.

Inclus dès l'inscription : notre sélection des meilleurs guides & comparatifs IA.

Choisis ton rythme

Gratuit · Pas de spam · Désabonnement en 1 clic

📄
Full Analysis

LLMs : réduire les prompts pour baisser coûts et améliorer qualité

Modèle de Langage de Grande Taille

Le contexte long n'est pas gratuit — un dirigeant a construit une couche de réduction de prompt sécurisée qui rend les systèmes LLM plus efficaces. Un prompt n'a pas besoin de se souvenir de tout. Il doit se souvenir de ce qui est encore pertinent.

Au fil des travaux, il a été constaté que l'état de la conversation tend à croître rapidement. Il est courant de renvoyer de grandes portions de l'historique à chaque tour, y compris des sorties d'outils anciennes, des récupérations RAG répétées, et un contexte qui n'est plus pertinent. À mesure que cela s'accumule, les prompts peuvent devenir significativement plus volumineux, ce qui peut augmenter le coût d'inférence et la latence, et dans certains cas, affecter les performances de raisonnement.

Un pipeline déterministe a été construit pour éliminer cet état redondant avant que le prompt n'atteigne le modèle. La version mise en œuvre évite les appels LLM, les embeddings et les dépendances externes. En s'appuyant strictement sur des composants de bibliothèque standard, chaque décision de réduction reste entièrement déterministe et reproductible.

Le suivi de l'état s'exécute en trois passes distinctes : Élimination du Contexte Expiré, Élimination du Contexte Dupliqué, et Restauration des Dépendances. La troisième passe est ce qui aide à rendre les deux premières plus sûres en pratique. Elle garantit qu'aucun message dont une réponse ultérieure dépend n'est accidentellement supprimé.

Problèmes rencontrés

En construisant cela, deux bogues ont été rencontrés, ce qui a changé le design. Le premier corpus de référence utilisait un nombre fixe de doublons et d'appels d'outils obsolètes, ce qui faisait que les pourcentages de réduction diminuaient à mesure que les conversations avançaient. Cela ne reflétait pas ce qui était attendu d'un comportement réel.

La logique de restauration des dépendances n'a également pas été testée au départ, car les données synthétiques ne créaient jamais de cas où un message requis était réellement supprimé. Ces deux problèmes sont abordés ici, ainsi que la manière dont les corriger a changé les résultats.

Après avoir corrigé le pipeline, celui-ci a été évalué sur trois charges de travail : chat simple, assistant RAG, et agent lourd en outils. Chacune a été testée à cinq tailles de conversation, pour un total de 15 configurations sur deux machines différentes. Au cours de ces tests, tous les faits requis étiquetés ont été préservés. Le système a également atteint un point fixe stable après un seul passage, ce qui signifie qu'une réduction d'un prompt déjà réduit ne produit aucun changement supplémentaire.

La réduction des tokens dépend de la charge de travail. Elle est d'environ 2 à 4 % pour un chat simple, 27 à 32 % pour un assistant RAG, et 33 à 34 % pour un agent lourd en outils. Même à 2 000 tours et 131 000 tokens, le prétraitement est resté en dessous de 50 millisecondes.

Problème de surcharge des prompts

Chaque prompt déployé devient de plus en plus lourd. Le même schéma apparaissait toujours dans chaque agent à long terme construit. Une conversation commence parfaitement propre. Cinquante tours plus tard, c'est un véritable désordre.

À ce cinquantième tour, le payload du prompt envoyé à chaque requête inclut le prompt système, l'historique complet de la conversation, quatre sorties d'outils (deux d'entre elles étant complètement obsolètes car l'outil a été exécuté deux fois), six morceaux récupérés (trois étant des quasi-doublons car l'utilisateur est revenu à un ancien sujet), un résultat SQL d'il y a vingt tours qui est juste là, et une seule préférence utilisateur exprimée une fois et jamais mentionnée à nouveau.

Aucun de ces éléments n'est techniquement incorrect. Chaque pièce avait du sens au moment où elle a été injectée. Le véritable problème est que rien n'est jamais nettoyé. Le prompt se transforme en un journal d'événements historiques en mode ajout uniquement, et tout cela est déversé sur le modèle, à chaque tour, indéfiniment.

Ce n'est pas seulement un problème de stockage. Les performances de raisonnement se dégradent mesurablement à mesure que la longueur d'entrée augmente, même lorsque le contenu ajouté est sans rapport avec la tâche, et les modèles sont moins efficaces pour utiliser des informations enfouies au milieu d'un long contexte que des informations proches des bords.

La réaction instinctive immédiate face à cette surcharge est simplement de tronquer. Coupez les N derniers messages et laissez tomber tout le reste. C'est littéralement une ligne de code. Le problème est qu'il brise silencieusement les chaînes de conversation de manière que l'on ne réalise même pas jusqu'à ce que cela explose sur un véritable utilisateur :

  • Tour 3 : Utilisateur : Mon format de sortie préféré est CSV.
  • Tour 4 : Assistant : Compris.
  • Tour 47 : Utilisateur : Exportez les résultats.

Si la fenêtre est limitée aux 20 derniers tours, le tour 3 disparaît au tour 47. L'assistant perd le contexte que l'utilisateur a demandé un CSV. Ces données n'étaient pas obsolètes ; c'était une dépendance forte, et une simple troncature positionnelle ne peut pas différencier entre un ancien contexte jetable et un ancien contexte dont un tour ultérieur dépend encore.

C'est exactement la contrainte de design que ce projet aborde. Tout mécanisme qui supprime l'état redondant doit faire la distinction entre ces deux catégories. La récence seule est insuffisante.

Emprunter une idée des systèmes d'exploitation

Voici le cadre autour duquel le reste a été construit : un système d'exploitation décide constamment quelles pages restent résidentes dans la RAM et lesquelles sont évincées. Une conversation LLM à long terme a exactement le même problème, sauf qu'aucun processus ne joue le rôle de gestionnaire de mémoire. Le contexte s'accumule indéfiniment parce qu'aucun processus n'est chargé de décider ce qui cesse de mériter sa place dans le prompt.

Le Prompt Pruner dans cet article est ce morceau manquant.

Chaque élément de l'état de conversation (un tour utilisateur, un tour assistant, une sortie d'outil, un morceau récupéré) est un objet Message avec un rôle, un numéro de tour, et un peu de métadonnées de gestion :

  • tool_call_key : Optional[str] = None
  • expires_after_turn : Optional[int] = None
  • defines_keys : list = field(default_factory=list)

Trois passes s'exécutent sur cette liste dans l'ordre. Chacune est une fonction pure : messages en entrée, liste filtrée en sortie. Aucun modèle n'est impliqué dans l'étape de réduction elle-même.

Pourquoi il n'y a pas de modèle à l'intérieur du pruner

Un modèle d'embedding aurait pu être utilisé pour évaluer la pertinence des messages, ce qui aurait rendu cela plus flou et beaucoup plus facile à écrire. Cela n'a pas été fait, et ce n'est pas par souci de pureté.

Une fois qu'une décision de réduction dépend du jugement d'un modèle, la capacité de raisonner sur ce à quoi ressemblera une conversation au tour suivant est perdue. La même entrée ne garantit plus la même sortie. Une couche de réduction destinée à rendre un système de production plus prévisible ne devrait pas être la partie la moins prévisible. Tout ici fonctionne sur des dataclasses, des regex, et des recherches dans des dictionnaires, qui est exactement l'ensemble d'outils dont ce problème a besoin.

Pass 1 : Élimination du Contexte Expiré

Si un outil est appelé plus d'une fois sous la même clé (la même requête de recherche, la même recherche SQL, la même lecture de fichier), seul le résultat le plus récent est encore fiable. Tout ce qui précède sous cette clé est expiré.

def _pass1_expired_context_elimination(self, messages):
    last_occurrence = {}
    for m in messages:
        if m.tool_call_key:
            last_occurrence[m.tool_call_key] = m.id
    kept, removed = [], []
    for m in messages:
        if m.tool_call_key and last_occurrence[m.tool_call_key] != m.id:
            removed.append(m)
    return kept, removed

Pass 2 : Élimination du Contexte Dupliqué

Les pipelines de récupération tirent constamment des passages identiques ou quasi-identiques, surtout lorsqu'un utilisateur revient à un sujet antérieur. Cette passe normalise les espaces et la casse, conserve uniquement la première occurrence, et supprime chaque doublon après.

def _pass2_duplicate_context_elimination(self, messages):
    seen, kept, removed = {}, [], []
    for m in messages:
        if m.role == ROLE_RETRIEVED_DOC:
            norm = " ".join(m.content.lower().split())
            if norm in seen:
                removed.append(m)
            seen[norm] = m.id
    return kept, removed

Pass 3 : Restauration des Dépendances (et le bogue qui a poussé à le construire correctement)

Cette passe est la raison pour laquelle les deux premières peuvent être exécutées en toute sécurité. Si la Passe 1 ou la Passe 2 supprime un message qui se trouve être le seul endroit où un fait encore référencé a été défini, cette passe le récupère et le remet dans la liste.

Le mécanisme est intentionnellement simple : un message se marque avec une balise DEFINE: littérale, et un message ultérieur le référence en utilisant REF:. Si un REF survit dans l'ensemble final conservé mais que son DEFINE correspondant a été supprimé en amont, la Restauration des Dépendances restaure ce message manquant dans la liste.

def _pass3_dependency_restoration(self, all_messages, kept_messages, removed_messages):
    kept_ids = {m.id for m in kept_messages}
    by_id = {m.id: m for m in all_messages}
    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())
    for key in referenced_keys:
        definer_id = key_definer.get(key)
        if definer_id and definer_id not in kept_ids:
            restored_msg = by_id[definer_id]
            kept_messages.append(restored_msg)
            kept_ids.add(definer_id)
            restored.append(restored_msg)
    kept_messages.sort(key=lambda m: (m.turn, m.id))
    return kept_messages, restored

Voici le bogue. Lors du premier benchmark complet sur toutes les trois charges de travail et cinq tailles, chaque ligne imprimait "Restauré (deps) : 0." Chaque ligne. La première réaction était que cela semblait génial — un bilan de sécurité parfait. Ce n'était pas le cas. Cela signifiait que la Passe 3 n'avait jamais réellement restauré un seul message lors de n'importe quel test à n'importe quelle taille.

Il a fallu retourner dans le générateur de corpus pour comprendre pourquoi, et la réponse était embarrassante une fois vue. Les conversations synthétiques n'attachaient jamais de marqueurs DEFINE à des messages utilisateurs simples, tandis que la Passe 1 et la Passe 2 ne supprimaient que des sorties d'outils et des documents récupérés. Les deux catégories ne se chevauchaient jamais. La Restauration des Dépendances était présente dans le code, entièrement écrite, mais complètement non testée par le benchmark car rien de généré ne lui donnait une raison de s'exécuter.

La solution a été de permettre à certaines sorties d'outils de définir également une dépendance, de la même manière qu'un véritable appel d'outil "obtenir les paramètres utilisateur" pourrait faire surface un fait dont la conversation dépend plus tard, même si cet appel d'outil exact pourrait être remplacé et marqué comme expiré par la Passe 1. Une fois cela ajouté, les chiffres ont immédiatement changé : 2 restaurés.

Brief IA — L'actualité IA en français

L'essentiel de l'actualité de l'intelligence artificielle, décrypté et expliqué chaque jour.