유대선
프로젝트로
·기술 회고·2

The cache knew what it embedded, not which model embedded it

Once the RAG cache is shared by two engines (Rust + the Python brain), it has to record which model produced each vector — otherwise a model change silently serves stale, wrong-space vectors. Fixed with a model-fingerprint stamp that self-invalidates.

The question that exposed it

Right after consolidating the RAG core onto one embedding model (brain-first, Rust fallback), the founder asked the right question: do I now have to keep the Rust engine and the Python brain upgraded in lockstep forever?

I went to answer "no" — and found the actual reason the answer was, quietly, "kind of yes."

What the cache stored

The persistent embedding cache is one JSONL line per node: {node_id, hash, vec}. The hash is a content hash — it answers "did this node's text change?" If not, the cached vector is reused.

What it does not record is which model produced the vector. So if the embedding model ever changed — bump the Rust fastembed model, or change the brain's MODEL_NAME — every node whose text was unchanged would keep serving its old-model vector. Two vector spaces, silently mixed in one cache, scored against each other as if they were comparable. With two engines now writing into the same cache, there are two independent ways to trigger that drift.

A content hash answers "did the input change?". It says nothing about "did the function change?" — and an embedding model is the function.

The fix

Stamp every vector with the model that made it:

const EMBED_MODEL_ID: &str = "paraphrase-multilingual-MiniLM-L12-v2@384";

persist_one writes the stamp; hydrate skips any line whose stamp doesn't match the current id (legacy lines with no stamp are skipped too). A changed model means old entries simply don't load → they get re-embedded on demand → the cache rebuilds itself.

So the answer to the founder's question becomes a real "no": you don't keep two engines in manual lockstep. When the model changes you bump one const, and the whole cache invalidates and rebuilds. The const's comment names both things it has to match — the Rust enum and the brain's MODEL_NAME — so the day they diverge is a one-line, obvious change, not a silent corruption.

Verified

The lesson

A derived cache has to version itself by what produced it, not just by its inputs. The standard cache-busting key — but pointed at model identity instead of source code. It also reframes the "two engines is a maintenance tax" worry: the tax isn't the second engine, it's an un-versioned shared cache. Version the cache and the engines can move independently.