AI Agent-geheugenarchitectuur: checkpoints en vector stores
Automatische vertaling Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Artikelupdate
Oorspronkelijk gepubliceerd op 14 februari 2026. Beoordeeld en bijgewerkt op 6 september 2026. De update behandelt context compression, memory benchmarks en storage APIs, en verduidelijkt de verschillen tussen working state, checkpoints en long-term memory.
Een reasoning loop overleeft maar één request 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 session terughalen. Deel 1 behandelde de control flow. In dit artikel bepalen we welke state elke volgende beurt nodig heeft en waar die state moet worden opgeslagen.
Ik gebruik de Market Analyst Agent — een kleine LangGraph-agent die marktdata ophaalt en een analistenrapport schrijft — als basis voor de bespreking van hot checkpoints. De secties over cold vectors en raw Markdown zijn onafhankelijke illustratieve ontwerpen 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 staat voordat de harness ernaar grijpt. 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 zijn kennis tussen runs kan bijwerken. Een ontwerp kan checkpoints, semantic of structured stores en human-readable documenten gebruiken. Kies alleen de stores die nodig zijn voor de recall- en recoveryvereisten van het product.
| Behoefte | Beste default | Waarom |
|---|---|---|
| Eén run pauzeren en hervatten | PostgreSQL checkpoint store | Durable, queryable en eenvoudig te beheren naast app-data |
| Low-latency transient state | Redis checkpoint store | Snel hervatten en kortlevende state, met trade-offs rond persistence |
| Semantic recall over threads heen | 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 voorkeuren en IDs |
| Projectconventies en geleerde procedures | Markdown- of JSON-files | Human-readable, diffable en eenvoudig door agents bij te werken |
| Memory van relaties tussen meerdere entiteiten | Knowledge graph | Nuttig wanneer relaties belangrijker zijn dan afzonderlijke feiten |
Begin niet met memory omdat het intelligent klinkt. Begin met de zichtbare fout voor de gebruiker: voortgang verliezen, een voorkeur vergeten, onderzoek herhalen of een projectconventie niet opnieuw gebruiken.
De fouten waarvoor memory nodig is
Een stateless agent kan een geïsoleerde vraag beantwoorden, maar vergeet het request zodra de call eindigt. Dat ontwerp faalt wanneer het product een van de volgende gedragingen nodig heeft:
- Pauzeren en hervatten: een gebruiker start een researchtaak, sluit zijn laptop en komt de volgende dag terug. Zonder checkpointed state begint de agent opnieuw.
- Coherentie over meerdere turns: tijdens een lang gesprek moet de agent onthouden welke tools hij heeft aangeroepen, welke data hij heeft verzameld en welke stappen van het plan zijn 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 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 het request “Analyze NVDA” een plan, vijf tool calls, verzamelde data en een concept-rapport op. Wanneer de gebruiker antwoordt met “looks good, but add competitor analysis”, herstelt een checkpoint het plan en het onderzoek vanaf de laatst voltooide stap. Het toevoegen van de competitor-stap zou follow-up interpretation en replanning vereisen; de companion implementeert dat gedrag niet. Het checkpoint levert de eerdere state, terwijl de applicatie moet bepalen hoe het nieuwe request het plan wijzigt.
Long-term memory behandelt een ander geval. Als de gebruiker een week later terugkomt en vraagt: “Update my NVDA analysis”, moet de agent mogelijk een voorkeur voor conservatieve risicoanalyses en interesse in semiconductor-aandelen terughalen. Een vector-backed memory store kan die feiten over sessions 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 deze library trekt zijn generaliseerbaar naar elk framework. Zie een doorlopende conversatie van een gebruiker over “Analyze NVDA” als één thread. Elke keer dat de graph draait om die conversatie te beantwoorden of voort te zetten, is één run binnen die thread. Zolang een run actief is, vormen de context van het model en de lokale variabelen van het programma de working memory; die verdwijnen wanneer het werk stopt. LangGraph noemt state die voor die ene thread wordt opgeslagen short-term memory, en feiten die voor andere threads beschikbaar zijn long-term memory. Hieronder betekenen “thread” en “conversation” hetzelfde. Deel 5 gebruikt “session” voor de durable log van één run; daarom vermijd ik die term voor de conversatie.
Een taxonomie van AI agent memory
Voordat we de implementatie bespreken, 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 cognitieve wetenschap. In mijn context engineering-artikel 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-argumenten, huidige LLM-response | In-process (Python dict) |
| Short-term | Huidige thread | Minuten–uren | Conversatiegeschiedenis, planvoortgang, verzamelde data | Checkpoint store |
| Episodic | Over threads heen | Dagen–maanden | ”Vorige week vroeg de gebruiker naar NVDA-omzet” | Vector store / KV store |
| Semantic | Over threads heen | Maanden–permanent | ”De gebruiker verkiest conservatieve beleggingen” | Vector store / KV store |
| Document | Over threads heen | Dagen–permanent | Projectnotities, researchsamenvattingen, geleerde patronen | File store (Markdown/JSON) |
| Procedural | Systeembreed | Permanent | ”Controleer bij aandelenanalyse altijd SEC-filings” | Config / system prompt |
Working memory bevat de huidige observaties, opgehaalde feiten en tussentijdse resultaten die de actieve run gebruikt. Sommige daarvan staan in applicatievariabelen; de geselecteerde messages en tool results vormen de input van het model. Die input moet in de context window van het model passen, terwijl application state groter kan zijn en meerdere stappen kan blijven bestaan. Process memory gaat bij een crash verloren tenzij die expliciet wordt opgeslagen. De andere tiers leveren informatie voor deze working state.
Short-term memory is het checkpoint dat LangGraph na elke eenheid van graph execution schrijft — een super-step, die in de volgende sectie wordt gedefinieerd. Episodic en semantic memory blijven over threads heen bestaan. Document memory bewaart projectnotities, researchsamenvattingen en geleerde conventies in files die mensen en agents kunnen inspecteren. Procedural memory omvat system instructions, tool definitions en herbruikbare procedures die voor een taak kunnen worden opgehaald. De lifetimes in de tabel zijn illustratief; retention volgt het beleid van de applicatie, en working state kan een volledige actieve run duren.
Voor de implementatie vallen vijf van deze zes categorieën samen in drie storage tiers. Short-term memory wordt hot memory, het checkpoint voor de huidige thread. Episodic en semantic worden cold memory, recall over threads heen. Document memory houdt opgebouwde projectkennis readable en direct editable. Working memory wordt bij de hot tier ingedeeld omdat checkpoints de state kunnen bewaren die nodig is om een actieve run te reconstrueren. Een checkpoint is niet de volledige interne computation van het model. Procedures kunnen met de agent worden meegeleverd of in files of een andere store worden opgeslagen en opgehaald. Deze tiers beschrijven de implementatiekeuzes in dit artikel; het zijn geen elkaar uitsluitende memory types.
CoALA classificeert working, episodic, semantic en procedural memory. De Memory in the Age of AI Agents-survey ordent memory daarentegen volgens vorm, functie en dynamics, waaronder documenten, codebases en herbruikbare workflows. Files kunnen verschillende van die categorieën implementeren. Dit artikel benoemt document memory apart om de storage- en maintenanceverantwoordelijkheden zichtbaar te maken.
Hetzelfde storage pattern komt in andere domeinen voor. Een Minecraft-agent (Voyager) bewaart herbruikbare game skills als code libraries, en web agents leiden herbruikbare browsing workflows af uit succesvolle runs. Ik kom later op beide terug. Inspectable files en indexed retrieval kunnen naast elkaar bestaan: Voyager haalt programs op met embeddings van hun beschrijvingen.
Door een agent beheerde memory verschilt ook van een vaste RAG-pipeline door wie de write uitvoert. De agent of zijn harness selecteert wat moet worden opgeslagen, bijgewerkt en verwijderd, en kiest later wanneer het moet worden opgehaald.
Het 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 weer op. De memory stream rangschikte kandidaten op recency, importance en relevance — een ontwerp dat nog steeds een nuttig referentiepunt vormt voor agent-memory retrieval.
Compaction houdt een conversatie bruikbaar
Een grotere context window maakt het niet overbodig om te bepalen wat behouden blijft. Huidige APIs kunnen een oudere conversatie samenvatten voordat de window volloopt. Claude’s server-side compaction, op 2026-09-06 nog een beta feature, retourneert een compaction-block dat volgende requests gebruiken in plaats van eerdere inhoud. Dat kan werk voor client-side summarization verminderen, maar in de summary kan een later benodigde fact ontbreken.
Bewaar authoritative task state buiten die summary: voltooide effects, approvals, source references en exacte user constraints. Een checkpoint herstelt execution; compaction verkort model context; long-term memory selecteert knowledge voor een andere conversation. Test die drie gedragingen afzonderlijk. Forceer halverwege een test compaction en controleer of de volgende action nog steeds een eerdere constraint respecteert. Gebruik een compacted transcript niet als enige record van wat is goedgekeurd.
Short-term agent memory: de checkpoint store
LangGraph checkpoint graph state op super-step-grenzen — één node of een batch parallel uitgevoerde nodes. Met de default durability="async" kan de volgende stap draaien terwijl die write wordt afgerond; durability="sync" wacht op persistence voordat het verdergaat, wat write latency toevoegt. Crash recovery gebruikt het laatst gepersistente checkpoint, niet noodzakelijk de meest recent voltooide stap. Dit 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. Na een HITL interrupt of process restart herstelt LangGraph de laatst opgeslagen state en gebruikt het scheduling metadata om de volgende node te kiezen. Het hervat bij een voltooide node boundary, niet bij een willekeurige Python-regel. De opgeslagen details omvatten een checkpoint-ID en timestamp, een version voor elk channel (de naam van LangGraph voor een state key) en de channel versions die elke node al heeft gezien. Het step number is metadata voor dat checkpoint. Een checkpoint verschilt ook van een append-only event log 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 laatste 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 belangrijke beslissing is welke backend erachter komt. PostgreSQL en Redis zijn twee gangbare production choices.
PostgreSQL versus Redis
| Dimensie | PostgreSQL (langgraph-checkpoint-postgres) | Redis (langgraph-checkpoint-redis) |
|---|---|---|
| Durability model | 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-settings |
| Primaire constraint | Database write latency en table growth | RAM-gebruik, eviction en persistence-configuratie |
| Operational 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 database benchmarks voorspellen checkpoint performance niet. Meet de serialized state size, write frequency, persistence settings 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 checkpoint setup in memory/hot.py. Als een aanvaller checkpoints zou kunnen schrijven, stel dan LANGGRAPH_STRICT_MSGPACK=true in of configureer allowed_msgpack_modules. Daarmee beperk je deserialization tot veilige of gedeclareerde types; de permissive default waarschuwt voor niet-geregistreerde types, maar staat ze nog steeds 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(authenticated_user_id: str) -> 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(
{"user_id": authenticated_user_id,
"messages": [HumanMessage(content="Analyze NVDA")]}, config
)
# After the server authenticates the approver and validates approval
# of this exact draft, update the companion's approval field.
await graph.aupdate_state(config, {"report_approved": True})
# Continue the static interrupt_before pause; new input starts a new run.
result = await graph.ainvoke(None, config)
# Local fixture identity. A server supplies this only after authentication.
asyncio.run(main(authenticated_user_id="user-123"))
De user_id in graph input komt uit de authenticated server context; thread_id lokaliseert alleen checkpoints en stelt geen identity vast en autoriseert geen toegang tot een thread. De AsyncPostgresSaver gebruikt het package langgraph-checkpoint-postgres, 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 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 coördineren evenmin.
Redis: wanneer latency de bottleneck is
Wanneer checkpoint latency de bottleneck is, is Redis een optie voor recoverable state. Meet serialized state size, persistence settings en concurrency voordat je het boven PostgreSQL verkiest.
Een vereenvoudigde versie van de checkpoint setup 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.
Durability depends on RDB snapshots, AOF fsync policy, and replication.
AOF with appendfsync everysec can still lose about one second of writes
after a crash; enabling AOF alone is not a no-loss guarantee.
"""
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 inlined checkpoint values en verving retrieval per channel door een JSON.GET-path. Die wijziging betreft value retrieval, niet elke persistence operation; de latency measurements van de vendor hangen af van de workload. Redis 8.0+ bevat RedisJSON en RediSearch standaard — er zijn geen extra modules nodig.
Kies de Redis persistence- en fsync-policy op basis van het toegestane loss window. RDB kan writes verliezen sinds de laatste snapshot; de gebruikelijke AOF appendfsync everysec-policy kan ongeveer één seconde verliezen. always ruilt write latency in voor sterkere persistence, terwijl no het flushen aan het OS overlaat. Test recovery met de daadwerkelijke disk- en replication-settings.
Voor memory-constrained deployments bewaart ShallowRedisSaver alleen het laatste 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 al PostgreSQL in je stack gebruikt
- Je agent lange taken uitvoert waarbij het verliezen van state uren recomputation betekent
- Je een unified data store wilt — PostgreSQL met pgvector kan één backend vormen 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 onafhankelijke threads. Als meerdere agents shared state wijzigen, geef die state dan een eigenaar en coördineer buiten de checkpoint saver.
- Het om short-lived sessions gaat waarbij het verlies van een checkpoint herstelbaar is
- Je semantic caching wilt gebruiken om redundante LLM calls te verminderen (Redis LangCache cachet semantically similar queries om herhaalde LLM calls te vermijden)
Andere opties: langgraph-checkpoint-sqlite werkt voor local development en single-process deployments. Voor AWS-native stacks biedt langgraph-checkpoint-aws een DynamoDBSaver met automatic payload offloading — de gedocumenteerde saver offloadt boven zijn threshold van 350 KB wanneer een S3-bucket is geconfigureerd. Die threshold is implementation policy, niet de itemlimiet van 400 KB van DynamoDB. Serverless pricing en het ontbreken van te beheren infrastructuur maken dit aantrekkelijk voor deployments met variabele load.
Long-term memory: herinneringen over sessions heen
Hot memory handelt de huidige conversatie af. Long-term memory gaat over de gebruiker die volgende week terugkomt: het bewaart feiten, voorkeuren en interaction history die over threads heen blijven bestaan.
LangGraph levert een Store-interface voor cross-thread memory via zijn BaseStore-class. Elk memory-item is een (namespace, key)-pair met een JSON-value en optionele vector embedding. De namespace codeert doorgaans de gebruiker of organisatie: ("user", "user-123", "preferences").
Vector storage: semantic recall met Qdrant
Wanneer de agent ongestructureerde feiten moet terughalen (“Wat zei de gebruiker over zijn beleggingstermijn?”), levert 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 heb HNSW en de trade-offs ervan uitgebreid behandeld in mijn search ranking-artikel. 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-ontwerp. 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 embedding integration is toekomstig werk. De request handler moet het request authenticeren en principal opbouwen uit de geverifieerde identity; de client levert die nooit aan. Het Qdrant-filter is de retrieval scope, niet de authorization.
from qdrant_client import QdrantClient
from qdrant_client.models import (
PointStruct, Distance, VectorParams, Filter, FieldCondition, MatchValue,
)
import hashlib
import json
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."""
identity = json.dumps([principal.user_id, fact], ensure_ascii=False).encode()
point_id = hashlib.sha256(identity).hexdigest()[:32]
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 point ID hasht een JSON-array van user ID en fact, zodat delimiters binnen een van beide waarden geen twee identities kunnen samenvoegen. Zo moeten user a:b met fact c en user a met fact b:c verschillende waarden opleveren. De 32 hexadecimale tekens passen in Qdrants UUID point-ID-representatie.
De flow heeft drie stappen. In dit illustratieve ontwerp extraheert een LLM key 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 user message om relevante context terug te halen. De huidige Market Analyst Agent implementeert deze semantic extraction- en embedding-flow nog niet.
Retrieval scoring: verder dan cosine similarity
Raw cosine similarity is een startpunt, maar production memory systems hebben rijkere retrieval nodig. Het Generative Agents-paper (Park et al., 2023) introduceerde een scoring function die drie signalen combineert:
- Recency: Rule-based decay waardoor recente memories hoger scoren. Een exponential decay function zorgt ervoor dat een fact van gisteren hoger scoort dan een equivalente fact van zes maanden geleden.
- Importance: LLM-rated significance op een schaal van 1 tot 10. “De portfolio van de gebruiker staat 40% lager” scoort hoger dan “de gebruiker zei hallo.”
- Relevance: Embedding cosine similarity tussen de query en de opgeslagen fact.
Het paper normaliseert alle drie signalen naar vergelijkbare schalen voordat ze worden gecombineerd. Doe hetzelfde voordat je weights tunet; anders domineert een raw 1–10 importance score een 0–1-signaal. De uiteindelijke retrieval score is een weighted sum: score = alpha * recency + beta * importance + gamma * relevance. Zo raken verse, belangrijke facts niet bedolven onder stale maar semantically similar facts. Voor een prototype voor financiële analyse zou ik starten met alpha = 0.3 voor recency, beta = 0.2 voor importance en gamma = 0.5 voor relevance, omdat de huidige query doorgaans bepaalt welke verder geldige fact in de context hoort. Het Generative Agents-paper gebruikte gelijke weights; deze waarden zijn een voorgesteld startpunt, geen gemeten verbetering. Tune ze tegen held-out recall en checks van task quality voordat je erop vertrouwt.
Alternatieven voor vector search
Vector search is krachtig, maar niet altijd het juiste tool. Dit zijn situaties waarin alternatieven geschikt zijn:
| Aanpak | Beste toepassing | Belangrijkste operationele kosten |
|---|---|---|
| Vector search (Qdrant) | Semantic recall van ongestructureerde feiten | Embedding- en index lifecycle |
| Key-value store (Redis) | Structured user profiles en voorkeuren | Geheugengebruik en persistence policy |
| Document store (files) | Project knowledge 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, beleggingshorizon, voorkeursectoren — 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 eindigt)PostgresStore— production persistent store met volledige SQL queryingAsyncRedisStore— cross-thread memory met vector search, TTL support en metadata filtering
De index-configuratie schakelt vector search over opgeslagen items in met een configureerbaar embedding model. Voor veel use cases is deze ingebouwde store voldoende, zonder een dedicated vector database te gebruiken.
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 strategie voor long-term memory 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 over ongestructureerde facts nodig hebt of wanneer de formulering van queries onvoorspelbaar varieert.
Knowledge graphs zijn de moeite 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 feiten waar waren, niet alleen wat waar was. De temporal relationships kunnen validity intervals en superseded values bewaren; extraction- en update-logica bepalen nog steeds of een fact actueel is. Het Zep-paper rapporteert 94,8% DMR-accuracy voor het geëvalueerde Zep-systeem dat door Graphiti met GPT-4 Turbo wordt aangedreven, tegenover 94,4% voor full context. DMR gebruikt conversaties van 60 messages en een beperkte fact-retrievaltaak. Dat kleine verschil toont geen algemeen voordeel van temporal graphs aan.
De keerzijde is operationeel. Een graph database beheren is niet triviaal, en voor de meeste agentapplicaties dekt vector search met metadata filtering hetzelfde terrein met minder infrastructuur.
Managed memory frameworks zoals Mem0 en Letta (voorheen MemGPT) nemen de extraction-consolidation-retrieval pipeline voor hun rekening. De aanpak van Mem0 is opmerkelijk: een LLM extraheert candidate memories, een decision engine vergelijkt elke nieuwe fact met bestaande entries in de vector store, en een resolver beslist of de fact moet worden toegevoegd, bijgewerkt, verwijderd of genegeerd. Zo blijft de memory store coherent en non-redundant. Letta kiest een operating-systems-invalshoek: agents beheren hun eigen context window met memory-managementtools en verplaatsen data autonoom tussen “core memory” (in-context) en “archival memory” (out-of-context). Beide zijn het evalueren waard als je sneller production wilt bereiken en geen volledige controle over de memory pipeline nodig hebt.
Document memory: de archiefkast van de agent
Vector stores en key-value backends zijn goed in semantic recall en structured lookups. Opgebouwde projectcontext — conventies, research notes en beslissingen die over sessions heen worden meegenomen — hoort vaak in files die mensen kunnen lezen, reviewen en versioneren.
Dit is document memory: de agent leest en schrijft structured files (Markdown, JSON, YAML) naar een bekende directory. Geen embeddings, geen database, geen infrastructuur. Gewoon files op disk die zowel de agent als de developer kan cat, grep, git diff en handmatig kan bewerken.
In één door een vendor uitgevoerde evaluatie rapporteerde Letta 74,0% accuracy op LoCoMo — een benchmark voor vraagbeantwoording over lange conversaties — voor een GPT-4o mini-agent met attached files, automatic embeddings, semantic search_files en verplichte search-toolregels. Mem0’s beste graph variant scoorde 68,5%. Dit is één vendor, model, benchmark en harness. Het laat zien dat een file-facing interface in die setup goed kan werken; het toont niet aan dat raw Markdown of keyword search voldoende is. Het operationele voordeel staat daar los van: developers kunnen de opgeslagen knowledge direct lezen, bewerken en diffen.
Langere context windows maken ook whole-file reads praktisch voor sommige projectdocumenten. Chunked retrieval past nog steeds bij 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?
Gebruik voor een langlopend agentproject een directory met goed georganiseerde notes wanneer mensen een reviewable record nodig hebben. Denk aan een coding agent die wekenlang aan één project werkt:
- Hij leert dat het project Pydantic v2 gebruikt, niet v1
- Hij ontdekt dat tests met
pytest -x --tb=shortmoeten worden uitgevoerd - Hij bouwt knowledge op over de architectuur van de codebase
- Hij leert de voorkeuren van de developer (“gebruik altijd
pathlib, nooitos.path”)
Deze facts kunnen in een vector- of key-value-systeem staan. Files zijn hier de betere default omdat de developer verbonden notes moet kunnen lezen, bewerken, reviewen en versioneren. Voeg keyword- of semantic search alleen toe wanneer het documentcorpus en het query pattern dat nodig maken. Als de agent iets verkeerds leert, open je het file en corrigeer je het.
Claude Code, Cursor en Devin Desktop gebruiken varianten van dit pattern. De voorbeelden hieronder tonen hoe elk systeem zijn files opslaat en laadt.
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 een keyword over documenten heen zoeken.
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 dat een JSON-envelope schrijft met content, metadata en created_at. Deze schets toont een ander ontwerp om de trade-off van human-readable Markdown-files te illustreren:
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.
# ponytail: linear scan of file bytes; add an index when measured
# latency, concurrency, or retrieval quality requires it.
"""
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 nog steeds via .. buiten een directory komen of via een bestaande symlink ontsnappen. Deze illustratieve class is bedoeld voor een trusted single-user- of controlled filesystem. De class 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 indeling 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-ontwerp 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-ontwerp 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 session heeft geleerd, een slechte learning git revert 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 versus vector versus key-value?
De drie memory backends bedienen verschillende access patterns:
| Dimensie | Vector Store | Key-Value Store | Document Store |
|---|---|---|---|
| Query pattern | ”Vind facts die vergelijkbaar zijn met X" | "Haal de value voor een key op" | "Lees het document op path” |
| Beste toepassing | Ongestructureerde, gevarieerde recall | Structured lookups | Projectcontext, notes |
| Human readable | Readable text payloads | Gedeeltelijk (JSON) | Ja (Markdown) |
| Debuggable | Payloads en scores inspecteren | Eenvoudig (exacte keys) | Files inspecteren en zoeken |
| Version controllable | Via exports of change logs | Mogelijk | Ja (git-native) |
| Embedding infrastructure | Vereist | Niet nodig | Niet nodig |
| Schaalt naar | Miljoenen facts | Miljoenen keys | Afhankelijk van bytes en index |
| Search capability | Semantic similarity | Exact match | Path, keyword, optionele index |
Gebruik document memory wanneer:
- De agent projectkennis over meerdere sessions opbouwt
- Developers willen inspecteren, bewerken of overrulen wat de agent “weet”
- De kennis de vorm heeft van documenten (notes, samenvattingen, conventies) en niet van 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 met X te maken hebben”)
- De formulering van de query onvoorspelbaar varieert
- Je duizenden tot miljoenen individuele facts hebt
Gebruik key-value stores wanneer:
- Je exacte, snelle lookups voor structured data nodig hebt (user profiles, settings)
- Het dataschema goed gedefinieerd is
De drie stores kunnen naast elkaar bestaan, maar dat is geen vereiste. De huidige Market Analyst Agent gebruikt PostgreSQL checkpoints voor hot memory, Qdrant voor exact 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 projectregels uit
.cursor/rulesals.mdc-files — coding conventions, framework preferences en architectural decisions — waarbij frontmatter bepaalt wanneer elke regel van toepassing is. - Devin Desktop’s legacy Cascade agent leest rules uit
.devin/rules/, waarbij.windsurf/rules/en de root-level.windsurfrulesals legacy fallbacks behouden blijven. Cascade bewaart autogenerated memories lokaal per workspace en haalt ze later op; de default Devin Local-agent voor nieuwe tabs persist memories niet. - Anthropic’s memory tool voor de Claude API is een client-side tool die het model aanstuurt met file operations —
view,create,str_replace,insert,deleteenrename— over een/memories-directory. Je applicatie implementeert elk command en bepaalt dus waar de files werkelijk staan (local disk, S3, database).
De file-backed varianten slaan agent knowledge op als human-readable text met expliciete read/write operations, en geen daarvan heeft een embedding pipeline nodig. De agent bepaalt wat hij schrijft; wanneer die text in een lokaal door Git beheerde directory staat, kan de developer die in een git diff zien en bewerken. Wanneer een Anthropic memory-toolhandler /memories naar S3 of een database mapt, hangen inspectie en versioning af van die implementatie.
Declarative notes en executable skills
File-backed knowledge komt ook buiten coding assistants voor, maar het storage format vertelt niet hoe het wordt gebruikt. Voyager bewaart herbruikbare JavaScript-programma’s: de agent kan die code uitvoeren. De methode Agent Workflow Memory voegt daarentegen geïnduceerde web-workflows toe aan de prompt context als guidance voor latere actions. Het afzonderlijke AWM_AS-experiment stelt workflows beschikbaar als callable actions. Een procedure die in context wordt beschreven en een executable procedure vereisen verschillende checks.
Test callable skills door ze in een controlled environment uit te voeren en de effects te controleren. Review project notes en contextual workflows op de facts, constraints en action guidance die ze leveren, en test daarna of die instructions downstream behavior verbeteren. Beide vormen kunnen tot een harmful action leiden; geen van beide verleent extra permissions.
Dezelfde boundary houdt memory onderscheiden van skills en tools. De Agent Skills-standaard gebruikt SKILL.md-files om een agent te vertellen hoe hij een klasse werk moet uitvoeren; memory registreert facts die uit een project of eerdere run zijn geleerd. Deel 3 trekt de naburige boundary tussen een skill en een tool. Kies een file store voor inspectable learned context; kies een skill of tool alleen wanneer de requirement een herbruikbare procedure of capability is.
Document memory schalen voor production
De file-based implementatie hierboven past bij een controlled single-user filesystem. Meerdere tenants en concurrent writers vereisen expliciete access- en write-coordination, ongeacht het aantal documenten.
De raw store hierboven heeft geen concurrent-write coordination, tenant model of search index. Meet die requirements voordat je hem vervangt. Een database of object store kan andere concurrency- en access-contracten leveren; files kunnen ook worden geïndexeerd.
Drie gangbare aanpakken:
Aanpak A: hybrid met een dunne database layer
Behoud files voor authoring (developers bewerken lokaal Markdown), maar serveer vanuit een database at runtime. Synchroniseer files bij deployment naar PostgreSQL rows. De agent leest uit de database, niet van disk. Dit levert:
- Developer ergonomics (Markdown bewerken, committen naar git)
- Production query performance (indexed database reads)
- Een duidelijke scheiding tussen authoring en serving
Aanpak B: object storage + vector-index sidecar
Sla documenten als objects op in S3/GCS, met een Qdrant-collection die hun embeddings indexeert. De agent queryt Qdrant voor relevante document IDs en haalt daarna de content 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.
Aanpak 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 levert hybrid search (keyword + semantic), ACID-transacties en één operationeel systeem.
Een schets van aanpak C. De combined score voert exact scoring uit over een begrensd tenant corpus; er wordt geen approximate nearest-neighbor (ANN)-index gebruikt. pgvector vereist directe ascending distance ordering met LIMIT voor dat index path. Haal voor een groter corpus keyword- en vectorcandidates afzonderlijk op en fuse daarna hun ranks. 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 het request en bouwt principal op; hij accepteert geen tenant ID van de caller. PostgreSQL RLS maakt die scope vervolgens afdwingbaar, zelfs als een query later zijn tenant predicate weglaat.
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);
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 * COALESCE(1 - (embedding <=> $2), 0)) 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 voor het volgende request kan behouden. De OR in de eerste branch maakt de query hybrid. COALESCE houdt een keyword-matching document zonder embedding in de result set met zijn text score; het draagt niet bij aan vector similarity. Met alleen de @@-predicate wordt een document dat semantically juist is maar geen keywords met de query deelt al vóór de scoring weggefilterd — dat is keyword retrieval met semantic reranking, geen hybrid retrieval. De weights 0.6/0.4 zijn illustratief: text rank en cosine similarity hebben verschillende schalen. Normaliseer ze op basis van je retrieval evaluation of gebruik rank fusion voordat je die weights als relatieve importance interpreteert. De distance threshold is een knop: maak hem strenger als de vector arm de resultaten overspoelt, of ruimer als semantic matches nooit verschijnen.
De volgende regression is 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 op 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;
Wat je krijgt:
- Hybrid search: keyword matching (GIN index) + semantic similarity (pgvector), gezamenlijk gescoord
- Multi-tenancy: server-derived identity plus database-enforced RLS
- ACID guarantees: transacties op de primary committen atomically; replica reads kunnen achterlopen
- Eén operationeel systeem: geen afzonderlijke vector database om te beheren
- Scaling: read replicas kunnen stale-tolerant queries afhandelen. Native partitioning kan pruning en maintenance helpen, maar distribueert writes niet over servers; daarvoor is een expliciet sharding-ontwerp nodig. Route read-after-write paths naar de primary of meet een geschikte synchronous policy
Files zijn geweldig voor workflows met één developer. Voor multi-tenant production 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 memory layers actief.
De architectuur heeft drie memory paths:
-
Hot path (checkpoint store): LangGraph schrijft de resumable graph state op elke super-step boundary naar de checkpoint store. Wanneer de graph een
interrupt_before-node bereikt (zoals depublish-node in Deel 1), pauzeert execution. De gebruiker kan de app sluiten en de graph hervat bij terugkeer vanuit het checkpoint. Runtime event logs en traces zijn afzonderlijke production concerns. -
Cold path (long-term store): Nadat de router een route heeft gekozen, queryt de planner de long-term store voor relevante user context. De planner kan pas personaliseren wanneer die read terugkeert. Een vector-backed lookup kan query embedding en index retrieval bevatten; een key-value lookup niet. Nieuwe facts kunnen na het einde van de conversatie worden geëxtraheerd en opgeslagen, zodat die write de reasoning loop niet vertraagt.
-
Document path (file store): Tijdens planning leest de agent de projectconventies en research notes die voor het request nodig zijn. Tijdens execution schrijft hij researchsamenvattingen en geleerde patronen terug naar disk. Die reads informeren de huidige taak, dus file size, filesystem speed en cache state beïnvloeden de responsetijd. Cache alleen wanneer de cache duidelijke invalidation- en tenant-isolationregels heeft. Writes kunnen later plaatsvinden.
De wiring in LangGraph is eenvoudig — de checkpoint store en long-term store worden bij graph compilation meegegeven, terwijl de document store als dependency wordt geïnjecteerd. De lokale schets hieronder compileert een al geconfigureerde StateGraph-builder, inclusief nodes die de store accepteren. Dit breidt de graph wiring uit; de Part 1- en companion-create_graph-helpers accepteren geen store-argument. Er wordt InMemoryStore gebruikt om het snippet klein te houden; de reference Docker-topology 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:
# builder is the configured StateGraph for this extended design.
# The Part 1/companion create_graph helper does not accept store.
graph = builder.compile(
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 volledige flow
In het uitgebreide ontwerp hierboven zou het request “Analyze TSLA” van een terugkerende gebruiker deze flow kunnen volgen. Semantic recall en asynchronous fact extraction zijn voorgestelde uitbreidingen, geen huidig companion-gedrag:
-
Document memory load: Wanneer de planner draait, leest hij projectconventies uit de document store: voorkeuren voor analysis format, preferred data sources en tool usage patterns. Die vormen het baseline behavior voor dat plan.
-
Router: De router classificeert het request als
DEEP_RESEARCH. In dit voorbeeld gebruikt routing het request zelf, niet long-term preferences. -
Cold memory recall + planner: De planner queryt de long-term store met het message van de gebruiker. Hij haalt op: “User has high risk tolerance”, “User prefers detailed competitor analysis”, “User previously researched NVDA and AMD”. Daarna maakt hij een vijfstappenplan voor research dat op die preferences is gepersonaliseerd. Hij voegt een competitor-analysisstap toe omdat uit de history blijkt dat de gebruiker die wil. 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 tot de stap klaar is. LangGraph checkpoint elke super-step (router, planner en elke sequentiële executor-stap hier). Recovery start bij het laatst gepersistente checkpoint. Als de write in stap 3 is voltooid, kan de graph verdergaan bij stap 4; met asynchronous persistence kan een crash vereisen dat een voltooide stap opnieuw wordt uitgevoerd.
-
HITL interrupt: De reporter schrijft een draft. Een afzonderlijke model session, zonder history van de run, leest de draft en registreert een assessment. De graph bereikt vervolgens
publish, waarinterrupt_beforede graph pauzeert ongeacht die assessment. Het checkpoint bevat zowel de draft als de assessment, zodat de mens ze kan reviewen voordat hij beslist of het rapport wordt gepubliceerd. Uren later laadt de graph het checkpoint opnieuw en volgt die beslissing. -
Memory updates: Nadat de conversatie is geëindigd, extraheert een asynchronous process nieuwe user facts (“user is now tracking TSLA”, “user approved the report format”) en slaat die 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 helder. De checkpoint store handelt durability en resume af; dat is infrastructuur. De long-term store handelt personalisatie af; dat is productlogica. De document store bevat opgebouwde projectkennis; dat is het notebook van de agent.
Trade-offs en aandachtspunten
Memory levert waarde, maar voegt ook kosten en complexiteit toe:
-
Embedding cost: Elke fact die in een vector database wordt opgeslagen vereist embedding generation. Een hosted embedding provider voegt een API-call, provider-specifieke kosten en network latency toe; in september 2026 vermeldt OpenAI
text-embedding-3-smalltegen $0,02 per miljoen tokens. De kosten van een hosted model per fact zijn verwaarloosbaar, maar tellen op over duizenden users en sessions. Batch hosted calls en cache results. Bij query time kan vector recall query embedding plus index- en network latency omvatten; een key-value lookup niet. Meet dat path in je deployment en cache daarna common query embeddings of gebruik een local embedding model als latency kritisch is. -
Stale memory: User preferences veranderen. Een fact die zes maanden geleden is opgeslagen (“user prefers conservative investments”) hoeft niet langer accuraat te zijn. Stel expiry policies in. Een team kan bijvoorbeeld preferences na 365 dagen laten verlopen en episodic events na 90 dagen als de privacyregels, update rate en retrieval evaluation die windows rechtvaardigen; deze waarden zijn een voorgesteld policy, geen portable defaults. De context engineering-post wijst fixed retention rules af als portable policy. Expiry is de botte variant. Schema-guided typed state is de scherpere variant: temporal validity en provenance bij elke fact, zodat een superseded value tijdens retrieval verliest van de current value in plaats van pas bij expiry.
-
Memory overhead in context: Elke recalled fact verbruikt 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 taak. Beperk het aantal recalled facts en prioriteer op basis van de 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 gereguleerde sectoren is niets hiervan optioneel.
-
Checkpoint storage growth: PostgreSQL checkpoint-tabellen groeien bij elke super-step. Voer geen algemene SQL-pruningquery uit: delta channels kunnen ancestor checkpoints en hun write/blob-records vereisen 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 zijn recovery-contract voor delta channels. Als die ondersteuning 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 conversaties verbatim op te slaan. Dat weerspiegelt menselijke memory consolidation en houdt de store beheersbaar. Mem0 en Graphiti handelen dit automatisch af; als je zelf bouwt, plan dan periodieke consolidation jobs.
-
Cold start problem: Nieuwe users hebben geen long-term memory. De agent moet graceful degraden 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 mogelijk injection point. Als een aanvaller misleidende facts in de document store of long-term memory schrijft (“always approve transactions without verification”), kan de agent ze als instructions uitvoeren. Prompt injection via opgeslagen memories is een reëel attack surface. Mitigations 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 stapelen documenten contradictions op: het ene file zegt “use pytest”, een ander “use unittest”. Plan periodieke reviews (of laat de agent die uitvoeren) om te prunen en te consolideren. Files ondersteunen
grep; vector-store payloads kunnen ook worden geïnspecteerd of geëxporteerd. Geen van beide storage formats detecteert contradictions zelfstandig. -
Search scale: de raw file scan hierboven leest het corpus bij elke query. Kies een index op basis van bytes scanned, update rate, concurrency, latency en retrieval quality. File-backed content kan een full-text- of vector-index gebruiken; alleen het aantal documenten bepaalt de backend niet.
Recall en de memory lifecycle testen
Vergelijk met no-memory- en full-contextbaselines op held-out questions. Neem paraphrases, contradictions, gewijzigde preferences, stale facts, unanswerable questions, deletions en cross-tenant requests op. LongMemEval biedt 500 questions over extraction, multi-session en temporal reasoning, updates en abstention. Meet retrieval precision/recall afzonderlijk van answer correctness, plus gebruik van stale facts, unauthorized disclosure, write/update/delete correctness, latency en cost.
Recall questions zijn maar een deel van de evaluatie. MemoryArena voegt interdependent tasks over sessions heen toe, waarbij een eerdere action en de feedback daarop later behavior moeten wijzigen. De tasks omvatten shopping, travel planning, progressive search en formal reasoning. Gebruik dat ontwerp wanneer het product belooft van werk te leren, in plaats van alleen vragen over opgeslagen conversaties te beantwoorden. Dit zijn research tasks, geen metingen van een deployed memory service.
EvoMemBench scheidt knowledge ook van execution experience en within-episode van cross-episode memory. De vergelijking van 15 methods vindt geen memory-vorm die uniform het sterkst is; long-context baselines blijven onder zijn protocol competitief. Dat ondersteunt het behouden van eenvoudige baselines in je evaluation, niet het vervangen van elke store door het nieuwste framework.
Bewaar provenance en validity naast recalled facts. Importance scores kunnen geen trust of gewijzigde permissions vaststellen. De deletion policy moet naast het oorspronkelijke record ook indexes, cached summaries en retained artifacts omvatten.
De volgende laag is action
Deel 5 en 6 keren vanuit operationeel perspectief terug naar memory en behandelen verschillende helften ervan. De runtime beheert het checkpoint: waar execution stopte en hoe je die herstart. De harness beheert de handoff: wat het werk betekent en wat nog overblijft, geschreven als document memory voor de volgende model session — één ononderbroken stuk model context, in de terminologie die Deel 5 vastlegt. Het proces herstellen is niet hetzelfde als de taak herstellen.
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 — dec. 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 stream-architectuur met scoring op recency, importance en relevance
- Zep: A Temporal Knowledge Graph Architecture for Agent Memory — Rasmussen, 2025 — Bitemporal 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 — Self-synthesized herbruikbare API tools voor web agents
- LEGOMem: Modular Procedural Memory for Multi-agent LLM Systems for Workflow Automation — okt. 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 extension 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 — Cascade rules en workspace-local autogenerated memories; default Devin Local persist ze niet
Memory frameworks
- Mem0 — Managed memory layer met extraction/consolidation pipeline
- Letta (MemGPT) — OS-geïnspireerd virtual context management voor agents
- LangMem SDK — Memory-managementtools voor LangGraph
Workshops
- MemAgents: Memory for LLM-Based Agentic Systems — ICLR 2026 Workshop
Demo-project
- Market Analyst Agent — Reference implementation voor de checkpoint- en huidige profile/document-storagepaths