AI Agent Memory: schemagestuurde state en herkomst

Automatische vertaling

Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

Dit artikel is voor engineers die langlopende of multi-tenant agents bouwen. Eenvoudige semantic memory heeft geen regel voor welke waarde van een feit de huidige is, dus agents geven verouderde feiten terug. Wijzigt een gebruiker een paspoortdeadline van 15 juli naar 30 juni, dan kan vector search beide uitspraken ophalen. Een stateful geheugenlaag moet vastleggen dat de waarde van 30 juni de eerdere vervangt.

Je leert hoe je geheugen opslaat als getypeerde records met geldigheidsperiodes, tenant-scope en bronverwijzingen, hoe je een correctie schrijft zonder de historie te verliezen en hoe je huidige en point-in-time-reads test.


Waarom context en vector-recall verouderde feiten teruggeven

Het context window is de input die beschikbaar is voor één modelaanroep. Een applicatie kan berichten meenemen naar latere aanroepen, maar het applicatiebeleid moet bepalen welke oude feiten nog waar zijn en wie ze mag zien.

Langlopende agents moeten voorkeuren, taakstatus, klantfeiten, tool-beslissingen, compliance-notities en eerdere fouten onthouden. De makkelijke aanpak is samenvattingen toevoegen of oude notities in een vector store gooien. Dat werkt totdat een van de onthouden feiten verandert.

Dan heeft de agent twee paspoortdeadlines, twee voorkeursformaten of twee projectbeslissingen. Semantic search kan beide ophalen. Een samenvatting kan er één overschrijven. Een lange context kan de verouderde naast de actieve bevatten. Deze ontwerpen geven tekst terug, maar kunnen niet zeggen welke waarde actueel is.

Het geheugencontract moet concrete vragen beantwoorden:

  • Wat is nu waar?
  • Wat was waar op 2 juni?
  • Wie zei het?
  • Bij welke tenant hoort het?
  • Welk ouder feit heeft dit feit vervangen?
  • Kan ik het verwijderen of laten verlopen?

Schema-Guided Agent Memory (SGAM) slaat die antwoorden op als velden en relaties in plaats van ze impliciet in proza te laten.


Wat SGAM betekent

Twee onderzoeksideeën met een vergelijkbare naam bepalen de reikwijdte van SGAM.

Schema-Guided Memory (SGM) is de onderzoeksterm van Mei et al. in According to Me: Long-Term Personalized Referential Memory QA. Het paper vergelijkt Descriptive Memory (DM) in vrije tekst met key-value-geheugenitems met een vast schema. Beide representaties bevatten dezelfde broninformatie in een andere structuur. Een eerder gebruik van de naam, de Schema-Guided Dialogue dataset, volgt de dialoogstatus voor service-APIs en staat los van long-term memory.

In dit artikel gebruik ik Schema-Guided Agent Memory (SGAM) voor een engineeringpatroon waarin schema’s het schrijven, bijwerken, ophalen en verwijderen sturen. Het schema definieert de applicatiestate en de levenscyclus ervan.

ATM-Bench laat zien waarom de representatie ertoe doet. Het gebruikt ongeveer vier jaar aan persoonlijke data uit e-mails, afbeeldingen en video’s. De vragen vereisen persoonlijke verwijzingen, locatie, meerdere bewijsstukken en updates in de tijd. SGM legt velden zoals tijd, bron, locatie, entiteiten en tags vast in een vaste representatie. Op de hard split van 25 vragen meldt Tabel 3 van het paper dat de totaalscore (QS) stijgt van 25,6 met DM naar 47,3 met SGM wanneer het antwoordmodel het gold evidence krijgt. Met retrieval in het ATM-RAG-systeem van de auteurs stijgt de score op de hard split slechts van 14,1 naar 16,3, dus het vinden van het bewijs blijft de grootste kloof.

De grafiek zet de vier scores van de hard split naast elkaar. Vergelijk de winst van vaste velden met gold evidence en met retrieval.

ATM-Bench hard-split-scores: met gold evidence DM 25.6 en SGM 47.3; met ATM-RAG-retrieval DM 14.1 en SGM 16.3ATM-Bench hard-split-scores: met gold evidence DM 25.6 en SGM 47.3; met ATM-RAG-retrieval DM 14.1 en SGM 16.3

