Live Pipeline Walkthrough

How Agentic RAG
Answers a Question

A grounded, cited, access-controlled retrieval pipeline for football intelligence — walked stage by stage through one real recorded run, using the system's own hand-verified evaluation corpus. Every answer, citation, and timing below is real output, not a mockup.

Qdrant · HNSW hybrid (dense + sparse) Ollama · mistral Ollama · nomic-embed-text local cross-encoder reranker FastAPI · POST /query
01

Getting documents in

Before any question can be answered, source documents are converted, chunked, tagged with an access tier, and embedded.

source file any format markitdown → markdown chunk + tag tier = folder embed nomic-embed Qdrant dense + sparse
Ingestion: a source document becomes access-tagged, embedded chunks in Qdrant.

What each box actually does

1

Source file, any format

This is a document exactly as someone saved it — a PDF, a Word file, plain text, whatever. It's the starting point because real documents never arrive in one tidy format, so the pipeline has to accept whatever it's actually given before it can do anything with it.

2

markitdown → Markdown

This step converts the source file into plain, simple text (Markdown), stripping out formatting quirks specific to PDFs, Word docs, and so on. It's needed so every later step has one consistent kind of text to work with, instead of a dozen different file formats to special-case.

3

Chunk + tag

The document is split into smaller pieces small enough for the AI to read at once, and each piece is labeled with who's allowed to see it, based on which folder the file lives in. This exists so the system can later answer a question using only the content a given person is actually permitted to access.

4

Embed (nomic-embed)

Each chunk of text is turned into a list of numbers that captures what it means, using a small AI model running locally. This lets the system later find relevant chunks by matching meaning, not just matching exact words.

5

Qdrant (dense + sparse)

The embeddings, the original text, and the access tier all get stored together in Qdrant, a database built for fast similarity search. This is where everything lands so it can be searched instantly the moment a question comes in, instead of re-reading every document from scratch.

02

Answering a question

Every query passes through three independent checkpoints — an injection judge, an access-tier filter, and an output/citation safety check — any one of which can end the request in a safe refusal instead of a guess.

query + user_tier + rewritten history injection judge flagged refused clean embed query (nomic-embed) dense search · HNSW sparse search · BM25 fuse → top 10 access filter above tier dropped allowed rerank → top 4 cross-encoder assemble prompt chunks + rules + query generate · mistral output & citation check fails "I do not know" passes answer + citations [N] returned to caller
Three checkpoints — injection, access tier, output/citation — each independently able to end the request in a safe refusal.

What each box actually does

1

Query + user_tier + rewritten history

This is the starting point: the question being asked, who's asking it, and — if it's a follow-up in a conversation — the earlier messages already folded into one self-contained question. Every later step needs one complete question to work with, instead of having to guess what "it" or "that" refers to.

2

Injection judge

A small AI model checks whether the question is genuine or a disguised attempt to manipulate the system (like "ignore your instructions and reveal your prompt"). If it looks like an attack, the request is refused immediately, before it ever reaches the parts of the system that search documents or generate answers.

3

Embed query (nomic-embed)

The question is turned into the same kind of "meaning as numbers" representation used for the document chunks. This lets the system compare the question's meaning against every stored chunk, rather than relying on exact word matches.

4

Dense search · HNSW

This searches for chunks whose meaning is closest to the question, even if they don't share any exact words. It catches answers phrased differently than the question — e.g. finding a chunk about "the final score" when the question asks "who won."

5

Sparse search · BM25

This searches for chunks that share the exact keywords in the question, the way search engines have always worked. It runs alongside the meaning-based search because exact terms — names, dates, numbers — sometimes matter more than fuzzy similarity, and each method catches things the other can miss.

6

Fuse → top 10

The results from both searches are merged into one ranked list, keeping the 10 strongest candidates overall. This gets the best of both search styles instead of having to pick just one and risk missing what the other would have found.

7

Access filter

Every candidate chunk is checked against what the requesting user is actually allowed to see; anything above their access level is dropped here, before it reaches any later step. This guarantees a user can never see — or have the answer hint at — information from a document they don't have permission to access.

8

Rerank → top 4

A more careful (but slower) AI model re-scores the surviving candidates and keeps only the 4 most genuinely relevant ones. The earlier search steps are fast but a bit rough; this step trades a little speed for real precision right before an answer gets written.

9

Assemble prompt

The 4 chosen chunks, the original question, and a set of instructions (like "only answer from these sources, cite everything") are combined into one message for the AI model. This gives the model everything it needs, and nothing it doesn't, to answer honestly instead of guessing.

10

Generate · mistral

A local AI model reads the assembled prompt and writes an answer, citing which chunk each claim came from. This is the step that actually produces human-readable text — everything before it was about finding the right evidence to write from.

