Mémoire des AI agents : état guidé par schéma et provenance

Traduction automatique Cet article a été traduit automatiquement depuis la version originale en anglais.

Les agents qui s’exécutent sur de longues durées récupèrent souvent des faits obsolètes, car la mémoire sémantique classique ne fournit aucune règle pour déterminer quelle valeur est actuelle.

Lorsqu’un utilisateur modifie une échéance de passeport, en la faisant passer du 15 juillet au 30 juin, la recherche vectorielle peut récupérer les deux déclarations. Une couche de mémoire avec état doit enregistrer que la valeur du 30 juin remplace la précédente.

Pour les ingénieurs qui conçoivent des agents persistants ou multi-tenant, cet échec lié aux faits obsolètes explique pourquoi la mémoire doit survivre à des exécutions distinctes, tout en garantissant l’état actuel et les frontières entre tenants. La conception ci-dessous montre comment écrire des enregistrements typés et tester les lectures de l’état actuel et de l’état à une date donnée, sans considérer le rappel vectoriel comme la source de vérité.

En bref : traitez la mémoire durable d’un agent comme un état applicatif typé. Extrayez les candidats à mémoriser via une frontière de structured output. Stockez les enregistrements avec leur portée tenant, leurs fenêtres de validité, leur supersession, leur provenance et leur version de schéma, puis récupérez la plus petite tranche d’état actuel sur le read path. Réservez la recherche vectorielle au rappel approximatif. Lisez les faits mutables à partir d’enregistrements bornés et actuels.


Le piège de la fenêtre de contexte

La fenêtre de contexte correspond aux entrées disponibles pour un appel de modèle. Une application peut transmettre des messages lors d’appels ultérieurs, mais sa politique applicative doit déterminer quels faits anciens restent vrais et qui peut les consulter.

Les agents persistants doivent se souvenir des préférences, de l’état des tâches, des informations client, des décisions prises via des outils, des notes de conformité et des erreurs précédentes. La solution simple consiste à ajouter des résumés ou à déverser d’anciennes notes dans un vector store. Cela fonctionne jusqu’à ce qu’un fait mémorisé change.

L’agent dispose alors de deux échéances de passeport, de deux formats préférés ou de deux décisions de projet. La recherche sémantique peut récupérer les deux. Un résumé peut écraser l’un d’eux. Un contexte long peut placer l’ancien fait à côté du fait actif. Ces conceptions rappellent du texte sans imposer la valeur actuelle.

Le contrat de mémoire doit répondre à des questions concrètes :

  • Qu’est-ce qui est vrai aujourd’hui ?
  • Qu’est-ce qui était vrai le 2 juin ?
  • Qui l’a déclaré ?
  • À quel tenant cela appartient-il ?
  • Quel fait plus ancien ce fait a-t-il remplacé ?
  • Puis-je le supprimer ou le faire expirer ?

Schema-Guided Agent Memory (SGAM) stocke ces réponses sous forme de champs et de relations, au lieu de les laisser implicites dans la prose.


Ce que signifie SGAM

Trois notions aux noms similaires permettent de préciser la portée de SGAM.

Schema-Guided Dialogue (SGD) est le dataset de dialogue orienté tâche de Google publié en 2019. Son schéma décrit les service APIs, les intents et les slots afin qu’un modèle de dialogue puisse suivre l’état de services qu’il n’a pas vus auparavant. Il constitue un précédent utile pour le suivi fondé sur un schéma, mais sa portée se limite aux services de dialogue.

Schema-Guided Memory (SGM) est le terme de recherche utilisé par Mei et al. dans According to Me: Long-Term Personalized Referential Memory QA. L’article compare la Descriptive Memory (DM) en texte libre à une mémoire composée d’éléments clé-valeur au schéma fixe. Les deux représentations contiennent les mêmes informations sources, mais sous des structures différentes.

Dans cet article, j’utilise Schema-Guided Agent Memory (SGAM) pour désigner un pattern d’ingénierie dans lequel les schémas gouvernent les écritures, les mises à jour, la récupération et la suppression. Le schéma définit l’état applicatif et son cycle de vie.

