RAG vs agent memory: what the source code actually shows
Is agent memory just RAG with extra steps?
This argument is also on video, with every mechanism animated rather than described: RAG vs agent memory: is it just RAG with extra steps?, 12 minutes, chaptered.
At query time, substantially yes, and you can read the code. At write time, no, and the difference is narrower and more specific than the category usually claims.
Before anything else: we build Mnemoverse, a memory layer for AI agents connected over MCP (C41). So this is written by a vendor in the category it is about, it names competitors and quotes their source, and you should weigh it accordingly. One thing that follows from what we sell and is worth stating early, because vendors including us blur it: MCP defines tool discovery and invocation over JSON-RPC and specifies no memory, retrieval or storage semantics of its own (the tools specification, C44). Being connected over MCP says how a memory layer is reached, not that it remembers anything.
There are no performance numbers anywhere in this article — not ours, and none borrowed from anyone else to fill the gap. That is a choice rather than an absence of data: our benchmark page publishes numbers, and it also shows why we do not lead with them here. Two paper-comparable LongMemEval runs of the same engine disagree, roughly 0.62 against 0.79, and without a frozen judge-prompt hash we cannot say which is canonical — so that page presents both and headlines neither (C33). The reason that is a defensible position rather than an evasion comes later, in the chapter on when you should not buy any of this.
What does RAG actually do at query time?
It runs a bi-encoder retriever over a document index, resolves it as a maximum inner product search, takes the top-k passages and concatenates them with the input for the generator. That is the whole read path in Lewis et al. 2020 (C1).
The part our own industry keeps forgetting is one sentence further on. The authors write that "we can update RAG's world knowledge by simply replacing its non-parametric memory" (C2). The index is swappable at test time with no retraining. Every memory pitch that opens by describing retrieval as a static or frozen store is arguing with something that was never in the paper. That includes pitches from people who sell what we sell.
What does a memory layer do at query time?
It runs hybrid retrieval. Embed the query, over-fetch candidates from a vector store, run a keyword search alongside, fuse the scores, optionally rerank, filter out anything expired. Here is mem0's shipping search path (C3).
mem0/memory/main.py, lines 1633 to 1649, at commit 001c235
# Step 1: Preprocess query
query_lemmatized = lemmatize_for_bm25(query)
query_entities = extract_entities(query)
# Step 2: Embed query
embeddings = self.embedding_model.embed(query, "search")
# Step 3: Semantic search (over-fetch for scoring pool)
internal_limit = max(limit * 4, 60)
semantic_results = self.vector_store.search(
query=query, vectors=embeddings, top_k=internal_limit, filters=filters
)
# Step 4: Keyword search (if store supports it)
keyword_results = self.vector_store.keyword_search(
query=query_lemmatized, top_k=internal_limit, filters=filters
)The next steps compute BM25 scores from the keyword hits, drop expired payloads out of the candidate set, and hand everything to a single fusion function.
mem0/memory/main.py, lines 1679 to 1687
# Step 8: Score and rank
scored_results = score_and_rank(
semantic_results=candidates,
bm25_scores=bm25_scores,
entity_boosts=entity_boosts,
threshold=threshold,
top_k=limit,
explain=explain,
)A reranker exists and is applied at line 1500 if you configured one, and the rerank argument to search() defaults to False. Nothing in that sequence is a second mechanism. It is a textbook hybrid retrieval pipeline with a fusion step, and if you have built retrieval before you have built this.
LangGraph is more explicit. Long-term memory there is a JSON document stored under a namespace and a key, and semantic search is opt-in (C4).
libs/checkpoint/langgraph/store/base/__init__.py, lines 578 to 583, at commit 644815f
class IndexConfig(TypedDict, total=False):
"""Configuration for indexing documents for semantic search in the store.
If not provided to the store, the store will not support vector search.
In that case, all `index` arguments to `put()` and `aput()` operations will be ignored.
"""Then there is the one that should settle the argument. Anthropic ships a first-party memory tool with no embeddings, no retrieval scoring and no decay of any kind. It is six file commands, and the documentation states that "the memory tool operates client-side: Claude requests file operations, and your application executes them" (memory tool docs, C5). A memory tool from the company that makes the model, and it is a filesystem.
So here is the concession, said plainly because our category usually does not say it. For these systems, at read time, there is no distinct memory mechanism. The skeptic in the comments is right. If somebody drew you two different diagrams for the read path, one of them was fiction.
What is actually different: the write path
Three things happen when a memory layer writes that do not happen when you index a document. A model decides whether the thing is worth storing at all. The new item is reconciled against what is already there before it lands. And the stored unit can carry a validity interval that is separate from when the system learned it.
The first is older than the current category. Generative Agents scores importance by asking a language model to rate the poignancy of an observation on a one to ten scale (C22). It is a judgement at admission time, not a similarity computation at read time, and a RAG ingest pipeline has no equivalent because a RAG pipeline is not deciding whether your documents deserve to exist.
The third is the one worth seeing in code. In Graphiti, which is Zep's engine and Zep is a competitor of ours, every entity edge carries four timestamps (C6).
graphiti_core/edges.py, lines 271 to 279, at commit 96ef997
expired_at: datetime | None = Field(
default=None, description='datetime of when the node was invalidated'
)
valid_at: datetime | None = Field(
default=None, description='datetime of when the fact became true'
)
invalid_at: datetime | None = Field(
default=None, description='datetime of when the fact stopped being true'
)The fourth, created_at, sits on the base Edge class at line 54. Two axes: when the system learned something, and when that something was true in the world. Follow one fact through it.
| Event | created_at | valid_at | invalid_at | expired_at |
|---|---|---|---|---|
| March, fact stated | March | March | null | null |
| June, contradicted | March | March | June | June |
| the new edge | June | June | null | null |
The old edge is still there. The Zep paper describes the mechanism directly: on contradiction "it invalidates the affected edges by setting their t_invalid to the t_valid of the invalidating edge" (Zep paper, C6). Nothing is deleted. The record now says this used to be true, and here is when it stopped.
Compare that with the alternatives. Mem0's only open-source expiry mechanism is a user-supplied expiration_date, and the docstring says expired memories "are hidden from search and get_all unless show_expired is True" (C8).
mem0/memory/main.py, lines 442 to 451
def _payload_is_expired(payload: Optional[Dict[str, Any]]) -> bool:
if not payload:
return False
expiration_date = payload.get("expiration_date")
if not expiration_date:
return False
try:
return date.fromisoformat(str(expiration_date)) < datetime.now(timezone.utc).date()
except ValueError:
return FalseThat is demotion, not forgetting. And A-MEM takes the opposite approach: when a new memory arrives, its evolution step rewrites the neighbouring memories with a language model and the evolved version replaces the original (C9). The prior text is gone and nothing in the record tells you it changed.
Can you just add timestamps to your RAG metadata?
Yes. Nothing stops you building bitemporal validity on a vector store with metadata filters, and people do, and it works.
The difference is where the reconciliation code lives and who maintains it (C10). With metadata, contradiction handling happens at query time, in your application, written by you, and it grows a branch every time a new kind of conflict shows up. In a memory layer it happens once, at write time, in code you did not write and do not own.
That is a weaker claim than our industry usually makes. It is also the true one. The distinction is not what is achievable. It is what is amortised, and who is on the hook for maintaining it. Anyone selling you a capability gap here is selling you something that is not there.
Why does a memory layer read from a corpus it wrote itself?
Because extraction and retrieval are wired into a loop, and that loop is the one asymmetry that survives everything above (C11). Extraction writes the memories. Retrieval reads what extraction wrote. The output of that retrieval feeds the next extraction. The store is both the input and the output of the same process.
A RAG corpus is not in that loop. Your documents were authored somewhere else, by people, in a system that does not read its own retrieval results back into its own index. A RAG pipeline can retrieve badly. It cannot corrupt its source.
Once memory writes its own corpus, its extraction errors become its retrieval corpus. This is not a quality problem that a better extraction prompt closes out. It is the structure. And it is the direct explanation for the complaint that shows up under every memory launch: it remembers the wrong details and applies them in the wrong places.
Why does my AI agent remember the wrong things?
Because the hard decision is admission, not eviction, and almost nothing in the category is built around admission.
ConsistencyGate states it directly: existing memory management addresses retrieval and capacity, not write-time correctness, and the problem cannot be solved with utility or recency based criteria (C12). Their reason is the mechanism, not a preference. A hallucinated fact written at one step persists as a false premise for every step after it, which they name memory contamination (C13). Contamination compounds along a trajectory because each downstream step treats the bad premise as ground truth.
Now the part that made us uncomfortable when we checked it. In every reinforcement scheme we opened, retrieval is what keeps a memory alive (C14).
Generative Agents rewrites last_accessed on every node it returns.
reverie/backend_server/persona/cognitive_modules/retrieve.py, lines 266 to 267, at commit fe05a71
for n in master_nodes:
n.last_accessed = persona.scratch.curr_timeMemoryBank increases a memory's strength and resets its clock on recall, in their words: "When a memory item is recalled during conversations, it will persist longer in memory. We increase S by 1 and reset t to 0."
LangGraph's store refreshes an item's time-to-live on read, and that is the default rather than an option you switch on.
libs/checkpoint/langgraph/store/base/__init__.py, lines 545 to 551
class TTLConfig(TypedDict, total=False):
"""Configuration for TTL (time-to-live) behavior in the store."""
refresh_on_read: bool
"""Default behavior for refreshing TTLs on read operations (`GET` and `SEARCH`).
If `True`, TTLs will be refreshed on read operations (get/search) by default.The default is resolved at line 1292, where the absent config returns True, and the put() docstring says the same thing in prose: "the expiration timer refreshes on both read operations (get/search) and write operations (put/update)".
Put those three together. A wrong fact that keeps getting retrieved is protected by the exact signal that was supposed to prune it (C15). The more an error gets used, the more entrenched it becomes, and every mechanism in that list was designed on the assumption that retrieval frequency is evidence of value. For a corpus the system did not write, that assumption is reasonable. For a corpus the system wrote itself, it is a feedback loop with the sign pointing the wrong way. This is the classic ACT-R framing, where a memory's activation is a function of how often and how recently it was used (ACT-R base-level activation, C23), applied to a corpus whose contents were never independently checked.
The other half of the same problem is that discarding happens blind. Compaction mechanisms decide what to drop using attention magnitude or recency, before the query is known, with no way to undo the decision (rate-distortion analysis, C16).
Where our own system is worse
Our shipped write-path gate scores geometric novelty against the nearest existing memory in the same domain. It does not judge importance and it does not judge factuality, which makes the name we gave it, an importance gate, wrong (C40).
The consequence is the failure mode this whole article has been building toward, and it is ours. A correction is by its nature phrased almost exactly like the thing it corrects. So it scores as a near-duplicate. So it is, of all possible inputs, the one most likely to be rejected. The stale fact then survives as the sole record, and having no competitor in the store, it looks more authoritative than it did before the correction was attempted. That is written into our own package as a user-facing known defect in the 0.8.1 changelog. We have not solved it.
What do AI memory papers claim that the shipping code does not implement?
Enough that reading a paper is not a way to choose a memory system. This is checkable in an afternoon, and here is what we found.
| Paper claim | What the shipping code does | File and line | Checked |
|---|---|---|---|
| Mem0's update phase emits ADD, UPDATE, DELETE and NOOP against the top-s similar memories (C7) | DEFAULT_UPDATE_MEMORY_PROMPT and get_update_memory_messages are referenced only in prompts.py and tests. The production add path imports ADDITIVE_EXTRACTION_PROMPT and records the superseded memory's UUID in linked_memory_ids (C17) | mem0/memory/main.py lines 20 and 942 | 001c235, 2026-08-16 |
| Mem0-graph is a variant of the system (C7) | No graph module exists in mem0/memory/ in the open-source Python package (C18) | mem0/memory/ directory listing | 001c235, 2026-08-16 |
| Scored decay is part of the memory model | decay=True raises ValueError in both the sync and async OSS classes, and the line after that check raises unconditionally, so project.update is not in the open library at all (C19) | mem0/memory/main.py lines 461 to 484 | 001c235, 2026-08-16 |
| Generative Agents sets all three retrieval weights to 1 (C20) | gw = [0.5, 3, 2], with # gw = [1, 1, 1] commented out directly above | retrieve.py line 244 | fe05a71, 2026-08-16 |
| Recency decays with elapsed time, constant 0.995 (C21) | recency_decay ** i over a list sorted by last-accessed time, so it decays over rank position, and the constant in scratch.py is 0.99 | retrieve.py lines 145 and 224 to 228 | fe05a71, 2026-08-16 |
The Generative Agents weights are the sharper example because the code says out loud that it is unfinished.
reverie/backend_server/persona/cognitive_modules/retrieve.py, lines 238 to 249
# Computing the final scores that combines the component values.
# Note to self: test out different weights. [1, 1, 1] tends to work
# decently, but in the future, these weights should likely be learned,
# perhaps through an RL-like process.
# gw = [1, 1, 1]
# gw = [1, 2, 1]
gw = [0.5, 3, 2]
master_out = dict()
for key in recency_out.keys():
master_out[key] = (persona.scratch.recency_w*recency_out[key]*gw[0]
+ persona.scratch.relevance_w*relevance_out[key]*gw[1]
+ persona.scratch.importance_w*importance_out[key]*gw[2])And the recency term, which most people who cite this paper describe as time-based decay, is an exponential over position in a sorted list.
reverie/backend_server/persona/cognitive_modules/retrieve.py, lines 145 to 146
recency_vals = [persona.scratch.recency_decay ** i
for i in range(1, len(nodes) + 1)]Two consequences worth naming. None of this makes the papers dishonest. Research code moves, production code diverges from the write-up, and the authors of the second example annotated their own shortcut in a comment. But if you are choosing a memory system by reading its paper, you are choosing something that may not be running.
The second consequence is about the field rather than any one repository. In none of the systems we opened does the open-source default read path implement ACT-R-style or Ebbinghaus-style scored decay (C30). That scope is the systems named in this article, and it is a real limit: we did not audit Cognee, Supermemory, or the internals of any hosted platform, and nothing here should be read as a claim about the whole category. Two independent parties moved in the opposite direction entirely. Anthropic's documentation delegates expiry to you, its only guidance being to "periodically delete memory files that haven't been accessed in a long time" (C24), and Letta, a competitor of ours, describes agent memory that "lives in a git repository owned by the agent" with background consolidation subagents rather than a scored decay store (C26).
When do you not need an agent memory layer?
Below roughly a hundred and fifty conversations per user, the evidence points at doing nothing clever, and that number is ours to hand you honestly because it costs us.
ConvoMem is academic work and not from a memory vendor. It finds that simple full-context methods outperform retrieval-based memory systems on its hardest multi-message evidence cases, and its authors put the practical crossover around that scale (C28). Letta reports that an agent given nothing but filesystem tools over a plain file of the conversation scored above mem0's published benchmark result, and argues that current memory benchmarks may not be very meaningful (C29). Letta is a vendor, mem0 is a vendor, we are a vendor, and that is exactly why the finding is worth repeating in their words: "memory is more about how agents manage context than the exact retrieval mechanism used."
Part of why the null baseline keeps winning is a property of the benchmark rather than of anyone's system. LoCoMo conversations, the ones this category cites most, fit comfortably inside every current context window (C31). A benchmark whose entire input can be pasted into the prompt cannot distinguish a memory architecture from a large context. If you are below that scale, use files and a long context. Anthropic gives away a file-based memory primitive at no cost. We would rather you learn that here than from an invoice.
The counterweight, stated in the same breath so this does not read as false modesty: plain context does degrade as input length grows, and it degrades on tasks as simple as replicating a word sequence (context rot, C34). Chroma is a vector database vendor, so disclose that too. The authors are explicit that their evaluation is not exhaustive of real-world use cases and that they do not explain the mechanism behind the degradation, and citing that work without both of those caveats is the easiest way to overclaim in this whole discussion. The honest answer is that it depends on your horizon, not that memory always helps or never does.
On benchmarks generally, be suspicious of all of us (C32). Each leading vendor reports its lead on a benchmark it selected, and when challenged, vendors dispute one another's methodology rather than reproducing it. That pattern includes mem0's paper, Zep's paper, and Zep's public rebuttal of mem0. It includes us. That is the reason there is not a single performance figure in this article.
Two more reasons to be careful, and both apply to us more than to a read-only retrieval stack. Writable memory that persists across sessions is a qualitatively different threat surface, characterised by persistence, statefulness and propagation, and the research is explicit that protection cannot be retrofitted at retrieval or execution time alone but has to be anchored in storage-time provenance, versioning and policy-aware retention (memory security survey, C35). The lifecycle in that work names a share and propagate phase, which is precisely what a shared memory layer over MCP adds. MINJA demonstrates that an agent's memory can be poisoned through ordinary query interaction alone, with no privileged access to the memory bank (C36).
And erasure is not solved by anyone in this category, us included. GDPR Article 17(2) requires a controller obliged to erase public personal data to take reasonable steps to inform other controllers that erasure of links, copies and replications has been requested (C37). Once a fact has been summarised, consolidated into a derived record and embedded into a vector, deleting the original row does not obviously delete its descendants. Nobody here ships a demonstrated answer to that.
The honest state of the field is that several of these questions are open. A March 2026 survey still lists learned forgetting among the field's open challenges (C38). The repeated compaction that agents actually perform is almost never measured, and no benchmark holds one budget axis across all the layers at once (C39). Whether any forgetting policy beats simply bounding what gets loaded in the first place is not known.
How do you evaluate an agent memory layer?
Three checks, all runnable this afternoon against any vendor, including us. None of them is a question to ask on a sales call, because a sales call is where this category is at its least reliable.
One. Open the read path and see whether it differs from hybrid retrieval. If the system is open source, find the search or retrieve function and read it top to bottom, the way you just read mem0's. Look for an embed call, an over-fetch, a keyword or BM25 branch, and a fusion step. If it is closed, ask for the read path in writing and compare the answer with what the API exposes: if the query parameters are a string, a filter and a limit, and the response carries a similarity score, you are looking at retrieval. That is not a criticism. Retrieval is the floor. It is only a problem if the pricing is built on the claim that it is something else.
Two. Store a fact, then contradict it, and watch what happens to the original. Write "the project deploys on Fridays". Then write "the project no longer deploys on Fridays". Then do two reads: query for the fact, and separately list every record the system holds on that subject. Three outcomes tell you three different things. The original is still there carrying a validity interval that closed, which is the Graphiti behaviour. The original is present but hidden from reads, which is mem0's expiry behaviour and is demotion rather than forgetting. Or the original is gone, or silently rewritten, which is the A-MEM behaviour and means you have lost the ability to audit what the system used to believe.
Three. Submit a correction and see whether the system accepts it, or decides it is a duplicate. Write a fact. Then write a correction phrased as closely to the original as a real correction would be, changing only the part that was wrong. Read back. If the correction is not in the store, the write gate rejected it as a near-duplicate, and the wrong fact is now the only record on that subject.
That third check is where we fail today, for the reason described above, in our own changelog, in public. Run it on us.
Sources
Repositories, read 2026-08-16 at the commits shown.
- mem0, commit
001c235: github.com/mem0ai/mem0, file at mem0/memory/main.py (C3, C8, C17, C18, C19) - LangGraph, commit
644815f: libs/checkpoint/langgraph/store/base/__init__.py (C4, C14) - Graphiti, commit
96ef997: graphiti_core/edges.py (C6) - Generative Agents, commit
fe05a71: github.com/joonspk-research/generative_agents (C14, C20, C21, C22)
Papers and documentation, fetched 2026-08-16.
- Lewis et al. 2020, Retrieval-Augmented Generation: arxiv.org/abs/2005.11401, full text at ar5iv (C1, C2)
- Anthropic memory tool: platform.claude.com (C5, C24)
- Model Context Protocol, tools specification: modelcontextprotocol.io (C44)
- Zep and Graphiti paper: arxiv.org/html/2501.13956v1 (C6, C32)
- Mem0 paper: arxiv.org/html/2504.19413v1 (C7, C11, C32)
- A-MEM: arxiv.org/html/2502.12110v1 (C9)
- Generative Agents paper: ar5iv.labs.arxiv.org/html/2304.03442 (C20, C22)
- ConsistencyGate: arxiv.org/abs/2607.22962 (C12, C13)
- MemoryBank: arxiv.org/html/2305.10250v3 (C14)
- Rate-distortion analysis of agent context compaction: arxiv.org/abs/2607.08032 (C16, C39)
- ACT-R base-level activation: ar5iv.labs.arxiv.org/html/1306.0125 (C23)
- Letta memory docs: docs.letta.com/agent-sdk/memory (C26)
- Letta on memory benchmarking: letta.com/blog/benchmarking-ai-agent-memory (C29, C32)
- ConvoMem: arxiv.org/abs/2511.10523 (C28)
- LoCoMo: arxiv.org/abs/2402.17753 (C31)
- Context rot, Chroma: trychroma.com/research/context-rot (C34)
- Agent memory security survey: arxiv.org/abs/2604.16548 (C35)
- MINJA: arxiv.org/abs/2503.03704 (C36)
- GDPR Article 17: gdpr-info.eu/art-17-gdpr (C37)
- Survey of agent memory, March 2026: arxiv.org/abs/2603.07670 (C38)
- Zep on mem0's benchmark methodology: blog.getzep.com (C32)
Our own surfaces, cited the same way as everyone else's.
- Mnemoverse, what we are and what we ship: mnemoverse.com (C41)
- Our benchmark page, including the two LongMemEval runs we decline to adjudicate: mnemoverse.com/docs/technology/benchmarks (C33)
- Our write-path gate, documented under the name this article argues is wrong: mnemoverse.com/docs/api/overview (C40)
Analytical claims made by this article itself, derived from the code and papers cited above rather than from a single external source: C10, C15, C30.
Code excerpts are reproduced for commentary under the upstream licenses linked from each repository (mem0 and Graphiti: Apache-2.0; LangGraph and Generative Agents: MIT), and are limited to what the commentary needs.
Vendors named here, all of them competitors of ours: mem0, Zep and Graphiti, Letta, Chroma. We are one too. Mnemoverse is a shared memory layer for AI agents connected over MCP: mnemoverse.com, setup docs, github.com/mnemoverse.
Related
- Agent memory: knowledge graph or retrieval? the same read-path question asked of graph stores
- Bi-temporal memory for AI agents the four timestamps in this article, treated at length
- Agent memory is not a database what the write path buys you, argued from the other direction
- AI memory solutions compared, Q3 2026 the wider vendor landscape
- The GraphRAG tax what the graph layer costs before it pays
Edward Izgorodin · Mnemoverse · 2026-08-16
Mnemoverse is a persistent-memory API for AI agents. Free key: console.mnemoverse.com · Docs: Getting Started
