Lección 3 — Résumés et mémoire long terme

Le résumé glissant : une conversation qui ne grossit plus

6 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Le résumé glissant : une conversation qui ne grossit plus

Le principe

Une conversation qui dure finit par dépasser n'importe quel budget. Plutôt que de couper les vieux messages (et perdre ce qu'ils contenaient), on les résume :

  • les keep_last derniers tours restent mot pour mot (le modèle en a besoin pour suivre le fil : « et le deuxième ? ») ;
  • quand on dépasse max_turns, les tours plus anciens sont fondus dans un résumé qui, lui, se met à jour à chaque fois.
flowchart LR
  subgraph AV["Avant : 13 tours (max 12)"]
    O["Tours 1 à 7"] --- K["Tours 8 à 13"]
  end
  O --> S["LLM : résumé + tours 1 à 7 = nouveau résumé"]
  subgraph AP["Après"]
    S2["Résumé (≤ 120 mots)"] --- K2["Tours 8 à 13, mot pour mot"]
  end
  S --> S2
  K --> K2

Le code

files/contexte.py
class RollingSummary:
    def __init__(self, summarize, max_turns: int = 12, keep_last: int = 6):
        self.summarize, self.max_turns, self.keep_last = summarize, max_turns, keep_last
        self.summary, self.turns = "", []   # turns : [(role, texte)], role = "user" | "model"

    def add(self, role: str, text: str):
        self.turns.append((role, text))
        if len(self.turns) > self.max_turns:
            old, self.turns = self.turns[:-self.keep_last], self.turns[-self.keep_last:]
            self.summary = self.summarize(self.summary, old)

La fonction summarize est injectée : un appel LLM en production (llm_summarizer), une fonction triviale dans les tests. C'est ce qui permet de tester la logique (quand résumer, quoi garder) sans clé :

files/test_memoire.py
def test_rolling_summary_keeps_last_turns():
    calls = []
    conv = contexte.RollingSummary(lambda s, old: calls.append(len(old)) or f"résumé de {len(old)} tours",
                                   max_turns=4, keep_last=2)
    for i in range(5):
        conv.add("user", f"m{i}")
    assert calls == [3] and conv.summary == "résumé de 3 tours"
    assert conv.turns == [("user", "m3"), ("user", "m4")]

Avec max_turns=12 et keep_last=6, combien de tours sont envoyés mot pour mot juste après un résumé ?

Une consigne de résumé qui compte

files/contexte.py
SUMMARY_PROMPT = """Mets à jour le résumé d'une conversation entre un utilisateur et un assistant.
Garde : les décisions, les demandes en cours, les informations données par l'utilisateur.
Supprime : les politesses, les répétitions. Si une information a changé, ne garde que la nouvelle.
120 mots maximum, en français. Réponds uniquement par le texte du résumé, sans titre.
..."""

Les deux dernières phrases ont été ajoutées après notre premier essai réel (voir le résultat ci-dessous).

Résultat obtenu

Fin de la session 1 de Camille (10 messages, donc 20 tours), le 2026-10-15. Premier essai, avec la consigne sans les deux dernières phrases (gemini-3.5-flash) :

résumé glissant en fin de session : "**Nouveau résumé :**\n\nCamille est une nouvelle employée de l'entreprise Atelier à Nantes,
au sein de l'équipe Paiements. Elle utilise principalement le langage Go et effectue ses trajets domicile-trav"

Deuxième essai, consigne corrigée, sur Vertex AI (gemini-2.5-flash) :

résumé glissant en fin de session : "Camille est une nouvelle collaboratrice chez Atelier, membre de l'équipe Paiements.
Elle a une allergie aux arachides, à considérer pour les propositions (ex: traiteur). Son langage de programmation p"

Deux leçons :

  1. Le titre parasite « Nouveau résumé : » a disparu avec la consigne « sans titre ». Un résumé est réinjecté à chaque appel : chaque mot inutile se paie à chaque fois.
  2. « équipe Paiements » est toujours là, et c'est normal : le changement d'équipe est le tout dernier message, il fait partie des 6 tours gardés mot pour mot, pas encore résumés. Résumé + tours récents restent cohérents ensemble. Mais le résumé, lui, ne survit pas à la session, et c'est pourquoi l'équipe actuelle doit vivre dans la mémoire long terme (leçon suivante), qui l'a bien mise à jour.

Pourquoi l'équipe Paiements apparaît-elle encore dans le résumé, alors que Camille a annoncé son changement ?

Exercice : mesurer la compression

Écrivez une fonction qui, pour une conversation donnée, compare le nombre de tokens de l'historique complet à celui de « résumé + tours récents », et dites à partir de combien de messages le résumé devient rentable compte tenu de son propre coût d'appel.

Voir la solution
from contexte import approx_tokens

def compression(conv, all_turns):
    full = sum(approx_tokens(t) for _, t in all_turns)
    kept = approx_tokens(conv.summary) + sum(approx_tokens(t) for _, t in conv.turns)
    return full, kept

Le résumé coûte un appel qui lit les tours anciens (environ leur taille) et écrit 120 mots (environ 160 tokens). Il est rentable dès que l'économie cumulée sur les appels suivants dépasse ce coût : si chaque appel suivant économise EE tokens, il suffit de ⌈couˆt/E⌉\lceil \text{coût} / E \rceil appels. Dans une conversation de support de 40 messages, l'économie se compte vite en milliers de tokens ; dans une conversation de 5 messages, le résumé ne se déclenche même pas.

Je sais faire

  • Coder un résumé glissant avec un seuil et un nombre de tours gardés
  • Tester sa logique sans clé grâce à une fonction de résumé injectée
  • Écrire une consigne de résumé courte, sans titre, qui remplace les informations périmées
  • Expliquer pourquoi le résumé ne remplace pas la mémoire long terme
Siguiente