Aller au contenu principal

Compaction : compresser les conversations longues

Mis à jour le 28 juillet 2026

Le problème des conversations longues

Chaque message ajouté à une conversation augmente le nombre de tokens envoyés à l’API. Une conversation de 50 échanges peut facilement atteindre 30 000 tokens en entrée, ce qui augmente la latence et le coût. Le phénomène est insidieux : le premier échange d’un assistant de support coûte quelques centaines de tokens, le trentième en coûte plusieurs milliers alors que l’utilisateur pose exactement le même type de question. Vous payez de plus en plus cher pour un service identique, et l’utilisateur attend de plus en plus longtemps sa réponse.

La compaction résout ce problème en compressant l’historique sans perdre les informations essentielles. Elle consiste à résumer les anciens échanges pour réduire le nombre de tokens tout en préservant le contexte nécessaire. Au lieu d’envoyer l’intégralité de l’historique, vous envoyez un résumé des échanges passés plus les messages récents. Le modèle continue de savoir que le client s’appelle Dupont, qu’il a une commande en litige et que vous lui avez déjà proposé un remboursement, sans avoir à relire les quarante messages qui l’ont établi.

Le mécanisme

La fonction ci-dessous applique la logique dans sa forme la plus simple : elle estime le volume, ne fait rien tant que le seuil n’est pas franchi, puis coupe l’historique en deux parties. Les messages anciens partent au résumé, les derniers restent tels quels.

import openai

client = openai.OpenAI()

