εΊδΊ Flow-Engineering ηζεζη―εΎοΌDCGοΌθͺιεΊζ£η΄’ε’εΌΊηζεΌζ
Production-grade Adaptive RAG with self-correcting hallucination loop, async concurrent grading, and graceful degradation.
Traditional RAG pipelines suffer from a fundamental architectural limitation: they operate as acyclic, unidirectional flows β retrieve once, generate once, output. When the retrieved context is stale, irrelevant, or insufficient, the system either hallucinate silently or produce low-confidence answers with no self-healing mechanism.
LangGraph Adaptive RAG Engine redesigns this paradigm using a Directed Cyclic Graph (DCG) topology built on LangGraph's state machine primitives. The core innovation is a self-correcting reflection loop β after generation, the system automatically evaluates hallucination risk against source documents. When ungrounded claims are detected, the engine triggers a query rewrite β re-retrieve β re-grade β re-generate cycle, capped by a bounded iteration guard to guarantee termination.
This is not a wrapper around LangChain. It is a from-scratch state machine architecture with:
- 8 typed graph nodes implementing a complete query lifecycle
- 3 conditional routing edges with branch-specific fallback policies
- Async-first design with
asyncio.gatherparallel document grading - Graceful degradation from Qdrant vector store to web search on connection failure
- Mock LLM layer for deterministic local testing without API keys
The entire engine is a single compiled LangGraph StateGraph. Below is the exact state machine topology β every node, every conditional edge, and every terminal state:
flowchart TD
START((βΆ START)) --> ROUTE{{"π§ Router\n(Query Classification)"}}
ROUTE -- "noise" --> NOISE["π¬ Noise Reply\n(Direct LLM Response)"]
NOISE --> END_NOISE((βΌ END))
ROUTE -- "vector_store" --> RETRIEVE["π Retrieve\n(Qdrant Vector Store)"]
ROUTE -- "web_search" --> WEB_SEARCH["π Web Search\n(Fallback / Time-Sensitive)"]
RETRIEVE --> GRADE{{"π Grade Documents\n(Async Relevance Scoring)"}}
GRADE -- "relevant" --> GENERATE["βοΈ Generate\n(LLM Answer Synthesis)"]
GRADE -- "not relevant\n& search_count < 2" --> WEB_SEARCH
GRADE -- "not relevant\n& search_count β₯ 2" --> GENERATE
WEB_SEARCH --> GENERATE
GENERATE --> HALLUCINATION{{"π Hallucination Grader\n(Groundedness Verification)"}}
HALLUCINATION -- "SUPPORTED β" --> END_OK((βΌ END))
HALLUCINATION -- "NOT_SUPPORTED β\n& search_count < 2" --> REWRITE["π Rewrite Query\n(Adaptive Retrieval)"]
HALLUCINATION -- "NOT_SUPPORTED β\n& search_count β₯ 2" --> END_FALLBACK((βΌ END))
REWRITE --> RETRIEVE
style START fill:#22c55e,color:#fff,stroke:none
style END_NOISE fill:#6b7280,color:#fff,stroke:none
style END_OK fill:#22c55e,color:#fff,stroke:none
style END_FALLBACK fill:#f59e0b,color:#fff,stroke:none
style REWRITE fill:#f97316,color:#fff,stroke:none
style GRADE fill:#3b82f6,color:#fff,stroke:none
style HALLUCINATION fill:#8b5cf6,color:#fff,stroke:none
style ROUTE fill:#ec4899,color:#fff,stroke:none
| Edge | Condition | Behavior |
|---|---|---|
| Router β Noise Reply | destination == "noise" |
Casual/small-talk β direct LLM answer, no retrieval |
| Router β Web Search | destination == "web_search" |
Time-sensitive queries β bypass vector store entirely |
| Router β Retrieve | destination == "vector_store" |
Factual/domain queries β proceed with RAG pipeline |
| Grade β Generate | is_relevant == True |
Sufficient context found β synthesize answer |
| Grade β Web Search | is_relevant == False AND search_count < 2 |
Insufficient context β fallback to web search |
| Hallucination β END | verdict == "SUPPORTED" |
Answer is fully grounded β pipeline complete |
| Hallucination β Rewrite | verdict == "NOT_SUPPORTED" AND search_count < 2 |
Ungrounded claims detected β adaptive retry loop |
| Rewrite β Retrieve | Always | Query rewritten β re-enter retrieval phase |
Termination guarantee: Both _grade_route and _hallucination_route enforce search_count < 2, capping total retrieval iterations at 2 cycles to prevent infinite loops.
Source Map β every feature below links to its exact implementation file and line, so you can jump straight to the source in under 30 seconds.
AgentStatemodel withField(...)constraints on every field βsrc/state.py:17HallucinationReportwithge=0.0, le=1.0bounds βsrc/state.py:9QueryRoutewithLiteral["noise", "vector_store", "web_search"]βsrc/chains/router.py:9DocGrade/HallucinationGradestructured output models βsrc/chains/doc_grader.py:7/src/chains/hallucination_grader.py:9
- Parallel grading implementation using
asyncio.gatherwithreturn_exceptions=Trueβsrc/nodes/grade_documents.py:18-33 - Reduces wall-clock from
O(n Γ LLM_latency)toO(LLM_latency)by concurrently scoring all retrieved chunks.
- Qdrant exception handling returns empty docs on connection failure β
src/nodes/retrieve.py:24-30 - Downstream
grade_documentsflagsis_relevant=False, triggering automatic web search fallback via conditional edge insrc/graph.py.
- Graph compilation with
MemorySaverβsrc/graph.py:232-233 - Thread-level session isolation via
thread_idconfig insrc/main.py:30,41 - Enables breakpoint recovery and full state replay across pipeline runs.
MockChatModel(extendsBaseChatModel) βsrc/mock_llm.py:12- Keyword-driven deterministic responses; auto-activates when API key is empty/placeholder β
src/graph.py:16 - Zero network calls β entire pipeline testable offline.
langgraph_adaptive_rag_engine/
βββ .env # Environment variables (API keys, Qdrant config)
βββ .gitignore # Git exclusion rules
βββ .python-version # Python 3.12
βββ pyproject.toml # Project metadata & tool config
βββ test_sanity.py # Unit tests for state models & node imports
βββ main.py # Root entry stub
β
βββ scripts/
β βββ populate_qdrant.py # Qdrant seed script (SiliconFlow embeddings)
β
βββ src/
βββ __init__.py
βββ main.py # π Primary entry point β runs both demo cases
βββ graph.py # π§ Core graph topology β 8 nodes, 3 conditional edges
βββ state.py # π Pydantic state models (AgentState, HallucinationReport)
βββ mock_llm.py # π§ͺ Deterministic mock LLM for offline testing
β
βββ config/
β βββ __init__.py # Re-exports Settings
β βββ settings.py # Pydantic BaseSettings from .env
β
βββ chains/
β βββ __init__.py
β βββ router.py # Query classification chain (noise/vector/web)
β βββ doc_grader.py # Document relevance grading chain
β βββ hallucination_grader.py # Hallucination detection chain
β
βββ nodes/
βββ __init__.py
βββ retrieve.py # Qdrant retrieval with graceful degradation
βββ grade_documents.py # Async concurrent document grading
βββ web_search.py # Simulated web search fallback
βββ generate.py # LLM answer synthesis
- Python 3.12+
- uv β fast Python package manager
- Qdrant (optional) β for vector store retrieval; without it, the pipeline degrades to web search mode
git clone https://github.com/YOUR_USERNAME/langgraph-adaptive-rag-engine.git
cd langgraph-adaptive-rag-engine
uv synccp .env.example .env # or create .env manuallyEdit .env with your credentials:
OPENAI_API_KEY=sk-your-key-here
OPENAI_API_BASE=https://api.openai.com/v1 # or any OpenAI-compatible endpoint
LLM_MODEL=gpt-4o-mini
LLM_PROVIDER=openai
QDRANT_URL=http://localhost:6333
QDRANT_COLLECTION=adaptive_rag
TOP_K=4
SCORE_THRESHOLD=0.5No API key? The engine auto-detects empty/placeholder keys and switches to the built-in
MockChatModelβ run it fully offline with zero configuration.
# Start Qdrant locally
docker run -p 6333:6333 qdrant/qdrant
# Seed with sample documents
uv run scripts/populate_qdrant.pyuv run src/main.pyThis executes two demo cases:
- Case A β
"What is Retrieval-Augmented Generation (RAG)?"β vector_store route, document grading, generation, hallucination check - Case B β
"What are the latest developments in quantum computing as of 2026?"β web_search route with full reflection loop
uv run pytest test_sanity.py -v| Want to... | Change this |
|---|---|
| Use Anthropic instead of OpenAI | Set LLM_PROVIDER=anthropic in .env |
| Add real web search | Replace src/nodes/web_search.py with Tavily/SerpAPI/Bing integration |
| Use vector similarity search | Replace src/nodes/retrieve.py scroll+keyword with Qdrant's query_points API |
| Increase retry depth | Modify search_count < 2 guard in src/graph.py edges |
| Add persistence | Swap MemorySaver for SqliteSaver or PostgresSaver in graph compilation |
Why DCG (Directed Cyclic Graph) over DAG? Traditional RAG is a DAG β data flows one way. The hallucination reflection loop introduces a cycle that enables self-correction. LangGraph's state machine naturally supports cycles with explicit termination guards, making this both safe and expressive.
Why Pydantic BaseModel over TypedDict for AgentState?
TypedDict provides structural typing at the class level but no runtime validation. Pydantic BaseModel enforces field types, default values, and custom validators on every state transition β catching corruption before it propagates through the graph.
Why search_count as a circuit breaker?
Unbounded retry loops are the #1 failure mode in self-correcting architectures. A simple integer counter in the state, checked at every conditional edge, provides a deterministic termination guarantee with zero external dependencies.
Contributions are welcome. Please open an issue first to discuss proposed changes.
MIT License β see LICENSE for details.