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

Mémoire long terme : extraire, mettre à jour, oublier

7 min de lectura

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

Mémoire long terme : extraire, mettre à jour, oublier

Pourquoi des faits, et pas l'historique ?

Le résumé glissant vit le temps d'une conversation. La mémoire long terme survit d'une session à l'autre : c'est ce qui permet à l'assistant de savoir, lundi, ce que Camille lui a dit vendredi. Stocker tout l'historique serait cher à relire et plein de bruit (« merci », « tu peux reformuler ? »). On stocke plutôt des faits : des phrases courtes, autonomes, durables.

Message de CamilleFait retenuPourquoi
« Je suis allergique aux arachides, pense-y si tu me proposes un traiteur un jour. »L'utilisateur est allergique aux arachides.contrainte durable, importante
« Tu peux me rappeler ce qu'est un webhook ? »(rien)demande ponctuelle
« J'ai quitté l'équipe Paiements, je suis maintenant dans l'équipe Logistique. »remplace « … équipe Paiements »mise à jour d'un fait existant
sequenceDiagram
  participant U as "Camille"
  participant A as "Assistant"
  participant DB as "SQLite"
  participant X as "LLM d'extraction"
  U->>A: "Je suis maintenant dans l'équipe Logistique"
  A->>DB: "recall(message, k=4)"
  DB-->>A: "7 : équipe Paiements, 1 : Camille..."
  A->>X: "message + faits connus proches (id : texte)"
  X-->>A: "JSON : ajouter, remplacer, supprimer"
  A->>DB: "apply_operations (ids montrés uniquement)"

Le schéma des opérations

files/memoire.py
class Remplacement(BaseModel):
    id: int
    texte: str


class Operations(BaseModel):
    ajouter: list[str]
    remplacer: list[Remplacement]
    supprimer: list[int]

La consigne d'extraction (memoire.py, EXTRACT_PROMPT) précise quoi retenir (identité, préférences, contraintes, contexte de travail, outils), quoi ignorer (demandes ponctuelles, politesses) et comment formuler (« L'utilisateur … », une phrase autonome). Surtout, elle reçoit les faits connus proches du message, avec leur identifiant : c'est ce qui permet de mettre à jour au lieu de dupliquer.

Appliquer sans faire confiance aveuglément

files/memoire.py
def apply_operations(mem, user, ops, allowed_ids):
    """N'applique un remplacement/une suppression qu'aux faits montrés au LLM (allowed_ids)."""
    for r in ops.remplacer:
        if r.id in allowed_ids:
            mem.replace(user, r.id, r.texte)
    for i in ops.supprimer:
        if i in allowed_ids:
            mem.delete(user, i)
    for t in ops.ajouter:
        mem.add(user, t)

Deux protections :

  1. allowed_ids : le LLM ne peut toucher qu'aux faits qu'on lui a montrés. Un identifiant halluciné est ignoré.
  2. WHERE user_id=? dans chaque requête SQL de Memory : même avec un identifiant valide, on ne modifie jamais le fait d'un autre utilisateur.

L'extracteur renvoie remplacer id=42 alors que seuls les faits 7 et 1 lui ont été montrés. Que se passe-t-il ?

Résultat obtenu