def compacter_historique(
    messages: list[dict],
    seuil_tokens: int = 10_000,
    garder_recents: int = 6,
) -> list[dict]:
    """Compacte l'historique quand il dépasse le seuil."""

    # Estimer le nombre de tokens (approximation)
    tokens_estimes = sum(len(m["content"]) // 4 for m in messages)

    if tokens_estimes < seuil_tokens:
        return messages  # Pas besoin de compacter

    # Séparer : messages anciens vs récents
    anciens = messages[:-garder_recents]
    recents = messages[-garder_recents:]

    # Résumer les messages anciens
    texte_anciens = "\n".join(
        f"{m['role']}: {m['content']}" for m in anciens
    )

    response = client.responses.create(
        model="gpt-5.6-terra",
        input=(
            "Résumez cette conversation de manière concise. "
            "Gardez les informations factuelles clés, les décisions prises, "
            "et le contexte nécessaire pour comprendre la suite.\n\n"
            f"{texte_anciens}"
        ),
        max_output_tokens=500,
    )

    # Reconstruire l'historique compacté
    message_resume = {
        "role": "system",
        "content": f"Résumé de la conversation précédente :\n{response.output_text}",
    }

    return [message_resume] + recents

Deux détails méritent votre attention. Le prompt de résumé demande explicitement les décisions prises, parce qu’un résumé qui ne retient que les thèmes abordés fait perdre au modèle la mémoire de ses propres engagements. Et max_output_tokens=500 borne le résumé : sans cette limite, le résumé grossit à chaque compaction et vous retrouvez le problème que vous cherchiez à éliminer.

Deux stratégies à connaître

Fenêtre glissante avec résumé

La classe suivante industrialise le principe. Elle conserve un résumé unique qu’elle réécrit à chaque compaction en y intégrant les nouveaux échanges, et maintient une fenêtre de messages bruts. Le contexte envoyé à l’API se compose donc toujours de trois blocs : le prompt système, le résumé cumulatif, la fenêtre récente.

class ConversationCompactee:
    """Gère une conversation avec compaction automatique."""

    def __init__(
        self,
        prompt_systeme: str,
        seuil_compaction: int = 8_000,
        taille_fenetre: int = 8,
    ):
        self.prompt_systeme = prompt_systeme
        self.seuil = seuil_compaction
        self.taille_fenetre = taille_fenetre
        self.resume: str | None = None
        self.messages: list[dict] = []

    def construire_contexte(self) -> list[dict]:
        """Construit le contexte à envoyer à l'API."""
        contexte = [{"role": "system", "content": self.prompt_systeme}]

        if self.resume:
            contexte.append({
                "role": "system",
                "content": f"Contexte précédent : {self.resume}",
            })

        contexte.extend(self.messages[-self.taille_fenetre:])
        return contexte

    def ajouter_message(self, role: str, contenu: str):
        self.messages.append({"role": role, "content": contenu})
        self._verifier_compaction()

    def _verifier_compaction(self):
        tokens = sum(len(m["content"]) // 4 for m in self.messages)
        if tokens > self.seuil:
            self._compacter()

    def _compacter(self):
        anciens = self.messages[:-self.taille_fenetre]
        if not anciens:
            return

        texte = "\n".join(
            f"{m['role']}: {m['content']}" for m in anciens
        )

        contexte_existant = f"Résumé existant : {self.resume}\n\n" if self.resume else ""

        response = client.responses.create(
            model="gpt-5.6-terra",
            input=(
                f"{contexte_existant}"
                f"Nouveaux échanges à intégrer au résumé :\n{texte}\n\n"
                "Produisez un résumé mis à jour, concis et factuel."
            ),
            max_output_tokens=400,
        )

        self.resume = response.output_text
        self.messages = self.messages[-self.taille_fenetre:]

Notez que _compacter réinjecte le résumé existant dans le prompt avant les nouveaux échanges. C’est ce qui empêche l’amnésie progressive : sans cette réinjection, chaque compaction effacerait tout ce que la précédente avait retenu.

Compaction sélective

Le résumé est destructeur, et certains contenus ne survivent pas à une reformulation. Un extrait de code, une réponse JSON d’API, un tableau de chiffres perdent leur utilité dès qu’on les paraphrase. La compaction sélective trie donc les messages anciens avant de résumer : ceux qui contiennent du code ou des données structurées passent intacts, seul le texte conversationnel est compressé.

def compaction_selective(
    messages: list[dict],
    garder_recents: int = 6,
) -> list[dict]:
    """Compacte en gardant les messages structurés intacts."""
    anciens = messages[:-garder_recents]
    recents = messages[-garder_recents:]

    a_resumer = []
    a_garder = []

    for msg in anciens:
        contenu = msg["content"]
        # Garder les messages contenant du code ou des données
        if "```" in contenu or "{" in contenu:
            a_garder.append(msg)
        else:
            a_resumer.append(msg)

    # Résumer uniquement le texte conversationnel
    resume = ""
    if a_resumer:
        texte = "\n".join(f"{m['role']}: {m['content']}" for m in a_resumer)
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=f"Résumez ces échanges en 3 phrases :\n{texte}",
            max_output_tokens=200,
        )
        resume = response.output_text

    resultat = []
    if resume:
        resultat.append({
            "role": "system",
            "content": f"Résumé des échanges : {resume}",
        })
    resultat.extend(a_garder)
    resultat.extend(recents)
    return resultat

Le critère de tri retenu ici — présence de triples accents graves ou d’une accolade — est volontairement grossier. Adaptez-le à ce que produisent réellement vos outils : sur un assistant qui manipule des références de dossiers, vous voudrez peut-être préserver tout message contenant un identifiant client.

Vérifier que la compaction sert à quelque chose

Une compaction mal calibrée coûte un appel API supplémentaire sans gain notable, ou compresse si agressivement que le modèle perd le fil et repose des questions déjà résolues. Mesurez donc le ratio avant de fixer vos seuils définitifs, sur un échantillon de conversations réelles.

def rapport_compaction(
    avant: list[dict],
    apres: list[dict],
) -> dict:
    tokens_avant = sum(len(m["content"]) // 4 for m in avant)
    tokens_apres = sum(len(m["content"]) // 4 for m in apres)
    reduction = 1 - (tokens_apres / tokens_avant) if tokens_avant else 0

    return {
        "tokens_avant": tokens_avant,
        "tokens_apres": tokens_apres,
        "reduction": f"{reduction:.0%}",
        "messages_avant": len(avant),
        "messages_apres": len(apres),
    }

Prenez une dizaine de transcriptions issues de votre production, faites-les passer par la compaction, et lisez les résumés produits : si vous ne pouvez pas reprendre la conversation à partir du seul résumé, votre seuil est trop bas ou votre prompt de résumé trop vague.

Points clés à retenir

  • La compaction réduit les tokens en résumant les anciens messages
  • Gardez toujours les derniers messages intacts (fenêtre glissante)
  • Préservez les messages contenant du code ou des données structurées
  • Réinjectez le résumé existant à chaque nouvelle compaction pour éviter l’amnésie progressive
  • Mesurez le ratio de compression pour calibrer vos seuils