ATM-Bench montre pourquoi la représentation est importante. Il utilise environ quatre années de données personnelles issues d’e-mails, d’images et de vidéos. Les questions exigent des références personnelles, une localisation, plusieurs éléments de preuve et la prise en compte des mises à jour au fil du temps. Sur son split difficile, l’article rapporte que SGM surpasse DM pour la récupération et le question answering dans la configuration testée. SGM expose des champs tels que le temps, la source, la localisation, les entités et les tags dans une représentation fixe. Je considère cette représentation et les résultats rapportés comme la conclusion étayée ; l’article n’établit pas de mécanisme direct d’adressage des champs.

SGM contre DM répond à une question de stockage : la mémoire doit-elle rester en texte libre ou utiliser des champs nommés ? Un agent de production rencontre un autre problème avant le stockage. Il doit transformer une conversation non structurée en mise à jour de mémoire proposée. Schema-Guided Reasoning (SGR) rend ce chemin de décision intentionnel inspectable : la réponse peut inclure des éléments de preuve, le sujet et l’attribut, une comparaison avec l’état actuel et une écriture candidate. La réponse du modèle n’impose ni les dépendances ni l’ordre entre ces champs. Des appels séparés, des validateurs et la politique applicative imposent ces règles ; SGAM applique les règles de stockage et de cycle de vie après l’appel du modèle.


Séparer l’extraction par le modèle de la responsabilité de la mémoire

L’écriture en mémoire traverse trois couches. Structured Output (SO) impose la forme de l’objet candidat. Schema-Guided Reasoning (SGR) rend inspectables les champs visés par le modèle et son intention de décision dans une réponse structurée. Schema-Guided Agent Memory (SGAM) gère le candidat comme un état durable après l’appel du modèle. Des appels séparés, des validateurs et la politique applicative imposent les dépendances, l’ordre et le cycle de vie.

Un verdict, un routage ou un plan expire généralement avec la requête actuelle. Une autre exécution peut lire un candidat mémorisé plusieurs jours plus tard ou l’utiliser pour choisir un tool call. Cette durée de vie plus longue exige des règles de stockage que SGR ne fournit pas.

SGR structure un appel de modèle autour d’une topologie de raisonnement visée. Pour une écriture en mémoire, la réponse peut inclure les éléments de preuve sources, un sujet et un attribut normalisés, une comparaison avec l’état actuel et une mise à jour proposée. Pydantic ou JSON Schema décrit ces champs. Le Structured Output natif du fournisseur ou un runtime de guided decoding tel que XGrammar maintient la réponse dans cette forme.

Cette forme rend inspectables les champs déclarés par le modèle et son intention. Elle ne garantit pas une conclusion correcte et ne prouve pas que le modèle a utilisé ces champs dans l’ordre. Les appels séparés, les validateurs et la politique applicative constituent la frontière d’application des dépendances, de l’ordre et du cycle de vie.

SGAM décide de ce qui se passe une fois cet objet créé. Faut-il le stocker ? Remplace-t-il un fait plus ancien ? Quel tenant peut le consulter ? Est-il actuel ou historique ? De quelle source l’épisode d’origine provient-il ?

Le tableau précise la responsabilité et le mode d’échec de chaque couche :

DimensionSOSGRSGAM
ObjectifRetourner un objet conforme à un schémaRendre inspectables les champs visés et l’intention de décisionGérer la mémoire durable après l’appel du modèle
PortéeUne réponse généréeUne réponse de modèle ; la politique inter-étapes peut s’étendre sur plusieurs appelsEnregistrements utilisés entre les appels, les sessions et les exécutions
Rôle du schémaDéfinit les champs de sortie, les types et les valeurs autoriséesDécrit les preuves, les champs intermédiaires et la décision candidateDéfinit les enregistrements stockés, les relations et le cycle de vie
EnforcementLe constrained decoding bloque les sorties non conformes au schémaForme uniquement ; les appels séparés, les validateurs et la politique imposent les dépendances et l’ordreLa validation applicative, les contraintes de base de données et les règles de conflit imposent le cycle de vie
Durée de vieAppel courant, sauf si l’application stocke l’objetLa trace de raisonnement est généralement supprimée après la décisionPersiste jusqu’à sa mise à jour, son expiration ou sa suppression
Mode d’échecForme valide, mais signification incorrecteLes champs sont présents, mais le raisonnement ou la gestion des dépendances peut rester incorrectÉtat obsolète, pollué, non borné ou impossible à auditer

