상담원이 이미 수정한 사실을 계속 반복하는 이유

작성자

카테고리:

← 피드로
DEV Community · Nam Bok Rodriguez · 2026-07-23 개발(SW)

Every agent I build eventually does the same thing. It repeats a fact I already corrected, stated with full confidence, as if the correction never happened. I tell it my coffee order changed from a black coffee to a latte, and a few turns later it hands me the black coffee. The memory didn’t exactly fail. It retrieved something, it retrieved the version I had already replaced, because a raw vector store can tell you two facts are similar and has no idea that one of them replaced the other.

You can fix that, and the usual way is to put a language model in front of the store: on each new message it reads the text, compares it to what is saved, and decides whether to add, update, or delete. Corrections collapse into the current fact. But now it also means a model call and a network round trip on every single write, usually across a few services you now run. It works, it just costs you a model call every time you remember something.

That is the one of the parts I wanted to change. Not the retrieval, the deciding. Whether a new fact is a duplicate, a correction, a conflict, or something genuinely new is the real job of a memory, and I did not want that job living in code on top of a store, or using a model I have to call on every write. A model call per write is slow, it costs money, it pulls in more services to run, and it can answer a little differently on a bad day.

And most of these calls are not that subtle once you look at the text. A correction usually keeps the whole sentence and just swaps the value. That is a rule you can write down, not a judgment you need a model for. So I wrote MenteDB, a single embeddable engine in Rust, and I put the deciding inside it. When the latte arrives, it sees the same fact with a changed value and supersedes the black coffee. Same answer every time, no model asked.

What supersession does under the hood

The old fact isn’t deleted. It gets a Supersedes edge in the graph, and recall stops returning it because superseded memories are excluded by belief propagation, so the history stays queryable while your prompt only sees the current value.

process_turn wraps this in one call: embed the message, run hybrid recall, store the turn, reconcile against what is already there, and return context for the next prompt. Here it is end to end using the API:

import os, requests

MENTEDB = "https://api.mentedb.com"
H = {"Authorization": f"Bearer {os.environ['MENTEDB_API_KEY']}"}

_n = 0
def turn(user, assistant=""):
    global _n
    _n += 1
    return requests.post(f"{MENTEDB}/v1/process_turn", headers=H, json={
        "user_message": user,
        "assistant_response": assistant,
        "turn_id": _n,
        "agent_id": "demo-user",
    }).json()

turn("My coffee order is a black coffee")
turn("Actually I switched to lattes")

r = turn("What's my coffee order?")
for m in r["context"]:
    print(m["content"])

Enter fullscreen mode Exit fullscreen mode

Supersession is the sharpest example because it is obvious when it goes wrong, but it is one of many. Deciding that a memory has gone stale and should fade, that two are near duplicates and should merge, that one user’s memories must never surface for another, that this handful is what is actually relevant to the current turn: those are the same kind of decision, made continuously, about what the agent should see right now. A raw vector store leaves all of that to your application code. The model on every write approach pays a model for it. I wanted it to be what the database does, so decay, deduplication, owner isolation, and relevance ordered recall are engine operations, not things you deal with.

One engine, not a stack of services

process_turn handles recall for you. It blends vector similarity with keyword search, so a query catches both the meaning and the exact words, and it puts the most relevant memories at the start and end of the context window, where the model pays the most attention. It only sends what changed since the last turn instead of replaying everything every message.

All of that is one Rust crate. Its own page based storage and WAL, its own HNSW index, its own graph. No external vector database to run alongside it, no separate graph store, no Postgres to manage. That is the part I actually care about, memory as a single dependency you embed, not a distributed system you operate.

Run it three ways, same engine

Hosted, if you just want a key:

export MENTEDB_API_KEY=mdb_your_key   # from app.mentedb.com
# then POST to https://api.mentedb.com/v1/process_turn (above)

Enter fullscreen mode Exit fullscreen mode

Self host, one container, local embeddings bundled, no keys, nothing leaves the box:

docker run -p 6677:6677 -v mentedb-data:/data \
  ghcr.io/nambok/mentedb:latest

Enter fullscreen mode Exit fullscreen mode

Embed the engine directly (Python over the Rust core):

from mentedb import MenteDB

db = MenteDB("./data", embedding_provider="candle")   # local, no key
db.store("The user prefers dark mode")
db.store("The user's editor is Neovim")

result = db.process_turn("what are their UI preferences?", turn_id=1)
for m in result.context:
    print(m.content)

Enter fullscreen mode Exit fullscreen mode

Same engine underneath all three. The hosted product is that engine on managed infra.

Try it live

I created a demo here: demo.mentedb.com. Add facts, ask questions, and explore the memory graph they build.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다