Portfolio RAG Pipelines Corrective Retrieval-Augmented Generation (CRAG)
Agentic Orchestration

Corrective Retrieval-Augmented Generation (CRAG)

AI Pipeline Architecture Model

Back to Portfolio
Corrective Retrieval-Augmented Generation (CRAG) Icon

An agentic self-correcting RAG pipeline that evaluates the quality of retrieved documents before generation. If the information is deemed insufficient or irrelevant, it triggers an autonomous web-search agent to pull live data.

🏗️
Engineering Value & Purpose
Significantly mitigates LLM hallucinations when dealing with queries that fall outside the pre-indexed knowledge base.
Technology Stack
LangGraph CrewAI Tavily API Local Evaluator LLMs

System Architecture Blueprint

RAG-003: Corrective Retrieval-Augmented Generation (CRAG) — Engineering Blueprint

This specification details the architecture, routing mechanics, evaluation models, and agentic orchestration code for RAG-003 (Corrective Retrieval-Augmented Generation).

  1. System Specifications & Performance SLA

Target Accuracy: $>99\%$ hallucination mitigation on out-of-vocabulary (OOV) or out-of-index queries.

Retrieval Decision Latency: Evaluator LLM routing decision made in $< 120\text{ms}$ (using local structured output parsing on quantized models).

End-to-End SLA:

Fast path (adequate retrieval context): $< 600\text{ms}$ total round-trip response time.

Corrective path (retrieval + parallel Tavily web search): $< 1,800\text{ms}$ total round-trip response time.

  1. Stateful Agentic Graph Architecture

Unlike traditional sequential pipelines, CRAG treats retrieval, grading, correction, and generation as nodes in a directed acyclic or cyclic graph (DAG/DCG).

                        [ Incoming User Query ]
                                   │
                                   ▼
                         [ Retrieve Documents ]
                                   │
                                   ▼
                     [ Grade Document Relevance ]
                                   │
             ┌─────────────────────┴─────────────────────┐
             ▼ (All Correct)        ▼ (Some Correct)     ▼ (All Incorrect)
       [ Generate ]           [ Web Search Tool ]   [ Web Search Tool ]
             ▲                      │                    │
             │                      ▼                    ▼
             │             [ Merge Contexts ]     [ Swap Context ]
             │                      │                    │
             └──────────────────────┴────────────────────┘

2.1 Graph State Definition

The orchestrator maintains a transaction-safe execution state passed between nodes:

from typing import List, TypedDict

class CRAGState(TypedDict): query: str documents: List[Dict[str, Any]] web_search_results: List[str] run_web_search: bool relevance_grading: str # "CORRECT" | "INCORRECT" | "AMBIGUOUS" generation: str

  1. Mathematical Evaluation Scoring & Decision Boundaries

Let $Q$ represent the user query, and $\mathcal{D} = {D_1, D_2, \dots, D_N}$ represent the set of $N$ retrieved document chunks.

An evaluator model $E$ estimates a joint semantic match and confidence score $S_i$ for each retrieved document chunk $D_i$:

$$S_i = P(\text{Relevant} \mid D_i, Q) \in [0.0, 1.0]$$

3.1 Decision Boundaries

We define two critical classification thresholds: $\tau_{\text{high}}$ (threshold for verified accuracy) and $\tau_{\text{low}}$ (threshold for total irrelevance).

The system-wide grading status $\mathcal{G}$ is computed as:

$$\mathcal{G} = \begin{cases} \text{CORRECT}, & \text{if } \forall S_i \ge \tau_{\text{high}} \ \text{AMBIGUOUS}, & \text{if } \exists S_i \in [\tau_{\text{low}}, \tau_{\text{high}}) \ \text{INCORRECT}, & \text{if } \forall S_i < \tau_{\text{low}} \end{cases}$$

Configuration Targets: In production, $\tau_{\text{high}} = 0.85$ and $\tau_{\text{low}} = 0.40$.

Routing Decisions:

$\mathcal{G} = \text{CORRECT}$: Transition directly to the generator node.

$\mathcal{G} = \text{AMBIGUOUS}$: Keep high-scoring document chunks, trigger parallel web search to patch missing data, and synthesize both.

$\mathcal{G} = \text{INCORRECT}$: Completely discard the retrieved documents, flag the state, trigger web search, and generate using only search results.

  1. LangGraph Implementation Specification

This runnable Python module implements the core state machine, document grading logic, Tavily web-search invocation, and dynamic edge routing.

import os import json from typing import Dict, Any, List from pydantic import BaseModel, Field from langgraph.graph import StateGraph, END from qdrant_client import QdrantClient from openai import OpenAI from tavily import TavilyClient