Sur le write path, la séquence est :

SGR reasoning schema + Structured Output -> candidate write -> SGAM policy and persistence

L’extrait illustratif suivant de memory_models.py définit l’objet transmis du module d’extraction au service d’écriture SGAM. Ce bloc nécessite Pydantic ; il est donc marqué no-run dans les vérifications d’exemple pour les environnements qui n’installent pas cette dépendance optionnelle :

from datetime import datetime
from pydantic import BaseModel, Field

class MemoryDelta(BaseModel):
    tenant_id: str = Field(description="Isolation boundary, e.g. acme")
    subject: str = Field(description="Normalized entity ID, e.g. mira")
    attribute: str = Field(description="Property being updated")
    value: str = Field(description="New value")
    valid_from: datetime
    source_episode_id: str

MemoryDelta capture ce que le modèle a extrait. Le service d’écriture SGAM décide toujours de le rejeter, de le fusionner ou de le stocker.


Les write paths et read paths ont des rôles différents

Seul le write path modifie l’état stocké. Le read path sélectionne les enregistrements pour la requête actuelle.

Le flux d’ingestion est le write path :

  1. Capturer un épisode brut à partir de messages, de tool results ou d’événements métier.
  2. Extraire des candidats typés via structured output.
  3. Valider le schéma et rejeter les écritures mal formées.
  4. Réconcilier les conflits, clôturer les faits obsolètes et conserver la provenance.
  5. Valider l’enregistrement dans le store SGAM.

Le flux de requête est le read path :

  1. Commencer par la question de l’utilisateur.
  2. Déterminer si la question nécessite l’état actuel ou l’état à une date donnée.
  3. Filtrer par tenant, type de mémoire, sujet, attribut et fenêtre de validité.
  4. Ajouter une expansion vectorielle ou par graphe uniquement si la recherche exacte de l’état ne suffit pas.
  5. Constituer le contexte cité le plus réduit possible pour le modèle.

Architecture de Schema-Guided Agent MemoryArchitecture de Schema-Guided Agent Memory

Lisez ce diagramme de gauche à droite, selon deux voies. La voie supérieure écrit la mémoire et la voie inférieure la lit. Les deux utilisent le même store.


Ce qui doit figurer dans un schéma de mémoire

Un enregistrement SGAM minimal doit contenir davantage que text.

tenant_id
memory_id
subject
attribute
value
memory_type
schema_version
valid_from
valid_to
supersedes_memory_id
source_episode_id
confidence
retention_policy

Avec ces champs, une nouvelle échéance de passeport peut clôturer la précédente sans effacer l’historique. La même table peut répondre aux requêtes sur l’état actuel et l’état à une date donnée, puis remonter jusqu’à l’épisode source. schema_version prend en charge les migrations, tandis que retention_policy indique aux jobs de suppression ce qu’ils doivent également supprimer.

Utilisez RAG pour récupérer des documents et SGAM pour maintenir un état mutable. La recherche vectorielle reste utile dans le système pour le rappel approximatif, le clustering et l’expansion. La valeur actuelle de mira.passport_deadline doit provenir d’un enregistrement mémoire borné, et non du chunk arrivé en tête du classement.


Exemple de fait obsolète

Considérons une trace synthétique composée de deux épisodes, représentée sous la forme d’une baseline DM et d’un registre SGAM :

e1: Mira prefers concise answers. Her passport deadline is 2026-07-15.
e2: Mira corrected the deadline. It is now 2026-06-30.

Une baseline DM conserve les deux épisodes sous forme de texte libre ; la recherche textuelle peut donc retourner e1 parce qu’il contient les bons mots. SGAM extrait un fait typé de chaque épisode, indexé par tenant, sujet et attribut. Il doit retourner e2 comme état actuel et conserver e1 pour une requête historique.

Le write path SQLite autonome suivant utilise des chaînes UTC au format ISO-8601 pour les horodatages, configure sqlite3.Row avant la lecture par nom de colonne et représente les faits sous forme d’intervalles semi-ouverts : [valid_from, valid_to). La table rejette les intervalles invalides et possède un index unique partiel autorisant un seul fait ouvert par tenant, sujet et attribut. replace_fact gère une transaction unique ; SQLite sérialise les écritures concurrentes, les appelants doivent donc relancer toute la transaction après une erreur de verrouillage ou d’unicité.