SGM tegenover DM beantwoordt een opslagvraag: moet geheugen vrije tekst blijven, of benoemde velden gebruiken? Een productie-agent heeft vóór de opslag een ander probleem. Hij moet een ongestructureerd gesprek omzetten in een voorgestelde geheugenupdate. De volgende sectie laat zien hoe Schema-Guided Reasoning (SGR) die modelaanroep vormgeeft en waar SGAM het overneemt.


Scheid modelextractie van geheugeneigenaarschap

Het schrijven van geheugen doorloopt drie lagen. Structured Output (SO) dwingt de vorm van het kandidaatobject af. Schema-Guided Reasoning (SGR) maakt de beoogde velden en de beslissingsintentie van het model inspecteerbaar in een gestructureerd antwoord. Schema-Guided Agent Memory (SGAM) beheert de kandidaat als duurzame state na de modelaanroep. Aparte aanroepen, validators en applicatiebeleid dwingen afhankelijkheden, volgorde en levenscyclusregels af.

Eerdere artikelen passen SGR toe op gestructureerde judges, planners en routers en RAG-rubrics. In elk daarvan vervalt een oordeel, route of plan meestal met het huidige verzoek. Een andere run kan een geheugenkandidaat dagen later lezen of gebruiken om een tool call te kiezen. Die langere levensduur vraagt om opslagregels die SGR niet biedt.

SGR legt de velden van één modelantwoord neer in de volgorde waarin het model ze moet invullen. Bij een geheugenwrite kan het antwoord bronbewijs bevatten, een genormaliseerd onderwerp en attribuut, een vergelijking met de huidige state en een voorgestelde update. Pydantic of JSON Schema beschrijft die velden. Provider-native Structured Output of een guided-decoding-runtime zoals XGrammar houdt het antwoord in die vorm. De vorm kan geen juiste conclusie garanderen en bewijst niet dat het model die velden in volgorde heeft gebruikt.

SGAM beslist wat er gebeurt zodra dat object bestaat. Moet het worden opgeslagen? Vervangt het een ouder feit? Welke tenant mag het zien? Is het actueel of historisch? Welke bronepisode ondersteunt het?

De tabel toont het eigenaarschap en de faalwijze van elke laag:

DimensieSOSGRSGAM
DoelEen object teruggeven dat aan een schema voldoetBeoogde velden en beslissingsintentie inspecteerbaar makenDuurzaam geheugen beheren na de modelaanroep
BereikEén gegenereerd antwoordEén modelantwoord; beleid over meerdere stappen kan meerdere aanroepen beslaanRecords die tussen aanroepen, sessions en runs worden gebruikt
Rol schemaDefinieert outputvelden, typen en toegestane waardenBeschrijft bewijs, tussenvelden en de kandidaatbeslissingDefinieert opgeslagen records, relaties en levenscyclus
AfdwingingConstrained decoding blokkeert output die het schema schendtAlleen de vorm; aparte aanroepen, validators en beleid dwingen afhankelijkheden en volgorde afApplicatievalidatie, databaseconstraints en conflictregels dwingen de levenscyclus af
LevensduurDe huidige aanroep, tenzij de applicatie het object opslaatDe reasoning trace wordt na de beslissing meestal weggegooidBlijft bestaan tot het wordt bijgewerkt, verloopt of wordt verwijderd
FaalwijzeGeldige vorm met de verkeerde betekenisDe velden zijn aanwezig, maar de reasoning of afhankelijkheidsafhandeling kan nog fout zijnVerouderde, vervuilde, niet afgebakende of niet controleerbare state

Het diagram volgt de correctie uit het onderstaande voorbeeld naar één opgeslagen rij en toont wie elke kolom invult: het model, de write service op basis van het verzoek, of replace_fact.

De opgeslagen rij voor episode e2: de modelaanroep zet subject, attribute, value en valid_from; de write service voegt tenant_id en source_episode_id toe uit het verzoek; replace_fact zet valid_to en supersedes_fact_idDe opgeslagen rij voor episode e2: de modelaanroep zet subject, attribute, value en valid_from; de write service voegt tenant_id en source_episode_id toe uit het verzoek; replace_fact zet valid_to en supersedes_fact_id