1. Grader Data Schema for Structured Output

class DocumentGrade(BaseModel): relevance_score: float = Field(description="Relevance confidence score between 0.0 and 1.0") assessment: str = Field(description="Detailed reason for relevance or irrelevance")

2. Pipeline Orchestrator

class CorrectiveRAGOrchestrator: def init(self, qdrant_url: str, qdrant_api_key: str): self.db_client = QdrantClient(url=qdrant_url, api_key=qdrant_api_key) self.llm_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) self.search_client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))

    # Threshold Configurations
    self.tau_high = 0.85
    self.tau_low = 0.40

def node_retrieve(self, state: Dict[str, Any]) -> Dict[str, Any]:
    """Node 1: Pull Document Chunks from Vector Space"""
    query = state["query"]
    # Fast mock query. Replace with actual dense/hybrid retrieval queries
    retrieved_chunks = [
        {"id": "doc_1", "text": "Company revenues in Q3 hit $14.2B with steady growth."},
        {"id": "doc_2", "text": "Our current software uses Python 3.11 with FastAPI backend."}
    ]
    return {"documents": retrieved_chunks}

def node_grade_documents(self, state: Dict[str, Any]) -> Dict[str, Any]:
    """Node 2: Evaluate Context Relevance against Query"""
    query = state["query"]
    docs = state["documents"]

    valid_docs = []
    max_score = 0.0
    all_below_low = True

    for doc in docs:
        # Leverage LLM Structured Outputs for fast deterministic evaluations
        response = self.llm_client.beta.chat.completions.parse(
            model="gpt-4o-mini",  # Replace with fast local model like Llama-3-8B via vLLM
            messages=[
                {"role": "system", "content": "You are a factual retrieval grader. Grade the document's relevance to the user query."},
                {"role": "user", "content": f"Query: {query}\n\nDocument Chunk:\n{doc['text']}"}
            ],
            response_format=DocumentGrade
        )

        grade = response.choices[0].message.parsed
        score = grade.relevance_score

        if score >= self.tau_low:
            all_below_low = False
            valid_docs.append(doc)

        if score > max_score:
            max_score = score

    # Evaluate final grading classification state
    if all_below_low:
        grading = "INCORRECT"
    elif max_score >= self.tau_high and len(valid_docs) == len(docs):
        grading = "CORRECT"
    else:
        grading = "AMBIGUOUS"

    return {
        "documents": valid_docs, 
        "relevance_grading": grading, 
        "run_web_search": grading in ["AMBIGUOUS", "INCORRECT"]
    }

def node_web_search(self, state: Dict[str, Any]) -> Dict[str, Any]:
    """Node 3: Fallback External Web Search"""
    query = state["query"]
    # Executing external search using Tavily API
    response = self.search_client.search(query=query, max_results=3)
    results = [item["content"] for item in response.get("results", [])]
    return {"web_search_results": results}

def node_generate(self, state: Dict[str, Any]) -> Dict[str, Any]:
    """Node 4: Synthesize verified response"""
    query = state["query"]
    docs = state["documents"]
    search_results = state.get("web_search_results", [])

    # Merge available contextual segments
    local_context = "\n".join([d["text"] for d in docs])
    web_context = "\n".join(search_results)

    combined_context = f"--- LOCAL DOCUMENTS ---\n{local_context}\n\n--- WEB SEARCH RESULTS ---\n{web_context}"

    response = self.llm_client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "You are a corrective generator. Synthesize a clean response. Use the provided web contexts to resolve gaps or omissions present in your local context records."},
            {"role": "user", "content": f"Query: {query}\n\nMerged Context:\n{combined_context}"}
        ]
    )
    return {"generation": response.choices[0].message.content}

def route_grading(self, state: Dict[str, Any]) -> str:
    """Conditional Routing Edge"""
    if state["run_web_search"]:
        return "trigger_search"
    return "trigger_generation"

def compile_graph(self):
    """Construct LangGraph State Engine"""
    workflow = StateGraph(dict)

    # Add Execution Nodes
    workflow.add_node("retrieve", self.node_retrieve)
    workflow.add_node("grade", self.node_grade_documents)
    workflow.add_node("web_search", self.node_web_search)
    workflow.add_node("generate", self.node_generate)

    # Define Fixed Entry and Connections
    workflow.set_entry_point("retrieve")
    workflow.add_edge("retrieve", "grade")

    # Define Conditional Routing from Grader
    workflow.add_conditional_edges(
        "grade",
        self.route_grading,
        {
            "trigger_search": "web_search",
            "trigger_generation": "generate"
        }
    )

    # Connect remaining nodes to completion
    workflow.add_edge("web_search", "generate")
    workflow.add_edge("generate", END)

    return workflow.compile()
  1. CrewAI Multi-Agent Synthesis Configuration