import sqlite3
from dataclasses import dataclass

@dataclass(frozen=True)
class MemoryFact:
    tenant_id: str
    subject: str
    attribute: str
    value: str
    valid_from: str
    source_episode_id: str

def open_db() -> sqlite3.Connection:
    db = sqlite3.connect(":memory:")
    db.row_factory = sqlite3.Row
    db.executescript(
        """
        create table memory_facts (
            fact_id integer primary key,
            tenant_id text not null,
            subject text not null,
            attribute text not null,
            value text not null,
            valid_from text not null,
            valid_to text,
            source_episode_id text not null,
            check (valid_to is null or valid_from < valid_to)
        );
        create unique index one_open_fact
        on memory_facts (tenant_id, subject, attribute)
        where valid_to is null;
        """
    )
    return db

def replace_fact(db: sqlite3.Connection, fact: MemoryFact) -> None:
    with db:
        exact = db.execute(
            """
            select fact_id
            from memory_facts
            where tenant_id = ? and subject = ? and attribute = ? and valid_from = ?
            """,
            (fact.tenant_id, fact.subject, fact.attribute, fact.valid_from),
        ).fetchone()
        if exact:
            raise ValueError("equal valid_from requires an application conflict policy")

        containing = db.execute(
            """
            select fact_id, valid_to
            from memory_facts
            where tenant_id = ?
              and subject = ?
              and attribute = ?
              and valid_from < ?
              and (valid_to is null or valid_to > ?)
            limit 1
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.valid_from,
                fact.valid_from,
            ),
        ).fetchone()
        if containing:
            db.execute(
                "update memory_facts set valid_to = ? where fact_id = ?",
                (fact.valid_from, containing["fact_id"]),
            )
            successor_boundary = containing["valid_to"]
        else:
            successor = db.execute(
                """
                select valid_from
                from memory_facts
                where tenant_id = ? and subject = ? and attribute = ? and valid_from > ?
                order by valid_from
                limit 1
                """,
                (fact.tenant_id, fact.subject, fact.attribute, fact.valid_from),
            ).fetchone()
            successor_boundary = successor["valid_from"] if successor else None

        db.execute(
            """
            insert into memory_facts
                (tenant_id, subject, attribute, value, valid_from, valid_to, source_episode_id)
            values (?, ?, ?, ?, ?, ?, ?)
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.value,
                fact.valid_from,
                successor_boundary,
                fact.source_episode_id,
            ),
        )

def fact_at(db: sqlite3.Connection, timestamp: str) -> sqlite3.Row:
    return db.execute(
        """
        select value, source_episode_id
        from memory_facts
        where tenant_id = ?
          and subject = ?
          and attribute = ?
          and valid_from <= ?
          and (valid_to is null or valid_to > ?)
        limit 1
        """,
        ("acme", "mira", "passport_deadline", timestamp, timestamp),
    ).fetchone()

db = open_db()
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-07-15", "2026-06-01T09:00:00Z", "e1"),
)
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-06-30", "2026-06-03T10:00:00Z", "e2"),
)
current = fact_at(db, "2026-06-04T00:00:00Z")
assert (current["value"], current["source_episode_id"]) == ("2026-06-30", "e2")

historical = fact_at(db, "2026-06-02T00:00:00Z")
assert (historical["value"], historical["source_episode_id"]) == ("2026-07-15", "e1")

# A delayed extraction predates e1. It ends at e1's existing boundary,
# rather than closing e2, which remains current.
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-08-01", "2026-05-30T08:00:00Z", "e0"),
)
delayed = fact_at(db, "2026-05-31T00:00:00Z")
assert (delayed["value"], delayed["source_episode_id"]) == ("2026-08-01", "e0")
current = fact_at(db, "2026-06-04T00:00:00Z")
assert (current["value"], current["source_episode_id"]) == ("2026-06-30", "e2")
intervals = db.execute("select valid_from, valid_to from memory_facts").fetchall()
assert all(row["valid_to"] is None or row["valid_from"] < row["valid_to"] for row in intervals)

episodes = (
    "e1: Mira prefers concise answers. Her passport deadline is 2026-07-15.",
    "e2: Mira corrected the deadline. It is now 2026-06-30.",
)
naive_match = next(episode for episode in episodes if "passport deadline" in episode)
assert naive_match.startswith("e1:")