Het volgende schema definieert het object dat van extractie naar de SGAM-writeservice gaat. Het vereist Pydantic. De applicatie zet de nu opgeslagen waarde in de prompt, zodat het model die kan vergelijken met het nieuwe bewijs:

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field

class MemoryUpdate(BaseModel):
    evidence: str = Field(description="Exact quote from the episode")
    subject: str = Field(description="Normalized entity ID, e.g. mira")
    attribute: str = Field(description="Property being updated")
    current_value: str | None = Field(description="Stored value shown in the prompt")
    change: Literal["new", "update", "no_change"]
    value: str | None = Field(description="New value; None when change is no_change")
    valid_from: datetime = Field(description="When the new value starts to apply")

De writeservice laat no_change-resultaten vallen, voegt tenant_id en source_episode_id uit het verzoek toe en geeft de rest door aan replace_fact in het voorbeeld hieronder. Het model kiest nooit de tenant of de bronepisode.


Schrijf- en leespad hebben een verschillende taak

Alleen het schrijfpad verandert opgeslagen state. Het leespad selecteert records voor het huidige verzoek.

De ingestion flow is het schrijfpad:

  1. Leg een ruwe episode vast uit berichten, tool results of bedrijfsgebeurtenissen. Registreer de tenant en de bewaarklasse.
  2. Extraheer getypeerde kandidaten met structured output.
  3. Valideer de payload met Pydantic of de schemalaag die je stack al gebruikt, en wijs onjuiste writes af.
  4. Los conflicten op vóór de insert: bepaal of het nieuwe record een oud record vervangt en sluit het verouderde feit af.
  5. Bewaar een bronverwijzing naar de ruwe episode, het tool result, het bestand, het ticket of de gebruikersbevestiging die het record heeft opgeleverd.
  6. Commit het record naar de SGAM-store met zijn schema_version, in plaats van de versie alleen in applicatiecode te laten.

De request flow is het leespad:

  1. Begin met de vraag van de gebruiker.
  2. Bepaal of de vraag de huidige state of de state op een bepaald moment nodig heeft.
  3. Filter op tenant, geheugentype, onderwerp, attribuut en geldigheidsperiode.
  4. Voeg vector- of grafuitbreiding alleen toe als de exacte state-lookup niet genoeg is. Gebruik die voor ondersteunend bewijs, gerelateerde entiteiten en voorbeelden, niet als autoriteit voor huidige feiten.
  5. Stel de kleinst mogelijke context met bronvermelding samen voor het model.

Het schrijfpad voor episode e2 sluit rij e1 af en voegt rij e2 in memory_facts in; een read van de huidige state op dezelfde rijen geeft 2026-06-30 terug en een read van 2 juni geeft 2026-07-15 terugHet schrijfpad voor episode e2 sluit rij e1 af en voegt rij e2 in memory_facts in; een read van de huidige state op dezelfde rijen geeft 2026-06-30 terug en een read van 2 juni geeft 2026-07-15 terug

Lees dat diagram van boven naar beneden. Het schrijfpad sluit e1 af en voegt e2 in één store in. Het leespad kiest een filter voor huidige of point-in-time-state en selecteert daarna rijen uit diezelfde store.


Wat in een geheugenschema thuishoort

Een minimaal SGAM-record heeft meer nodig dan text. Elk veld beantwoordt een van de vragen uit de eerste sectie of doet één taak in de levenscyclus:

VeldVraag die het beantwoordt of taak die het doet
tenant_idBij welke tenant hoort het? Elke read en write filtert erop.
memory_idEen stabiel ID waar andere records en verwijderjobs naar verwijzen.
subject, attributeDe sleutel: welke entiteit en welke eigenschap, zoals mira en passport_deadline.
valueDe opgeslagen waarde.
memory_typeHet soort geheugen: semantisch (stabiele feiten en profielen), episodisch (eerdere gebeurtenissen en traces) of procedureel (instructies en promptregels), zoals LangGraph ze beschrijft.
schema_versionWelk schema het record schreef, zodat een migratie oude records kan vinden en omzetten.
valid_from, valid_toWat is nu waar? valid_from <= now en (valid_to is null of now < valid_to). Wat was waar op 2 juni? Het interval bevat dat tijdstip.
supersedes_memory_idWelk ouder feit heeft dit feit vervangen?
source_episode_idWie zei het? Het verwijst naar de ruwe episode.
confidenceEen score die de writeservice met een drempel vergelijkt: de kandidaat promoveren tot huidige state, of de gebruiker om bevestiging vragen.
retention_policyKan ik het verwijderen of laten verlopen? Het vertelt verwijderjobs wat ze nog meer moeten wissen.