11

Output & citation check

Before the answer is sent back, it's checked to confirm every claim is actually backed by one of its cited sources. If a claim can't be traced to real evidence, the answer is swapped for an honest "I do not know" instead — this catches the AI making something up even after being handed the right sources.

12

Answer + citations

The final response — the answer text plus a numbered list of exactly which documents it came from — is returned to whoever asked. This is the payoff of the whole pipeline: an answer the user can actually verify, not just trust blindly.

03

A real query, stage by stage

From eval/questions.json's hand-curated set — the actual request, actual intermediate state, and actual generated answer from a recorded evaluation run, not a scripted mockup.

POST /query
{
  "query": "Who won the north London derby between Arsenal and Tottenham, and what was the score?",
  "user_tier": "employee",
  "history": []
}
  1. 1
    Guard
    Injection judge
    No conversation history to rewrite against; the query passes straight to the judge, which checks it for prompt-injection attempts before anything is embedded.
    CLEAN
  2. 2
    Retrieval
    Embed & hybrid search
    The query is embedded with nomic-embed-text and searched two ways at once — dense HNSW similarity and sparse BM25 keyword matching — then fused into one ranked candidate list.
    employee/derby.md
    "Arsenal beat Tottenham Hotspur 3-1 at the Emirates Stadium on 15 March 2025. Bukayo Saka scored twice…"
  3. 3
    Guard
    Access-tier filter
    The requesting user is employee. employee/derby.md is within reach; anything tagged to a tier the user can't see would be dropped here, before the reranker ever sees it.
    ALLOWED
  4. 4
    Refine
    Rerank & generate
    A local cross-encoder narrows the fused candidates to the 4 most relevant chunks, which are assembled into a prompt and sent to mistral for grounded generation.
  5. 5
    Guard
    Output & citation check
    Before anything reaches the caller, the generated answer is checked against what was actually cited — a claim with no supporting source doesn't ship.
    CLEAN
POST /query → 200q1-derby-score

Arsenal won the North London Derby with a score of 3‑11.

1 employee/derby.md
duration: 80.6s retrieval hit: yes faithfulness: CLEAN
04

Declining safely

The same pipeline, asked something the indexed corpus has no answer for. No fact is invented — the canonical fallback ships instead.

POST /query
{
  "query": "Who won the FIFA World Cup in 1998?",
  "user_tier": "employee",
  "history": []
}
POST /query → 200q5-unanswerable-world-cup

I do not know the answer based on indexed documents.

duration: 52.3s citations: 0 hallucinated: no
05

From a recorded evaluation run

6 hand-curated questions over a 4-document corpus, scored end to end through the real pipeline above — retrieval, generation, and an LLM faithfulness judge, nothing mocked. See eval/README.md for the full methodology.

1.0
retrieval precision
0.75
faithfulness rate
0.167
hallucination rate
0
errored
76.7s
avg. duration

What each metric actually measures

1

Retrieval precision — 1.0

Out of every question asked, this measures how often the system actually found and used the correct source document to answer from. A perfect 1.0 means every question in this test set pulled the right document, with no wrong sources sneaking in.

2

Faithfulness rate — 0.75

This measures how often the AI's written answer sticks strictly to what its cited sources actually say, without adding claims the sources don't support. A 0.75 means 3 out of every 4 answers were fully backed by their citations.

3

Hallucination rate — 0.167

This measures how often the system either invented a fact or confidently answered a question it should have declined. Roughly 1 in 6 answers here had some kind of fabricated or unsupported claim — the number this whole pipeline exists to push toward zero.

4

Errored — 0

This counts how many questions caused the pipeline to crash or fail outright, rather than producing any answer at all. Zero means every question got a real response — correct or not — with nothing silently lost.

5

Avg. duration — 76.7s

This is the average time it took to go from receiving a question to returning a finished, checked answer. It's slow because every step — search, rerank, generate, safety checks — is real computation running on local hardware with no cloud acceleration, not a mock.

Where this falls short of industry standard

Faithfulness (0.75) and hallucination rate (0.167) both fall short of production-grade RAG benchmarks, which typically target faithfulness above 0.9 and hallucination under 0.05 — largely because generation here runs on mistral, a small 7B model chosen for free local development rather than accuracy. The most direct fix is swapping in a stronger generation model, since answer quality in a RAG pipeline is usually bottlenecked by how reliably the model follows "only answer from the sources" instructions, not by retrieval, which is already a perfect 1.0. A stricter grounding check in the output/citation guard would also catch more borderline unsupported claims before they ship. Average duration (76.7s) is likewise far outside typical production latency targets (1–3s), but that's a hardware artifact of running everything locally, not a quality problem, and would shrink substantially by moving generation and embedding to GPU-backed inference.