Module 02
Hybrid Memory with Mem0
On this page
In plain words
Think of the placement cell register. Some questions need a vague memory of a chat, some need one exact line from a register, and some need to follow who is connected to whom. Agent memory has the same three kinds of questions, so you store every fact in three places at once and merge the answers when you read.
How it flows
- 1Get a new fact
- 2Write to vector
- 3Write to KV
- 4Write to graph
- 5Score all hits
- 6Return top few
A tiny example
def add(fact):
vector.save(fact)
kv[(user, kind, entity)] = fact
graph.add_edge(fact)
def search(q):
hits = vector.find(q) + kv.find(q) + graph.walk(q)
return sorted(hits, key=lambda r: 0.5*relevance(r) + 0.3*importance(r) + 0.2*recency(r), reverse=True)[:3]Notice that one write goes to all three stores, and the read adds three scores together instead of picking one store as the winner.
What you will learn
- Why one type of memory store is never enough for an agent.
- What the three stores are: vector, key-value, and graph.
- How to mix their results into one ranked list using relevance, importance and recency.
- How to keep old facts around when a new fact contradicts them.
The problem, simply
Think about your college placement cell. Three very different questions come to them every day.
"Which company was talking about backend roles last week?" The coordinator has to remember the general topic of a conversation. Nobody has that written in a table.
"What is Priya's roll number?" That one is a straight lookup. It is in a register, one line, one answer, two seconds.
"Which students share the same faculty mentor as Rahul?" Now you need to follow connections. Student to mentor, mentor back to students. A register cannot do this. A vague memory of a chat cannot do this either.
See, an agent's memory has exactly the same three kinds of questions. Fuzzy recall, exact lookup, and connections. If you build memory with only one kind of store, you are automatically bad at the other two. The fix is to keep all three, write to all three, and merge the answers when you read.
The idea
Three stores, one door
You write once. Behind the scenes, the same fact goes into three different places.
- Vector store — keeps the meaning of a sentence as numbers, so "what did we discuss about slow deliveries?" finds a chat about "orders arriving late". Good for fuzzy, similar-meaning search.
- KV store — a plain dictionary. Key is something like (user, fact type, thing). Value is the fact. Instant exact lookup, no guessing.
- Graph store — stores facts as small arrows: Rahul → mentored_by → Prof. Rao. Good for "who is connected to whom".
- 1New fact
- 2Pull out facts
- 3Write to vector
- 4Write to KV
- 5Write to graph
One popular open-source library that does exactly this is called Mem0. Its graph part is called Mem0g. You do not need the library to understand the pattern; the pattern is the interview answer.
Reading: everyone answers, then you score
On a search, all three stores answer. The vector store returns the closest-meaning records. The KV store returns exact hits. The graph store returns whatever is reachable from the entities in the question.
Now you have three piles. You cannot just show all of them. So you score every record on three things and add the scores up:
score = w1 * relevance + w2 * importance + w3 * recency
- Relevance — how well it matches the question.
- Importance — some facts just matter more. A phone number, an allergy, a refund policy.
- Recency — newer facts get a boost, older ones fade slowly.
Here is the trick people miss. This is a weighted sum, not a priority order. You are not saying "check KV first, then vector". A slightly older but very important fact can beat a fresh but useless one. That is the whole point of adding instead of ranking.
And the weights change per product. A chat assistant wants recency high, because what you said two minutes ago matters most. A compliance bot wants importance high, because a policy from last year still binds you. A document search agent wants relevance high.
TipTip: Start with equal weights, then look at ten real bad results and adjust. Do not tune weights on day one from a whiteboard.
A worked example
Suppose Priya uses a food-delivery assistant.
In March she says "I live in Kochi". The system stores it: vector gets the sentence, KV gets (priya, city, home) = Kochi, graph gets Priya → lives_in → Kochi.
In August she says "I moved to Pune". Now there is a contradiction. Mem0g does not delete the Kochi edge. It marks it invalid from August onwards and adds the Pune edge.
Why keep the dead one? Because next month someone asks "where was Priya ordering from in April?" With soft invalidation the answer is Kochi. With a hard delete, that history is gone forever and you cannot answer time-based questions at all.
- 1Question
- 2Vector hits
- 3KV hits
- 4Graph hits
- 5Score and merge
- 6Top answers
Scopes: who is allowed to see what
Every memory write also picks a scope.
- User memory — sticks around across all sessions, tied to that one user.
- Session memory — lives only inside one conversation thread.
- Agent memory — the agent's own working state.
Remember: mixing scopes carelessly is how an assistant ends up telling Priya about Rahul's private order. Pick the scope at write time, every single time.
Build it
# Toy hybrid memory: vector + KV + graph, fused on read. Standard library only.
import math, time
NOW = time.time()
class Memory:
def __init__(self):
self.vec = [] # (fact, token set, importance, timestamp)
self.kv = {} # (user, kind, entity) -> fact
self.graph = [] # (subject, relation, object, valid?)
def add(self, user, kind, entity, fact, importance=0.5, age_days=0):
ts = NOW - age_days * 86400
# 1. vector-ish: we fake an embedding with a bag of words
self.vec.append((fact, set(fact.lower().split()), importance, ts))
# 2. exact lookup
self.kv[(user, kind, entity)] = fact
# 3. relationships; a new fact invalidates the old edge, never deletes it
for i, (s, r, o, ok) in enumerate(self.graph):
if s == user and r == kind and ok:
self.graph[i] = (s, r, o, False)
self.graph.append((user, kind, entity, True))
def search(self, user, query, w=(0.5, 0.3, 0.2)):
q = set(query.lower().split())
out = []
for fact, toks, imp, ts in self.vec:
overlap = len(q & toks) / max(len(q), 1) # relevance
recency = math.exp(-(NOW - ts) / (30 * 86400)) # fades in ~a month
score = w[0] * overlap + w[1] * imp + w[2] * recency
out.append((round(score, 3), fact))
return sorted(out, reverse=True)[:3]
m = Memory()
m.add("priya", "city", "Kochi", "Priya lives in Kochi", 0.9, age_days=180)
m.add("priya", "city", "Pune", "Priya moved to Pune", 0.9, age_days=2)
m.add("priya", "food", "dosa", "Priya likes dosa for breakfast", 0.2, age_days=10)
print("KV exact :", m.kv[("priya", "city", "Pune")])
print("Graph :", m.graph)
print("Fused, chat weights :", m.search("priya", "where does priya live"))
print("Fused, recency heavy :", m.search("priya", "where does priya live", w=(0.2, 0.1, 0.7)))Look at three things in the output. The KV line answers instantly with no scoring at all. The graph line still holds the Kochi edge, just marked False instead of removed. And the last two lines are the same question with different weights, giving a different order, which is exactly what weight tuning does in a real product.
Where you will see this
- Coding assistants like Claude Code and Cursor remembering your project conventions across sessions, not just the current file.
- ChatGPT-style assistants that recall your name and preferences weeks later, while still answering about the current chat.
- Customer support bots that must look up your exact order ID and also recall the vibe of last week's complaint.
- Swiggy or Zomato style assistants keeping your default address as an exact fact and your taste as a fuzzy one.
- Fraud and billing systems asking "which accounts share this address?", which only a graph can answer.
Common mistakes
- Using only a vector store because it is easiest. Exact questions like "what is my order ID" come back with a similar-sounding wrong record, which is worse than saying "I don't know".
- Treating the score as a priority ladder. If you always let one store win, you lose the whole benefit; the point is that a very important old fact can outrank a fresh irrelevant one.
- Hard-deleting a contradicted fact. Once Kochi is deleted you can never answer "where was she in April", and audit or compliance work becomes impossible.
- Letting the KV key space grow wild. Every team invents its own fact type, and soon nobody knows whether it is
cityorhome_cityorlocation. Review the type list regularly. - Never re-embedding. Vector search that felt great on the first hundred records slowly rots as the corpus grows; refresh the most-used records from time to time.
If they ask in an interview
Q: Why not just use a vector database for agent memory?
A: Vector search is built for similar-meaning retrieval, so it is great for "what did we discuss about X". But an exact fact lookup is a dictionary hit, and a relationship question needs graph traversal. A vector store answers those two badly, so production designs run all three and merge the results.
Q: How do you rank results coming from three different stores?
A: Score every candidate on relevance, importance and recency, then take a weighted sum. Keeping it a sum rather than a fixed order lets an important older fact beat a fresh but weak match, and the weights are tuned per product: recency high for chat, importance high for compliance.
Q: What happens when a new fact contradicts an old one?
A: You mark the old one invalid with a timestamp instead of deleting it. That keeps time-based questions answerable, like "what was this user's city in March", and it gives you an audit trail.
Try these
- Add an
as_ofargument tosearchso it only returns facts that were valid at that time. Check that asking for March still returns Kochi. - Add a fourth score dimension for user feedback, like a thumbs up. Then think about how you stop the agent from only ever showing records it already liked.
- Add a scope field to every write, and make search refuse to return another user's records. Try to break it on purpose.
- Change the toy relevance from word overlap to something slightly smarter, like ignoring common words. See how the ranking shifts on the same three queries.
Words, simply
| Word | Meaning in simple words |
|---|---|
| Hybrid memory | Keeping the same fact in three stores and merging them when you read |
| Vector store | Search by meaning, not by exact words |
| KV store | A dictionary: exact key in, exact fact out |
| Graph store | Facts as arrows between things, so you can follow connections |
| Fact extraction | Breaking a chat message into small clean facts before storing |
| Fusion scoring | Adding up relevance, importance and recency to rank records |
| Scope | Whether a memory belongs to a user, one session, or the agent itself |
| Soft invalidation | Marking an old fact as no longer true instead of deleting it |
Quick recap
- One store is always wrong for two out of the three question types, so write every fact to a vector, a KV and a graph store.
- On read, score each candidate on relevance, importance and recency and add them up; tune the weights for your product.
- Never delete a contradicted fact, just mark it invalid with a time, so history stays answerable.