Met deze velden kan een nieuwe paspoortdeadline de vorige afsluiten zonder de historie te wissen. Dezelfde tabel kan huidige en point-in-time-queries beantwoorden en het resultaat herleiden tot de bronepisode.

Gebruik RAG om documenten op te halen en SGAM om veranderlijke state bij te houden. Vector search hoort nog steeds in het systeem voor vage recall, clustering en uitbreiding. De huidige waarde van mira.passport_deadline moet uit een afgebakend geheugenrecord komen, niet uit de chunk die toevallig als eerste scoorde.


Een voorbeeld met een verouderd feit

Neem een synthetische trace met twee episodes, weergegeven als DM-baseline en als SGAM-ledger:

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

Een DM-baseline bewaart beide episodes als vrije tekst, dus tekstzoeken kan e1 teruggeven omdat die de juiste woorden bevat. SGAM haalt uit elke episode een getypeerd feit, gesleuteld op tenant, onderwerp en attribuut. Het hoort e2 terug te geven als huidige state en e1 te bewaren voor een historische query.

Het script hieronder beantwoordt twee van de eerdere vragen: wat nu waar is en wat waar was op 2 juni. Kijk naar replace_fact: het sluit de oude rij af in plaats van die te verwijderen en legt vast welke rij de nieuwe vervangt. De aanroeper moet elke opgeslagen of voor een query gebruikte tijdstempel normaliseren naar één UTC-stringformaat met een vaste precisie van seconden: YYYY-MM-DDTHH:MM:SSZ. SQLite vergelijkt deze TEXT-waarden lexicografisch; gemengde tijdzone-offsets of precisies kunnen feiten daardoor verkeerd ordenen. Elke rij is geldig vanaf valid_from tot valid_to, die zelf niet meer meetelt. De tabel wijst ongeldige intervallen af en heeft een partiële unieke index voor één open feit per tenant, onderwerp en attribuut. replace_fact beheert één transactie en start die met BEGIN IMMEDIATE voordat de functie rijen leest. Roep de functie aan op een verbinding zonder actieve transactie. SQLite serialiseert gelijktijdige writers, dus aanroepers moeten de hele transactie opnieuw proberen na een lock- of uniciteitsfout. Aan het eind staat een keyword-match in plaats van vector search. Elke ranking die de correctie niet kent, kan e1 teruggeven.

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,
            supersedes_fact_id integer references memory_facts (fact_id),
            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:
        db.execute("BEGIN IMMEDIATE")
        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,
                 supersedes_fact_id, source_episode_id)
            values (?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.value,
                fact.valid_from,
                successor_boundary,
                containing["fact_id"] if containing else None,
                fact.source_episode_id,
            ),
        )

def fact_at(
    db: sqlite3.Connection, tenant_id: str, subject: str, attribute: str, timestamp: str
) -> sqlite3.Row | None:
    return db.execute(
        """
        select fact_id, value, valid_from, supersedes_fact_id, 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
        """,
        (tenant_id, subject, attribute, timestamp, timestamp),
    ).fetchone()

KEY = ("acme", "mira", "passport_deadline")

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

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

# A delayed extraction predates e1. It ends at e1's existing boundary,
# rather than closing e2, which remains current.
replace_fact(db, MemoryFact(*KEY, "2026-08-01", "2026-05-30T08:00:00Z", "e0"))
delayed = fact_at(db, *KEY, "2026-05-31T00:00:00Z")
assert (delayed["value"], delayed["source_episode_id"]) == ("2026-08-01", "e0")
current = fact_at(db, *KEY, "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:")