replace_fact rejette d’abord un valid_from identique : ce conflit nécessite une politique applicative, telle qu’une priorité entre sources ou une révision confirmée par l’utilisateur, plutôt qu’un écrasement silencieux. Il recherche ensuite l’intervalle contenant l’horodatage entrant. S’il en trouve un, il clôture ce prédécesseur à la nouvelle limite et attribue à la ligne insérée la limite de fin précédente du prédécesseur. Si l’horodatage précède tous les intervalles stockés, il utilise le valid_from de l’intervalle suivant comme limite de fin de la nouvelle ligne. Ainsi, un fait retardé antérieur à e1 devient [2026-05-30, 2026-06-01) et laisse e1 ainsi que e2 actuels intacts. La row factory rend row["fact_id"] et les champs de résultat nommés valides sur la connexion par défaut créée ici. L’index unique partiel constitue la garde-fou de la base de données pour une seule ligne ouverte ; un déploiement multi-writer doit choisir une base de données et une politique de retry adaptées à sa charge. DM ne possède aucune étape de mise à jour équivalente : l’ancien texte peut donc toujours être mieux classé que la correction.

Les assertions vérifient la ligne actuelle e2, la ligne historique e1, l’insertion retardée avant e1, l’invariant d’intervalle et le premier épisode textuel correspondant avec une approche naïve :

Naive text memory:
  returned episode: e1 -> passport deadline is 2026-07-15

SGAM current state:
  mira.passport_deadline = 2026-06-30
  valid_from=2026-06-03T10:00:00Z, source=e2

SGAM point-in-time state:
  on 2026-06-02, mira.passport_deadline = 2026-07-15

En production, associez cette transaction à une extraction structurée sur le write path. La transaction de base de données met à jour la validité temporelle. Le modèle extrait un fait candidat, mais ne décide pas quelle ligne stockée reste actuelle.


Les choix de stockage dépendent du pattern de récupération

Les projets utilisent plusieurs appellations pour les composants de ce pattern : memory stores, context graphs, profils, long-term stores, graph RAG et agents avec état.

Outil ou frameworkCouche de stockage principaleMécanisme d’état temporelMécanisme de schémaCas d’usage pratique
GraphitiNeo4j, FalkorDB et Amazon Neptune ; Kuzu est obsolèteIntervalles de validité des faits et provenance des épisodes sourcesTypes d’entités et d’arêtes Pydantic, arêtes temporelles, provenanceMémoire temporelle auto-hébergée sur graphe
ZepContext Graph Engine propriétaireContext Graphs temporels gérés qui conservent les faits évolutifsTypes d’entités et d’arêtes personnalisés et types par défautMémoire d’agent gérée et gouvernance
LangGraph / LangMemStores LangGraph, stores adossés à PostgresHorodatages et champs gérés par l’application dans les enregistrements du storeStores JSON et extraction de profils ou collections avec PydanticApplications agent déjà construites sur LangGraph
Mem0Stack gérée, Valkey / Redis / backends vectoriels dans les configurations OSSMises à jour de mémoire ; politique temporelle gérée par l’applicationTypes de mémoire, catégories personnalisées, prompts d’extractionMémoire utilisateur, agent et session en tant que service
Letta / MemGPTÉtat et blocs mémoire d’agent adossés à une base de donnéesBlocs éditables sans intervalles de validité au niveau des champsBlocs mémoire éditables et étiquetésAgents avec état et gestion de contexte de type OS
CogneeBackends graphe, vectoriel et relationnelL’historique dépend de l’ontologie et du backend sélectionnéExtraction et validation orientées ontologieMémoire de knowledge graph d’entreprise
LlamaIndex property graphStores de property graph et stores vectorielsLes champs temporels dépendent du schéma du graphe et du storeSchemaLLMPathExtractor avec entités et relations autoriséesExtraction de graphe sur documents et traces

Graphiti est une implémentation open source concrète d’une mémoire relationnelle et temporelle. Il suit l’évolution des faits, conserve des pointeurs vers les épisodes sources et prend en charge la récupération hybride. LangGraph sépare les checkpoints de thread des stores inter-threads. Mem0 fournit les opérations de mémoire sous forme de service géré. Letta utilise des blocs de contexte éditables plutôt que SGAM au niveau des champs, mais traite lui aussi l’état de l’agent comme des données persistantes.

