In this video, I’ve got a super quick tutorial showing you how to create a multi-agent chatbot using the Latest Langchain Upgraded Lazy Graph and Ollama to build a powerful agent chatbot for your business or personal use.
Just last week, I dropped a video on Auto Schema Knowledge Graph, and it sparked a huge buzz across the global AI community!
As I do every week, I wanted to create a video, but I ran out of ideas. So, I scrolled through LinkedIn to see what was happening in the AI community — and that’s when I came across a cool library called Graph Retriever.
We all have thousands of documents that increase each month, making it difficult to find them quickly using the standard search function. We need to organise and classify documents as they are created, but when we checked the number of edits made in the past using the analytics function Tool, we found that the number was a huge 76,000, making it difficult to search by keyword based on memory.
The challenges of search accuracy and efficiency grow. New papers are published every day on methods to improve RAG, but this time, I will introduce an attempt to integrate the “Graph Retriever library” with Langchain.
So let me give you a quick demo of the live chatbot in action — just to show you exactly what I mean.
Check the video
I used an animal dataset provided graph_rag_example_helpers for validation. Each entry has the animal's name in the id. Its attributes are stored inmetadata, and a description is found in page_contents. The metadata type refers to the species, number_of_legs indicates the number of legs, keyword highlights features, and habitat shows the animal's habitat.
The agent then used those documents to initialize an In Memory vectorStore in the setup_vector_store method, converting them into dense vectors using the embedding model, and saving the store to self.vector_store.
To enhance context exploration, it is used Graph Retriever to build relationships through their shared habitat field using relation("habitat", "habitat"). It chose an Eager strategy with custom k, start_k, and max_depth values to control how many and how far the retriever explores documents in the graph.
The output from the graph retriever is combined with the user’s question into a formatted context string using a prompt template and passed to the LLM to generate the final response.
Lastly, the method compare_with_standard_retrieval takes a user question, retrieves documents, and generates answers using both the standard and graph-based pipelines. It formats the results with clear labels and separators, helping you compare how much deeper or more connected the graph-based reasoning is compared to plain similarity search
Functionality Overview
From the langchain-graph-retriever provides a LangChain retriever that combines unstructured similarity search on vectors with structured traversal of metadata properties. This enables graph-based retrieval over an existing vector store.
So… it allows graph-based search on an existing vector store?
It lets you perform relationship-aware searches without building a knowledge graph using a graph database like Neo4j…
Sounds kind of similar to what Microsoft announced with “LazyGraphRAG”…
I haven’t seen many articles about people using this, so even though I’m just building a cool project for fun, I decided to give it a try.
There might be plenty of things to nitpick, so if you notice something off, please point it out gently.
Lazy GraphRAG :
LazyGraphRAG is Microsoft’s latest graph-based retrieval enhancement generation method. Compared with traditional GraphRAG, it greatly reduces the cost of data indexing and improves query efficiency and answer quality.
Where does “Lazy” come from?
Lazy GraphRAG is called “lazy” because it postpones the use of LLM. During the indexing phase, Lazy GraphRAG utilizes lightweight NLP technology to process text, deferring the invocation of LLM until the actual query is made. This “lazy” strategy avoids the high initial indexing cost and achieves efficient resource utilization.
Let’s Start Coding
Let us now explore step by step and unravel the answer to how to create a multi-agent chatbot using the Latest Langchain Upgraded Lazy Graph. We will install the libraries that support the model. For this, we will do a pip install requirements.
pip install requirements
The next step is the usual one: We will import the relevant libraries, the significance of which will become evident as we proceed.
Langchain_graph_retriever: LangChain Graph Retriever is a Python library that supports traversing a document graph on top of vector-based similarity search. It works seamlessly with LangChain’s retriever framework and supports various graph traversal strategies for efficient document discovery.
Features
Vector Search: Perform similarity searches using vector embeddings.
Graph Traversal: Apply traversal strategies such as breadth-first (Eager) or Maximal Marginal Relevance (MMR) to explore document relationships.
Customizable Strategies: Easily extend and configure traversal strategies to meet your specific use case.
Multiple Adapters: Support for various vector stores, including AstraDB, Cassandra, Chroma, OpenSearch, and in-memory storage.
Synchronous and Asynchronous Retrieval: Supports both sync and async workflows for flexibility in different applications.
Graph_rag_example_helpers: It contains utilities used in some examples, such as recoverably loading large datasets.
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_graph_retriever import GraphRetriever
from graph_retriever.strategies import Eager
from graph_rag_example_helpers.datasets.animals import fetch_documents
from langchain_ollama.chat_models import ChatOllama
from langchain_community.embeddings import OllamaEmbeddings
import os
import logging
from typing import List, Optional
I created a GraphRAG system and defined a constructor to set up all the key components needed for retrieval-augmented generation using a knowledge graph.
I added attributes for storing document embeddings, a vector store, a graph retriever, and both standard and graph-based chains to process user queries. I initialized self.embeddings using OllamaEmbeddings with the bge-m3 model, which I chose for its balance between speed and quality in generating dense vector representations.
Next, I configured the LLM by setting self.llm to use ChatOllama with the mistral-small3.2 model and a temperature of 0 to ensure that responses remain deterministic and factual.
class GraphRAG:
"""GraphRAG class"""
def __init__(self):
"""Constructor"""
self.embeddings: Optional[OllamaEmbeddings] = None
self.vector_store: Optional[InMemoryVectorStore] = None
self.graph_retriever: Optional[GraphRetriever] = None
self.graph_chain = None
self.standard_retriever = None
self.standard_chain = None
self.llm: Optional[ChatOllama] = None
self.documents: List[Document] = []
self.embeddings = OllamaEmbeddings(model="bge-m3")
self.llm = ChatOllama(model="mistral-small3.2", temperature=0)
After that, I created a load_sample_data function to load a sample dataset of animal-related documents into the system. This function uses a fetch_documents() Method to pull in preprocessed data for testing.
Once the data is fetched, I store it self.documents so it can be used later by the retriever. I also added error handling with a try-except block to catch any issues during data loading and return either a success message showing how many entries were loaded or a clear error message if something went wrong.
def load_sample_data(self) -> str:
"""Load sample data (animal dataset)"""
try:
# Create sample animal data
self.documents = fetch_documents()
return f"[Success] Loaded {len(self.documents)} animal data entries"
except Exception as e:
return f"[Error] Data loading error: {str(e)}"
Each entry has the animal’s name in idIts attributes in metadata, and a description in page_contents.
In metadata, type Is the species, number_of_legs Is the number of legs, keyword indicates features, and habitat shows its habitat.
Not sure what tags is for.
GraphRetriever Uses this metadata to perform graph-based retrieval...
[Document(id='aardvark', metadata={'type': 'mammal', 'number_of_legs': 4, 'keywords': ['burrowing', 'nocturnal', 'ants', 'savanna'], 'habitat': 'savanna', 'tags': [{'a': 5, 'b': 7}, {'a': 8, 'b': 10}]}, page_content='the aardvark is a nocturnal mammal known for its burrowing habits and long snout used to sniff out ants.'),
Document(id='albatross', metadata={'type': 'bird', 'number_of_legs': 2, 'keywords': ['seabird', 'wingspan', 'ocean'], 'habitat': 'marine', 'tags': [{'a': 5, 'b': 8}, {'a': 8, 'b': 10}]}, page_content='the albatross is a large seabird with the longest wingspan of any bird, allowing it to glide effortlessly over oceans.'),
Document(id='alligator', metadata={'type': 'reptile', 'number_of_legs': 4, 'keywords': ['reptile', 'jaws', 'wetlands'], 'diet': 'carnivorous', 'nested': {'a': 5}}, page_content='alligators are large reptiles with powerful jaws and are commonly found in freshwater wetlands.')]
I then created asetup_vector_store function to initialise an in-memory vector store using the documents and embeddings I had already prepared. I developed this using a In Memory VectorStore.from_documents() method which takes my input self.documents and converts it into dense vectors using the self.embeddings model I set earlier.
I stored the resulting vector store self.vector_store so it can be used later for similarity search and retrieval tasks. I wrapped the process in a try-except block to handle any potential errors and return a success message if everything works, or a detailed error message if something fails.
def setup_vector_store(self) -> str:
"""Set up vector store"""
try:
# Create InMemoryVectorStore
self.vector_store = InMemoryVectorStore.from_documents(
documents=self.documents,
embedding=self.embeddings
)
return "[Success] Vector store created successfully"
except Exception as e:
return f"[Error] Vector store creation error: {str(e)}"
I created the setup_graph_retriever function to initialize a GraphRetriever, which enables GraphRAG to explore connections between documents using graph relationships. This is the core component of GraphRetriever.
You define:
edges: the relationships between fields used to connect documents (e.g., ("habitat", "habitat") means linking documents that share the same habitat),
strategy: the traversal strategy,
k: the final number of documents to retrieve,
start_k: the initial number of documents to start the search from,
max_depth: how far the retriever can explore the graph.
In this case, I used edges=[("habitat", "habitat")] to link documents by their shared habitat field. I also reused the InMemoryVectorStore I had previously built it as the base store.
To define graph connections, I usedrelation("habitat", "habitat"), enabling the retriever to connect documents that share the same habitat value.
I chose the Eager strategy and set custom values for k, start_k, and max_depth to control the number of documents retrieved, the number to start with, and the depth of graph traversal.
This setup is wrapped in a try-except block to return a success message with the configuration if everything works correctly, or a detailed error message if it fails.
def setup_graph_retriever(self, k: int = 5, start_k: int = 1, max_depth: int = 2) -> str:
"""Set up GraphRetriever"""
try:
# Create GraphRetriever
self.graph_retriever = GraphRetriever(
store=self.vector_store,
edges=[("habitat", "habitat")],
strategy=Eager(k=k, start_k=start_k, max_depth=max_depth)
)
return f"[Success] GraphRetriever setup complete (k={k}, start_k={start_k}, max_depth={max_depth})"
except Exception as e:
return f"[Error] GraphRetriever creation error: {str(e)}"
After that, I developed the setup_graph_chain function to build the Graphrag pipeline that uses my GraphRetriever for enhanced document retrieval. I designed a custom prompt ChatPromptTemplate that instructs the model to answer based only on the given context and question.
I then made an A format_docs function to convert the retrieved documents into a readable string format that includes each document’s ID, description, and metadata. I built the full RAG chain using a dictionary that "context" is the result of passing the output self.graph_retriever through my format_docs function, and "question" is piped directly using RunnablePassthrough().
I chained this to the prompt, followed by the LLM, and finally used it StrOutputParser() to extract the plain text answer. I wrapped it all in a try-except block so I could catch any setup issues and return clear success or error messages.
def setup_graph_chain(self) -> str:
"""Set up RAG chain for GraphRAG"""
try:
# Create prompt template
prompt = ChatPromptTemplate.from_template("""
Answer the question based only on the following context.
Context: {context}
Question: {question}
Answer: """)
def format_docs(docs: List[Document]) -> str:
"""Format documents"""
formatted_docs = []
for doc in docs:
doc_text = f"Animal: {doc.id}\n"
doc_text += f"Description: {doc.page_content}\n"
doc_text += f"Metadata: {doc.metadata}"
formatted_docs.append(doc_text)
return "\n\n".join(formatted_docs)
# Create GraphRAG RAG chain
self.graph_chain = (
{"context": self.graph_retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| self.llm
| StrOutputParser()
)
return "[Success] GraphRAG RAG chain set up successfully"
except Exception as e:
return f"[Error] GraphRAG chain creation error: {str(e)}"
I created a standard vector-based retrieval pipeline for my RAG system. First, in setup_standard_retrieverI developed a retriever from the existing one InMemoryVectorStore using the as_retriever() method with a k value to control how many similar documents to fetch.
I stored this self.standard_retriever and returned a message confirming the setup. Then, in setup_standard_chain, I built the full RAG chain for this standard retriever. I made a custom prompt using ChatPromptTemplate.from_template and included a helper function format_docs to format each document showing its ID, description, and metadata in a readable way.
I connected the retriever to the formatter, passed the question usingRunnablePassthrough(), merged them with the prompt, and finally sent the output to my LLM StrOutputParser to produce a clean answer. I wrapped both functions in try-except blocks to catch any errors and make sure the system returns a clear success or error message.
def setup_standard_retriever(self, k: int = 5) -> str:
"""Set up retriever for standard vector search"""
try:
# Create standard vector search retriever
self.standard_retriever = self.vector_store.as_retriever(search_kwargs={"k": k})
return f"[Success] Standard vector search retriever setup complete (k={k})"
except Exception as e:
return f"[Error] Standard retriever creation error: {str(e)}"
def setup_standard_chain(self) -> str:
"""Set up RAG chain for standard vector search"""
try:
# Create prompt template
prompt = ChatPromptTemplate.from_template("""
Answer the question based only on the following context.
Context: {context}
Question: {question}
Answer: """)
def format_docs(docs: List[Document]) -> str:
"""Format documents"""
formatted_docs = []
for doc in docs:
doc_text = f"Animal: {doc.id}\n"
doc_text += f"Description: {doc.page_content}\n"
doc_text += f"Metadata: {doc.metadata}"
formatted_docs.append(doc_text)
return "\n\n".join(formatted_docs)
# Create standard vector search RAG chain
self.standard_chain = (
{"context": self.standard_retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| self.llm
| StrOutputParser()
)
return "[Success] Standard vector search RAG chain set up successfully"
except Exception as e:
return f"[Error] Standard RAG chain creation error: {str(e)}"
I made the A compare_with_standard_retrieval function to evaluate how well my GraphRAG system performs compared to standard vector search. I checked if the user entered a valid question, and if not, I returned a message asking for one.
Then, I developed two parallel steps: first, I used self.standard_retriever and self.standard_chain to get documents and answers through standard dense vector search, collecting each document's ID, content, and metadata into a readable list. Next, I did the same with self.graph_retriever and self.graph_chain, which includes knowledge graph traversal, to get documents and answers with deeper context.
I formatted both sets of results into clearly labelled sections showing the answer and retrieved documents for each method. I used neat separators to make the comparison easy to read and wrapped everything in a try-except block to catch and report errors.
def compare_with_standard_retrieval(self, question: str) -> str:
"""Compare with standard retrieval"""
try:
if not question.strip():
return "Please enter a question"
# 1. Standard vector search
standard_docs = self.standard_retriever.invoke(question)
standard_answer = self.standard_chain.invoke(question)
# Standard search results (with full metadata)
standard_info_list = []
for doc in standard_docs:
doc_text = f"• {doc.id}: {doc.page_content}\n"
doc_text += f" {doc.metadata}"
standard_info_list.append(doc_text)
standard_info = "\n".join(standard_info_list)
# 2. GraphRAG search
graph_docs = self.graph_retriever.invoke(question)
graph_answer = self.graph_chain.invoke(question)
# GraphRAG search results (with full metadata)
graph_info_list = []
for doc in graph_docs:
doc_text = f"• {doc.id}: {doc.page_content}\n"
doc_text += f" {doc.metadata}"
graph_info_list.append(doc_text)
graph_info = "\n".join(graph_info_list)
# Format comparison results
comparison = f"""
═══════════════════════════════════════════════════════════════
【Standard Vector Search Answer】
{standard_answer}
【Documents Retrieved by Standard Vector Search】({len(standard_docs)} items):
{standard_info}
═══════════════════════════════════════════════════════════════
【GraphRAG Answer】
{graph_answer}
【Documents Retrieved by GraphRAG】({len(graph_docs)} items):
{graph_info}
═══════════════════════════════════════════════════════════════
"""
return comparison
except Exception as e:
return f"[Error] Error: {str(e)}"
Conclusion :
Graph Retriever can be used not only by itself, but also in a hybrid approach with Vector RAG, and it will play an important role in the future evolution of RAG and agent systems.
This provides important insights for building GraphRAG systems in general, not just for specific products. In other words, even if you build a system like Graph Retriever on your own, the key lies in how efficiently and seamlessly you manage the knowledge graph structure, link it with vector data, and maintain consistency with the original data source.
🧙♂️ I am an AI Generative expert! If you want to collaborate on a project, drop an inquiry here or Book a 1-on-1 Consulting Call With Me.
I would highly appreciate it if you
❣ Join my Patreon: https://www.patreon.com/GaoDalie_AI
- Book an Appointment with me: https://topmate.io/gaodalie_ai
- Support the Content (every Dollar goes back into the -video):https://buymeacoffee.com/gaodalie98d
- Subscribe to the Newsletter for free:https://substack.com/@gaodalie
Top comments (0)