درس 6 — Mémoire, état et MCP

Mémoire courte, résumé et mémoire long terme

6 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Mémoire courte, résumé et mémoire long terme

Un LLM n'a aucune mémoire

Chaque appel à call_llm est indépendant : le modèle ne « se souvient » que de ce que vous lui renvoyez dans messages. La mémoire d'un agent est donc entièrement de votre ressort.

flowchart LR
  subgraph COURT["Mémoire courte (une session)"]
    H["Historique messages"] --> T["Tronquer ou résumer"]
  end
  subgraph LONG["Mémoire long terme (entre sessions)"]
    F["Faits du client (fichier, base)"]
  end
  T --> P["Contexte envoyé au LLM"]
  F --> P
  P --> L["call_llm"]
NiveauContenuDurée de vieStockage typique
Historiquetous les messages de la sessionune conversationliste en mémoire, cache
Résuméles faits importants des anciens toursune conversationun message dans l'historique
Long termepréférences, adresse habituelle, dernière commandedes moisbase de données
Étatpanier en cours, étape du processusune conversationdictionnaire structuré, pas du texte

Niveau 1 : garder l'historique d'une question à l'autre

Jusqu'ici, agent() repartait de zéro à chaque question. On garde les messages dans un objet (tronquer est définie juste après) :

memoire.py
import json
from pathlib import Path

from agent import SYSTEM, TOOLS, call_llm, run_tool


class Conversation:
    def __init__(self, llm=call_llm):
        self.llm = llm
        self.messages = [{"role": "system", "content": SYSTEM}]

    def demander(self, question, max_steps=6):
        self.messages.append({"role": "user", "content": question})
        self.messages = tronquer(self.messages)
        for _ in range(max_steps):
            msg = self.llm(self.messages, TOOLS)
            self.messages.append(msg)
            calls = msg.get("tool_calls") or []
            if not calls:
                return msg.get("content") or ""
            for call in calls:
                self.messages.append(run_tool(call))
        return "Désolé, je n'ai pas abouti. Je transmets à un conseiller."

Niveau 2 : tronquer, puis résumer

L'historique grossit à chaque tour, et tout l'historique est refacturé à chaque appel. Première solution, la plus simple : ne garder que les derniers messages, en commençant toujours par un message user (un message tool privé de son tool_calls provoque une erreur 400 chez la plupart des fournisseurs).

memoire.py (suite)
def tronquer(messages, garder=20):
    if len(messages) <= garder + 1:
        return messages
    recent = messages[-garder:]
    while recent and recent[0]["role"] != "user":   # jamais de message tool orphelin
        recent = recent[1:]
    return [messages[0]] + recent

Quand l'oubli pose problème (le client a donné sa référence au début), on résume les anciens tours avec le LLM lui-même :

memoire.py (suite)
def compacter(messages, garder=10, llm=call_llm):
    if len(messages) <= garder + 1:
        return messages
    cut = len(messages) - garder
    while cut < len(messages) and messages[cut]["role"] != "user":
        cut += 1
    ancien = messages[1:cut]
    texte = "\n".join(f"{m['role']}: {m.get('content') or m.get('tool_calls')}"
                      for m in ancien)
    resume = llm([{"role": "user", "content":
                   "Résume en 5 lignes maximum les faits utiles de cet échange "
                   "(produits, références, quantités, décisions) :\n" + texte}], [])
    return [messages[0],
            {"role": "user", "content": "Résumé de la conversation précédente : "
             + (resume.get("content") or "")},
            {"role": "assistant", "content": "Bien noté."}] + messages[cut:]

Pourquoi ne pas simplement garder tout l'historique, puisque les modèles récents acceptent de très longs contextes ?

Niveau 3 : la mémoire long terme

Certains faits doivent survivre à la session : « livrer le samedi », « allergique aux arachides » (attention, donnée sensible, voir plus bas). On les stocke par client et on les injecte dans le prompt système.

memoire.py (suite)
MEMOIRE = Path("memoire")


def lire_faits(client_id):
    f = MEMOIRE / f"{client_id}.json"
    return json.loads(f.read_text()) if f.exists() else []


def ajouter_fait(client_id, fait):
    if not client_id.isalnum():          # client_id finit dans un chemin de fichier
        raise ValueError("client_id invalide")
    MEMOIRE.mkdir(exist_ok=True)
    faits = lire_faits(client_id)
    if fait not in faits:
        faits.append(fait)
    (MEMOIRE / f"{client_id}.json").write_text(json.dumps(faits, ensure_ascii=False))

Qui alimente cette mémoire ? Le code (après chaque commande, on enregistre l'adresse), plus prévisible, ou le modèle via un outil memoriser(fait), plus souple.

Où doit vivre le panier en cours d'un client (références et quantités) ?

Exercice : relier la mémoire long terme à la conversation

Modifiez Conversation.__init__ pour qu'il prenne un client_id, lise ses faits et les ajoute au prompt système sous la forme « Ce que l'on sait du client : … ». Puis écrivez oublier(client_id).

Voir la solution
memoire.py (solution)
class Conversation:
    def __init__(self, client_id, llm=call_llm):
        self.llm = llm
        faits = lire_faits(client_id)
        system = SYSTEM
        if faits:
            system += "\nCe que l'on sait du client : " + " ; ".join(faits)
        self.messages = [{"role": "system", "content": system}]

    # demander() est inchangée


def oublier(client_id):
    (MEMOIRE / f"{client_id}.json").unlink(missing_ok=True)

Si vous laissez le modèle mémoriser lui-même via un outil memoriser(fait), liez cet outil au client_id dans le code (par une fonction définie dans __init__) : le modèle ne doit jamais pouvoir choisir dans quelle mémoire il écrit. C'est ce que fait ToolContext dans ADK (leçon 3) et le projet final.

Je sais faire

  • Expliquer pourquoi la mémoire d'un agent est entièrement gérée par le code
  • Tronquer un historique sans laisser de message tool orphelin
  • Résumer les anciens tours avec le LLM
  • Stocker et injecter des faits long terme, avec une possibilité d'effacement
التالي