By default, LLM agents have no memory. Every request starts fresh with only what is in the current context window. For a demo this is fine. For a user who returns the next day expecting the agent to remember their preferences, tech stack, or prior conversation - it is broken by design.
The fix is not one thing. In 2026, production agent memory splits into three distinct tiers: in-context memory (what the agent sees right now), semantic memory (facts retrieved from a vector store), and episodic memory (persistent records of past interactions). Getting the architecture wrong means either hitting context limits on every long conversation or building a system that surfaces irrelevant memories and confuses the model.
This guide covers all three tiers with working code, a framework comparison, and clear decision rules for which to reach for.
Why Agents Keep Forgetting
The core constraint is the context window. A model with a 200,000-token context sounds like infinite memory, but sending the entire conversation history on every call is expensive and inefficient. More importantly, context quality matters more than context volume - a 200,000-token window stuffed with irrelevant history produces worse outputs than a 10,000-token window with precisely the right information.
Anthropic's 2026 Agentic Coding Trends Report found that the majority of teams don't come close to using their models' full context window. The bottleneck is not size - it is identifying which information the model actually needs for the current task.
That problem is memory architecture.
The Three Memory Tiers
| Tier | Scope | Stored where | Lifetime |
|---|---|---|---|
| In-context | Current session | RAM / prompt | Until context ends |
| Semantic | Facts and knowledge | Vector database | Persistent |
| Episodic | Past interactions | Structured DB or vector store | Persistent |
Tier 1: In-Context Memory
The simplest memory is a conversation buffer - the last N messages appended to the system prompt. This covers multi-turn conversations within a single session. The challenge is keeping it bounded.
Pythonfrom collections import deque from openai import OpenAI client = OpenAI() class ConversationAgent: def __init__(self, system_prompt: str, max_messages: int = 20): self.system_prompt = system_prompt self.history = deque(maxlen=max_messages) def chat(self, user_message: str) -> str: self.history.append({"role": "user", "content": user_message}) response = client.chat.completions.create( model="gpt-5.4", messages=[ {"role": "system", "content": self.system_prompt}, *list(self.history), ], ) reply = response.choices[0].message.content self.history.append({"role": "assistant", "content": reply}) return reply
The deque(maxlen=20) automatically drops the oldest messages when the buffer fills. This prevents runaway token costs but loses early context. For conversations where early messages are critical - an initial requirements brief, for example - use a summary buffer instead: compress older messages with an LLM call rather than discarding them.
Pythondef summarize_history(self, messages: list) -> str: summary_prompt = f"Summarize this conversation in 3-5 sentences:\n{messages}" response = client.chat.completions.create( model="gpt-5.4-nano", # cheap model for summarization messages=[{"role": "user", "content": summary_prompt}], ) return response.choices[0].message.content def compress_history(self): if len(self.history) > 16: old_messages = list(self.history)[:8] summary = self.summarize_history(old_messages) # Replace old messages with a single summary message new_history = [{"role": "system", "content": f"[Earlier conversation summary]: {summary}"}] new_history.extend(list(self.history)[8:]) self.history = deque(new_history, maxlen=20)
Use gpt-5.4-nano (or Haiku 4.5) for summarization - it is fast and cheap for this mechanical task.
Tier 2: Semantic Memory (Vector Database)
Semantic memory answers "what does this agent know?" - product documentation, user preferences, company policies, or any knowledge base that should survive session boundaries. Instead of sending the entire knowledge base on every call, retrieve only the most relevant chunks.
Bashpip install qdrant-client openai sentence-transformers
Pythonfrom qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct from openai import OpenAI import uuid client = OpenAI() qdrant = QdrantClient(":memory:") # Use QdrantClient(url="...") for production COLLECTION = "agent_knowledge" EMBED_MODEL = "text-embedding-3-small" def setup_collection(): qdrant.create_collection( collection_name=COLLECTION, vectors_config=VectorParams(size=1536, distance=Distance.COSINE), ) def embed(text: str) -> list[float]: return client.embeddings.create(input=text, model=EMBED_MODEL).data[0].embedding def store_fact(text: str, metadata: dict = None): qdrant.upsert( collection_name=COLLECTION, points=[PointStruct(id=str(uuid.uuid4()), vector=embed(text), payload={"text": text, **(metadata or {})})] ) def retrieve_relevant(query: str, top_k: int = 5) -> list[str]: results = qdrant.search( collection_name=COLLECTION, query_vector=embed(query), limit=top_k, ) return [r.payload["text"] for r in results] def rag_chat(user_message: str, conversation_history: list) -> str: relevant_facts = retrieve_relevant(user_message) context = "\n".join(f"- {f}" for f in relevant_facts) response = client.chat.completions.create( model="gpt-5.4", messages=[ {"role": "system", "content": f"You are a helpful assistant.\n\nRelevant context:\n{context}"}, *conversation_history, {"role": "user", "content": user_message}, ], ) return response.choices[0].message.content
The key discipline: retrieve relevant chunks at query time and inject them into the system prompt. This keeps token usage constant regardless of how large the knowledge base grows.
Tier 3: Episodic Memory (Cross-Session Persistence)
Episodic memory is the hard part. It stores what happened in past sessions so an agent can reference "last time we spoke, you mentioned X." This requires extracting meaningful facts from conversations and storing them in a way that enables accurate retrieval later.
Mem0 is the most mature open-source solution for this in 2026. It handles extraction, deduplication, and retrieval automatically.
pip install mem0ai
Pythonfrom mem0 import Memory from openai import OpenAI client = OpenAI() memory = Memory() def chat_with_memory(user_id: str, message: str) -> str: # Retrieve relevant memories for this user relevant_memories = memory.search(query=message, user_id=user_id, limit=5) memory_context = "\n".join([m["memory"] for m in relevant_memories]) system_prompt = "You are a personal coding assistant." if memory_context: system_prompt += f"\n\nWhat you remember about this user:\n{memory_context}" response = client.chat.completions.create( model="gpt-5.4", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": message}, ], ) reply = response.choices[0].message.content # Store this interaction as a memory memory.add( messages=[ {"role": "user", "content": message}, {"role": "assistant", "content": reply}, ], user_id=user_id, ) return reply
Mem0 uses an LLM to extract key facts from each message pair and runs deduplication before storing them. The next time this user_id sends a message, those facts surface in the memory.search() results. No manual extraction logic required.
Framework Comparison
| Framework | Memory types | Self-hosted | Managed cloud | Best for |
|---|---|---|---|---|
| Mem0 | Semantic + episodic | Yes | Yes | General-purpose, easiest integration |
| Zep | Short-term + long-term + entity | Yes | Yes | Conversation apps, user profiles |
| LangMem | In-context + semantic | Yes | No | LangChain/LangGraph ecosystems |
| Letta | All three tiers + self-editing | Yes | Yes | Stateful agent infrastructure |
| MemoryOS | Hierarchical (STM/LTM) | No | Yes | Enterprise, large user bases |
Mem0 is the fastest to integrate and works with any LLM. Zep has the strongest entity extraction for conversation apps where user profile tracking matters. Letta is overkill for most projects but the right answer if you need agents that rewrite their own memory policies. LangMem is the natural choice if you are already inside the LangChain ecosystem.
Choosing Storage Backends
The memory framework sits on top of a storage backend. The backend affects retrieval speed, query expressiveness, and operational complexity.
| Backend | Best for | Limitations |
|---|---|---|
| Vector DB (Qdrant, Pinecone, Weaviate) | Semantic similarity search | Poor at multi-hop reasoning |
| Graph DB (Neo4j, Memgraph) | Relationships between memories, entity tracking | More setup, query language learning curve |
| PostgreSQL + pgvector | Simple semantic search with ACID guarantees | Slower at scale vs dedicated vector DB |
| Redis | Short-term session buffers | Not built for semantic search |
For most applications: start with PostgreSQL + pgvector. It handles semantic search well up to millions of vectors, requires no additional infrastructure, and is easy to query alongside your application data. Move to Qdrant or Pinecone when you hit performance limits or need more advanced indexing.
For agents that need to reason about relationships - "which users have the same tech stack?" or "what issues does this codebase repeatedly have?" - add a graph layer. LinkedIn's Cognitive Memory Agent (CMA) uses a hybrid: vector store for semantic retrieval, graph store for entity relationships across sessions.
Architecture by Use Case
Customer support chatbot: In-context buffer (20 messages) + Mem0 episodic memory keyed by user_id. On first contact, retrieve past tickets and resolutions. The agent handles the current issue without asking the user to repeat their setup.
Coding assistant: Semantic memory over codebase docs + in-context buffer. Add episodic memory to track which libraries and patterns the developer prefers. Skip graph storage - relationship tracking is not needed.
Research agent: All three tiers. In-context for the current research session. Semantic memory over a curated knowledge base. Episodic memory for hypotheses tested, dead ends encountered, and findings from prior runs.
Personal productivity agent: Zep or Mem0 with entity extraction. The agent builds a profile: user timezone, recurring task patterns, preferred communication style. Retrieval is personalization-first, not similarity-first.
Common Pitfalls
- Storing everything. Not every message contains a memorable fact. Mem0 and Zep use LLMs to extract only signal - implementing your own extraction naively leads to bloated, noisy memory stores.
- Missing the forgetting mechanism. Production agents need expiry policies. A preference stated a year ago may no longer be valid. Implement TTL on episodic memories or let users explicitly clear them.
- User ID collisions. Episodic memory is keyed by user identifier. If two users share an ID (common in multi-tenant apps with poor isolation), their memories merge. Namespace user IDs by organization:
org_id:user_id. - Retrieval without reranking. Top-5 cosine similarity returns the most semantically similar chunks, not the most useful ones. Add a reranker (Cohere Rerank, cross-encoder models) before injecting memories into the prompt.
- Prompt injection through memory. If user-controlled text gets stored in memory without sanitization, a malicious user can plant instructions that surface in future sessions. Treat retrieved memories as untrusted user content, not trusted system instructions.
Related DevToolLab Tools
- JSON Formatter - Inspect raw Mem0 or Qdrant API payloads when debugging memory extraction
- JWT Decoder - Decode auth tokens in memory API calls without sending credentials to a third-party
- Diff Checker - Compare two versions of a memory extraction prompt when tuning extraction quality
- Regex Tester - Test patterns for memory sanitization before storing user content
- JSON Viewer - Explore hierarchical memory payload structures as a tree
- cURL Command Generator - Build test requests to Qdrant, Pinecone, or Mem0 cloud APIs
Conclusion
In 2024, "give an agent memory" meant "add a vector database and do RAG." In 2026, that is tier two of three. The gap between an agent that seems to remember and one that reliably does so across sessions, users, and time is an architecture problem, not a model problem.
Start with an in-context buffer. Add semantic retrieval once you have a knowledge base worth querying. Add episodic long-term memory with Mem0 when you need cross-session personalization. Most production applications need all three; the question is which tier is the bottleneck today.
The models are capable enough. What they need is the right context - and memory architecture is how you build the system that delivers it.
