Building a MAS‑Powered RAG System for Blog Search and Q&A
This article walks through constructing an agentic RAG pipeline that combines LangChain, LangGraph, Google Gemini embeddings, and Qdrant vector storage to enable automatic query rewriting, relevance grading, and concise answers to blog‑post questions via a Streamlit UI.
Overview
The author documents the creation of an agentic Retrieval‑Augmented Generation (RAG) system that integrates Multi‑Agent System (MAS) concepts with RAG techniques to provide searchable, question‑answering capabilities over blog content.
Architecture Overview
The system leverages four main components:
LangChain for document loading and processing.
LangGraph to orchestrate the agent workflow.
Google Gemini (model gemini-2.0-flash) for embedding generation and relevance grading.
Qdrant as the vector database for semantic similarity search.
A Streamlit front‑end collects the Qdrant host, API key, and Gemini API key, then drives the RAG pipeline.
Core Functional Steps
Document Loading : WebBaseLoader fetches HTML from a user‑provided blog URL.
Text Splitting : RecursiveCharacterTextSplitter breaks the HTML into semantic chunks (paragraphs, sentences, or words).
Embedding : Each chunk is transformed into a vector using Gemini’s embedding-001 model.
Vector Storage : Vectors are stored in Qdrant, enabling fast similarity retrieval.
Agentic Query Handling : A LangGraph‑based agent decides whether to rewrite the user query, continue retrieval, or generate a final answer.
Relevance Scoring : Gemini grades each retrieved document with a binary “yes/no” score; irrelevant results are filtered.
User Interface : Streamlit presents input fields for API credentials, the blog URL, and the user query, and displays the generated answer.
Detailed Code Walk‑through
The sidebar setup function set_sidebar() collects the three required keys and stores them in st.session_state. The main routine validates the keys, initializes the embedding model, Qdrant client, and retriever, then proceeds as follows:
def set_sidebar():
"""Setup sidebar for API keys and configuration."""
with st.sidebar:
st.subheader("API Configuration")
qdrant_host = st.text_input("Enter your Qdrant Host URL:", type="password")
qdrant_api_key = st.text_input("Enter your Qdrant API key:", type="password")
gemini_api_key = st.text_input("Enter your Gemini API key:", type="password")
if st.button("Done"):
if qdrant_host and qdrant_api_key and gemini_api_key:
st.session_state.qdrant_host = qdrant_host
st.session_state.qdrant_api_key = qdrant_api_key
st.session_state.gemini_api_key = gemini_api_key
st.success("API keys saved!")
else:
st.warning("Please fill all API fields")The relevance grading prompt sent to Gemini asks the model to return a binary score indicating whether a retrieved document is pertinent to the user question. The chain invocation looks like:
# LLM
model = ChatGoogleGenerativeAI(api_key=st.session_state.gemini_api_key, temperature=0, model="gemini-2.0-flash", streaming=True)
llm_with_tool = model.with_structured_output(grade)
prompt = PromptTemplate(
template="""You are a grader assessing relevance of a retrieved document to a user question.
Here is the retrieved document:
{context}
Here is the user question: {question}
If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant.
Give a binary score 'yes' or 'no' to indicate whether the document is relevant to the question.""",
input_variables=["context", "question"],
)
chain = prompt | llm_with_tool
score = chain.invoke({"question": question, "context": docs}).binary_score
if score == "yes":
print("---DECISION: DOCS RELEVANT---")
return "generate"
else:
print("---DECISION: DOCS NOT RELEVANT---")
print(score)
return "rewrite"The main() function ties everything together: after verifying API configuration, it calls initialize_components() to obtain the embedding model, Qdrant client, and database handle. It then creates a retriever tool that searches for blog posts about LLM agents, prompt engineering, and adversarial attacks. Users paste a blog URL, which triggers document ingestion, chunking, embedding, and storage. When a query is submitted, the graph runs the agentic workflow, either returning a concise answer or rewriting the query once before giving up, thereby avoiding infinite loops and saving tokens.
def main():
set_sidebar()
if not all([st.session_state.qdrant_host, st.session_state.qdrant_api_key, st.session_state.gemini_api_key]):
st.warning("Please configure your API keys in the sidebar first")
return
embedding_model, client, db = initialize_components()
if not all([embedding_model, client, db]):
return
retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 5})
retriever_tool = create_retriever_tool(
retriever,
"retrieve_blog_posts",
"Search and return information about blog posts on LLMs, LLM agents, prompt engineering, and adversarial attacks on LLMs.",
)
tools = [retriever_tool]
url = st.text_input(":link: Paste the blog link:", placeholder="e.g., https://lilianweng.github.io/posts/2023-06-23-agent/")
if st.button("Enter URL"):
if url:
with st.spinner("Processing documents..."):
if add_documents_to_qdrant(url, db):
st.success("Documents added successfully!")
else:
st.error("Failed to add documents")
else:
st.warning("Please enter a URL")
graph = get_graph(retriever_tool)
query = st.text_area(":bulb: Enter your query about the blog post:", placeholder="e.g., What does Lilian Weng say about the types of agent memory?")
if st.button("Submit Query"):
if not query:
st.warning("Please enter a query")
return
inputs = {"messages": [HumanMessage(content=query)]}
with st.spinner("Generating response..."):
try:
response = generate_message(graph, inputs)
st.write(response)
except Exception as e:
st.error(f"Error generating response: {str(e)}")
st.markdown("---")
st.write("Built with :blue-background[LangChain] | :blue-background[LangGraph] by [Charan](https://www.linkedin.com/in/codewithcharan/)")
if __name__ == "__main__":
main()The system ensures that each query undergoes at most one rewrite, preventing endless retrieval loops and conserving computational resources.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Hailey Says
🎨 Sharing AI experiences, insights, and taste 🗺️ Code & Design & KOL
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
