Self-RAG Graph Route · Grade · Correct · Verify

Adaptive + Corrective + Self-RAG Graph

One LangGraph pipeline that folds three retrieval patterns into a single self-correcting graph — it routes each question to the right source, grades what it retrieves, corrects itself with a rewrite and a web fallback, and verifies the answer is grounded before it ships. The full production version — runnable, with loop guards.

Route (adaptive) Grade (corrective) Correct (rewrite + web) Verify (self-RAG)
Query routing Per-document relevance grading Query-rewrite loop Web-search fallback Groundedness verification Loop guards
127.0.0.1:8300 · Chat

Chat

qwen.qwen3-235b-a22b 8 chunks
How does the collection-rights policy handle disputed debts?
Graph trajectory
Routed to the vectorstoreKnowledge base
Question targets the user's private policy documents.
Graded 4 passages1/4 relevant → correcting
[1] [2] [3] [4]
Rewrote the querycorrection
“disputed debts”“debt collection dispute rights & validation notice rules”
Web search · 5 resultsWeb fallback
Drafted a grounded answer
Verified — shippingaccept
Groundedness Answers question
A debtor may dispute a debt in writing within the validation window; the collector must then pause collection and mail verification before resuming [1]. Disputed amounts can't be reported to bureaus without noting the dispute [5].
[1]KB
Collection_Rights_Cheatsheet.pdf · p.1
[5]WEB
consumerfinance.gov — debt validation

The Chat view — a live graph trajectory (route → grade → correct → verify), then the grounded, cited answer with its merged KB + web sources.

What it is

A RAG pipeline that checks its own work

Most RAG retrieves once and hopes for the best. This graph makes retrieval a decision it revisits: it picks the right source, throws out passages that don't help, rewrites and reaches for the web when it comes up short, and refuses to ship an answer it can't ground in the sources — all inside a single LangGraph with bounded loops.

Adaptive query routing

An LLM router sends each question to the cheapest source that can answer it — the private vectorstore, the live web_search, or a direct answer with no retrieval at all.

Per-document relevance grading

Every retrieved passage is graded relevant / off-topic on its own. Off-topic chunks never reach the generator, so a lucky-but-wrong match can't poison the answer.

Query-rewrite loop

When too few passages survive grading, the graph rewrites the question into a sharper, higher-signal query and tries again — the corrective move that rescues a bad first retrieval.

Web-search fallback

If the private corpus comes up short, the rewritten query goes to the live web (Tavily or DuckDuckGo). Web hits join the same citation space as KB passages.

Groundedness verification

Before an answer ships, a judge checks every claim is supported by the sources. A hallucination sends it back to regenerate — strictly from the evidence.

Answer-relevance check

A second judge asks whether the answer actually resolves the question. If not, the graph rewrites and retries instead of handing back something off-target.

Loop guards — it always halts

max_rewrites bounds the corrective loop; max_generations bounds regeneration; a recursion_limit backstops both. Every grader fails open, so a flaky judge can't trap the graph.

Per-thread memory

A LangGraph SqliteSaver checkpointer keeps each conversation's history server-side, so follow-ups resolve references and it all survives restarts.

Live streaming trajectory

Nodes emit NDJSON events; the UI renders the route, the per-passage grade chips, the corrective rewrite & web hop, and the Self-RAG verdicts as a live timeline — answer tokens stream in.

Private vectorstore ingestion

Upload .pdf, .md or .txt (or paste text); recursive chunking + sentence-transformers embeddings build the dense store the router can route to.

Password-protected settings

Model, embedding and Bedrock connection live server-side in SQLite, edited behind a password — never in the browser.

Same graph in the terminal

A streaming CLI (chat.py) drives the identical graph and prints the full trajectory — and shares threads.db with the web UI.

Why it's useful

When a wrong answer costs more than a slow one

Plain RAG will confidently answer from an irrelevant chunk. This pipeline is built for the cases where that's not acceptable — where the answer has to be grounded, sourced, and worth trusting, even if it means a second pass.

Enterprise knowledge assistants

Answer over private policies, reports and wikis with citations — and a groundedness gate that blocks confident hallucinations before they reach a user.

Mixed private + current-events Q&A

One assistant that knows when to read your documents and when to hit the live web — the router decides per question, so users don't have to.

Compliance & support desks

Where every answer must trace to a source. The self-check + citations make replies auditable, and off-topic retrievals are filtered out, not paraphrased.