When raw search payloads are returned from the web, they are often messy, cluttered, and contains noise (HTML tags, navigation elements, ads). In the web_search phase, we can leverage a CrewAI multi-agent team to dynamically crawl, filter, and extract verified data points before merging.

from crewai import Agent, Task, Crew, Process

class SearchSynthesisCrew: def init(self): # Agent 1: Raw Web Information Harvester self.harvester = Agent( role="Information Harvester", goal="Scrape and extract raw textual facts from web search returns.", backstory="Expert researcher specializing in raw data gathering, filtering out marketing boilerplate and structural HTML noise.", verbose=False, memory=False )

    # Agent 2: Context Synthesizer
    self.synthesizer = Agent(
        role="Information Synthesizer",
        goal="Consolidate facts into structured markdown tables.",
        backstory="Analytical operations researcher focused on merging disparate web signals into clean structural reference data.",
        verbose=False
    )

def run_synthesis_task(self, query: str, raw_web_data: List[str]) -> str:
    task_harvest = Task(
        description=f"Filter and isolate facts answering: '{query}' from this raw data: {raw_web_data}",
        expected_output="Bullet points of verified facts, ignoring marketing filler.",
        agent=self.harvester
    )

    task_synthesize = Task(
        description="Format the extracted facts into concise markdown lists or tables.",
        expected_output="Markdown formatted data summary matching the user's intent.",
        agent=self.synthesizer
    )

    crew = Crew(
        agents=[self.harvester, self.synthesizer],
        tasks=[task_harvest, task_synthesize],
        process=Process.sequential
    )
    return crew.kickoff()
  1. Verification & Validation Protocols

6.1 State Machine Routing Assertions

To guarantee that the grading boundaries execute safely without dropping states, run testing sequences against mocked scoring structures:

def test_crag_correct_routing(): orchestrator = CorrectiveRAGOrchestrator(qdrant_url="http://localhost", qdrant_api_key="test")

# Assert CORRECT state routes directly to generation without triggering search
state_correct = {"run_web_search": False, "query": "Test"}
route = orchestrator.route_grading(state_correct)
assert route == "trigger_generation"

# Assert INCORRECT state routes to web search trigger
state_incorrect = {"run_web_search": True, "query": "Test"}
route = orchestrator.route_grading(state_incorrect)
assert route == "trigger_search"

6.2 Air-Gapped Fallback Integrity

Confirm that if the external network interface fails or web search is blocked, the engine logs the boundary gracefully and uses the best available local ambiguous documents rather than failing the API request:

def test_search_timeout_fallback(): # Force search client to throw network connection errors orchestrator = CorrectiveRAGOrchestrator(qdrant_url="http://localhost", qdrant_api_key="test") orchestrator.search_client = None # Simulating connection loss

# Run node_web_search and assert it falls back to an empty result set cleanly
state = {"query": "Test query"}
try:
    results = orchestrator.node_web_search(state)
except Exception:
    results = {"web_search_results": []}

assert "web_search_results" in results
assert len(results["web_search_results"]) == 0

Required Technical Skills

Advanced Engineering Skills Inventory: Corrective RAG (CRAG) (RAG-003)

This matrix maps out the advanced systems engineering, stateful graph orchestration, multi-agent execution, and real-time streaming competencies required to build and maintain autonomous, self-correcting retrieval architectures.

  1. Stateful Graph Orchestration & LangGraph Mechanics

Unlike static sequential chains, agentic RAG requires maintaining, persistence, and routing execution state through cyclic computational graphs.

              [ State Store (PostgreSQL RLS) ]
                             │
               [ Graph State (TypedDict) ]
                             │
                 [ LangGraph Orchestrator ]
                 • Conditional Routing
                 • Thread-safe Checkpoints
                 • Node State Transitions

Dynamic Graph Design:

Constructing cyclic and acyclic graphs using StateGraph configurations to handle multi-step agentic workflows.