print("Naive text memory:")
print(f"  returned episode: {naive_match}")
print()
print("SGAM current state:")
print(f"  mira.passport_deadline = {current['value']}")
print(f"  valid_from={current['valid_from']}, source={current['source_episode_id']}")
print()
print("SGAM point-in-time state:")
print(f"  on 2026-06-02, mira.passport_deadline = {historical['value']}")

Het script geeft als uitvoer:

Naive text memory:
  returned episode: e1: Mira prefers concise answers. Her 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

replace_fact wijst eerst een gelijke valid_from af: dat conflict vraagt om applicatiebeleid, zoals bronvoorrang of een door de gebruiker bevestigde herziening, in plaats van stil overschrijven. Anders zoekt het het interval dat de binnenkomende timestamp bevat. Vindt het er een, dan sluit het die voorganger af op de binnenkomende grens, geeft het de ingevoegde rij het vroegere einde van de voorganger en legt het de voorganger vast in supersedes_fact_id. Ligt de timestamp vóór alle opgeslagen intervallen, dan gebruikt het de valid_from van het volgende interval als einde van de nieuwe rij. Een laat aankomend feit vóór e1 wordt dus [2026-05-30, 2026-06-01) en laat e1 en het huidige e2 intact. De partiële unieke index is de databasebeveiliging voor één open rij. DM heeft geen equivalente updatestap, dus de oude tekst kan nog steeds hoger scoren dan de correctie.

De tijdlijn toont de rijen na elke replace_fact-aanroep in het script, daarna de drie reads. Kijk naar stap 2, waarin e2 e1 afsluit, en naar stap 3, waarin de vertraagde e0 eindigt waar e1 begint.

Rijen voor mira.passport_deadline op een datumas na elke replace_fact-aanroep: e1 open vanaf 1 juni; daarna e1 afgesloten op 3 juni en e2 open; daarna e0 toegevoegd van 30 mei tot 1 juni; reads op 31 mei, 2 juni en 4 juni geven e0, e1 en e2 terugRijen voor mira.passport_deadline op een datumas na elke replace_fact-aanroep: e1 open vanaf 1 juni; daarna e1 afgesloten op 3 juni en e2 open; daarna e0 toegevoegd van 30 mei tot 1 juni; reads op 31 mei, 2 juni en 4 juni geven e0, e1 en e2 terug

Combineer deze transactie in productie met gestructureerde extractie op het schrijfpad. De databasetransactie werkt de temporele geldigheid bij. Het model extraheert een kandidaatfeit, maar beslist niet welke opgeslagen rij actueel blijft.


Opslagkeuzes volgen het retrievalpatroon

Projecten gebruiken meerdere namen voor delen van dit patroon: geheugenstores, context graphs, profielen, langetermijnstores, graph RAG en stateful agents.

Tool of frameworkBelangrijkste opslaglaagMechanisme voor temporele stateSchemamechanismeGebruik wanneer
GraphitiNeo4j, FalkorDB en Amazon Neptune; Kuzu is deprecatedGeldigheidsintervallen van feiten plus herkomst via bronepisodesPydantic-entiteit- en edgetypen, temporele edges, herkomstJe geldigheidsintervallen en graaftraversal nodig hebt en de graafdatabase zelf draait
ZepPropriëtaire Context Graph EngineBeheerde temporele Context Graphs die veranderende feiten bewarenStandaard en eigen entiteit- en edgetypenJe hetzelfde temporele graafmodel als beheerde dienst wilt
LangGraph / LangMemLangGraph-stores, stores op basis van PostgresDoor de applicatie beheerde timestamps en velden in storerecordsJSON-stores plus Pydantic-extractie van profielen of collectiesJe al LangGraph gebruikt en zelf timestamps en conflictregels wilt beheren
Mem0Beheerde stack, Valkey / Redis / vectorbackends in OSS-opstellingenGeheugenupdates; temporeel beleid blijft bij de applicatieGeheugentypen, eigen categorieën, extractiepromptsJe beheerde add, search, update en delete wilt, afgebakend per gebruiker, agent, app of run, en zelf het temporele beleid beheert
Letta / MemGPTDoor een database ondersteunde agentstate en geheugenblokkenBewerkbare blokken zonder geldigheidsintervallen per veldBewerkbare geheugenblokken met labelsJe wilt dat de agent zijn eigen geheugenblokken bewerkt, of meerdere agents één gedeeld blok nodig hebben dat ze allemaal zien
CogneeGraaf plus vector- en relationele backendsHistorie hangt af van de ontologie en de gekozen backendOntologiegerichte extractie en validatieJe extractie valideert tegen een domeinontologie
LlamaIndex property graphProperty graph-stores plus vector storesTijdvelden hangen af van het graafschema en de storeSchemaLLMPathExtractor met toegestane entiteiten en relatiesJe een property graph uit documenten of traces haalt

