Memory for agents: Mem0, LangGraph store, DIY
Short-term vs long-term memory, semantic recall, and the trade-offs of managed vs hand-rolled stores.
Why this matters
Agents without memory are amnesiac. They forget your name, your preferences, what you tried last week. Memory is what turns an agent from "tool" into "assistant." It also adds a measurable +5-15% on long-task success rates per recent benchmarks, which is why the skill shows up in nearly every senior agentic AI job description.
Learning objectives
- Distinguish short-term, long-term, semantic, episodic, procedural memory.
- Use Mem0 as a managed memory layer (OSS + cloud).
- Use LangGraph's built-in store for cross-thread memory.
- Roll your own memory with pgvector + a summariser.
1. The 4 memory tiers
| Tier | What it stores | Lifetime | Example |
|---|---|---|---|
| Conversation (short-term) | Current chat history, tool traces | One turn / one session | Working buffer |
| Session | Multi-step task state | Minutes to hours | Onboarding flow |
| User (long-term) | Preferences, profile, past topics | Weeks to forever | "user prefers metric units" |
| Organizational | Shared FAQs, policies, catalogs | Configured globally | "Refund policy is 30 days" |
Map these onto common memory types:
- Semantic memory β facts ("user lives in Pune"). Lives in user tier.
- Episodic memory β past events ("last Tuesday user complained about X"). Lives in user or session.
- Procedural memory β how to do tasks (recipes/playbooks). Often in system prompts.
2. Mem0 β the managed memory layer
Mem0 is the most popular dedicated memory library in 2026. It does:
- Extraction (LLM-based) of facts from conversations.
- Deduplication and conflict resolution.
- Vector + BM25 + entity-link retrieval (multi-signal).
- Multi-tenant scoping by
user_id,agent_id,run_id.
Install
uv add mem0aiLocal self-hosted
import os
from mem0 import Memory
# Default uses OpenAI for embeddings + LLM, Chroma for vector store
memory = Memory()
# Add facts (Mem0 extracts atomic memories from messages)
memory.add(
[
{"role": "user", "content": "I'm Asha. I prefer boutique hotels and vegetarian food."},
{"role": "assistant", "content": "Noted!"},
],
user_id="asha",
metadata={"app": "travel-agent"},
)
# Retrieve
hits = memory.search(query="What food does the user like?", user_id="asha", limit=5)
for h in hits["results"]:
print(h["memory"], "-", h["score"])Cloud (free tier)
from mem0 import MemoryClient
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
client.add("I prefer boutique hotels", user_id="asha")
client.search("hotel preferences", user_id="asha")Wiring Mem0 into an agent loop
from openai import OpenAI
oa = OpenAI()
def chat(message: str, user_id: str) -> str:
# 1. Retrieve relevant memories
mems = memory.search(message, user_id=user_id, limit=5)["results"]
mem_block = "\n".join(f"- {m['memory']}" for m in mems)
# 2. Generate
msgs = [
{"role": "system", "content": f"You are a helpful assistant.\nKnown about user:\n{mem_block}"},
{"role": "user", "content": message},
]
resp = oa.chat.completions.create(model="gpt-4.1-mini", messages=msgs)
reply = resp.choices[0].message.content
# 3. Capture new memories from this turn
msgs.append({"role": "assistant", "content": reply})
memory.add(msgs, user_id=user_id)
return replyThat 25-line pattern is what gives your agent a personality across sessions. Add it to every chat-style project.
Scoping rules (multi-tenant safe)
Always pass user_id. Optionally agent_id (memory shared by one agent), run_id (session memory), and metadata for filters.
3. LangGraph store β built-in cross-thread memory
LangGraph 1.x ships a store abstraction for memory that lives outside any one thread. Use it when you do not want a separate library.
from langgraph.store.memory import InMemoryStore # dev
# from langgraph.store.postgres import PostgresStore # prod
store = InMemoryStore(index={"embed": "openai:text-embedding-3-small", "dims": 1536})
graph = builder.compile(checkpointer=cp, store=store)
# Inside a node, access via the StoreContext:
def remember(state, *, store):
namespace = ("memories", "user-asha")
store.put(namespace, "pref-1", {"text": "prefers metric units"})
hits = store.search(namespace, query="units", limit=3)
return {"memories": [h.value for h in hits]}The index config makes searches semantic. Without it, you only have key-based get / put.
Use the LangGraph store when memory is tightly coupled to a single graph; reach for Mem0 when memory is cross-app or you want better extraction/dedup out of the box.
4. DIY memory in 100 lines (pgvector)
Sometimes you do not want a third party. Roll your own:
CREATE TABLE user_memory (
id BIGSERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
text TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
embedding VECTOR(1536)
);
CREATE INDEX ON user_memory USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON user_memory (user_id);class UserMemory:
def __init__(self, conn, oa): self.conn, self.oa = conn, oa
def _embed(self, text):
return self.oa.embeddings.create(model="text-embedding-3-small", input=text).data[0].embedding
def add(self, user_id: str, text: str, meta: dict | None = None):
v = self._embed(text)
self.conn.execute(
"INSERT INTO user_memory (user_id, text, metadata, embedding) VALUES (%s,%s,%s,%s)",
(user_id, text, meta or {}, v),
)
self.conn.commit()
def search(self, user_id: str, query: str, k: int = 5):
v = self._embed(query)
cur = self.conn.execute(
"""
SELECT text, 1 - (embedding <=> %s::vector) AS score, created_at
FROM user_memory WHERE user_id = %s
ORDER BY embedding <=> %s::vector LIMIT %s
""",
(v, user_id, v, k),
)
return cur.fetchall()Add an LLM-based extractor that runs on every assistant turn to decide what is worth saving. Add a consolidator that runs nightly to summarise old memories ("Asha is a 30y-old in Pune, prefers metric units, likes vegetarian food, asked about Goa trip on 2025-12-10").
You now have what Mem0 has, just less polished. Build it once to understand; then use Mem0 in shipping code.
5. Trim and summarise the chat buffer (the easy 80%)
Even before "real" memory, do this in every multi-turn chat to control cost and quality:
from langchain_core.messages.utils import trim_messages
trimmed = trim_messages(
state["messages"],
max_tokens=4000,
strategy="last",
token_counter=ChatOpenAI(model="gpt-4.1-mini"),
include_system=True,
allow_partial=False,
start_on="human",
)Or summarise older turns into a single system message ("Earlier the user asked about pricing, complained about latency, then provided their email") and prepend it.
LangChain RunnableWithMessageHistory and ConversationSummaryMemory give you this for free.
6. Privacy and data lifecycle
Memory is sensitive. Hard-won rules:
- Allow the user to view, export, and delete their memories. Add
mem.delete(user_id="asha", id=...). - Tag memories with sensitivity ("PII", "PHI", "PCI") and route accordingly.
- TTLs and decay β old preferences should age out unless reinforced.
- Do not memorise messages flagged as toxic / malicious β adds attack surface.
- Encrypt at rest for memory stores in regulated industries.
GDPR / DPDP rights apply. Mem0 supports per-memory delete and bulk delete by user_id.
Hands-on lab (5 hours)
Build a personal travel-planning chatbot that remembers across sessions.
Requirements:
- Streamlit UI with sidebar showing "Known about you:" memories.
- Mem0 with
user_idfrom a login (use a fake login in the UI). - After each user turn, capture a memory if appropriate (preferences, dates, prior trips).
- On each turn, retrieve top 5 memories and inject into the system prompt.
- Add a "Forget this" button next to each memory card that calls
memory.delete(...). - Persist to Sqlite via
mem0defaults; make path configurable.
Acceptance:
- Restarting Streamlit preserves memories.
- Across sessions, the bot greets the user and references prior preferences.
- An end-to-end test logs in, chats 5 turns, restarts, asks "what do I like?" and the bot answers correctly.
Common pitfalls
- Adding raw chat into memory. Extract atomic facts, not the whole turn.
- Forgetting
user_id. Cross-tenant leakage = lawsuit. - Storing PII without explicit consent. Add gating.
- Unbounded memory growth. Set caps; consolidate old memories.
- Trusting memory blindly. Old memories can be stale ("user lives in Pune" might be false now). Add a confidence/decay model or a refresh-on-conflict step.
Self-check
- What is the difference between episodic and semantic memory?
- Why does Mem0 use multi-signal retrieval (vector + BM25 + entity)?
- When do you use
user_idvsrun_idin Mem0? - What does the LangGraph
storegive you that the checkpointer does not? - Why is "delete by user_id" a feature you should ship from day one?
References
- Mem0 GitHub
- Mem0 docs
- LangGraph store
- LangChain
trim_messages - Letta / MemGPT (alternative)
- Zep memory (alternative)
Sign in to save your progress and earn badges.