Designing clear State variables (using Python's TypedDict or Pydantic) to store, update, and pass context between execution nodes safely.

Formulating custom routing logic using conditional edges to dynamically redirect the execution thread based on node evaluation outputs.

State Persistence & Rehydration:

Implementing persistent database checkpointers to support long-running multi-turn sessions, manual human-in-the-loop approvals, and graph state restoration.

Writing thread-safe database connection adapters to handle concurrent graph writes securely in high-traffic SaaS environments.

  1. Dynamic Evaluators & Structured Output Extraction

To route queries safely, the system must parse and grade contextual relevance deterministically using local and cloud-based language models.

Structured JSON Outputs:

Utilizing Pydantic models with schema definitions (like gpt-4o-mini structured parsing or local JSON-mode execution) to force models to output binary ratings or exact confidence scores.

Writing validation layers to handle schema mismatch failures or partial JSON outputs without crashing the parent process thread.

Mathematical Decision Calibration:

Establishing dual-boundary classification rules based on two distinct confidence thresholds: $\tau_{\text{high}}$ (verified local knowledge match) and $\tau_{\text{low}}$ (complete irrelevance boundary).

Dynamically computing document grading classifications $\mathcal{G}$ using joint probability scores:

$$\mathcal{G} \in {\text{CORRECT}, \text{AMBIGUOUS}, \text{INCORRECT}}$$

  1. Autonomous Web Harvesting & Scraper Engineering

When local knowledge fails, the system must safely reach out to external index pools to extract, clean, and deliver context.

Search API Orchestration:

Interfacing with specialized semantic search proxies (e.g., Tavily, Jina AI, Google Search API) to retrieve clean factual summaries matching ambiguous query targets.

Parsing API returns dynamically, extracting high-signal snippets while filtering out recurring navigation, footer, and promotional boilerplate.

Evasion & Scraping Resilience:

Routing web-crawling sub-tasks through rotating forward proxy networks to bypass IP rate-limiting blocks.

Utilizing headless browser frameworks (such as Playwright or Puppeteer) inside isolated Docker sandboxes to render dynamic single-page applications (SPAs) safely.

  1. Multi-Agent Coordination & Context Synthesis

Processing messy, raw search payloads requires splitting processing tasks across specialized, autonomous AI agents.

                       [ Raw Web Search Data ]
                                  │
               ┌──────────────────┴──────────────────┐
               ▼                                     ▼
   [ Harvester Agent ]                       [ Synthesizer Agent ]
    Extracts structural facts                 Formats into markdown tables
               │                                     │
               └──────────────────┬──────────────────┘
                                  ▼
                    [ Clean Contextual Payload ]

Multi-Agent Orchestration (CrewAI / AutoGen):

Configuring task division pipelines across dedicated agents with isolated personas, backstories, and private system instructions.

Managing execution flows (sequential, hierarchical, or consensual) to allow parallel search-engine harvesting and contextual synthesis.

Token Economic Analysis:

Implementing cost-containment mathematical models to track operational expenditures:

$$C_{\text{avg}} = C_{\text{eval}} + C_{\text{gen}} + \left[ P(A) + P(I) \right] \cdot \left( C_{\text{search}} + C_{\text{synth}} \right)$$

Gracefully degrading agent execution to fallback-only modes if a tenant surpasses their monthly quota allocation.

  1. Async Event-Driven thought Streaming

Because multi-agent runs introduce higher processing latency ($1,200\text{ms to } 1,800\text{ms}$), you must provide immediate visual progress loops to the browser.

Server-Sent Events (SSE) & WebSocket Architectures:

Implementing non-blocking asynchronous generators in FastAPI (StreamingResponse) to stream JSON chunks continuously over active HTTP channels.

Consuming internal graph events in real-time by intercepting LangGraph’s astream_events protocol.

Mapping internal execution milestones (such as "retrieve", "grade", "web_search") to clean user-facing visual steps.

Full-Stack Connection Maintenance:

Handling sudden client disconnects cleanly on the server to prevent orphaned agent threads and database lock leaks.

Building resilient retry-reconnect logic on the frontend (using React/Next.js and EventSource APIs) to handle network interruptions gracefully.

🚀 Quick Installation & Setup Guide

To run this project locally, follow these simple steps designed for both developers and users with no prior programming experience.

  1. Download Python: Make sure you have Python 3.10+ installed on your computer. Download it from the official Python Website. During installation, make sure to check the box "Add Python to PATH".
  2. Extract / Setup Code: Open the terminal (Command Prompt on Windows) and navigate to the project directory, or download/extract the project files.
  3. Install Dependencies (pip): In Command Prompt, run the following command to install all necessary packages automatically:
    pip install -r requirements.txt
  4. Setup API Keys / Environment: Open the .env file in a text editor (like Notepad) and paste your API keys:
    GEMINI_API_KEY=your_gemini_api_key_here
  5. Launch the Application: Double-click the run_project.bat or run_pito.bat file, or run the following command:
    python app.py
    (or app_gui.py)