LangGraph scheidt thread-checkpoints, die één gesprek hervatten, van cross-thread-stores, die data bevatten die latere threads lezen. Het Zep-paper meldt winst op Deep Memory Retrieval en LongMemEval, en het Mem0-paper meldt winst op LoCoMo. Elke leverancier schreef zijn eigen paper, dus lees de cijfers als leveranciersresultaten, niet als garanties voor jouw workload.

Begin met het datamodel. Als exacte feitenlookup de hoofdbewerking is, is een relationele tabel met JSON-payloads, geldigheidskolommen, tenant-indexen en een vector-sidecar meestal genoeg. Voeg een graaf toe wanneer de queries van het product relaties volgen.


Bouw het schrijfpad vóór de graaf

Bepaal eerst wat het product mag onthouden. De keuze tussen graaf en vector komt later.

Een support-agent kan accounttier, open cases en duurzame contactvoorkeuren onthouden. Hij moet niet elke gefrustreerde opmerking tussendoor promoveren tot profielstate. Een coding agent kan repoconventies en openstaande taken onthouden. Hij moet een privénotitie niet eeuwig bewaren omdat die notitie toevallig één keer is opgehaald.

De eerste SGAM-store kan een relationele tabel zijn met een JSON-kolom en een paar indexen, geschreven via het zesstappen-schrijfpad hierboven. Een graaf wordt nuttig wanneer het product relaties moet doorlopen, zoals klant-naar-account, account-naar-beleid, taak-naar-artefact of project-naar-beslissing.

Hot path en achtergrondwrites

Directe extractie loont wanneer de volgende beurt van het nieuwe geheugen afhangt. Zegt de gebruiker “onthoud dat ik korte antwoorden wil”, dan mag het systeem geen nachtelijke job nodig hebben voordat het zich anders gedraagt.

De meeste beurten hebben geen directe write nodig. Sla de ruwe episode op met tenant-, session- en toolmetadata en laat een achtergrondworker later kandidaten extraheren. Bij consolidatie op basis van herhaling buffert de worker zwakke signalen en promoveert hij een feit pas nadat vergelijkbaar bewijs zich herhaalt of de gebruiker het bevestigt. Dit voegt vertraging in actualiteit toe. Dat is acceptabel voor “gebruiker vraagt vaak om CSV-exports” en riskant voor “klant heeft het afleveradres gewijzigd”.

Behandel schemamigratie als een productwijziging, omdat die verandert wat de agent kan herinneren, citeren of verwijderen. Ze kan ook veranderen welke historische feiten als actueel gelden. Plan migratiescripts, backfills, dual-read-periodes en verwijdergedrag in dezelfde release.


Wanneer SGAM de complexiteit waard is

Gebruik SGAM wanneer feiten in de tijd kunnen veranderen:

  • gebruikersvoorkeuren die kunnen worden bijgewerkt of ingetrokken
  • klant- of accountfeiten met auditvereisten
  • taakstatus voor langlopende assistenten
  • projectgeheugen van coding agents
  • gedeelde state van multi-agent-systemen
  • compliance-notities waarbij herkomst telt
  • temporele vragen zoals “wat geloofden we vóór de migratie?”

SGAM is overkill wanneer geheugen kortlevend, verkennend of goedkoop opnieuw te berekenen is. Heeft de agent maar een paar beurten continuïteit nodig, dan volstaan een checkpoint en een ingekorte berichtenhistorie. Vraag-en-antwoord over statische documenten heeft misschien alleen RAG nodig. En als het domein zo onrustig is dat het schema elke dag verandert, vertraagt getypeerd geheugen het team.