Commencez par le modèle de données. Si la recherche exacte de faits constitue l’opération principale, une table relationnelle avec des payloads JSON, des colonnes de validité, des index tenant et un sidecar vectoriel suffit généralement. Ajoutez un graphe lorsque le produit doit parcourir des relations, et non parce que la démo du graphe est impressionnante.


Construire le write path avant le graphe

Commencez par décider ce que le produit est autorisé à mémoriser. Le choix entre graphe et vecteur vient ensuite.

Un agent de support peut mémoriser le niveau de compte, les tickets ouverts et les préférences de contact durables. Il ne doit pas transformer chaque remarque agacée en état de profil. Un agent de programmation peut mémoriser les conventions d’un dépôt et les tâches non résolues. Il ne doit pas conserver indéfiniment une note privée simplement parce qu’elle a été récupérée une fois.

Commencez par le write path et traitez la mémoire comme une petite mutation d’état :

  1. Nommer le type de mémoire, le sujet, la portée tenant et la classe de rétention.
  2. Extraire les enregistrements candidats avec structured output.
  3. Valider le payload avec Pydantic ou la couche de schéma déjà utilisée par votre stack.
  4. Résoudre les conflits avant l’insertion, notamment déterminer si le nouvel enregistrement remplace un ancien.
  5. Conserver un pointeur vers l’épisode brut, le tool result, le fichier, le ticket ou la confirmation utilisateur à l’origine de l’enregistrement.
  6. Écrire la version du schéma avec chaque enregistrement au lieu de la laisser uniquement dans le code applicatif.

Le premier store SGAM peut être une table relationnelle avec une colonne JSON et quelques index. Un graphe devient utile lorsque le produit doit parcourir des relations telles que client-vers-compte, compte-vers-politique, tâche-vers-artifact ou projet-vers-décision.

Hot path et écritures en arrière-plan

L’extraction immédiate est utile lorsque le tour suivant dépend de la nouvelle mémoire. Si l’utilisateur dit « souviens-toi que je préfère les réponses courtes », le système ne devrait pas attendre un job nocturne avant de modifier son comportement.

La plupart des tours ne nécessitent pas d’écriture immédiate. Enregistrez l’épisode brut avec les métadonnées du tenant, de la session et des outils, puis laissez un worker en arrière-plan extraire les candidats ultérieurement. Avec une consolidation fondée sur la récurrence, le worker met en tampon les signaux faibles et ne promeut un fait qu’après la répétition d’éléments similaires ou la confirmation de l’utilisateur. Cela ajoute un délai de fraîcheur. C’est acceptable pour « l’utilisateur demande souvent des exports CSV », mais risqué pour « le client a modifié l’adresse de livraison ».

Gardez le read path déterministe. Appliquez d’abord la portée tenant et la validité, puis utilisez la récupération approximative uniquement lorsqu’elle peut apporter un contexte utile.

  1. Filtrer par tenant, type de mémoire et fenêtre de validité.
  2. Récupérer l’état structuré exact avant les voisins sémantiques.
  3. Utiliser l’expansion vectorielle ou par graphe pour les éléments de preuve complémentaires, les entités associées et les exemples, et non comme autorité pour les faits actuels.
  4. Constituer le contexte cité le plus réduit possible pour répondre à la question.

Traitez la migration de schéma comme une évolution produit, car elle modifie ce que l’agent peut rappeler, citer ou supprimer. Elle peut également modifier les faits historiques considérés comme actuels. Planifiez les scripts de migration, les backfills, les périodes de double lecture et le comportement de suppression dans la même release.


Quand SGAM justifie sa complexité

Utilisez SGAM lorsque les faits peuvent évoluer dans le temps :

  • préférences utilisateur susceptibles d’être mises à jour ou révoquées
  • informations client ou compte soumises à des exigences d’audit
  • état de tâches pour des assistants persistants
  • mémoire de projet pour un agent de programmation
  • état partagé entre plusieurs agents
  • notes de conformité pour lesquelles la provenance est importante
  • questions temporelles telles que « que pensions-nous avant la migration ? »

