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 :
| Dimension | SO | SGR | SGAM |
|---|---|---|---|
| Objectif | Retourner un objet conforme à un schéma | Rendre inspectables les champs visés et l’intention de décision | Gérer la mémoire durable après l’appel du modèle |
| Portée | Une réponse générée | Une réponse de modèle ; la politique inter-étapes peut s’étendre sur plusieurs appels | Enregistrements utilisés entre les appels, les sessions et les exécutions |
| Rôle du schéma | Définit les champs de sortie, les types et les valeurs autorisées | Décrit les preuves, les champs intermédiaires et la décision candidate | Définit les enregistrements stockés, les relations et le cycle de vie |
| Enforcement | Le constrained decoding bloque les sorties non conformes au schéma | Forme uniquement ; les appels séparés, les validateurs et la politique imposent les dépendances et l’ordre | La validation applicative, les contraintes de base de données et les règles de conflit imposent le cycle de vie |
| Durée de vie | Appel courant, sauf si l’application stocke l’objet | La trace de raisonnement est généralement supprimée après la décision | Persiste jusqu’à sa mise à jour, son expiration ou sa suppression |
| Mode d’échec | Forme valide, mais signification incorrecte | Les 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 :
- Capturer un épisode brut à partir de messages, de tool results ou d’événements métier.
- Extraire des candidats typés via structured output.
- Valider le schéma et rejeter les écritures mal formées.
- Réconcilier les conflits, clôturer les faits obsolètes et conserver la provenance.
- Valider l’enregistrement dans le store SGAM.
Le flux de requête est le read path :
- Commencer par la question de l’utilisateur.
- Déterminer si la question nécessite l’état actuel ou l’état à une date donnée.
- Filtrer par tenant, type de mémoire, sujet, attribut et fenêtre de validité.
- Ajouter une expansion vectorielle ou par graphe uniquement si la recherche exacte de l’état ne suffit pas.
- Constituer le contexte cité le plus réduit possible pour le modèle.
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 framework | Couche de stockage principale | Mécanisme d’état temporel | Mécanisme de schéma | Cas d’usage pratique |
|---|---|---|---|---|
| Graphiti | Neo4j, FalkorDB et Amazon Neptune ; Kuzu est obsolète | Intervalles de validité des faits et provenance des épisodes sources | Types d’entités et d’arêtes Pydantic, arêtes temporelles, provenance | Mémoire temporelle auto-hébergée sur graphe |
| Zep | Context Graph Engine propriétaire | Context Graphs temporels gérés qui conservent les faits évolutifs | Types d’entités et d’arêtes personnalisés et types par défaut | Mémoire d’agent gérée et gouvernance |
| LangGraph / LangMem | Stores LangGraph, stores adossés à Postgres | Horodatages et champs gérés par l’application dans les enregistrements du store | Stores JSON et extraction de profils ou collections avec Pydantic | Applications agent déjà construites sur LangGraph |
| Mem0 | Stack gérée, Valkey / Redis / backends vectoriels dans les configurations OSS | Mises à jour de mémoire ; politique temporelle gérée par l’application | Types de mémoire, catégories personnalisées, prompts d’extraction | Mémoire utilisateur, agent et session en tant que service |
| Letta / MemGPT | État et blocs mémoire d’agent adossés à une base de données | Blocs éditables sans intervalles de validité au niveau des champs | Blocs mémoire éditables et étiquetés | Agents avec état et gestion de contexte de type OS |
| Cognee | Backends graphe, vectoriel et relationnel | L’historique dépend de l’ontologie et du backend sélectionné | Extraction et validation orientées ontologie | Mémoire de knowledge graph d’entreprise |
| LlamaIndex property graph | Stores de property graph et stores vectoriels | Les champs temporels dépendent du schéma du graphe et du store | SchemaLLMPathExtractor avec entités et relations autorisées | Extraction 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 :
- Nommer le type de mémoire, le sujet, la portée tenant et la classe de rétention.
- Extraire les enregistrements candidats avec structured output.
- Valider le payload avec Pydantic ou la couche de schéma déjà utilisée par votre stack.
- Résoudre les conflits avant l’insertion, notamment déterminer si le nouvel enregistrement remplace un ancien.
- Conserver un pointeur vers l’épisode brut, le tool result, le fichier, le ticket ou la confirmation utilisateur à l’origine de l’enregistrement.
- É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.
- Filtrer par tenant, type de mémoire et fenêtre de validité.
- Récupérer l’état structuré exact avant les voisins sémantiques.
- 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.
- 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’écriture | L’agent a manqué un fait, en a inventé un ou produit une forme invalide | Taux d’écritures valides selon le schéma, précision/rappel de l’extraction, couverture des épisodes sources |
| Gestion des conflits | Un 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 politique | La 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 lecture | Le 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éponse | La réponse a utilisé la mémoire sans support ou cité la mauvaise source | Support des affirmations par rapport aux épisodes sources, exactitude des citations, exactitude de la résolution des conflits |
| Opérations | Le chemin mémoire est trop lent, trop obsolète ou trop coûteux | Latence 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
- According to Me: Long-Term Personalized Referential Memory QA — article de Mei et al. présentant ATM-Bench et Schema-Guided Memory.
- Towards Scalable Multi-Domain Conversational Agents: The Schema-Guided Dialogue Dataset — article de Rastogi et al. sur le dataset SGD.
- LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory — benchmark de Wu et al. sur les capacités de mémoire à long terme.
- Graphiti: Build Temporal Context Graphs for AI Agents
- Zep: A Temporal Knowledge Graph Architecture for Agent Memory
- Mem0 Platform Overview
- Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory
- LangGraph Memory Overview
- LangGraph Persistence
- LangMem documentation
- Letta: Introduction to Stateful Agents
- Microsoft GraphRAG documentation
- LlamaIndex: Using a Property Graph Index
- Cognee Documentation
- Pydantic model validation docs
- Python sqlite3 documentation