Het raster plaatst elk ontwerp op twee vragen: veranderen je feiten, en volgen je queries verbanden tussen records?

Twee-bij-twee-raster van geheugenontwerpen: history en RAG, een documentgraaf, een validity-tabel met de markering begin hier, en een temporele graaf met Graphiti of ZepTwee-bij-twee-raster van geheugenontwerpen: history en RAG, een documentgraaf, een validity-tabel met de markering begin hier, en een temporele graaf met Graphiti of Zep


Evaluatiechecklist

Evalueer de levenscyclus van het geheugen en niet alleen het eindantwoord. Een systeem kan een plausibel antwoord geven nadat het het verkeerde feit schreef, een verouderd feit ophaalde of een tenantgrens overschreed.

Ik gebruik dezelfde fase-voor-fase-indeling als in mijn artikel over RAG-evaluatie. Meet de fase waarin een fout kan optreden in plaats van de evaluatie te beperken tot de gegenereerde tekst. De tracediscipline uit het artikel over agent-evaluatie is ook van toepassing, want een geheugenbug verschijnt vaak in de runhistorie voordat hij het antwoord bereikt.

Ik zou SGAM testen met replay. Voer een vaste reeks episodes in de geheugenwriter in en inspecteer de ledger na elke betekenisvolle beurt. Stel daarna vragen over de huidige state en over de state op een bepaald moment aan de resulterende store.

LaagFout waar je naar zoektMetingen
Write-extractieDe agent miste een feit, verzon er een of produceerde een ongeldige vormPercentage schemageldige writes, extractieprecisie/-recall, dekking van bronepisodes
ConflictafhandelingEen verouderd feit bleef actueel of een geldig oud feit werd overschrevenCorrectheid van supersession, duplicaatpercentage, correctheid van invalidatie van verouderde feiten
Isolatie en beleidGeheugen lekte tussen gebruikers of overleefde zijn beleidsperiodeTenant-isolatiefouten, correctheid van verwijdering, naleving van bewaartermijnen
LeesretrievalHet juiste record bestaat, maar de reader haalde het niet opNauwkeurigheid huidige state, nauwkeurigheid point-in-time, recall@k over geheugenrecords
AntwoordgroundingHet antwoord gebruikte geheugen zonder onderbouwing of citeerde de verkeerde bronClaimonderbouwing tegen bronepisodes, citatienauwkeurigheid, correctheid van conflictoplossing
OperatieHet geheugenpad is te traag, te verouderd of te duurp95-writelatency, vertraging in actualiteit, leeslatency, kosten per query

Benchmarks zoals LoCoMo, LongMemEval en ATM-Bench bieden openbare testcases. Ze vervangen geen domeinspecifieke testsuite. Een coding assistant, een klantenservicebot en een compliance-copilot hebben elk andere schema’s, filters, bewaarregels en faaltests nodig.

From traces to better memory voert een verbeterlus uit op een geheugentool die feiten opslaat met een eigenaar, een brongebeurtenis en een geldigheidsinterval, volgens deze regels.


Kanttekeningen

SGAM is mijn label voor een patroon, geen standaard. De tools in de tabel hierboven gebruiken andere namen voor delen ervan: geheugenstores, temporele context graphs, geheugenblokken en knowledge graphs. Microsoft GraphRAG beschrijft een verwant deel van het probleem als graafgebaseerde retrieval over documenten.

Een gebruikersprofiel, een episodelog, een documentgraaf en een door de agent bewerkbaar geheugenblok lossen verschillende retrieval- en updateproblemen op. Ik reserveer SGAM voor duurzaam geheugen dat de huidige applicatiestate weergeeft en daarom schema, geldigheid, herkomst, conflictafhandeling, bewaring en migratie nodig heeft.

Getypeerd geheugen kan nog steeds fout zijn. Een schema maakt slechte writes makkelijker te inspecteren; het maakt ze niet betrouwbaar. Je hebt nog steeds brontrust, gebruikersbevestiging voor gevoelige feiten, conflictbeleid, verwijdering en monitoring nodig.


Referenties