Sparse or incomplete knowledge bases

When the corpus doesn't cover a question, the corrective rewrite + web fallback quietly fills the gap instead of returning "I couldn't find anything."

A reference implementation to learn from

A clean, commented example of routing, CRAG grading and Self-RAG verification wired into one graph — the canonical patterns, in one runnable place.

Tech stack

What it's built on

A small Python backend and a vanilla-JS frontend with no build step. Five tiny LLM "judges" steer a seven-node graph; the only heavyweight is the model itself.

Orchestration
LangGraph 1.xStateGraphConditional edgesSqliteSaver checkpointerstream_mode="custom"
Model / inference
Amazon BedrockAnthropic MantleClaude (streamed)Qwen (OpenAI-compat SSE)
The judges
RouterDocument graderQuery rewriterGroundedness graderAnswer graderFail-open JSON
Backend
FastAPIUvicornPydanticNDJSON streaming
Retrieval & web
sentence-transformersCosine similarityRecursive chunkingTavilyDuckDuckGopypdf
Storage & frontend
SQLitePBKDF2 authVanilla JSNo build stepManrope · JetBrains Mono
How it works

Route → grade → correct → verify, in one graph

Seven nodes and a handful of conditional edges. The graders decide the branches; the loop guards decide when to stop.

START
  │
  ▼
route_question ─"direct"────────────▶ direct_answer ─────────────▶ END
  │  "web_search"                                   ▲
  │      └────────────▶ web_search ──┐             │
  │  "vectorstore"                    │             │
  ▼                                   ▼             │
retrieve ─▶ grade_documents ─────▶ generate ─▶ (Self-RAG grade)
                 │  too few relevant    │              │
                 ▼                       │  grounded & useful ─▶ END
            transform_query ◀───────────┤  not grounded ─▶ generate   (≤ max_generations)
                 │                       └─ not useful  ─▶ transform_query (≤ max_rewrites)
                 ▼
             web_search ─▶ generate

Route the question (adaptive)

One LLM call classifies the turn as vectorstore, web_search, or direct. Hard constraints are enforced in code — an empty KB never routes to the vectorstore, a disabled web tool never routes to the web — so the router can't pick an impossible branch.

Retrieve, then grade every passage (corrective)

The vectorstore route pulls the top-k passages, and a grader scores each one relevant or off-topic independently. Only survivors reach the generator. If fewer than a threshold survive, the graph flags a correction.

Correct — rewrite the query & reach for the web

The correction rewrites the question into a sharper query, then searches the live web as a fallback source. KB and web hits flow into one citation space, so [3] means the same passage wherever it came from. The rewrite counter starts a fresh retrieval attempt.

Generate a grounded, cited answer

The generator answers strictly from the selected sources, citing [n] for every factual claim, and streams token by token — natively for anthropic.*, over the OpenAI-compatible SSE endpoint for other families like Qwen.

Verify before shipping (Self-RAG)

Two more judges run on the draft. Groundedness checks it invents no facts absent from the sources — a failure regenerates. Answer relevance checks it resolves the question — a failure rewrites and retries. Only when both pass does the turn end.

Stay bounded — loop guards & fail-open graders

max_generations caps consecutive regenerations; max_rewrites caps rewrites per turn; a recursion_limit backstops both, and every grader returns the permissive verdict when its reply can't be parsed. The graph is guaranteed to halt.

Stream everything to the browser

Nodes call get_stream_writer() to emit events — route, retrieve, doc_grade, grade_summary, transform, web, generate_start, text_delta, self_check, decision, sources, usage. The server forwards them as NDJSON; the UI renders the live trajectory.

⚡ Every branch is decided by a small LLM judge, and every judge fails open — so the graph self-corrects when it helps, and never loops forever when a judge misbehaves.
Get started

Run it in two commands

Ships with a pre-seeded app_config.db and a sample index/, so the Bedrock region, key, model and a working knowledge base carry over — it runs out of the box.

# install & launch
pip install -r requirements.txt
python server.py                 # → http://127.0.0.1:8300

# then, in the browser:
1. Knowledge Base → upload .pdf / .md / .txt, Build index   # optional
2. Chat          → watch route → grade → correct → verify live
3. Settings      → model & Bedrock connection (password: bala@143)

# this overview page is served at:
http://127.0.0.1:8300/overview
📄 The server exposes this document at GET /overview (and /overview.html) — open http://127.0.0.1:8300/overview while it's running.