AI Agent Memory Architecture: Checkpoints en Vector Stores
Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Een reasoning loop houdt het niet langer dan één request vol, tenzij de state buiten de worker wordt opgeslagen. Zonder agent memory kan de agent een gepauzeerd plan niet hervatten, niet herstellen na een crash en geen voorkeur uit een eerdere sessie terughalen. Deel 1 behandelde de control flow. In deze post bepaal ik welke state elke volgende beurt nodig heeft en waar die state moet leven.
Ik gebruik de Market Analyst Agent — een kleine LangGraph-agent die marktdata ophaalt en een analistenrapport schrijft — als uitgangspunt voor de bespreking van hot checkpoints. De secties over cold vectors en raw Markdown zijn onafhankelijke, illustratieve designs die uitbreidingen tonen die het huidige project nog niet implementeert. Daarna bespreek ik wanneer PostgreSQL, Redis, Qdrant, key-value stores en gewone Markdown-bestanden elk zinvol zijn.
Elke store hieronder wordt gelezen door de harness, de code die de loop rond het model aanstuurt. De harness bepaalt welke inhoud in de context window terechtkomt; de stores doen dat niet. Dit artikel gaat over waar die state leeft voordat de harness haar ophaalt. Deel 3 en deel 4 behandelen wat de harness daarna met de prompt doet.
Wat is AI agent memory?
AI agent memory is de state-laag waarmee een agent taakvoortgang kan bewaren, eerdere kennis kan ophalen en wat hij weet over meerdere runs heen kan bijwerken. In productie is dit niet één vector database. Het is een combinatie van hot checkpoints, cold semantic of structured stores en human-readable document memory.
| Behoefte | Beste default | Waarom |
|---|---|---|
| Eén run pauzeren en hervatten | PostgreSQL checkpoint store | Durable, queryable en eenvoudig te beheren naast appdata |
| Low-latency transient state | Redis checkpoint store | Snel hervatten en short-lived state, met trade-offs rond persistence |
| Cross-thread semantic recall | Qdrant of pgvector | Haalt memories op basis van betekenis op, niet alleen via exacte keys |
| Structured user facts | PostgreSQL of key-value store | Deterministic updates zijn beter dan fuzzy retrieval voor preferences en IDs |
| Projectconventies en geleerde procedures | Markdown- of JSON-bestanden | Human-readable, diffable en eenvoudig door agents bij te werken |
| Multi-entity relationship memory | Knowledge graph | Nuttig wanneer relaties belangrijker zijn dan individuele facts |
Begin niet met memory omdat het intelligent klinkt. Begin met de user-visible failure: voortgang verliezen, een preference vergeten, onderzoek herhalen of een projectconventie niet opnieuw gebruiken.
Failures waarvoor memory nodig is
Een stateless agent kan een geïsoleerde vraag beantwoorden, maar vergeet de request zodra de call eindigt. Dat design faalt wanneer het product een van de volgende gedragingen nodig heeft:
- Pauzeren en hervatten: een gebruiker start een researchtaak, sluit zijn laptop en komt morgen terug. Zonder checkpointed state begint de agent opnieuw.
- Multi-turn coherence: in een lange conversatie moet de agent onthouden welke tools hij heeft aangeroepen, welke data hij heeft verzameld en welke planstappen hij heeft afgerond.
- Personalisatie: een terugkerende gebruiker verwacht dat de agent zijn risicotolerantie, gewenste analysediepte en eerdere interacties kent.
- Human-in-the-loop (HITL): de agent verzamelt zijn bewijsmateriaal en wacht tot een mens de volgende stap goedkeurt. De state “waiting” moet process restarts overleven.
In de Market Analyst Agent uit deel 1 levert de request “Analyze NVDA” een plan, vijf tool calls, verzamelde data en een draft report op. Wanneer de gebruiker antwoordt met “looks good, but add competitor analysis”, kan een checkpoint store de state van de laatst voltooide stap laden en de competitor-stap toevoegen. Zonder checkpointed state kan de agent niet bepalen waar “looks good” naar verwijst en moet hij opnieuw beginnen.
Long-term memory behandelt een ander scenario. Wanneer de gebruiker een week later terugkomt en vraagt: “Update my NVDA analysis”, moet de agent mogelijk een preference voor conservatieve risicoanalyses en interesse in semiconductor stocks terughalen. Een vector-backed memory store kan die facts over sessies heen ophalen zonder er opnieuw naar te vragen.
De implementatievoorbeelden hieronder gebruiken LangGraph, LangChain’s open-source library voor het bouwen van agents als expliciete state graphs; de storage boundaries die LangGraph definieert zijn toepasbaar op elk framework. LangGraph splitst memory op scope. Elke graph execution draait binnen een thread, dus één conversatie of taak. Persisted state binnen die thread is short-term memory. State die tussen threads wordt gedeeld is long-term memory. Hieronder zijn “thread” en “conversation” uitwisselbaar; ik vermijd “session” voor die span, omdat deel 5 die term reserveert voor de durable log van één run, waarvan er meerdere op één thread kunnen accumuleren. De huidige context van het model en in-process variables vormen de working-memorylaag boven beide stores.
Een taxonomie van AI agent memory
Voordat we naar de implementatie gaan, helpt het om te classificeren wat agents moeten onthouden. Het CoALA-framework — Cognitive Architectures for Language Agents (Sumers, Yao et al., 2023) — is een veelgeciteerde taxonomie die voortbouwt op de cognitieve wetenschappen. In mijn post over context engineering introduceerde ik memory scoping; hier werk ik dat uit in zes categorieën:
| Memory Type | Scope | Lifetime | Voorbeeld | Storage Pattern |
|---|---|---|---|---|
| Working | Huidige stap | Milliseconden | Tool call arguments, huidige LLM-response | In-process (Python dict) |
| Short-term | Huidige thread | Minuten–uren | Conversation history, plan progress, verzamelde data | Checkpoint store |
| Episodic | Cross-thread | Dagen–maanden | ”Vorige week vroeg de gebruiker naar NVDA-earnings” | Vector store / KV store |
| Semantic | Cross-thread | Maanden–permanent | ”De gebruiker geeft de voorkeur aan conservatieve beleggingen” | Vector store / KV store |
| Document | Cross-thread | Dagen–permanent | Project notes, research summaries, learned patterns | File store (Markdown/JSON) |
| Procedural | System-wide | Permanent | ”Controleer bij stock analysis altijd SEC filings” | Config / system prompt |
Working memory is wat de LLM op dit moment actief gebruikt voor reasoning: Python-variabelen in de huidige functie, de inhoud van de context window en tool call arguments tijdens de execution. Dit is de snelste en meest ephemeral laag. Niets blijft bewaard na de huidige stap. Working memory wordt begrensd door de context window van het model, waardoor dit de bottleneck is. Alles wat de agent op het moment van beslissen “weet”, moet hierin passen, ongeacht of het uit de checkpoint store, een vector query of een file komt. De andere tiers bestaan om op het juiste moment de juiste informatie aan working memory te leveren.
Short-term memory is het checkpoint dat LangGraph schrijft na elke unit van graph execution — een super-step, die in de volgende sectie wordt gedefinieerd. Episodic en semantic memories blijven tussen threads bestaan. Document memory bewaart project notes, research summaries en learned conventions in bestanden die mensen en agents kunnen inspecteren. Procedural memory leeft in system instructions en tool definitions en verandert niet per gebruiker.
Voor de implementatie vallen vijf van die zes typen samen in drie storage tiers. Short-term memory wordt hot memory, het checkpoint voor de huidige thread. Episodic en semantic worden cold memory, recall tussen threads. Document memory houdt opgebouwde projectkennis leesbaar en direct bewerkbaar. Working memory wordt in de taxonomie hierboven met de hot tier gegroepeerd, maar is het enige type dat eigenlijk nooit wordt opgeslagen: het bestaat één stap lang, in process, en is de context window waarin de drie storage tiers worden geladen. Procedural memory staat buiten alle drie: het leeft in de system prompt en tool definitions en wordt dus met de agent meegeleverd in plaats van opgeslagen en opgehaald.
CoALA classificeert working, episodic, semantic en procedural memory. De Memory in the Age of AI Agents-survey benadrukt vector stores en knowledge graphs, terwijl LangGraph checkpoints en zijn Store-interface documenteert. File-backed project knowledge valt buiten die taxonomieën, hoewel Claude Code, Cursor en Devin Desktop allemaal persistente projectbestanden laden.
Ditzelfde storage pattern verschijnt in andere domeinen. Een Minecraft-agent (Voyager) bewaart herbruikbare game skills als code libraries, teams in een enterprise document-QA-competitie itereren op procedural prompt documents en web agents leiden herbruikbare browsing workflows af uit succesvolle runs. Later kom ik op alle drie terug; het punt hier is dat files die kennis inspecteerbaar en versionable maken zonder een aparte embeddingservice.
Door agents beheerde memory verschilt ook van een vaste RAG-pipeline wat betreft wie de write uitvoert. De agent of zijn harness bepaalt wat wordt opgeslagen, bijgewerkt en verwijderd en kiest later wanneer het wordt opgehaald.
De Generative Agents-paper (Park et al., 2023) liet zien hoe ver dit kan gaan: gesimuleerde agents sloegen hun eigen memories op, reflecteerden erop en haalden ze terug. De memory stream rangschikte kandidaten op recency, importance en relevance, een design dat nog altijd een nuttig referentiepunt biedt voor retrieval in agent memory.
Short-term agent memory: de checkpoint store
Elke keer dat LangGraph een super-step afrondt — één node of een batch nodes die parallel draaiden — serialiseert het framework de volledige graph state en schrijft die naar een checkpoint store. Dat vormt de basis voor pause/resume, time-travel debugging en HITL-workflows.
Een checkpoint bevat de graph state die nodig is om te hervatten: de AgentState uit deel 1 — messages, identity, user profile, plan steps, research data en execution mode. Daarnaast bewaart LangGraph zijn eigen bookkeeping: de ID en timestamp van het checkpoint, één version per channel (LangGraph’s naam voor een individuele state key) en een afzonderlijk record van welke channel versions elke node al heeft gezien. Het stepnummer staat in de metadata van het checkpoint en niet op het checkpoint zelf. Door die twee te vergelijken bepaalt de graph wat er vervolgens moet draaien. Na een HITL-interrupt of process restart laadt de graph het checkpoint dat op de laatst voltooide boundary is geschreven en gaat hij opnieuw de volgende node binnen. Hij gaat niet verder vanaf een willekeurige Python-regel. Een checkpoint verschilt ook van een append-only eventlog of trace; deel 5 scheidt die runtime observability-surfaces expliciet.
Hoe LangGraph checkpointing werkt
LangGraph’s BaseCheckpointSaver is een eenvoudige interface: put() schrijft een checkpoint, get_tuple() leest het nieuwste checkpoint voor een thread en list() retourneert de history. Elk checkpoint wordt gesleuteld op (thread_id, checkpoint_ns, checkpoint_id), waarbij thread_id de conversatie identificeert, checkpoint_ns subgraph namespacing afhandelt en checkpoint_id een unieke version is.
De relevante beslissing is welke backend erachter komt. PostgreSQL en Redis zijn twee gangbare keuzes voor productie.
PostgreSQL vs Redis
| Dimensie | PostgreSQL (langgraph-checkpoint-postgres) | Redis (langgraph-checkpoint-redis) |
|---|---|---|
| Durabilitymodel | ACID-transacties, WAL en replication | Configureerbare persistence: een append-only command log (AOF) of periodieke snapshots (RDB) |
| Checkpoint history | Durable history voor resume en debugging | Retention hangt af van saver- en eviction-instellingen |
| Primaire constraint | Database write latency en table growth | RAM-gebruik, eviction en persistenceconfiguratie |
| Operationele fit | Teams die al relationele databases beheren | Teams die Redis al op hoge throughput beheren |
| Beste default voor | Durable resume en reproduceerbare debugging | Latency-sensitive, recoverable session state |
Generieke databasebenchmarks voorspellen de checkpoint performance niet. Meet de serialized state size, write frequency, persistencesettings en concurrency van je eigen graph.
PostgreSQL: de durable default
PostgreSQL is voor de meeste teams de veiligere default. Checkpoints overleven crashes, je krijgt volledige transaction semantics en de checkpoint history maakt time-travel debugging eenvoudig.
Een vereenvoudigde versie van de checkpointsetup in memory/hot.py. Stel voor productie LANGGRAPH_STRICT_MSGPACK=true in of configureer een expliciete allowed_msgpack_modules-allowlist, zodat checkpoint deserialization alleen veilige of gedeclareerde types toestaat; de permissive default waarschuwt voor niet-geregistreerde types maar staat ze wel toe.
import asyncio
from contextlib import asynccontextmanager
from langchain_core.messages import HumanMessage
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
@asynccontextmanager
async def postgres_checkpointer(connection_string: str):
"""Yield a PostgreSQL-backed checkpoint store.
PostgreSQL gives us ACID guarantees — if a checkpoint write succeeds,
the state is durable even if the process crashes immediately after.
`from_conn_string` is itself an async context manager: it owns the
connection and closes it on exit, so the graph has to run inside it.
"""
async with AsyncPostgresSaver.from_conn_string(connection_string) as checkpointer:
# Create the checkpoint tables if they don't exist.
# This is idempotent — safe to call on every startup.
await checkpointer.setup()
yield checkpointer
async def main() -> None:
# The graph lives inside the context manager's scope.
async with postgres_checkpointer(
"postgresql://user:pass@localhost:5432/agent_memory"
) as checkpointer:
graph = create_graph(checkpointer=checkpointer)
# Every invoke/stream call now persists state automatically.
config = {"configurable": {"thread_id": "user-123-session-1"}}
result = await graph.ainvoke(
{"messages": [HumanMessage(content="Analyze NVDA")]}, config
)
# Resume later on the same thread_id — loads the latest checkpoint.
result = await graph.ainvoke(
{"messages": [HumanMessage(content="approved")]}, config
)
asyncio.run(main())
De AsyncPostgresSaver gebruikt het langgraph-checkpoint-postgres-package, dat vier tabellen aanmaakt: checkpoints (de serialized state), checkpoint_blobs (grote binary data), checkpoint_writes (pending writes voor crash recovery) en checkpoint_migrations (schema version). Concurrent writers worden van elkaar gescheiden door de primary key (thread_id, checkpoint_ns, checkpoint_id) en upserts, niet door locking — twee workers op dezelfde thread zullen elkaar niet corrumperen, maar ze coördineren ook niet.
Redis: wanneer latency de bottleneck is
Wanneer checkpoint latency de bottleneck is, is Redis een optie voor recoverable state. Meet serialized state size, persistencesettings en concurrency voordat je Redis boven PostgreSQL verkiest.
Een vereenvoudigde versie van de checkpointsetup in memory/hot.py:
import asyncio
from contextlib import asynccontextmanager
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
@asynccontextmanager
async def redis_checkpointer(redis_url: str):
"""Yield a Redis-backed checkpoint store.
Redis keeps checkpoints in memory for low-latency access.
Trade-off: less durable than PostgreSQL unless you enable AOF,
Redis's append-only log — snapshots alone lose recent writes on a crash.
"""
async with AsyncRedisSaver.from_conn_string(redis_url) as checkpointer:
# Initialize Redis data structures
await checkpointer.asetup()
yield checkpointer
async def main() -> None:
# Same graph API, different backend.
async with redis_checkpointer("redis://localhost:6379") as checkpointer:
graph = create_graph(checkpointer=checkpointer)
asyncio.run(main())
De AsyncRedisSaver uit langgraph-checkpoint-redis slaat elk checkpoint op als een eigen RedisJSON-document, onder dezelfde (thread_id, checkpoint_ns, checkpoint_id)-key als de Postgres saver. De v0.1.0-redesign verving meerdere search operations door één JSON.GET-call, waardoor de latency aanzienlijk afnam. Redis 8.0+ bevat RedisJSON en RediSearch standaard — je hoeft geen extra modules te installeren.
Voor memory-constrained deployments bewaart ShallowRedisSaver alleen het nieuwste checkpoint per thread — geen history, maar minimaal RAM-gebruik. Gebruik dit wanneer je pause/resume nodig hebt maar geen time-travel debugging.
Wanneer gebruik je welke?
Gebruik PostgreSQL wanneer:
- Je volledige checkpoint history nodig hebt voor time-travel debugging of reproduceerbare resume
- Durability niet onderhandelbaar is (financial services, healthcare)
- Je PostgreSQL al in je stack gebruikt
- Je agent long-running tasks uitvoert waarbij stateverlies uren recomputation betekent
- Je een unified data store wilt — PostgreSQL met pgvector kan één backend zijn voor checkpoints, long-term memory en vector search
Gebruik Redis wanneer:
- Checkpoint latency je bottleneck is (real-time chat, streaming UX)
- Je voice bots of streaming experiences bouwt waarbij checkpoint access op een gemeten latency-critical path staat
- Je horizontaal wilt schalen over veel concurrent threads
- Je high-concurrency fan-out patterns hebt waarbij meerdere agents state delen
- Je short-lived sessions hebt waarbij het verlies van een checkpoint herstelbaar is
- Je semantic caching wilt gebruiken om redundante LLM calls te verminderen (Redis LangCache cached semantically similar queries om herhaalde LLM calls te voorkomen)
Andere opties: langgraph-checkpoint-sqlite werkt voor lokale development en single-process deployments. Voor AWS-native stacks biedt langgraph-checkpoint-aws een DynamoDBSaver met automatische payload offloading — kleine checkpoints (<350 KB) blijven in DynamoDB, grotere worden naar S3 weggeschreven. Serverless pricing en geen te beheren infrastructuur maken dit aantrekkelijk voor deployments met variabele load.
Long-term memory: onthouden over sessies heen
Hot memory handelt de huidige conversatie af. Long-term memory gaat over de gebruiker die volgende week terugkomt: het bewaart facts, preferences en interaction history die tussen threads blijven bestaan.
LangGraph biedt via zijn BaseStore-class een Store-interface voor cross-thread memory. Elk memory-item is een (namespace, key)-paar met een JSON-value en optionele vector embedding. De namespace encodeert doorgaans de gebruiker of organisatie: ("user", "user-123", "preferences").
Vector storage: semantic recall met Qdrant
Wanneer de agent ongestructureerde facts moet terughalen (“Wat zei de gebruiker over zijn investeringstermijn?”), biedt vector search semantic recall. In plaats van exacte key lookups queryt de agent op betekenis.
Qdrant is een purpose-built vector database, geschreven in Rust, die embedding storage, indexing (Hierarchical Navigable Small World, of HNSW) en filtered search afhandelt. Ik behandelde HNSW en de trade-offs ervan uitgebreid in mijn post over search ranking. Qdrant biedt ook een MCP-server die als semantic memory layer fungeert — nuttig als je agent framework het Model Context Protocol ondersteunt.
Het volgende is een onafhankelijk, illustratief Qdrant-design. Het is geen vereenvoudigde versie van het huidige memory/long.py. Het huidige project bewaart user profiles met exacte user_id-filtering en een zero-vector placeholder. Echte embeddingintegratie blijft toekomstig werk. De request handler moet de request authenticeren en principal construeren op basis van de geverifieerde identity; de client levert deze nooit aan. Het Qdrant-filter bepaalt de retrieval scope, niet de authorization.
from qdrant_client import QdrantClient
from qdrant_client.models import (
PointStruct, Distance, VectorParams, Filter, FieldCondition, MatchValue,
)
import hashlib
from dataclasses import dataclass
@dataclass(frozen=True)
class AuthenticatedPrincipal:
"""Created by the server after authentication, never from request JSON."""
user_id: str
class UserMemoryStore:
"""Long-term memory backed by Qdrant vector search.
Stores user facts as embedded vectors for semantic retrieval.
Each fact is a short natural-language statement about the user.
"""
def __init__(self, qdrant_url: str, collection_name: str = "user_memory"):
self.client = QdrantClient(url=qdrant_url)
self.collection_name = collection_name
self._ensure_collection()
def _ensure_collection(self):
"""Create the collection if it doesn't exist."""
collections = [c.name for c in self.client.get_collections().collections]
if self.collection_name not in collections:
self.client.create_collection(
collection_name=self.collection_name,
vectors_config=VectorParams(
size=1536, # text-embedding-3-small dimensions
distance=Distance.COSINE,
),
)
def store_fact(
self, principal: AuthenticatedPrincipal, fact: str, embedding: list[float]
):
"""Store a user fact with its embedding."""
point_id = hashlib.md5(f"{principal.user_id}:{fact}".encode()).hexdigest()
self.client.upsert(
collection_name=self.collection_name,
points=[PointStruct(
id=point_id,
vector=embedding,
payload={"user_id": principal.user_id, "fact": fact},
)],
)
def recall(
self,
principal: AuthenticatedPrincipal,
query_embedding: list[float],
top_k: int = 5,
):
"""Retrieve the most relevant facts for a user given a query."""
results = self.client.query_points(
collection_name=self.collection_name,
query=query_embedding,
query_filter=Filter(
must=[FieldCondition(
key="user_id", match=MatchValue(value=principal.user_id)
)]
),
limit=top_k,
)
return [hit.payload["fact"] for hit in results.points]
De flow bestaat uit drie stappen. In dit illustratieve design extraheert een LLM belangrijke facts uit de interactie (“user has high risk tolerance”, “user is interested in semiconductor stocks”). Die facts worden geëmbed en in Qdrant opgeslagen. Aan het begin van de volgende conversatie levert de server de authenticated principal aan en queryt de agent Qdrant met het nieuwe bericht van de gebruiker om relevante context terug te halen. De huidige Market Analyst Agent implementeert deze semantic extraction- en embeddingflow nog niet.
Retrieval scoring: verder dan cosine similarity
Raw cosine similarity is een startpunt, maar production memory systems hebben rijkere retrieval nodig. De Generative Agents-paper (Park et al., 2023) introduceerde een scoring function die drie signalen combineert:
- Recency: Rule-based decay zodat recente memories hoger scoren. Een exponential decay function zorgt ervoor dat een fact van gisteren hoger scoort dan een gelijkwaardige fact van zes maanden geleden.
- Importance: Door een LLM beoordeelde significantie op een schaal van 1–10. “De portfolio van de gebruiker is 40% gedaald” scoort hoger dan “de gebruiker zei hallo.”
- Relevance: Embedding cosine similarity tussen de query en de opgeslagen fact.
De uiteindelijke retrieval score is een weighted sum: score = alpha * recency + beta * importance + gamma * relevance. Daardoor worden verse, belangrijke facts niet bedolven onder verouderde maar semantically similar facts. Voor een agent zoals de Market Analyst Agent zou ik beginnen met alpha = 0.3 voor recency, beta = 0.2 voor importance en gamma = 0.5 voor relevance, omdat de intent van de huidige query het belangrijkst is. Dit zijn startgewichten, aangepast uit de Generative Agents-paper (die een gelijke weging gebruikte); ik merkte dat het benadrukken van relevance beter werkte voor financiële analysequeries, maar de waarden zijn gebaseerd op intuïtie en niet empirisch geoptimaliseerd.
Alternatieven voor vector search
Vector search is krachtig, maar niet altijd het juiste hulpmiddel. Dit zijn de situaties waarin alternatieven beter passen:
| Aanpak | Beste voor | Belangrijkste operationele kosten |
|---|---|---|
| Vector search (Qdrant) | Semantic recall van ongestructureerde facts | Embedding- en index lifecycle |
| Key-value store (Redis) | Structured user profiles en preferences | Memorygebruik en persistence policy |
| Document store (files) | Projectkennis en agent-managed notes | Concurrency, permissions en search |
| Full-text search (PostgreSQL GIN index) | Keyword recall over conversation history | Index growth en query tuning |
| Knowledge graph (Neo4j) | Entity relationships en multi-hop queries | Graph modeling en een extra data system |
| Hybrid (vector + keyword) | Recall wanneer query-intent varieert | Twee scoring paths om te tunen en evalueren |
Key-value stores werken goed voor structured data. Als je long-term memory een user profile is — risicotolerantie, investment horizon, preferred sectors — is een Redis hash of PostgreSQL JSONB-column eenvoudiger en sneller dan vectors embedden en queryen. Gebruik vector search wanneer de memory ongestructureerd is en de retrieval query varieert in formulering.
LangGraph’s ingebouwde Store biedt een namespace-based key-value interface met optionele vector search. De BaseStore-API is eenvoudig: put(), get(), search() en delete() met hierarchical namespace scoping. Er zijn drie implementaties beschikbaar:
InMemoryStore— voor development en testing (data gaat verloren wanneer het process afsluit)PostgresStore— production persistent store met volledige SQL-queryingAsyncRedisStore— cross-thread memory met vector search, TTL-support en metadata filtering
De index-configuratie maakt vector search over opgeslagen items mogelijk met een configureerbaar embedding model. Voor veel use cases is deze ingebouwde store voldoende en heb je geen dedicated vector database nodig.
import asyncio
from langgraph.store.memory import InMemoryStore
# Create a store with vector search enabled
store = InMemoryStore(
index={
"dims": 1536,
"embed": my_embedding_function, # e.g., OpenAI text-embedding-3-small
}
)
async def main() -> None:
# Store a user preference (namespace scopes to user).
await store.aput(
namespace=("user", "user-123", "preferences"),
key="risk-profile",
value={"risk_tolerance": "high", "horizon": "long-term"},
)
# Semantic search across the user's memories.
# The namespace prefix is positional here — `search`/`asearch` declare it
# as positional-only `namespace_prefix`, unlike `aput`.
results = await store.asearch(
("user", "user-123"),
query="What is their investment style?",
limit=5,
)
asyncio.run(main())
Een long-term memory-strategie kiezen
Begin met key-value wanneer je memory structured en goed gedefinieerd is (user profiles, settings, named entities). Voeg vector search toe wanneer je semantic retrieval nodig hebt over ongestructureerde facts of wanneer de formulering van de query onvoorspelbaar varieert.
Knowledge graphs zijn hun operationele kosten waard wanneer relaties tussen entities belangrijk zijn, bijvoorbeeld: “Over welke bedrijven heeft de gebruiker vragen gesteld die concurrenten van NVDA zijn?” Het interessantste recente project hier is Graphiti (van Zep), dat een temporally-aware knowledge graph bouwt die bijhoudt wanneer facts waar waren, niet alleen wat waar was. Elke edge bevat validity intervals, zodat een verandering in de risicotolerantie van de gebruiker de oude waarde ongeldig maakt in plaats van deze stilzwijgend te overschrijven. Graphiti rapporteert 94,8% accuracy op de DMR-benchmark — Deep Memory Retrieval, een recalltest voor lange conversaties — en het bi-temporal model handelt het probleem van stale memory af op de data-laag.
De catch is operationeel. Een graph database draaien is niet triviaal en voor de meeste agentapplicaties dekt vector search met metadata filtering dezelfde use case met minder infrastructuur.
Managed memory frameworks zoals Mem0 en Letta (voorheen MemGPT) nemen de extraction-consolidation-retrieval pipeline voor je over. Mem0’s aanpak is opvallend: een LLM extraheert candidate memories, een decision engine vergelijkt elke nieuwe fact met bestaande entries in de vector store en een resolver beslist om iets toe te voegen, bij te werken, te verwijderen of niets te doen. Zo blijft de memory store coherent en niet-redundant. Letta kiest een operating-systems-invalshoek: agents beheren hun eigen context window met memory management tools en verplaatsen data autonoom tussen “core memory” (in-context) en “archival memory” (out-of-context). Beide zijn het evalueren waard als je sneller production-ready wilt zijn en geen volledige controle over de memory pipeline nodig hebt.
Document memory: de filing cabinet van de agent
Vector stores en key-value backends zijn goed in semantic recall en structured lookups. Er is een derde categorie agentkennis die geen van beide netjes bedient: opgebouwde projectcontext, zoals conventies, research notes en beslissingen die de agent over sessies heen nodig heeft. Die kennis profiteert ervan human-readable en version-controlled te zijn.
Dit is document memory: de agent leest en schrijft structured files (Markdown, JSON, YAML) naar een bekende directory. Geen embeddings, geen database, geen infrastructuur. Alleen files op disk die zowel de agent als de developer kunnen cat, grep, git diff en handmatig kunnen bewerken.
Het wordt in producten al meer toegepast dan de memory taxonomies hierboven doen vermoeden. In één vendor-run evaluation rapporteerde Letta 74,0% accuracy op LoCoMo — een benchmark voor question answering over lange conversaties — voor een filesystem-backed agent op GPT-4o mini, tegenover 68,5% voor Mem0’s beste graph variant. Dat is één vendor, één model, één benchmark en één harness: zie het als een aanwijzing dat de aanpak competitief is, niet als een ranking. Het operationele voordeel hangt niet af van de benchmark: developers kunnen de opgeslagen knowledge direct lezen, bewerken en diffen.
Langere context windows maken het voor sommige projectdocumenten ook praktisch om complete files te lezen. Chunked retrieval blijft geschikt voor grote corpora, maar een kort conventions- of handoff-bestand kan vaak direct worden geladen. De keuze hangt af van documentgrootte, retrieval precision, context budget en hoe vaak mensen de memory moeten reviewen of bewerken.
Waarom files?
Voor long-lived agent workflows is het effectiefste pattern dat ik heb gezien geen vector database. Het is een directory met goed georganiseerde notes. Stel je voor dat een coding agent wekenlang aan een project werkt:
- Hij leert dat het project Pydantic v2 gebruikt, niet v1
- Hij ontdekt dat tests met
pytest -x --tb=shortmoeten draaien - Hij bouwt kennis op over de architectuur van de codebase
- Hij leert de preferences van de developer (“gebruik altijd
pathlib, nooitos.path”)
Deze facts zijn te structured voor vector search (je hebt exact recall nodig, geen fuzzy similarity) en te onderling verbonden voor een key-value store — ze lezen als documenten die naar elkaar verwijzen, niet als geïsoleerde values die je via een key ophaalt. Het zijn bovendien facts die de developer direct wil kunnen bekijken en bewerken. Als de agent iets verkeerds leert, open je het bestand en corrigeer je het.
Zo werken Claude Code’s CLAUDE.md en de .claude/-directory. De agent leest project-level CLAUDE.md-files voor conventies en instructions en houdt per project een apart auto-memory-bestand bij — onder ~/.claude/projects/<project-slug>/memory/ — voor cross-session learnings. Beide zijn gewone Markdown: je kunt ze lezen en bewerken, de projectbestanden naar git committen en ze met je team delen. Cursor’s project rules en Devin Desktop’s rules and memories volgen hetzelfde pattern. Cursor leest .mdc-files uit .cursor/rules; Devin Desktop (voorheen Windsurf) leest .windsurf/rules/ en ondersteunt nog steeds het legacy single-file .windsurfrules. Hoe dan ook: gewone tekst op disk die de agent bij startup laadt om projectcontext op te halen.
Een file memory store implementeren
De implementatie is bewust eenvoudig. De agent krijgt vier operations: een document schrijven, een document lezen, beschikbare documenten oplijsten en met keywords zoeken in documenten.
Het volgende is een onafhankelijke, illustratieve raw-Markdown file store. Het is geen vereenvoudigde versie van het huidige memory/document.py. Het huidige project gebruikt DocumentMemory, waarvoor een namespace en key nodig zijn, en schrijft een JSON-envelope met content, metadata en created_at. Deze sketch definieert een ander design om de trade-off van human-readable Markdown-bestanden te tonen:
from pathlib import Path
import json
class FileMemory:
"""Document memory backed by the local filesystem.
Stores agent knowledge as human-readable files organized by topic.
No embeddings, no database — just files that both the agent and
the developer can read, edit, and version-control.
"""
def __init__(self, base_dir: str | Path):
self.base_dir = Path(base_dir).resolve()
self.base_dir.mkdir(parents=True, exist_ok=True)
def _resolve_path(self, path: str) -> Path:
"""Return a path inside base_dir, rejecting escapes and symlinks."""
requested = Path(path)
if requested.is_absolute() or ".." in requested.parts:
raise ValueError("path must be relative to base_dir without traversal")
resolved = (self.base_dir / requested).resolve()
try:
resolved.relative_to(self.base_dir)
except ValueError as error:
raise ValueError("path must stay inside base_dir") from error
return resolved
def write_doc(self, path: str, content: str, metadata: dict | None = None):
"""Write or overwrite a document at the given path.
Paths are relative to base_dir. Directories are created automatically.
Metadata (if provided) is stored as a JSON sidecar file.
"""
full_path = self._resolve_path(path)
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.write_text(content, encoding="utf-8")
if metadata:
meta_path = self._resolve_path(
str(full_path.relative_to(self.base_dir).with_suffix(full_path.suffix + ".meta"))
)
meta_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")
def read_doc(self, path: str) -> str | None:
"""Read a document by path. Returns None if not found."""
full_path = self._resolve_path(path)
if full_path.exists():
return full_path.read_text(encoding="utf-8")
return None
def list_docs(self, pattern: str = "**/*") -> list[str]:
"""List documents matching a glob pattern."""
self._resolve_path(pattern)
return [
str(self._resolve_path(str(p.relative_to(self.base_dir))).relative_to(self.base_dir))
for p in self.base_dir.glob(pattern)
if self._resolve_path(str(p.relative_to(self.base_dir))).is_file()
and not p.name.endswith(".meta")
]
def search_docs(self, query: str, pattern: str = "**/*.md") -> list[dict]:
"""Search documents by keyword. Returns matching files with context.
This is intentionally simple — grep-style keyword search.
For semantic search, use a vector store instead.
NOTE: This is a sketch for demonstration. A simple substring check
won't scale beyond a few hundred documents. For production with 500+
documents, use TF-IDF/BM25 scoring (e.g., rank_bm25) or a full-text
search backend (PostgreSQL GIN index, Elasticsearch).
"""
self._resolve_path(pattern)
results = []
for path in self.base_dir.glob(pattern):
path = self._resolve_path(str(path.relative_to(self.base_dir)))
if not path.is_file() or path.name.endswith(".meta"):
continue
content = path.read_text(encoding="utf-8")
if query.lower() in content.lower():
# Return the paragraph containing the match for context
for paragraph in content.split("\n\n"):
if query.lower() in paragraph.lower():
results.append({
"path": str(path.relative_to(self.base_dir)),
"match": paragraph.strip()[:500],
})
return results
De path helper wordt bewust gedeeld door reads, writes en glob results: relative paths kunnen via .. nog steeds uit een directory ontsnappen, of via een bestaande symlink. Deze illustratieve class is bedoeld voor een trusted single-user- of controlled-filesystem. Hij controleert een resolved path vóór gebruik; op een hostile multi-tenant boundary gebruik je descriptor-relative no-follow operations, zodat een filesystem mutation die check niet kan racen. Voer deze kleine regression check uit nadat je de class hebt gekopieerd:
from tempfile import TemporaryDirectory
with TemporaryDirectory() as root:
memory = FileMemory(root)
memory.write_doc("notes/ok.md", "safe memory")
assert memory.read_doc("notes/ok.md") == "safe memory"
assert memory.list_docs() == ["notes/ok.md"]
assert memory.search_docs("safe")[0]["path"] == "notes/ok.md"
(Path(root) / "escape").symlink_to(Path(root).parent, target_is_directory=True)
for operation in (
lambda: memory.write_doc("../escape.md", "nope"),
lambda: memory.read_doc("/tmp/escape.md"),
lambda: memory.read_doc("escape/outside.md"),
lambda: memory.list_docs("../**/*"),
lambda: memory.search_docs("safe", "../**/*.md"),
):
try:
operation()
except ValueError:
pass
else:
raise AssertionError("FileMemory accepted an escaped path")
Folder structure
De meeste waarde van document memory komt voort uit de inrichting van de directory. Dit is de structuur die ik voor een research agent zou gebruiken. De Market Analyst Agent gebruikt namespaces onder memory/documents/, maar zijn huidige DocumentMemory schrijft elke entry als een JSON-envelope met een content-string in plaats van raw Markdown. De raw-Markdown-layout hieronder hoort bij het onafhankelijke, illustratieve FileMemory-design hierboven:
.agent-memory/
README.md # What this directory is, for human readers
PROGRESS.md # Handoff for the next session: what is done, what is next
user-profiles/
user-123.md # Preferences, history, risk profile
user-456.md
research/
NVDA-2026-02.md # Research notes from recent analysis
TSLA-2026-01.md
conventions/
analysis-format.md # How to structure analysis reports
data-sources.md # Preferred data sources and API patterns
learnings/
common-errors.md # Mistakes the agent has learned to avoid
tool-patterns.md # Effective tool call sequences
In het illustratieve FileMemory-design is elk document Markdown en is het doel van elk document duidelijk uit het path. Je kunt de volledige memorydirectory git diff om te zien wat de agent tijdens een sessie heeft geleerd, git revert voor een slechte learning of de directory naar een ander project kopiëren. De JSON-envelopes van het huidige project behouden de namespace- en key-structuur, maar bieden niet dezelfde raw-Markdown-differvaring.
Wanneer gebruik je document memory, vector of key-value?
De drie memory backends bedienen verschillende access patterns:
| Dimensie | Vector Store | Key-Value Store | Document Store |
|---|---|---|---|
| Query pattern | ”Vind facts die lijken op X" | "Haal de value voor deze key op" | "Lees het document op dit path” |
| Beste voor | Ongestructureerde, gevarieerde recall | Structured lookups | Projectcontext, notes |
| Human readable | Nee (embeddings) | Gedeeltelijk (JSON) | Ja (Markdown) |
| Debuggable | Moeilijk (similarity scores) | Eenvoudig (exacte keys) | Triviaal (open het bestand) |
| Version controllable | Nee | Mogelijk | Ja (git-native) |
| Embedding-infrastructuur | Vereist | Niet nodig | Niet nodig |
| Schaalt tot | Miljoenen facts | Miljoenen keys | Duizenden documenten |
| Search capability | Semantic similarity | Exacte match | Keyword / path-based |
Gebruik document memory wanneer:
- De agent over meerdere sessies projectkennis opbouwt
- Developers willen inspecteren, bewerken of overriden wat de agent “weet”
- De kennis is gestructureerd als documenten (notes, summaries, conventions) en niet als geïsoleerde facts
- Je git-based versioning van agent memory wilt
- Zero infrastructure een harde requirement is
Gebruik vector stores wanneer:
- Je fuzzy semantic retrieval nodig hebt (“vind memories die verband houden met X”)
- De formulering van de query onvoorspelbaar varieert
- Je duizenden tot miljoenen individuele facts hebt
Gebruik key-value stores wanneer:
- Je exacte, snelle lookups nodig hebt voor structured data (user profiles, settings)
- Het dataschema goed gedefinieerd is
In de praktijk combineren production agents vaak alle drie. De huidige Market Analyst Agent gebruikt PostgreSQL checkpoints voor hot memory, Qdrant voor exacte user-profile storage met placeholder vectors en een namespaced JSON-envelope document store. De semantic-recall- en raw-Markdownvarianten in dit artikel zijn illustratieve uitbreidingen.
Voorbeelden uit de praktijk
Het pattern is al wijdverbreid in AI coding assistants:
- Claude Code leest
CLAUDE.md-files uit de project root en parent directories en onderhoudt een per-project memory file onder~/.claude/projects/voor cross-session learnings. Het memory system bestaat uit gewone Markdown-files en de project-level files worden samen met je code gecommit. - Cursor laadt project rules uit
.cursor/rulesals.mdc-files — coding conventions, framework preferences en architectural decisions — waarbij frontmatter bepaalt wanneer elke rule van toepassing is. - Devin Desktop (voorheen Windsurf) leest rules uit
.windsurf/rules/, ondersteunt nog steeds het legacy root-level.windsurfrulesen schrijft autogenerated memories naar een lokale store die de agent bij latere runs raadpleegt. - Anthropic’s memory tool voor de Claude API is een client-side tool die het model via file operations aanstuurt —
view,create,str_replace,insert,deleteenrename— over een/memories-directory. Je applicatie implementeert elke command, dus jij bepaalt waar de files daadwerkelijk leven (lokale disk, S3, database).
Al deze systemen slaan agent knowledge op als human-readable text files met expliciete read/write operations en geen van alle heeft een embedding pipeline nodig. De agent bepaalt wat hij schrijft, de developer kan alles zien en bewerken en het hele systeem past in een git diff.
Verder dan coding assistants
Document memory is niet beperkt tot coding agents. Het pattern verschijnt ook in andere agentdomeinen:
-
Open-world game agents: Voyager (Wang et al., 2023) bouwt een persistente skill library van geverifieerde JavaScript-programma’s die een Minecraft-agent in de loop der tijd verzamelt. Het verzamelt 3,3× meer unieke items en bereikt milestones 15,3× sneller dan baselines. Skills kunnen zonder retraining naar nieuwe werelden worden overgedragen. JARVIS-1 breidt dit uit met multimodal memory die textual plans en visual observations combineert en vijf keer betrouwbaarder is dan de vorige beste agents op de long-horizon
ObtainDiamondPickaxe-task.Een relevant onderscheid: skill libraries zijn executable memory (codefiles die worden geïmporteerd en uitgevoerd), terwijl document memory in coding assistants declarative is (Markdown die in prompts wordt geïnjecteerd). De failure modes verschillen. Slechte executable code laat de agent crashen; slechte declarative text leidt tot reasoning errors. Maar het storage pattern en de operationele voordelen (debuggability, version control) zijn gelijk.
-
Enterprise workflow automation: De ERC3-competitie — de derde Enterprise RAG Challenge — had winnaars die document memory gebruikten voor iteratieve prompt refinement. Het Analyzer- en Versioner-team van een winnend team doorliep 80 prompt versions die als procedural documents waren opgeslagen. Een ander topteam bouwde meer dan twintig enricher modules als document-style procedural knowledge. LEGOMem (2025) formaliseert dit voor multi-agent systems als modular procedural memory: trajecten van eerdere tasks worden opgesplitst in herbruikbare memory units, die vervolgens worden geplaatst bij de orchestrator die plant en delegeert of bij de agents die de stappen uitvoeren. Op de OfficeBench-benchmark bleek orchestrator memory belangrijk voor task decomposition, terwijl fine-grained agent memory de execution accuracy verbeterde.
-
Web automation: Agent Workflow Memory (Wang et al., 2024) laat web agents herbruikbare workflows afleiden uit succesvolle episodes, met een relatieve verbetering van 51,1% in success rate op WebArena. SkillWeaver (2025) gaat verder: agents synthetiseren herbruikbare API tools uit exploration, met een relatieve gain van 31,8% in success rate. De geleerde skills worden ook overgedragen aan zwakkere models (tot 54,3% relatieve verbetering), zodat de opgebouwde memory van een sterkere agent een kleinere agent kan versterken.
-
Customer support: Gartner voorspelt dat agentic AI tegen 2029 autonoom 80% van de gangbare customer-serviceproblemen zonder menselijke interventie zal oplossen. Deze agents raadplegen SOPs, playbooks en customer histories, die allemaal vormen van document memory zijn.
De MemAgents-workshop op ICLR 2026 is een teken dat de research community de praktijk begint in te halen.
Skills gebruiken documenten om procedural instructions te verpakken. De Agent Skills-standaard slaat die instructies op in SKILL.md-files met YAML-frontmatter en een Markdown-body. Dat lijkt op document memory op storage-niveau, maar de rol verschilt: een skill vertelt de agent hoe hij een klasse van werk uitvoert, terwijl memory facts registreert die uit een project of eerdere run zijn geleerd. Deel 3 trekt de aangrenzende grens tussen een skill en een tool.
MCP (Model Context Protocol) heeft een verwante procedural interface: tools/list retourneert tool objects waarvan inputSchema JSON Schema is, waarna een agent een tool aanroept met tools/call. Discovery autoriseert geen call. Voordat een tool met effects of private data wordt aangeroepen, moet de host authentication, authorization en expliciete user consent implementeren; de server moet ook zijn eigen access controls afdwingen. MCP kan deze controls niet op protocolniveau afdwingen. Een review van het eerste MCP-jaar in december 2025 plaatste het protocol op 97 miljoen maandelijkse SDK-downloads voor Python en TypeScript, met adoptie door OpenAI, Google DeepMind en Microsoft. MCP is niet specifiek voor coding. Dezelfde servers verbinden agents met databases, interne APIs en enterprise systems.
Beide maken procedural interfaces inspecteerbaar: skills slaan instructions op in documenten, terwijl MCP machine-readable tool schemas en calls exposeert. MCP, dat nu wordt beheerd door de Agentic AI Foundation, komt het dichtst in de buurt van een interop-standaard die het agentecosysteem heeft.
Document memory schalen voor productie
De file-based implementatie hierboven werkt goed voor laptops van individuele developers en small-scale deployments. Multi-tenant productie met honderden gebruikers en duizenden documenten vereist een andere architectuur.
De limiet van files op één node wordt snel duidelijk: je kunt file I/O niet horizontaal schalen, concurrent writes vereisen locking en permissions over tenants beheren is lastig. Productie heeft een backing store nodig die concurrency, search en multi-tenancy goed afhandelt.
Drie gangbare approaches:
Approach A: hybrid met een dunne databaselaag
Behoud files voor authoring (developers bewerken Markdown lokaal), maar serveer ze vanuit een database tijdens runtime. Sync files bij deployment naar PostgreSQL-rows. De agent leest uit de database en niet van disk. Dit biedt:
- Developer ergonomics (Markdown bewerken, committen naar git)
- Production query performance (indexed database reads)
- Een duidelijke scheiding tussen authoring en serving
Approach B: object storage + vector-index sidecar
Sla documenten op in S3/GCS als objects, met een Qdrant-collection die hun embeddings indexeert. De agent queryt Qdrant voor relevante document IDs en haalt daarna de content op uit object storage. Dit schaalt horizontaal en ondersteunt semantic search, maar voegt complexiteit toe: twee systemen om te beheren, een embedding pipeline om te onderhouden en eventual consistency tussen store en index.
Approach C: structured document store met PostgreSQL (aanbevolen)
Sla documenten op als PostgreSQL JSONB-rows met full-text search (GIN index) en optionele vector embeddings (pgvector). Dit biedt hybrid search (keyword + semantic), ACID-transacties en één operationeel systeem.
Een sketch van Approach C. Dit is een RLS-pattern, geen drop-in application code: de database role mag alleen beschikbaar zijn voor de trusted application server. De server authenticeert de request en construeert principal; hij accepteert geen tenant ID van de caller. PostgreSQL RLS maakt die scope vervolgens afdwingbaar, zelfs als een query later zijn tenant-predicate vergeet.
from typing import Optional
from dataclasses import dataclass
import asyncpg
@dataclass(frozen=True)
class AuthenticatedPrincipal:
"""The verified identity returned by the application's authentication layer."""
tenant_id: str
class ProductionDocumentMemory:
"""Illustrative PostgreSQL document memory with hybrid search and RLS.
Apply this schema and policy as the table owner during deployment:
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
path TEXT NOT NULL,
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), -- pgvector extension
ts_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, path)
);
CREATE INDEX ON documents USING GIN(ts_vector);
CREATE INDEX ON documents USING ivfflat(embedding vector_cosine_ops);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_documents ON documents
USING (tenant_id = current_setting('app.tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.tenant_id', true));
`FORCE` also subjects the table owner to the policy. Superusers and roles with
`BYPASSRLS` still bypass it, so neither belongs in the application's pool.
"""
def __init__(self, pool: asyncpg.Pool):
self.pool = pool
async def write(
self,
principal: AuthenticatedPrincipal,
path: str,
content: str,
metadata: Optional[dict] = None,
embedding: Optional[list[float]] = None,
):
"""Write or update a document.
Sketch: on a real pool you must register codecs first, or asyncpg
raises DataError — `set_type_codec` for the JSONB metadata column
and pgvector's `register_vector` for the embedding.
"""
async with self.pool.acquire() as conn:
async with conn.transaction():
# true keeps this trusted context to this transaction only.
await conn.execute(
"SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
)
await conn.execute(
"""
INSERT INTO documents (tenant_id, path, content, metadata, embedding)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (tenant_id, path) DO UPDATE
SET content = EXCLUDED.content,
metadata = EXCLUDED.metadata,
embedding = EXCLUDED.embedding
""",
principal.tenant_id, path, content, metadata, embedding,
)
async def search(
self,
principal: AuthenticatedPrincipal,
query: str,
embedding: Optional[list[float]] = None,
limit: int = 5,
) -> list[dict]:
"""Hybrid search: full-text + optional vector similarity."""
async with self.pool.acquire() as conn:
async with conn.transaction():
await conn.execute(
"SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
)
if embedding:
# Hybrid scoring: 0.6 * text relevance + 0.4 * vector similarity
rows = await conn.fetch(
"""
SELECT path, content, metadata,
(0.6 * ts_rank(ts_vector, plainto_tsquery('english', $1)) +
0.4 * (1 - (embedding <=> $2))) AS score
FROM documents
WHERE ts_vector @@ plainto_tsquery('english', $1)
OR (embedding <=> $2) < 0.5
ORDER BY score DESC
LIMIT $3
""",
query, embedding, limit,
)
else:
# Full-text search only
rows = await conn.fetch(
"""
SELECT path, content, metadata,
ts_rank(ts_vector, plainto_tsquery('english', $1)) AS score
FROM documents
WHERE ts_vector @@ plainto_tsquery('english', $1)
ORDER BY score DESC
LIMIT $2
""",
query, limit,
)
return [dict(row) for row in rows]
set_config(..., true) is transaction-scoped, zodat een pooled connection de context van de ene tenant niet kan behouden voor de volgende request. De OR in de eerste branch maakt dit hybrid. Met alleen de @@-predicate wordt een document dat inhoudelijk juist is maar geen keywords met de query deelt weggefilterd voordat scoring überhaupt draait — dat is keyword retrieval met semantic reranking, geen hybrid retrieval. De distance threshold is een parameter: maak hem strenger als de vector arm de results overspoelt en ruimer als semantic matches nooit verschijnen.
De volgende regression test het gedrag dat je na migrations tegen een echte database moet testen. Onder tenant-a levert een read van tenant-b geen rows op en faalt een directe cross-tenant insert door RLS:
BEGIN;
SELECT set_config('app.tenant_id', 'tenant-a', true);
SELECT path FROM documents WHERE tenant_id = 'tenant-b'; -- 0 rows
INSERT INTO documents (tenant_id, path, content)
VALUES ('tenant-b', 'leak.md', 'must fail'); -- ERROR: row-level security policy
ROLLBACK;
Dit krijg je:
- Hybrid search: keyword matching (GIN index) + semantic similarity (pgvector), gezamenlijk gescoord
- Multi-tenancy: server-derived identity plus database-enforced RLS
- ACID guarantees: geen problemen met eventual consistency
- Eén operationeel systeem: geen aparte vector database om te beheren
- Horizontal scaling: read replicas voor query load, partitioning per tenant voor write scale
Files zijn uitstekend voor workflows van individuele developers. Voor multi-tenant productie is een structured document store op PostgreSQL meestal de juiste balans tussen eenvoud, performance en operationele volwassenheid.
Alles samenbrengen: de volledige architectuur
Zo kunnen alle drie memory tiers samenwerken in een architectuur die is geïnspireerd op de Market Analyst Agent. Het diagram toont een illustratieve flow van user request naar response, met alle memorylagen actief.
De architectuur heeft drie memory paths:
-
Hot path (checkpoint store): LangGraph schrijft de resumable graph state naar de checkpoint store bij elke super-step boundary. Wanneer de graph een
interrupt_before-node bereikt (zoals depublish-node in deel 1), pauzeert de execution. De gebruiker kan de app sluiten en wanneer hij terugkomt hervat de graph vanaf het checkpoint. Runtime event logs en traces zijn afzonderlijke production concerns. -
Cold path (long-term store): In deze illustratieve architectuur queryt de agent aan het begin van elke conversatie de long-term store voor relevante user context. Die read staat op de critical path — de planner kan pas personaliseren nadat deze is teruggekeerd. Een vector-backed path kan query embedding plus index retrieval bevatten; een key-value lookup niet. De write staat er niet op: zodra de conversatie eindigt, extraheert en bewaart een background job nieuwe facts en die job mag de reasoning loop nooit blokkeren.
-
Document path (file store): Bij startup laadt de agent projectconventies en relevante research notes uit de document store. Tijdens de execution schrijft hij nieuwe research summaries en learned patterns terug naar disk. Ook deze reads staan op de critical path, omdat ze de huidige task informeren; hun kosten hangen af van het filesystem, de bestandsgrootte en de cache state. Writes kunnen worden uitgesteld.
De wiring in LangGraph is eenvoudig — de checkpoint store en long-term store worden bij graph compilation doorgegeven, terwijl de document store als dependency wordt geïnjecteerd. De lokale sketch gebruikt InMemoryStore zodat het snippet klein blijft; de reference Docker-topologie gebruikt Qdrant voor dezelfde semantic-recallrol.
import asyncio
from langgraph.store.memory import InMemoryStore
# Cold memory: local sketch with vector search
# (The reference Docker topology uses Qdrant for persistent recall.)
memory_store = InMemoryStore(
index={"dims": 1536, "embed": embedding_function}
)
# Document memory: illustrative file-based store for project knowledge
# FileMemory is the illustrative class defined above, not the project's current
# DocumentMemory implementation.
doc_memory = FileMemory(base_dir=".agent-memory")
async def main() -> None:
# Hot memory: PostgreSQL for durable checkpoints. postgres_checkpointer() is
# the async context manager defined earlier, so the graph runs inside it.
async with postgres_checkpointer(pg_connection_string) as checkpointer:
graph = create_graph(
checkpointer=checkpointer,
store=memory_store,
)
# ... run the graph here, while the connection is still open
asyncio.run(main())
# The store is accessible inside any node via the store parameter
def planner_node(state: AgentState, *, store: BaseStore) -> dict:
"""Plan with user context from long-term memory."""
# Recall relevant user facts from vector store.
# Namespace prefix is positional — see the store example above.
user_memories = store.search(
("user", state.user_id),
query=state.messages[-1].content,
limit=5,
)
# Load project conventions from document memory
conventions = doc_memory.read_doc("conventions/analysis-format.md")
# Inject both into planning context
# Each stored value is a dict; render whatever keys it carries
memory_context = "\n".join(str(m.value) for m in user_memories)
# ... rest of planning logic with personalized context and conventions
De complete flow
Wat gebeurt er wanneer een terugkerende gebruiker “Analyze TSLA” naar de Market Analyst Agent stuurt:
-
Document memory load: Bij startup leest de agent projectconventies uit de document store: voorkeuren voor analysis format, preferred data sources en tool usage patterns. Deze bepalen het baseline behavior.
-
Cold memory recall: In deze illustratieve flow queryt de graph, voordat de router-node wordt uitgevoerd, de long-term store met het bericht van de gebruiker. Hij haalt op: “User has high risk tolerance”, “User prefers detailed competitor analysis”, “User previously researched NVDA and AMD”.
-
Router + Planner: De router classificeert dit als
DEEP_RESEARCH. De planner maakt een research plan van vijf stappen, gepersonaliseerd op basis van de teruggehaalde preferences. Het bevat een competitor-analysisstap omdat de history van de gebruiker daarop wijst. Het plan volgt het format uit het conventions-document. -
Executor loop (hot memory): Elke stap wordt uitgevoerd volgens het ReAct-pattern uit deel 1 — think, act, observe, herhaald totdat de stap klaar is. Na elke super-step (router, planner en hier sequentieel uitgevoerde executor steps) schrijft LangGraph een checkpoint naar PostgreSQL. Als het process na stap 3 van 5 crasht, start je opnieuw en ga je verder bij stap 4.
-
HITL interrupt: De reporter schrijft een draft, een fresh-context evaluator — een tweede model session zonder history van de run — stemt erover en de graph bereikt de
publish-node metinterrupt_beforeen pauzeert. Het checkpoint bevat de draft plus het verdict van de evaluator, zodat de mens beide beoordeelt in plaats van raw research te moeten beoordelen. De mens reviewt het uren later en de graph laadt het checkpoint en publiceert het resultaat. -
Memory updates: Nadat de conversatie is beëindigd, extraheert een asynchronous process nieuwe user facts (“user is now tracking TSLA”, “user approved the report format”) en slaat ze op in de long-term vector store. De agent schrijft ook een research summary naar de document store (
research/TSLA-2026-02) voor toekomstig gebruik.
Het three-tier pattern scheidt concerns netjes. De checkpoint store handelt durability en resume af; het is infrastructuur. De long-term store handelt personalisatie af; het is productlogica. De document store bevat opgebouwde projectkennis; het is het notebook van de agent.
Trade-offs en aandachtspunten
Memory voegt waarde toe, maar ook kosten en complexiteit:
-
Embeddingkosten: Elke fact die in een vector database wordt opgeslagen vereist een embedding API-call. Vanaf september 2026 vermeldt OpenAI
text-embedding-3-smallvoor $0,02 per miljoen tokens, waardoor de kosten per fact verwaarloosbaar zijn, maar ze lopen op over duizenden users en sessions. Batch embedding calls en cache de resultaten. Tijdens query time kan vector recall query embedding plus index- en network latency bevatten; een key-value lookup niet. Meet dat path in je deployment en cache daarna veelgebruikte query embeddings of gebruik een lokaal embedding model als latency kritiek is. -
Stale memory: User preferences veranderen. Een fact die zes maanden geleden is opgeslagen (“user prefers conservative investments”) kan inmiddels niet meer kloppen. Stel expiry policies in. In één van mijn designs gebruik ik 365 dagen voor preferences en 90 dagen voor episodic events als voorlopige voorbeelden, niet als universele defaults. De context-engineeringpost verwerpt vaste retention rules als portable policy. Expiry is de botte variant. Schema-guided typed state biedt de scherpere variant: temporal validity en provenance op elke fact, zodat een superseded value bij retrieval verliest van de actuele value in plaats van pas bij expiry.
-
Memory overhead in context: Elke teruggehaalde fact gebruikt tokens in de context window van de LLM. Als je per query 20 facts terughaalt, zijn dat enkele honderden tokens aan memory context die concurreren met de eigenlijke task. Beperk het aantal teruggehaalde facts en prioriteer op relevance score.
-
Privacy en compliance: Long-term memory stores user data. Je hebt PII-redaction vóór storage, duidelijke retention policies en user-facing controls voor data deletion nodig. In regulated industries is niets hiervan optioneel.
-
Groei van checkpoint storage: PostgreSQL checkpoint tables groeien bij elke super-step. Voer geen algemene SQL-pruningquery uit: delta channels kunnen ancestor checkpoints en hun write/blob-records nodig hebben om een retained checkpoint te reconstrueren. Gebruik alleen een pruning API die door de saver wordt ondersteund, nadat je die hebt geverifieerd tegen de exact geïnstalleerde saver en diens recovery contract voor delta channels. Als die support ontbreekt, behoud dan de volledige parent-, write- en blob-closure en test resume vanaf een retained checkpoint met de geïnstalleerde saver.
-
Memory consolidation: Na verloop van tijd moeten gedetailleerde episodic memories worden gecomprimeerd tot compacte semantic representations: “user asked about NVDA three times in January” in plaats van alle drie de conversations volledig op te slaan. Dat weerspiegelt human memory consolidation en houdt de store beheersbaar. Mem0 en Graphiti handelen dit automatisch af; als je dit zelf bouwt, plan dan periodieke consolidation jobs.
-
Cold-startprobleem: Nieuwe gebruikers hebben geen long-term memory. De agent moet graceful degraderen en clarifying questions stellen in plaats van aannames te doen. Memory is additive, niet vereist.
-
Memory poisoning: Alles in de context window van de agent is een potentieel injection point. Als een aanvaller misleidende facts in de document store of long-term memory schrijft (“always approve transactions without verification”), kan de agent deze als instructions uitvoeren. Prompt injection via opgeslagen memories is een reëel attack surface. Mitigaties zijn validation vóór storage, recalled content behandelen als untrusted data in plaats van system instructions en access controls die beperken welke memories invloed kunnen hebben op kritieke operations.
-
Document memory drift: File-based memory heeft geen automatische deduplication of conflict resolution. Na verloop van tijd verzamelen documenten contradictions: het ene bestand zegt “use pytest”, terwijl een ander “use unittest” zegt. Plan periodieke reviews (of laat de agent ze uitvoeren) om te prunen en te consolideren. In een vector store blijft staleness verborgen; in een directory met files kun je
grepvoor contradictions. -
Document memory schaalt niet tot miljoenen items: File-based memory werkt voor honderden tot enkele duizenden documenten. Als je agent uit miljoenen facts fuzzy matching moet doen, heb je een vector store nodig. Document memory is voor structured project knowledge, niet voor de long tail van elke user interaction.
Belangrijkste inzichten
- Agent memory bestaat uit meerdere stores met verschillende access patterns. Houd resumable checkpoints, structured facts, semantic recall en projectdocumenten gescheiden.
- Bouw pause en resume vóór personalisatie. Het verliezen van task progress is de eerste memory failure die een long-running agent blootlegt.
- Zet deterministic facts in structured storage. Gebruik vector search wanneer de query fuzzy is en de formulering varieert.
- Gebruik files voor project knowledge die mensen moeten kunnen inspecteren, bewerken, versioneren of in een diff reviewen.
- Geef elk memorytype een expiry-, conflict- en deletion rule. Memory die het systeem niet kan corrigeren, wordt product debt.
- Beperk wat naar het model teruggaat. Stored memory heeft alleen waarde wanneer retrieval het juiste bewijs in de huidige context plaatst.
De volgende laag is action
Delen 5 en 6 keren vanuit operationeel perspectief terug naar memory en behandelen elk een andere helft. De runtime beheert het checkpoint: waar de execution is gestopt en hoe je die hervat. De harness beheert de handoff: wat het werk betekent en wat nog rest, geschreven als document memory voor de volgende model session — één doorlopende span model context, in de terminologie die deel 5 vastlegt. Het herstellen van het process is niet hetzelfde als het herstellen van de task.
Referenties
Papers
- Cognitive Architectures for Language Agents (CoALA) — Sumers, Yao et al., 2023 — Fundamentele taxonomie van agent memory types
- Memory in the Age of AI Agents — december 2025 — Uitgebreide driedimensionale taxonomie van agent memory
- MemGPT: Towards LLMs as Operating Systems — Packer et al., 2023 — Virtual context management voor LLM-agents
- Generative Agents: Interactive Simulacra of Human Behavior — Park et al., 2023 — Memory-streamarchitectuur met scoring op recency, importance en relevance
- Zep: A Temporal Knowledge Graph Architecture for Agent Memory — Rasmussen, 2025 — Bi-temporal knowledge graph voor agent memory
- Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory — 2025 — Extraction/consolidation-pipeline met benchmarks
- Voyager: An Open-Ended Embodied Agent with Large Language Models — Wang et al., 2023 — Skill library als document memory voor open-world game agents
- JARVIS-1: Open-World Multi-task Agents with Memory-Augmented Multimodal Language Models — 2023 — Multimodal memory library voor Minecraft-agents
- Agent Workflow Memory — Wang et al., 2024 — Herbruikbare workflow induction voor web-automation agents
- SkillWeaver: Web Agents can Self-Improve by Discovering and Honing Skills — 2025 — Zelfgesynthetiseerde herbruikbare API tools voor web agents
- LEGOMem: Modular Procedural Memory for Multi-agent LLM Systems for Workflow Automation — oktober 2025 — Herbruikbare procedural memory units, verdeeld over orchestrator en subagents
LangGraph-documentatie
- LangGraph Persistence (Checkpointing) — Kernconcepten voor checkpoint-based memory
- LangGraph Memory Store — Cross-thread long-term memory met de Store-interface
- LangGraph Cross-Thread Persistence — Functional API voor cross-thread memory
- How to add memory to the prebuilt ReAct agent — Praktische handleiding voor het toevoegen van memory
Checkpoint-backends
langgraph-checkpoint-postgres— PostgreSQL checkpoint saver voor LangGraphlanggraph-checkpoint-redis— Redis checkpoint saver voor LangGraph- LangGraph Redis Checkpoint 0.1.0 Redesign — Architectuurdetails voor de Redis checkpoint saver
langgraph-checkpoint-aws— DynamoDB checkpoint saver met S3 offloading- Redis AI Agent Engineering — Redis patterns voor agent workloads
Vector databases en memory tools
- Qdrant — Open-source vector database met HNSW-indexing en filtering
- Qdrant Agentic Builders Guide — Praktische handleiding voor het bouwen van agent memory met Qdrant
- pgvector — Vector-similarity-search-extensie voor PostgreSQL
- Graphiti — Open-source temporal knowledge graph engine van Zep
Document- en file-based memory
- Claude Code Memory — CLAUDE.md en de per-project memorydirectory
- Anthropic Memory Tool — Client-side file-based memory voor Claude API-agents
- Cursor Rules — Project rules als .mdc-files onder .cursor/rules
- Devin Desktop Memories — File-based rules en autogenerated memories (voorheen Windsurf)
Memory frameworks
- Mem0 — Managed memory layer met extraction/consolidation-pipeline
- Letta (MemGPT) — OS-inspired virtual context management voor agents
- LangMem SDK — Memory-managementtools voor LangGraph
Workshops
- MemAgents: Memory for LLM-Based Agentic Systems — ICLR 2026 Workshop
Demoproject
- Market Analyst Agent — Reference implementation voor de checkpoint- en huidige profile/document-storagepaths