SGAM est excessif lorsque la mémoire est éphémère, exploratoire ou peu coûteuse à recalculer. Si l’agent n’a besoin que de quelques tours de continuité, un checkpoint et un historique de messages réduit suffisent. Le question answering sur des documents statiques peut ne nécessiter que RAG. Et si le domaine est tellement instable que le schéma change chaque jour, la mémoire typée ralentira l’équipe.


Checklist d’évaluation

Évaluez le cycle de vie de la mémoire autant que la réponse finale. Un système peut produire une réponse plausible après avoir écrit le mauvais fait, récupéré un fait obsolète ou franchi une frontière tenant.

J’utilise le même découpage étape par étape que dans mon article sur l’évaluation de RAG. Mesurez l’étape où un échec peut se produire plutôt que de limiter l’évaluation au texte généré. La discipline de traçage présentée dans l’article sur l’évaluation des agents s’applique également, car un bug de mémoire apparaît souvent dans l’historique de l’exécution avant d’atteindre la réponse.

Je testerais SGAM avec du replay. Fournissez une séquence fixe d’épisodes au writer de mémoire et inspectez le registre après chaque tour significatif. Posez ensuite des questions sur l’état actuel et l’état à une date donnée dans le store obtenu.

CoucheÉchec recherchéMesures
Extraction à l’écritureL’agent a manqué un fait, en a inventé un ou produit une forme invalideTaux d’écritures valides selon le schéma, précision/rappel de l’extraction, couverture des épisodes sources
Gestion des conflitsUn fait obsolète est resté actuel ou un ancien fait valide a été écraséExactitude de la supersession, taux de doublons, exactitude de l’invalidation des faits obsolètes
Isolation et politiqueLa mémoire a fuité entre utilisateurs ou a persisté au-delà de sa fenêtre de politiqueÉchecs d’isolation tenant, exactitude des suppressions, conformité de la rétention
Récupération à la lectureLe bon enregistrement existe, mais le lecteur ne l’a pas récupéréExactitude de l’état actuel, exactitude à une date donnée, recall@k sur les enregistrements mémoire
Ancrage de la réponseLa réponse a utilisé la mémoire sans support ou cité la mauvaise sourceSupport des affirmations par rapport aux épisodes sources, exactitude des citations, exactitude de la résolution des conflits
OpérationsLe chemin mémoire est trop lent, trop obsolète ou trop coûteuxLatence d’écriture p95, délai de fraîcheur, latence de lecture, coût par requête

Des benchmarks tels que LoCoMo, LongMemEval et ATM-Bench fournissent des cas de test publics. Ils ne remplacent pas une suite de tests métier. Un assistant de programmation, un bot de support client et un copilote de conformité nécessitent des schémas, des filtres, des règles de rétention et des tests d’échec différents.


Réserves

SGAM est le nom que je donne à un pattern, pas un standard. Les projets existants répartissent le problème différemment. La mémoire de LangGraph et LangMem décrivent les stores court et long terme, les profils, les collections, les écritures sur le hot path et les gestionnaires de mémoire en arrière-plan. Zep Graphiti utilise le terme Context Graph temporel. Letta conserve des blocs mémoire éditables, tandis que Mem0 propose une couche de mémoire gérée. Microsoft GraphRAG, les property graphs de LlamaIndex et Cognee présentent des composants connexes du problème comme des knowledge graphs.

Un profil utilisateur, un journal d’épisodes, un graphe documentaire et un bloc mémoire éditable par l’agent répondent à des problèmes différents de récupération et de mise à jour. Je réserve SGAM à une mémoire durable qui représente l’état applicatif actuel et nécessite donc un schéma, une validité, une provenance, une gestion des conflits, une rétention et des migrations.

La mémoire typée peut malgré tout être erronée. Un schéma rend les mauvaises écritures plus faciles à inspecter ; il ne les rend pas fiables. Vous avez toujours besoin de sources fiables, de confirmations utilisateur pour les faits sensibles, d’une politique de conflit, de suppression et de supervision.

La migration de schéma demande du travail. Dès que la mémoire devient un état, vous êtes responsable du versioning, des backfills, des anciens enregistrements et du comportement de suppression. Si vous négligez ce travail, les anciens enregistrements survivront à la sémantique ou à la politique de rétention qui les a créés.


Références