The problem
Retrieval-augmented generation is meant to keep a language model honest: fetch the relevant passages, then let the model answer from them. In practice the model treats those passages as a suggestion. It blends what it retrieved with what it half-remembers, and the failure is invisible, a fluent, confident, plausible sentence that no source actually supports.
A retriever you can trust needs the opposite default. It should answer only from what it pulled back, point to exactly which evidence each claim rests on, and when the evidence is thin it should say it does not know rather than fill the gap.
What I built
grounded is a Python retrieval service that answers only from its sources. Two retrieval paths feed it, and one deterministic gate stands between the evidence and the answer. A vector search finds what is semantically related; a knowledge graph finds what is structurally connected, the multi-hop questions a similarity search cannot reach. Whatever comes back, the gate enforces the same rule: an answer must cite a retrieved chunk or map to a real graph path, or the system abstains.
GROUNDED_RETRIEVAL_MODE: hybrid # vector store + knowledge graph
GROUNDED_MIN_SCORE: 0.68 # abstain below this confidence floor
# the gate: every claim cites a retrieved chunk OR a real graph path, else abstain- Two retrieval sources. A Qdrant vector store over local embeddings for semantic recall, and an embedded KuzuDB knowledge graph with bounded multi-hop traversal for the structural questions a vector search misses. In hybrid mode an answer is allowed if either path grounds it.
- A deterministic grounding gate. Weak retrieval is rejected before a single generation call is spent. Any cited chunk that was not in the retrieved set is rejected. No fabricated graph relationships survive: the traversed path is what gets cited.
- Abstention over fabrication. When retrieval falls below a tuned threshold the system returns an honest "not in my sources" instead of inventing a sentence that reads true.
- Keyless core. Embeddings run locally and both stores are embedded, so ingest, retrieval, the gate and the eval run with no API key and no Docker.
- A governed tool surface over MCP. Since v0.2.0 the same retrieval ships as
grounded-mcp, an MCP server on the official Python SDK over stdio. Three protocol tools:retrievereturns top-k chunks with citations and scores,answer_evidencereturns cited evidence when the best hit clears the measured 0.68 floor and otherwise an explicit abstention carrying the floor and the best score, andgraph_pathsreturns real multi-hop traversals from the knowledge graph. Any MCP client, Claude Code, Cursor, anything speaking the protocol, can mount a retrieval tool that cites or abstains.
This is interlock's thesis applied to retrieval: wrap a stochastic step in deterministic gates so the output is something you can trust, not just something you can demo. Retrieval, the graph, the gate and the eval are the proven core and all run keyless. The final phrasing step, a language model that turns grounded evidence into one sentence, is wired behind the same interface but has not yet been run against a live key, so the part that is verified is the part that decides what may be said, not a live bot.
The project has grown in one direction. First a public corpus, then retrieval with an abstention floor, then a knowledge graph for the questions similarity search cannot reach, and now a tool surface agents can mount over the protocol they actually use. Each step widens what can be asked without loosening what may be answered.
Outcome
grounded is evaluated, not demoed. It carries a labelled eval set scored on the metrics a retriever is actually judged on: recall@5 of 15/15 with a tuned abstention threshold, plus graph-recall and groundedness for the graph path. The whole suite runs in CI with no key, no Docker and no model download, so every change is measured against real retrieval numbers rather than a single happy-path screenshot.
The MCP surface is held to the same standard. Eight tests drive a real server subprocess over stdio at the protocol level, from initialize through tool calls, and the v0.2.0 release was verified live from an MCP client: an in-corpus question answered with citations at a best score of 0.78, and an out-of-corpus probe refused with an explicit abstention at 0.47, below the floor.
Tech
Python and FastAPI. Local embeddings (bge-small-en-v1.5, CPU) so nothing leaves the box at ingest time, an embedded Qdrant vector store, and an embedded KuzuDB graph store with Cypher traversal bounded by a hop limit. The MCP server is built on the official Python MCP SDK, speaks stdio, and reads the same environment as the rest of the service. Generation is pluggable behind one interface: a deterministic stub by default for keyless development, with a real model swappable in by one environment variable. Tested with pytest over a faked-embedding seam, so the offline suite needs neither a key nor a GPU.