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.

Hailey Says
Hailey Says
Hailey Says
Building a MAS‑Powered RAG System for Blog Search and Q&A

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

LangChainRAGmulti-agentQdrantStreamlitLangGraphGoogle Gemini
Hailey Says
Written by

Hailey Says

🎨 Sharing AI experiences, insights, and taste 🗺️ Code & Design & KOL

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.