AI Agent Memory: Schema-Guided State und Provenance
Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.
Long-running Agents rufen häufig veraltete Fakten ab, weil gewöhnliches Semantic Memory keine Regel dafür besitzt, welcher Wert aktuell ist.
Wenn ein Benutzer eine Passfrist für den Reisepass vom 15. Juli auf den 30. Juni ändert, kann Vector Search beide Aussagen abrufen. Eine zustandsbehaftete Memory-Schicht muss festhalten, dass der Wert 30. Juni den früheren Wert ersetzt.
Für Engineers, die Long-running oder Multi-Tenant Agents entwickeln, zeigt dieses Problem mit veralteten Fakten, warum Memory separate Runs überdauern und dabei den aktuellen Zustand sowie Tenant-Grenzen durchsetzen muss. Das folgende Design zeigt, wie sich typisierte Records schreiben und aktuelle sowie Point-in-Time-Reads testen lassen, ohne Vector Recall als Source of Truth zu behandeln.
Die Context-Window-Falle
Das Context Window bezeichnet die Eingabe, die für einen einzelnen Model Call verfügbar ist. Eine Anwendung kann Messages in spätere Calls übernehmen, aber die Application Policy muss entscheiden, welche alten Fakten weiterhin gelten und wer sie sehen darf.
Long-running Agents müssen sich Preferences, Task-Status, Customer Facts, Tool Decisions, Compliance Notes und frühere Fehler merken. Die einfache Variante besteht darin, Summaries anzuhängen oder alte Notes in einen Vector Store zu kippen. Das funktioniert, bis sich einer der gespeicherten Fakten ändert.
Nun verfügt der Agent über zwei Passfristen, zwei bevorzugte Formate oder zwei Projektentscheidungen. Semantic Search kann beide abrufen. Eine Summary kann eine davon überschreiben. Ein langer Context kann den veralteten Wert neben dem aktiven enthalten. Diese Designs rufen Text ab, erzwingen aber nicht den aktuellen Wert.
Der Memory Contract muss konkrete Fragen beantworten:
- Was gilt jetzt?
- Was galt am 2. Juni?
- Wer hat es gesagt?
- Zu welchem Tenant gehört es?
- Welchen älteren Fakt hat dieser Fakt ersetzt?
- Kann ich ihn löschen oder auslaufen lassen?
Schema-Guided Agent Memory (SGAM) speichert diese Antworten als Fields und Relations, statt sie implizit in Prose zu belassen.
Was SGAM bedeutet
Drei ähnlich benannte Konzepte legen den Scope von SGAM fest.
Schema-Guided Dialogue (SGD) ist das 2019 Google task-oriented dialogue dataset. Sein Schema beschreibt Service-APIs, Intents und Slots, damit ein Dialogue Model den Zustand von Services verfolgen kann, die es noch nicht gesehen hat. Es ist ein nützlicher Präzedenzfall für Schema-basiertes Tracking, sein Scope ist jedoch auf Dialogue Services begrenzt.
Schema-Guided Memory (SGM) ist der Research-Begriff, den Mei et al. in According to Me: Long-Term Personalized Referential Memory QA verwenden. Das Paper vergleicht Free-Text-Descriptive Memory (DM) mit Memory Items in einem festen Key-Value-Schema. Beide Repräsentationen enthalten dieselben Quellinformationen, jedoch in unterschiedlichen Strukturen.
In diesem Artikel verwende ich Schema-Guided Agent Memory (SGAM) für ein Engineering Pattern, bei dem Schemas Writes, Updates, Retrieval und Deletion steuern. Das Schema definiert den Application State und seinen Lifecycle.
ATM-Bench zeigt, warum die Repräsentation relevant ist. Der Benchmark verwendet ungefähr vier Jahre persönliche Daten aus Emails, Bildern und Videos. Die Fragen erfordern persönliche Referenzen, den Ort, mehrere Evidenzstücke und Updates im Zeitverlauf. Für seinen Hard Split berichtet das Paper, dass SGM im getesteten Setup DM bei Retrieval und Question Answering übertrifft. SGM stellt Fields wie Time, Source, Location, Entities und Tags in einer festen Repräsentation bereit. Ich behandle diese Repräsentation und die berichteten Ergebnisse als unterstützte Schlussfolgerung; das Paper etabliert keinen direkten Field-Addressing-Mechanismus.
SGM versus DM beantwortet eine Storage-Frage: Soll Memory als Free Text erhalten bleiben oder benannte Fields verwenden? Ein produktiver Agent hat bereits vor dem Storage ein weiteres Problem. Er muss eine unstrukturierte Conversation in ein vorgeschlagenes Memory Update umwandeln. Schema-Guided Reasoning (SGR) macht diesen beabsichtigten Decision Path überprüfbar: Die Response kann Evidence, Subject und Attribute, einen Vergleich mit dem aktuellen State sowie einen Candidate Write enthalten. Die Model Response erzwingt keine Dependencies oder Reihenfolge zwischen diesen Fields. Separate Calls, Validators und Application Policy setzen diese Regeln durch; SGAM wendet die Storage- und Lifecycle-Regeln nach dem Model Call an.
Model Extraction und Memory Ownership trennen
Der Memory Write durchläuft drei Layers. Structured Output (SO) erzwingt die Form des Candidate Objects. Schema-Guided Reasoning (SGR) macht die beabsichtigten Fields und die Decision Intent des Models in einer strukturierten Response überprüfbar. Schema-Guided Agent Memory (SGAM) verwaltet den Candidate nach dem Model Call als dauerhaften State. Separate Calls, Validators und Application Policy setzen Dependencies, Reihenfolge und Lifecycle-Regeln durch.
Ein Verdict, eine Route oder ein Plan läuft normalerweise zusammen mit dem aktuellen Request ab. Ein anderer Run kann einen Memory Candidate Tage später lesen oder ihn zur Auswahl eines Tool Calls verwenden. Diese längere Lebensdauer erfordert Storage Rules, die SGR nicht bereitstellt.
SGR strukturiert einen Model Call um eine beabsichtigte Reasoning Topology. Für einen Memory Write könnte die Response Source Evidence, ein normalisiertes Subject und Attribute, einen Vergleich mit dem aktuellen State sowie ein vorgeschlagenes Update enthalten. Pydantic oder JSON Schema beschreibt diese Fields. Provider-natives Structured Output oder ein Guided-Decoding-Runtime wie XGrammar hält die Response in dieser Form.
Diese Form macht die deklarierten Fields und die Intent des Models überprüfbar. Sie garantiert weder eine korrekte Schlussfolgerung noch beweist sie, dass das Model diese Fields in der richtigen Reihenfolge verwendet hat. Separate Calls, Validators und Application Policy bilden die Enforcement Boundary für Dependencies, Reihenfolge und Lifecycle.
SGAM entscheidet, was geschieht, nachdem dieses Object existiert. Soll es gespeichert werden? Ersetzt es einen älteren Fakt? Welcher Tenant darf es sehen? Ist es aktuell oder historisch? Welche Source Episode stützt es?
Die Tabelle zeigt Ownership und Failure Mode jeder Layer:
| Dimension | SO | SGR | SGAM |
|---|---|---|---|
| Zweck | Ein Object zurückgeben, das einem Schema entspricht | Beabsichtigte Fields und Decision Intent überprüfbar machen | Dauerhaftes Memory nach dem Model Call verwalten |
| Scope | Eine generierte Response | Eine Model Response; Cross-Step Policy kann mehrere Calls umfassen | Records, die zwischen Calls, Sessions und Runs verwendet werden |
| Schema-Rolle | Definiert Output-Fields, Types und zulässige Werte | Beschreibt Evidence, Intermediate Fields und Candidate Decision | Definiert gespeicherte Records, Relations und Lifecycle |
| Enforcement | Constrained Decoding blockiert schema-invalid Output | Nur Shape; separate Calls, Validators und Policy erzwingen Dependencies und Reihenfolge | Application Validation, Database Constraints und Conflict Rules erzwingen den Lifecycle |
| Lebensdauer | Aktueller Call, sofern die Anwendung das Object speichert | Reasoning Trace wird nach der Decision meist verworfen | Bleibt bestehen, bis es aktualisiert, abgelaufen oder gelöscht wird |
| Failure Mode | Gültige Form mit falscher Bedeutung | Fields sind vorhanden, aber Reasoning oder Dependency Handling kann weiterhin falsch sein | Veralteter, verunreinigter, nicht gescopter oder nicht auditierbarer State |
Auf dem Write Path sieht die Sequenz folgendermaßen aus:
SGR reasoning schema + Structured Output -> candidate write -> SGAM policy and persistence
Der folgende illustrative Ausschnitt aus memory_models.py definiert das Object, das von der Extraction an den SGAM Write Service übergeben wird. Dieser Block benötigt Pydantic und ist deshalb im Beispiel mit no-run markiert, damit Environments ohne diese optionale Dependency die Checks überspringen können:
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 hält fest, was das Model extrahiert hat. Der SGAM Write Service entscheidet weiterhin, ob er den Candidate ablehnt, zusammenführt oder speichert.
Write Path und Read Path haben unterschiedliche Aufgaben
Nur der Write Path mutiert den gespeicherten State. Der Read Path wählt Records für den aktuellen Request aus.
Der Ingestion Flow ist der Write Path:
- Erfasse eine Raw Episode aus Messages, Tool Results oder Business Events.
- Extrahiere typisierte Candidates über Structured Output.
- Validiere das Schema und lehne fehlerhafte Writes ab.
- Reconcile Conflicts, schließe veraltete Fakten und bewahre Provenance.
- Committe den Record in den SGAM Store.
Der Request Flow ist der Read Path:
- Beginne mit der User Question.
- Entscheide, ob die Frage den aktuellen State oder einen Point-in-Time State benötigt.
- Filtere nach Tenant, Memory Type, Subject, Attribute und Validity Window.
- Füge Vector- oder Graph-Expansion nur hinzu, wenn der exakte State Lookup nicht ausreicht.
- Stelle den kleinsten zitierten Context für das Model zusammen.
Lies das Diagramm von links nach rechts in zwei Lanes. Die obere Lane schreibt Memory, die untere liest es. Beide verwenden denselben Store.
Was in ein Memory Schema gehört
Ein minimales SGAM Record benötigt mehr als 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
Mit diesen Fields kann eine neue Passfrist die vorherige schließen, ohne die History zu löschen. Dieselbe Tabelle kann Current- und Point-in-Time-Queries beantworten und das Ergebnis anschließend bis zu seiner Source Episode zurückverfolgen. schema_version unterstützt Migrations, während retention_policy Löschjobs mitteilt, was sie zusätzlich entfernen müssen.
Verwende RAG zum Abruf von Dokumenten und SGAM zur Verwaltung veränderlicher States. Vector Search gehört weiterhin für Fuzzy Recall, Clustering und Expansion ins System. Der aktuelle Wert von mira.passport_deadline sollte aus einem gescopten Memory Record stammen und nicht aus dem Chunk, der zufällig am höchsten gerankt wurde.
Ein Beispiel für einen veralteten Fakt
Betrachte einen synthetischen Trace mit zwei Episodes, dargestellt als DM-Baseline und 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.
Eine DM-Baseline bewahrt beide Episodes als Free Text auf. Daher kann Text Search e1 zurückgeben, weil dort die richtigen Wörter vorkommen. SGAM extrahiert aus jeder Episode einen typisierten Fakt, der nach Tenant, Subject und Attribute verschlüsselt ist. Es sollte e2 als aktuellen State zurückgeben und e1 für eine historische Query behalten.
Der folgende eigenständige SQLite Write Path verwendet ISO-8601-UTC-Strings für Timestamps, konfiguriert sqlite3.Row vor dem Lesen nach Column Name und stellt Fakten als Half-Open Intervals dar: [valid_from, valid_to). Die Tabelle lehnt ungültige Intervalle ab und besitzt einen Partial Unique Index für genau einen offenen Fakt pro Tenant, Subject und Attribute. replace_fact verwaltet eine Transaction; SQLite serialisiert Concurrent Writers, daher sollten Caller nach einem Lock- oder Uniqueness-Fehler die gesamte Transaction erneut versuchen.
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 lehnt zunächst ein identisches valid_from ab: Dieser Conflict benötigt eine Application Policy, etwa Source Precedence oder eine vom User bestätigte Revision, statt eines stillen Overwrites. Andernfalls findet der Code das Interval, das den eingehenden Timestamp enthält. Wird eines gefunden, schließt er diesen Vorgänger an der eingehenden Grenze und gibt der eingefügten Row das frühere Ende des Vorgängers. Liegt der Timestamp vor allen gespeicherten Intervals, verwendet er das valid_from des nächsten Intervals als Ende der neuen Row. So wird ein verspätet eingehender Fakt vor e1 zu [2026-05-30, 2026-06-01) und lässt e1 sowie e2 unverändert. Die Row Factory macht row["fact_id"] und benannte Result Fields auf der hier erzeugten Default Connection gültig. Der Partial Unique Index ist die Database Guard für genau eine offene Row; ein Multi-Writer Deployment sollte eine zu seinem Workload passende Database- und Retry-Policy wählen. DM besitzt keinen entsprechenden Update-Schritt, daher kann der alte Text die Korrektur weiterhin überranken.
Die Assertions prüfen die aktuelle e2-Row, die historische e1-Row, das verzögerte Einfügen vor e1, die Interval-Invariante und die erste passende naive Text Episode:
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
Kombiniere diese Transaction in Production mit Structured Extraction auf dem Write Path. Die Database Transaction aktualisiert die zeitliche Gültigkeit. Das Model extrahiert einen Candidate Fact, entscheidet aber nicht, welche gespeicherte Row aktuell bleibt.
Storage Choices folgen dem Retrieval Pattern
Projekte verwenden verschiedene Bezeichnungen für Teile dieses Patterns: Memory Stores, Context Graphs, Profiles, Long-Term Stores, Graph RAG und Stateful Agents.
| Tool oder Framework | Main Storage Layer | Mechanismus für Temporal State | Schema-Mechanismus | Praktischer Einsatzbereich |
|---|---|---|---|---|
| Graphiti | Neo4j, FalkorDB und Amazon Neptune; Kuzu ist deprecated | Fact Validity Intervals plus Source-Episode-Provenance | Pydantic Entity- und Edge-Types, Temporal Edges, Provenance | Self-hosted Temporal Graph Memory |
| Zep | Proprietary Context Graph Engine | Verwaltete Temporal Context Graphs, die sich ändernde Fakten behalten | Default- und Custom Entity- und Edge-Types | Managed Agent Memory und Governance |
| LangGraph / LangMem | LangGraph Stores, Postgres-backed Stores | Application-owned Timestamps und Fields in Store Records | JSON Stores plus Pydantic Profile- oder Collection-Extraction | Agent Apps, die bereits auf LangGraph basieren |
| Mem0 | Managed Stack, Valkey / Redis / Vector Backends in OSS Setups | Memory Updates; Temporal Policy bleibt Application-owned | Memory Types, Custom Categories, Extraction Prompts | User-, Agent- und Session-Memory als Service |
| Letta / MemGPT | Datenbankbasierter Agent State und Memory Blocks | Editierbare Blocks ohne Field-Level Validity Intervals | Editierbare gelabelte Memory Blocks | Stateful Agents mit OS-artigem Context Management |
| Cognee | Graph plus Vector- und Relational-Backends | History hängt von Ontology und ausgewähltem Backend ab | Ontology-orientierte Extraction und Validation | Enterprise Knowledge-Graph Memory |
| LlamaIndex Property Graph | Property-Graph Stores plus Vector Stores | Time Fields hängen von Graph Schema und Store ab | SchemaLLMPathExtractor mit zulässigen Entities und Relations | Graph Extraction über Documents und Traces |
Graphiti ist eine konkrete Open-Source-Implementierung von relationalem, temporalem Memory. Es verfolgt, wie sich Fakten ändern, hält Pointer auf Source Episodes vor und unterstützt Hybrid Retrieval. LangGraph trennt Thread Checkpoints von Cross-Thread Stores. Mem0 verpackt Memory Operations als Managed Service. Letta verwendet editierbare Context Blocks statt Field-Level-SGAM, behandelt Agent State aber ebenfalls als persistente Daten.
Beginne mit dem Data Model. Wenn der exakte Fact Lookup die wichtigste Operation ist, reicht eine Relational Table mit JSON Payloads, Validity Columns, Tenant Indexes und einem Vector Sidecar normalerweise aus. Füge einen Graph hinzu, wenn Relationship Traversal Teil des Produkts ist – nicht, weil die Graph-Demo beeindruckend aussieht.
Baue den Write Path vor dem Graph
Entscheide zuerst, woran sich das Produkt erinnern darf. Die Entscheidung zwischen Graph und Vector kommt später.
Ein Support Agent könnte sich Account Tier, Open Cases und dauerhafte Contact Preferences merken. Er sollte nicht jede frustrierte Nebenbemerkung in den Profile State übernehmen. Ein Coding Agent könnte sich Repo Conventions und ungelöste Tasks merken. Er sollte eine private Note nicht für immer behalten, nur weil sie einmal abgerufen wurde.
Beginne mit dem Write Path und behandle Memory als kleine State Mutation:
- Benenne den Memory Type, das Subject, den Tenant Scope und die Retention Class.
- Extrahiere Candidate Records mit Structured Output.
- Validiere das Payload mit Pydantic oder der Schema Layer, die dein Stack bereits verwendet.
- Löse Conflicts vor dem Insert, einschließlich der Frage, ob der neue Record einen alten ersetzt.
- Bewahre einen Source Pointer auf die Raw Episode, den Tool Result, die File, das Ticket oder die User Confirmation auf, durch die der Record entstanden ist.
- Schreibe die Schema Version mit jedem Record, statt sie nur im Application Code zu hinterlegen.
Der erste SGAM Store kann eine Relational Table mit einer JSON Column und einigen Indexes sein. Ein Graph wird nützlich, wenn das Produkt Relations wie Customer-to-Account, Account-to-Policy, Task-to-Artifact oder Project-to-Decision traversieren muss.
Hot Path und Background Writes
Immediate Extraction lohnt sich, wenn der nächste Turn vom neuen Memory abhängt. Wenn der User sagt „Merke dir, dass ich kurze Antworten bevorzuge“, sollte das System keinen Nightly Job benötigen, bevor es sich anders verhält.
Die meisten Turns benötigen keinen Immediate Write. Speichere die Raw Episode mit Tenant-, Session- und Tool-Metadata und lasse anschließend einen Background Worker Candidates extrahieren. Bei Recurrence-basierter Consolidation puffert der Worker schwache Signals und befördert einen Fakt erst, nachdem sich ähnliche Evidence wiederholt oder der User ihn bestätigt hat. Das erzeugt Freshness Lag. Für „Der User fragt häufig nach CSV-Exports“ ist das akzeptabel, für „Der Kunde hat die Lieferadresse geändert“ riskant.
Halte den Read Path deterministisch. Erzwinge Tenant Scope und Validity zuerst und verwende Fuzzy Retrieval anschließend nur dann, wenn es nützlichen Context ergänzen kann.
- Filtere nach Tenant, Memory Type und Validity Window.
- Rufe den exakten Structured State vor Semantic Neighbors ab.
- Verwende Vector- oder Graph-Expansion für unterstützende Evidence, verwandte Entities und Beispiele – nicht als Authority für aktuelle Fakten.
- Stelle den kleinsten zitierten Context zusammen, der die Frage beantworten kann.
Behandle eine Schema Migration als Product Change, weil sie verändert, woran sich der Agent erinnern, was er zitieren oder löschen kann. Sie kann auch ändern, welche historischen Fakten als aktuell gelten. Plane Migration Scripts, Backfills, Dual-Read Windows und Deletion Behavior im selben Release.
Wann sich SGAM lohnt
Verwende SGAM, wenn sich Fakten im Lauf der Zeit ändern können:
- User Preferences, die aktualisiert oder widerrufen werden können
- Customer- oder Account Facts mit Audit-Anforderungen
- Task State für Long-running Assistants
- Coding-Agent Project Memory
- von mehreren Agents geteilter State
- Compliance Notes, bei denen Provenance relevant ist
- Temporal Questions wie „Was haben wir vor der Migration geglaubt?“
SGAM ist Overkill, wenn Memory kurzlebig, explorativ oder günstig neu berechenbar ist. Wenn der Agent nur einige Turns Continuity benötigt, reichen ein Checkpoint und eine gekürzte Message History aus. Für statische Document QA genügt möglicherweise RAG. Und wenn die Domain so unbeständig ist, dass sich das Schema täglich ändert, wird Typed Memory das Team ausbremsen.
Evaluation Checklist
Evaluiere den Memory Lifecycle ebenso wie die finale Answer. Ein System kann eine plausible Response erzeugen, obwohl es den falschen Fakt geschrieben, einen veralteten abgerufen oder eine Tenant-Grenze überschritten hat.
Ich verwende dieselbe Stage-by-Stage-Aufteilung wie in meinem RAG-Evaluation-Artikel. Messe die Stage, in der ein Fehler auftreten kann, statt die Evaluation auf den generierten Text zu beschränken. Die Trace-Disziplin aus dem Agent-Evaluation-Artikel gilt ebenfalls, weil ein Memory Bug häufig bereits in der Run History sichtbar wird, bevor er die Answer erreicht.
Ich würde SGAM mit Replay testen. Führe eine feste Sequenz von Episodes in den Memory Writer ein und untersuche das Ledger nach jedem relevanten Turn. Stelle anschließend Current-State- und Point-in-Time-Fragen an den resultierenden Store.
| Layer | Gesuchter Fehler | Measures |
|---|---|---|
| Write Extraction | Der Agent hat einen Fakt übersehen, erfunden oder eine ungültige Form erzeugt | Schema-valid Write Rate, Extraction Precision/Recall, Source-Episode-Coverage |
| Conflict Handling | Ein veralteter Fakt blieb aktuell oder ein gültiger alter Fakt wurde überschrieben | Supersession Correctness, Duplicate Rate, Stale-Fact-Invalidation-Correctness |
| Isolation und Policy | Memory ist zwischen Usern durchgesickert oder überlebte sein Policy Window hinaus | Tenant-Isolation-Fehler, Deletion Correctness, Retention Compliance |
| Read Retrieval | Der richtige Record existiert, wurde aber nicht abgerufen | Current-State Accuracy, Point-in-Time Accuracy, Recall@k über Memory Records |
| Answer Grounding | Die Answer nutzte Memory ohne Support oder zitierte die falsche Source | Claim Support gegen Source Episodes, Citation Accuracy, Conflict-Resolution-Correctness |
| Operations | Der Memory Path ist zu langsam, zu veraltet oder zu teuer | p95 Write Latency, Freshness Lag, Read Latency, Cost per Query |
Benchmarks wie LoCoMo, LongMemEval und ATM-Bench stellen öffentliche Test Cases bereit. Sie ersetzen keine Domain Test Suite. Ein Coding Assistant, ein Customer-Support-Bot und ein Compliance Copilot benötigen unterschiedliche Schemas, Filters, Retention Rules und Failure Tests.
Caveats
SGAM ist meine Bezeichnung für ein Pattern, kein Standard. Bestehende Projekte teilen das Problem unterschiedlich auf. LangGraph Memory und LangMem beschreiben Short-Term- und Long-Term Stores, Profiles, Collections, Hot-Path Writes und Background Memory Managers. Zep Graphiti verwendet den Begriff Temporal Context Graph. Letta persistiert editierbare Memory Blocks, während Mem0 eine Managed Memory Layer anbietet. Microsoft GraphRAG, LlamaIndex Property Graphs und Cognee fassen verwandte Teile des Problems als Knowledge Graphs auf.
Ein User Profile, ein Episode Log, ein Document Graph und ein vom Agent editierbarer Memory Block lösen unterschiedliche Retrieval- und Update-Probleme. Ich verwende SGAM ausschließlich für dauerhaftes Memory, das den aktuellen Application State repräsentiert und deshalb Schema, Validity, Provenance, Conflict Handling, Retention und Migration benötigt.
Typed Memory kann weiterhin falsch sein. Ein Schema macht fehlerhafte Writes leichter überprüfbar; vertrauenswürdig werden sie dadurch nicht. Du benötigst weiterhin Source Trust, User Confirmation für sensitive Fakten, Conflict Policy, Deletion und Monitoring.
Schema Migration ist Arbeit. Sobald Memory zu State wird, gehören Versioning, Backfills, alte Records und Deletion Behavior zu deiner Verantwortung. Wenn du diese Arbeit überspringst, werden alte Records die Semantics oder Retention Policy überleben, unter denen sie entstanden sind.
References
- According to Me: Long-Term Personalized Referential Memory QA – Paper von Mei et al., das ATM-Bench und Schema-Guided Memory einführt.
- Towards Scalable Multi-Domain Conversational Agents: The Schema-Guided Dialogue Dataset – Paper von Rastogi et al. zum SGD-Dataset.
- LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory – Benchmark von Wu et al. für Long-Term-Memory-Fähigkeiten.
- 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