System Overview
The Advanced Multi-Step RAG System is a sophisticated information retrieval and generation pipeline that incorporates multiple decision-making nodes to improve the quality and relevance of responses. This system goes beyond simple RAG by adding intelligent routing, query processing, and response evaluation.
Architecture Components
1. Start Node
- Purpose: Entry point for the system
- Function: Initiates the workflow and passes the initial user query
2. Question Rewriter Node
- Purpose: Enhances query quality and context
- Function: Reformulates user queries for better retrieval performance
- Significance:
- Initial query: “What are Peak Performance Gym’s hours?”
- Follow-up query: “What about weekends?”
Without rephrasing, the follow-up query lacks context on its own and would likely return irrelevant results if sent directly to a retrieval system.
The rephrasing node transforms “What about weekends?” into “What are the Peak Performance Gym’s weekend hours?”
Benefits:
- Maintains conversation context
- Improves retrieval accuracy
- Handles ambiguous or incomplete queries
- Standardizes query format
3. Question Classifier Node
- Purpose: Determines the appropriate retrieval strategy
- Function: Analyzes the query type and routes to optimal processing path
- Classification Types:
- Factual queries: Direct information retrieval
- Complex queries: Multi-step reasoning required
- Conversational queries: Context-dependent responses
- Out-of-scope queries: Redirect or decline appropriately
4. Retrieve Node
- Purpose: Core information retrieval component
- Function: Searches the knowledge base using enhanced queries
- Features:
- Vector similarity search
- Semantic matching
- Relevance scoring
- Multiple retrieval strategies
5. Retrieval Grader Node
- Purpose: Quality control for retrieved documents
- Function: Evaluates relevance and usefulness of retrieved information
- Evaluation Criteria:
- Relevance: How well does the content match the query?
- Completeness: Does it provide sufficient information?
- Accuracy: Is the information correct and up-to-date?
- Context: Does it fit the conversation context?
Decision Paths:
- High Quality: Proceed to answer generation
- Medium Quality: Refine query and retrieve again
- Low Quality: Generate fallback response or request clarification
6. Processing Nodes
Generate Answer Node
- Purpose: Creates comprehensive responses from retrieved documents
- Function: Synthesizes information into coherent, contextual answers
- Features:
- Citation integration
- Response formatting
- Fact verification
- Tone adaptation
Refine Question Node
- Purpose: Improves query specificity when initial retrieval is insufficient
- Function: Iteratively enhances queries based on retrieval feedback
- Process:
- Analyzes retrieval gaps
- Adds missing context
- Adjusts query scope
- Incorporates domain-specific terms
Cannot Answer Node
- Purpose: Handles queries that cannot be adequately answered
- Function: Provides appropriate fallback responses
- Scenarios:
- Insufficient information in knowledge base
- Out-of-scope queries
- Ambiguous requests requiring clarification
- Privacy or safety concerns
Off-topic Response Node
- Purpose: Manages queries outside the system’s domain
- Function: Politely redirects or declines inappropriate requests
- Features:
- Domain boundary detection
- Helpful redirection suggestions
- Maintains conversation flow
7. End Node
- Purpose: Final output and workflow completion
- Function: Delivers the processed response to the user
Workflow Examples
Example 1: Gym Hours Inquiry
User: "What are Peak Performance Gym's hours?"
1. Start → question_rewriter
Input: "What are Peak Performance Gym's hours?"
Output: "What are the operating hours for Peak Performance Gym?"
2. question_rewriter → question_classifier
Classification: "Factual query - Business hours"
Route: Direct retrieval
3. question_classifier → retrieve
Search Query: Enhanced with context and synonyms
Retrieved: Gym schedule document, hours policy
4. retrieve → retrieval_grader
Grade: High relevance (95% confidence)
Decision: Proceed to generate_answer
5. retrieval_grader → generate_answer
Response: "Peak Performance Gym is open Monday-Friday 5:00 AM to 11:00 PM,
Saturday-Sunday 6:00 AM to 10:00 PM."
6. generate_answer → __end__
Example 2: Follow-up Question
User: "What about weekends?"
1. Start → question_rewriter
Input: "What about weekends?"
Context: Previous query about gym hours
Output: "What are Peak Performance Gym's weekend hours?"
2. question_rewriter → question_classifier
Classification: "Contextual follow-up - Business hours"
Route: Targeted retrieval
3. question_classifier → retrieve
Search: Weekend-specific schedule information
Retrieved: Weekend hours policy
4. retrieve → retrieval_grader
Grade: High relevance (92% confidence)
Decision: Proceed to generate_answer
5. retrieval_grader → generate_answer
Response: "On weekends, Peak Performance Gym is open Saturday-Sunday
from 6:00 AM to 10:00 PM."
6. generate_answer → __end__
Example 3: Complex Query with Refinement
User: "I need help with my workout routine"
1. Start → question_rewriter
Input: "I need help with my workout routine"
Output: "What assistance do you need with workout routine planning?"
2. question_rewriter → question_classifier
Classification: "Vague request - Requires clarification"
Route: Refinement needed
3. question_classifier → retrieve
Search: General workout information
Retrieved: Multiple fitness documents
4. retrieve → retrieval_grader
Grade: Medium relevance (60% confidence)
Reason: Query too broad
Decision: Refine query
5. retrieval_grader → refine_question
Refinement: "Could you specify your fitness goals, experience level,
and preferred workout types?"
Route: Back to retrieve or generate clarifying response
6. refine_question → generate_answer
Response: "I'd be happy to help with your workout routine! Could you tell me:
- Your fitness goals (weight loss, muscle gain, endurance)
- Your experience level (beginner, intermediate, advanced)
- Preferred workout types (cardio, strength training, yoga)"
7. generate_answer → __end__
Example 4: Out-of-Scope Query
User: "What's the weather like today?"
1. Start → question_rewriter
Input: "What's the weather like today?"
Output: "What is today's weather forecast?"
2. question_rewriter → question_classifier
Classification: "Off-topic - Weather inquiry"
Route: Off-topic handler
3. question_classifier → Off_topic_response
Decision: Outside gym/fitness domain
4. Off_topic_response → __end__
Response: "I'm designed to help with gym and fitness-related questions.
For weather information, I'd recommend checking a weather app
or website like Weather.com. Is there anything about Peak
Performance Gym I can help you with?"
Implementation Benefits
1. Improved Accuracy
- Multi-stage validation reduces hallucinations
- Quality grading ensures relevant responses
- Context preservation maintains conversation flow
2. Enhanced User Experience
- Handles ambiguous queries gracefully
- Provides appropriate fallbacks
- Maintains conversation context
3. Scalability
- Modular design allows easy component updates
- Flexible routing accommodates new query types
- Performance monitoring at each stage
4. Quality Control
- Multiple checkpoints ensure response quality
- Grading system prevents poor responses
- Iterative refinement improves over time
Technical Considerations
Performance Optimization
- Caching: Store frequent query results
- Parallel Processing: Execute independent operations simultaneously
- Load Balancing: Distribute computational load
- Response Time Monitoring: Track and optimize latency
Monitoring & Analytics
- Node Performance: Track success rates at each stage
- Query Classification Accuracy: Monitor routing decisions
- Retrieval Quality: Measure grading effectiveness
- User Satisfaction: Collect feedback on responses
Error Handling
- Graceful Degradation: Fallback to simpler RAG when nodes fail
- Retry Mechanisms: Handle temporary failures
- Circuit Breakers: Prevent cascade failures
- Logging: Comprehensive error tracking
import os
from dotenv import load_dotenv
from langchain.schema import Document
import google.generativeai as genai
import chromadb
from chromadb import Documents, EmbeddingFunction, Embeddings
from typing import List
# Load .env variables
load_dotenv()
# --------------------- Sample Documents -------------------------
docs = [
Document(
page_content="Peak Performance Gym was founded in 2015 by former Olympic athlete Marcus Chen. With over 15 years of experience in professional athletics, Marcus established the gym to provide personalized fitness solutions for people of all levels. The gym spans 10,000 square feet and features state-of-the-art equipment.",
metadata={"source": "about.txt"}
),
Document(
page_content="Peak Performance Gym is open Monday through Friday from 5:00 AM to 11:00 PM. On weekends, our hours are 7:00 AM to 9:00 PM. We remain closed on major national holidays. Members with Premium access can enter using their key cards 24/7, including holidays.",
metadata={"source": "hours.txt"}
),
Document(
page_content="Our membership plans include: Basic (₹1,500/month) with access to gym floor and basic equipment; Standard (₹2,500/month) adds group classes and locker facilities; Premium (₹4,000/month) includes 24/7 access, personal training sessions, and spa facilities. We offer student and senior citizen discounts of 15% on all plans. Corporate partnerships are available for companies with 10+ employees joining.",
metadata={"source": "membership.txt"}
),
Document(
page_content="Group fitness classes at Peak Performance Gym include Yoga (beginner, intermediate, advanced), HIIT, Zumba, Spin Cycling, CrossFit, and Pilates. Beginner classes are held every Monday and Wednesday at 6:00 PM. Intermediate and advanced classes are scheduled throughout the week. The full schedule is available on our mobile app or at the reception desk.",
metadata={"source": "classes.txt"}
),
Document(
page_content="Personal trainers at Peak Performance Gym are all certified professionals with minimum 5 years of experience. Each new member receives a complimentary fitness assessment and one free session with a trainer. Our head trainer, Neha Kapoor, specializes in rehabilitation fitness and sports-specific training. Personal training sessions can be booked individually (₹800/session) or in packages of 10 (₹7,000) or 20 (₹13,000).",
metadata={"source": "trainers.txt"}
),
Document(
page_content="Peak Performance Gym's facilities include a cardio zone with 30+ machines, strength training area, functional fitness space, dedicated yoga studio, spin class room, swimming pool (25m), sauna and steam rooms, juice bar, and locker rooms with shower facilities. Our equipment is replaced or upgraded every 3 years to ensure members have access to the latest fitness technology.",
metadata={"source": "facilities.txt"}
)
]
# ---------------------- Embedding Function ----------------------
class GeminiEmbeddingFunction(EmbeddingFunction):
def __init__(self):
pass
def __call__(self, input: Documents) -> Embeddings:
gemini_api_key = os.getenv("GEMINI_API_KEY")
if not gemini_api_key:
raise ValueError("Gemini API Key not provided. Please provide GEMINI_API_KEY as an environment variable")
genai.configure(api_key=gemini_api_key)
# Convert input to string (if input is a list of strings or a single string)
if isinstance(input, list):
input_text = " ".join(input)
else:
input_text = input
# Get the embedding from Gemini
response = genai.embed_content(
model="models/embedding-001",
content=input_text,
task_type="retrieval_document",
title="Custom query"
)
embedding = response["embedding"]
# If it's a NumPy array, convert it to a plain Python list
if hasattr(embedding, "tolist"):
embedding = embedding.tolist()
# Return a **single-level list of lists**, as expected by ChromaDB
return [embedding]
# ------------------------ Create ChromaDB -----------------------
# def create_chroma_db(documents: List[Document], path: str, name: str):
# chroma_client = chromadb.PersistentClient(path=path)
# db = chroma_client.get_or_create_collection(
# name=name,
# embedding_function=GeminiEmbeddingFunction()
# )
# for i, d in enumerate(documents):
# db.add(
# ids=[str(i)],
# documents=[d.page_content],
# metadatas=[d.metadata]
# )
# return db
# Create DB
# db = create_chroma_db(documents=docs, path="./chroma_db", name="rag_experiment")
# retriever = db.as_retriever(search_type="mmr", search_kwargs = {"k": 4})
from langchain_community.vectorstores import Chroma
from langchain_google_genai import GoogleGenerativeAIEmbeddings
# Initialize Embeddings
embedding_model = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
# Create Chroma vectorstore
db = Chroma.from_documents(
documents=docs,
embedding=embedding_model,
persist_directory="./chroma_db"
)
# Now you can use as_retriever()
retriever = db.as_retriever(search_type="mmr", search_kwargs={"k": 4})
# -------------------------- RAG Chain --------------------------
from langchain_core.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(model='gemini-2.0-flash')
template = """
Answer the question based on the following context and the ChatHistory. Especially take the latest messages into account.
ChatHistory: {history}
Context: {context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
rag_chain = prompt | llm
# -------------------- Graph Nodes --------------------
from typing import TypedDict, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain.schema import Document
from pydantic import BaseModel, Field
from langgraph.graph import StateGraph, END
# Agent State that holds the conversation history, documents, and other relevant information
class AgentState(TypedDict):
messages: List[BaseMessage]
documents: List[Document]
on_topic: str
rephrased_question: str
proceed_to_generate: bool
rephrase_count: int # max 3 rephrases
question: HumanMessage
# GradeQuestion model is to classify whether the question is about the specified topics
class GradeQuestion(BaseModel):
score: str = Field(
description="Question is about the specified topics? IF yes -> 'Yes', if not -> 'No'"
)
# Function to rewrite the question based on the context and chat history
def question_rewriter(state: AgentState) -> AgentState:
print("Rewriting question...")
# Reset state variables except for the 'question' and 'messages'
state["documents"] = []
state["on_topic"] = ""
state["rephrased_question"] = ""
state["proceed_to_generate"] = False
state["rephrase_count"] = 0
if "messages" not in state or state["messages"] is None:
state["messages"] = []
if state["question"] not in state["messages"]:
state["messages"].append(state["question"])
if len(state["messages"]) > 1:
conversation = state["messages"][:-1] # Exclude the last question message
current_question = state["question"].content
messages = [
SystemMessage(
content="You are an expert at rewriting questions based on context and chat history."
)
]
messages.extend(conversation)
messages.append(HumanMessage(content=current_question))
rephrase_prompt = ChatPromptTemplate.from_messages(messages)
rephrased_response = llm.invoke(rephrase_prompt.format_messages())
better_question = rephrased_response.content.strip()
print(f"Rephrased question: {better_question}")
state["rephrased_question"] = better_question
else:
print("No previous messages to rephrase the question.")
state["rephrased_question"] = state["question"].content
return state
# Function to classify the question
def question_classifier(state: AgentState) -> AgentState:
print("Classifying question...")
system_message = SystemMessage(
content="""
You are a classifier that determines whether a user's question is about one of the following topics:
1. Gym History & Founder
2. Operating Hours
3. Membership Plans
4. Fitness Classes
5. Personal Trainers
6. Facilities & Equipment
7. Anything else about any of these topics, response with 'Yes'. Otherwise, respond with 'No'.
"""
)
human_message = HumanMessage(
content=f"User question: {state["rephrased_question"]}"
)
# Create the prompt for classification
# The prompt is structured to include the system message and the user's question
grade_prompt = ChatPromptTemplate.from_messages([system_message, human_message])
# Create a structured LLM that will return the classification result
structured_llm = llm.with_structured_output(GradeQuestion)
# Invoke the LLM to classify the question
grader_llm = grade_prompt | structured_llm
# Invoke the LLM with the current state
result = grader_llm.invoke({})
state["on_topic"] = result.score.strip()
print(f"Classifier result: {state['on_topic']}")
return state
# Function to route based on on_topic_router
def on_topic_router(state: AgentState):
print("Routing based on topic...")
on_topic = state["on_topic"].lower()
if on_topic == "yes":
print("Routing to RAG generation...")
return "retrieve"
else:
print("Routing to off_topic response...")
return "off_topic_response"
# Retrieve relevant documents based on the rephrased question
def retrieve(state: AgentState) -> AgentState:
print("Retrieving relevent documents...")
documents = retriever.invoke(
state["rephrased_question"]
)
print(f"retrieve: Retrieved {len(documents)} documents")
state["documents"] = documents
return state
# GradeDocument model is to classify whether the document is relevant to the question
class GradeDocument(BaseModel):
score: str = Field(
description="Document is relevant to the question? If yes -> 'Yes', if not -> 'No'"
)
# Function to grade the retrieved documents based on their relevance to the question
def retrieval_grader(state: AgentState) -> AgentState:
print("Grading retrieved documents...")
system_message = SystemMessage(
content="""
You are a assessing the relevance of a retrieved document to a user question.
Only answer with 'Yes' if the document is relevant to the question, otherwise answer with 'No'.
"""
)
structured_llm = llm.with_structured_output(GradeDocument)
relevant_docs = []
for doc in state["documents"]:
human_message = HumanMessage(
content=f"user question: {state['rephrased_question']}\n\nRetrieved document: \n{doc.page_content}"
)
grade_prompt = ChatPromptTemplate.from_messages([system_message, human_message])
grader_llm = grade_prompt | structured_llm
result = grader_llm.invoke({})
if result.score.strip().lower() == "yes":
relevant_docs.append(doc)
print(f"Document '{doc.metadata['source']}' is relevant to the question.")
state["documents"] = relevant_docs
state["proceed_to_generate"] = len(relevant_docs) > 0
print(f"relevant_docs: {len(relevant_docs)} documents are relevant to the question.")
return state
# proceed_router function to determine the next step based on document relevance
def proceed_router(state: AgentState):
print("Routing based on document relevance...")
if state["proceed_to_generate"]:
print("Proceeding to answer generation...")
return "generate_answer"
elif state["rephrase_count"] >= 2:
print("Maximum rephrase attempts reached, routing to off_topic_response...")
return "cannot_answer"
else:
print("Routing to refine_qestion...")
return "refine_question"
# refine_question function to refine the rephrased question
def refine_question(state: AgentState) -> AgentState:
print("Refining question...")
rephrase_count = state.get("rephrase_count", 0)
if rephrase_count >= 2:
print("Maximum rephrase attempts reached, cannot refine further.")
return state
question_to_refine = state["rephrased_question"]
system_message = SystemMessage(
content="You are a helpful assessant that helps refine user questions to make them more specific and answerable."
)
human_message = HumanMessage(
content=f"Original question: {question_to_refine}\n\nProvide a slightly refined version of this question that is more specific and answerable."
)
refine_prompt = ChatPromptTemplate.from_messages([system_message, human_message])
response = llm.invoke(refine_prompt.format_messages())
refined_question = response.content.strip()
print(f"Refined question: {refined_question}")
state["rephrased_question"] = refined_question
state["rephrase_count"] = rephrase_count + 1
return state
# generate_answer function to generate the final answer based on the rephrased question and retrieved documents
def generate_answer(state: AgentState) -> AgentState:
print("Generating answer...")
if "messages" not in state or state["messages"] is None:
raise ValueError("State must contain 'messages' key with a list of messages.")
history = state["messages"]
documents = state["documents"]
rephrased_question = state["rephrased_question"]
response = rag_chain.invoke(
{
"history": history,
"context": "\n\n".join([doc.page_content for doc in documents]),
"question": rephrased_question
}
)
generation = response.content.strip()
state["messages"].append(AIMessage(content=generation))
print(f"Generated answer: {generation}")
return state
def cannot_answer(state: AgentState) -> AgentState:
print("Cannot answer the question, providing a default response.")
if not "messages" in state or state["messages"] is None:
state["messages"] = []
state["messages"].append(AIMessage(content="I'm sorry, I cannot answer that question."))
return state
def off_topic_response(state: AgentState) -> AgentState:
print("Off-topic response.")
if not "messages" in state or state["messages"] is None:
state["messages"] = []
state["messages"].append(AIMessage(content="I'm sorry! I cannot answer this question as it does not seem to be related to Peak Performance Gym."))
return state
# -------------------- Build & Compile the Graph --------------------
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
workflow = StateGraph(AgentState)
workflow.add_node("question_rewriter", question_rewriter)
workflow.add_node("question_classifier", question_classifier)
workflow.add_node("off_topic_response", off_topic_response)
workflow.add_node("retrieve", retrieve)
workflow.add_node("retrieval_grader", retrieval_grader)
workflow.add_node("generate_answer", generate_answer)
workflow.add_node("cannot_answer", cannot_answer)
workflow.add_node("refine_question", refine_question)
workflow.add_edge("question_rewriter", "question_classifier")
workflow.add_conditional_edges(
"question_classifier",
on_topic_router,
{
"retrieve": "retrieve",
"off_topic_response": "off_topic_response"
}
)
workflow.add_edge("retrieve", "retrieval_grader")
workflow.add_conditional_edges(
"retrieval_grader",
proceed_router,
{
"generate_answer": "generate_answer",
"refine_question": "refine_question",
"cannot_answer": "cannot_answer"
}
)
workflow.add_edge("refine_question", "retrieve")
workflow.add_edge("generate_answer", END)
workflow.add_edge("cannot_answer", END)
workflow.add_edge("off_topic_response", END)
workflow.set_entry_point("question_rewriter")
graph = workflow.compile(checkpointer=checkpointer)
# graph = workflow.compile()
config = {"config": {
"thread_id": 1,
}
}
# Print the graph structure
print("Graph structure:")
print(graph.get_graph().draw_mermaid())
graph.get_graph().print_ascii()
# -------------------- Example Run --------------------
# input_data = {"question": HumanMessage(content="What dos the company Apple do?")}
# print(graph.invoke(input_data, config={"thread_id": 1}))
#
# input_data = {
# "question": HumanMessage(
# content="What is the cancelation policy for Peak Performance Gym membership?"
# )
# }
# print(graph.invoke(input_data, config={"thread_id": 1}))
#
#
# input_data = {
# "question": HumanMessage(
# content="Who founded Peak Performance Gym and when?"
# )
# }
# print(graph.invoke(input_data, config={"thread_id": 1}))
input_data = {
"question": HumanMessage(
content="What are the timings of the gym?"
)
}
print(graph.invoke(input_data, config={"thread_id": 1}))
input_data = {
"question": HumanMessage(
content="What about the weekends?"
)
}
print(graph.invoke(input_data, config={"thread_id": 1}))