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

One embedding model, not two — routing the RAG core through the brain

The Tauri process and the Python brain were each loading the same 384-dim model into RAM. Consolidated the RAG core's four embed sites to brain-first with a Rust fallback, so normal operation runs one model.

The duplication

Phase 1 of the Python brain moved ranking over to the daemon: graph_query and workitem_context call brain_rank, and on a brain-down they fall back to keyword scoring. That part shipped and is observable (a 🧠/⚠️ keyword badge).

But the embeddings underneath the RAG core never moved. memory.rs still held its own in-process Rust fastembed model — ParaphraseMLMiniLML12V2, 384-dim — and called it directly at four sites: the semantic_rank document batch, the query embed, the single-node write-time embed, and the triples batch. Meanwhile the Python brain loads the identical model (paraphrase-multilingual-MiniLM-L12-v2) over fastembed/ONNX.

So a normal session kept the same ~90 MB ONNX model resident twice — once in the Tauri process, once in the brain. Whichever call site you hit decided which engine did the work. That's not a fallback; that's two owners of the same thing.

The fix: pick one owner

The reason this is safe and not just convenient: both engines run the same model in the same normalized 384-dim space. I verified that earlier (환불→refund cosine 0.529 both ways), so a vector the brain produced and a vector the Rust model produced are interchangeable. That's what lets the on-disk embedding cache stay valid regardless of which engine wrote a given vector — a brain-embedded query still scores correctly against Rust-embedded nodes cached from before the change. If the two sides had been different models, this same refactor would have silently corrupted the cache.

Verified

Normal operation now runs one model — the warm launchd brain. The Rust model stays in the binary purely as a cold fallback for the window where the brain is starting up or absent.

The lesson

When two services load the same model, don't let call sites each grab whatever engine is nearest — name one owner and make the other an explicit fallback. The trade is HTTP latency per embed, but the brain is a warm loopback daemon (sub-ms), and in return one model leaves RAM and the brain gets a single, observable embedding path. The whole thing only works because "same model" was measured, not assumed.