Session 1 réelle du 2026-10-15 (gemini-3.5-flash-lite pour l'extraction), extrait de python eval_rappel.py :

[camille] "Bonjour ! Je m'appelle Camille, je viens d'arriver chez Atel" -> {'ajoutés': ["L'utilisateur s'appelle Camille.", "L'utilisateur vient d'arriver chez Atelier."], ...}
[camille] 'Petite info utile : je suis allergique aux arachides, pense-' -> {'ajoutés': ["L'utilisateur est allergique aux arachides."], ...}
[camille] "J'habite à Nantes et je viens au bureau en vélo." -> {'ajoutés': ["L'utilisateur habite à Nantes.", "L'utilisateur vient au bureau en vélo."], ...}
[camille] "Tu peux me rappeler ce qu'est un webhook en une phrase ?" -> {'ajoutés': [], 'remplacés': [], 'supprimés': []}
[camille] "Ah, et changement depuis lundi : j'ai quitté l'équipe Paieme" -> {'ajoutés': ["L'utilisateur fait partie de l'équipe Logistique."], 'remplacés': [], 'supprimés': [7]}

La question ponctuelle sur les webhooks n'a rien ajouté. Pour le changement d'équipe, le modèle a choisi « supprimer 7 + ajouter » plutôt que « remplacer 7 » : le résultat est le même (une seule équipe en mémoire, la bonne). Un message contenant deux informations (« Nantes » et « vélo ») a donné deux faits séparés, ce qui permet de les rappeler indépendamment.

Même session sur Vertex AI, extraction par gemini-2.5-flash-lite :

[camille] "Je ne suis jamais disponible le mercredi, c'est mon jour de " -> {'ajoutés': [], 'remplacés': ["L'utilisateur n'est jamais disponible le mercredi."], 'supprimés': []}
[camille] "Ah, et changement depuis lundi : j'ai quitté l'équipe Paieme" -> {'ajoutés': [], 'remplacés': ["L'utilisateur fait partie de l'équipe Logistique."], 'supprimés': []}
faits stockés pour camille : [..., "L'utilisateur habite à Nantes.", "L'utilisateur n'est jamais disponible le mercredi.", "L'utilisateur fait partie de l'équipe Logistique.", ...]

Le changement d'équipe est correct (« remplacer » cette fois). Mais le message sur le mercredi a produit un remplacement au lieu d'un ajout : le modèle a écrasé un fait voisin qu'on lui avait montré, « L'utilisateur vient au bureau en vélo. », qui a disparu de la mémoire. allowed_ids n'y pouvait rien : l'identifiant était légitime, c'est la décision qui était fausse.

Ne jamais perdre un fait : le journal

D'où la table facts_history de memoire.py, ajoutée après cette observation : chaque remplacement ou suppression archive d'abord l'ancienne version.

files/memoire.py
def _archive(self, user: str, fact_id: int, op: str):
    self.db.execute("INSERT INTO facts_history SELECT id, user_id, text, ?, ? FROM facts WHERE id=? AND user_id=?",
                    (op, time.time(), fact_id, user))

def replace(self, user: str, fact_id: int, text: str):
    self._archive(user, fact_id, "remplacer")
    self.db.execute("UPDATE facts SET text=?, vec=?, updated_at=? WHERE id=? AND user_id=?", ...)

Le journal ne corrige pas l'erreur, il la rend visible et réparable : on peut auditer mem.history(user), restaurer un fait, et mesurer à quelle fréquence l'extracteur remplace à tort. Un modèle d'extraction plus capable, ou une consigne qui exige que le nouveau fait porte sur le même sujet que l'ancien, réduit le problème sans l'éliminer.

Quels messages devraient produire un fait en mémoire long terme ? (plusieurs réponses)

Exercice : un fait périmé

Camille dit en mars : « Je suis en congé tout le mois d'août. » En septembre, ce fait est toujours en mémoire. Proposez une modification minimale pour que les faits datés ne soient plus rappelés après leur date.

Voir la solution
  1. Ajoutez une colonne expires_at REAL (nullable) à la table facts.
  2. Ajoutez au schéma d'extraction un champ optionnel : ajouter: list[Fait] avec Fait(texte: str, expire_le: str | None) au format ISO ("2026-08-31"), et demandez-le dans la consigne quand le fait a une fin connue.
  3. Dans recall, filtrez : WHERE user_id=? AND (expires_at IS NULL OR expires_at > ?) avec time.time().
  4. Un test : un fait expiré hier n'est pas rappelé, un fait sans date l'est.

C'est l'équivalent d'un TTL (durée de vie) appliqué à la mémoire : oublier fait partie du travail.

Je sais faire

  • Distinguer un fait durable d'une demande ponctuelle
  • Faire produire par un LLM des opérations ajouter / remplacer / supprimer en JSON
  • Limiter les opérations aux faits montrés et à l'utilisateur courant
  • Expliquer pourquoi la mémoire ne doit jamais porter de droits
Siguiente