Problem Context
Long-term memory is what the agent remembers across conversations. Without it, every session starts from zero โ the user re-explains their job, their tone preferences, the project context, and the bug they reported yesterday. With it, the agent feels like a coworker; without it, it feels like a customer-service hold queue.
- Users complain the agent "forgets everything" between sessions
- You stuffed the whole user history into the system prompt and it costs $0.20 per question
- You don't know how to forget when the user asks you to
Three Tiers, One Strategy
- Profile memory โ durable facts about the user (name, role, tz, preferred tone). Small (โค 1 KB), structured, always in prompt.
- Episodic memoryโ "what happened" โ past conversations summarised into events. Vector-indexed, retrieved on demand.
- Semantic memoryโ "what the user knows / believes / has decided" โ distilled facts and preferences. Key-value or graph, retrieved by tag.
flowchart TD
Q[User query] --> R{Router}
R --> P[Profile<br/>always in prompt]
R --> E[Episodic<br/>vector search top-K]
R --> S[Semantic<br/>tag/graph lookup]
P & E & S --> A[Assemble context]
A --> L[LLM]
L --> W[Write-back:<br/>extract facts โ memory]
The Write-Back Loop
Most memory bugs come from writes, not reads. Two patterns work:
- End-of-turn extraction โ after every assistant turn, a small LLM call extracts new durable facts and updates the store. Cheap, immediate, occasionally wrong.
- End-of-session consolidation โ once the session ends (or N minutes idle), a more capable model summarises the conversation into an episodic event and updates the semantic store. Expensive, accurate, async.
Production systems usually run both: lightweight extraction inline, full consolidation on a queue.
Minimal Example with LangGraph + Postgres
from langgraph.store.postgres import AsyncPostgresStore
store = AsyncPostgresStore.from_conn_string(DATABASE_URL)
# Read at the start of a turn
profile = await store.aget(("profile", user_id), "facts")
episodes = await store.asearch(("episodic", user_id), query=user_msg, limit=5)
# Write after the turn
new_facts = await extract_facts(messages)
await store.aput(("profile", user_id), "facts", merge(profile, new_facts))
await store.aput(("episodic", user_id), session_id, {
"summary": session_summary,
"embedding": await embed(session_summary),
"ts": now(),
})Retrieval That Actually Helps
Naive cosine-similarity over episodes returns the same five memories every turn. Better retrieval combines:
- Recency boost โ
score = similarity * decay(age_days). - Diversity (MMR) โ penalise memories too similar to ones already selected.
- Tag filters โ only retrieve from
topic=billingwhen the query is about billing. - An explicit "is this relevant?" LLM filter as a final gate when the memory is going into a high-stakes prompt.
Forgetting (GDPR Is Not Optional)
- Every write tags the record with
user_idandsource_session_idโ both indexed. - Deletion is a single SQL statement plus an embedding-index purge โ test the path in CI.
- Soft-delete for 30 days, hard-delete after โ gives you a recovery window without breaking the right-to-be-forgotten clock.
- Memories about other people the user mentioned need their own consent model โ most teams just don't store them.
Failure Modes
- Memory poisoning โ a user says "remember I'm the CEO" and you persist it. Extraction should require multi-turn confirmation for privilege/identity claims.
- Stale facts โ "user lives in Berlin" from 2023. Store timestamps and re-ask when facts hit a TTL.
- Context bloat โ top-K=20 retrieved memories crowds out the actual question. Cap and rerank.
- Cross-tenant leakage โ store namespacing (
(tenant_id, user_id)) must be enforced at the query layer, not the application layer.
Production Checklist
- Tiered store: profile (KV) + episodic (vector) + semantic (KV or graph).
- Inline lightweight extraction + async consolidation worker.
- Recency + diversity + tag filters on retrieval.
- Tested delete path, namespaced by tenant, audit log on every write.
Key Takeaways
- Long-term memory is three stores, not one โ profile, episodic, semantic.
- The hard part is writes (what to remember and when), not reads.
- Forgetting is a feature; design it on day one or pay for it on day 400.

