GraphRAG Tutorial: Build Knowledge Graph Retrieval for Complex Questions
Quick Answer / TL;DR
This tutorial provides a hands-on guide to building a GraphRAG system. GraphRAG enhances traditional Retrieval-Augmented Generation (RAG) by using knowledge graphs to retrieve highly structured and contextual information, enabling Large Language Models (LLMs) to answer complex, multi-hop questions with greater accuracy and explainability. You'll learn how to construct a knowledge graph, perform graph-aware retrieval, and integrate it with LLMs for superior question answering.
Large Language Models (LLMs) have revolutionized how we interact with information, but they often struggle with complex, multi-hop questions that require synthesizing information from various sources or understanding nuanced relationships. Traditional Retrieval-Augmented Generation (RAG) helps ground LLMs in external data, but even RAG can fall short when the required context is buried in unstructured text or demands intricate logical inference. This is where GraphRAG shines, leveraging the power of knowledge graphs to provide structured, interconnected context for superior question answering.
In this comprehensive tutorial, we’ll walk through the process of building a GraphRAG system from the ground up. You’ll discover how knowledge graphs can unlock deeper insights, enhance retrieval accuracy, and provide explainability for LLM responses, especially for those challenging questions that stump vanilla RAG.
What You Will Learn
- Understand the limitations of traditional RAG and the advantages of GraphRAG.
- Set up a development environment for building knowledge graphs and integrating with LLMs.
- Learn to extract entities and relationships from unstructured text to construct a knowledge graph.
- Implement graph-aware retrieval techniques to fetch relevant context for complex queries.
- Integrate the retrieved graph data with LLMs for accurate and insightful question answering.
Table of Contents
- Understanding GraphRAG: Why Knowledge Graphs for RAG?
- Setting Up Your GraphRAG Environment
- Data Ingestion and Knowledge Graph Construction
- Implementing Graph-Aware Retrieval
- Integrating with Large Language Models for Complex Question Answering
- Frequently Asked Questions (FAQ)
- Further Reading
Understanding GraphRAG: Why Knowledge Graphs for RAG?
Vanilla RAG systems typically rely on vector databases to store chunks of unstructured text. When a query comes in, the system retrieves top-k similar chunks and feeds them to the LLM. While effective for many direct questions, this approach has limitations:
- Lack of Structural Context: Relationships between concepts are lost when text is chunked. A question like “What projects did Alice and Bob collaborate on, and what was their impact?” is hard to answer if “Alice,” “Bob,” “projects,” and “impact” are in different text chunks without explicit links.
- Difficulty with Multi-Hop Questions: Questions requiring information synthesis across multiple facts or inferences are challenging because the LLM might only receive isolated pieces of information.
- Hallucination Risk: Without clear paths of reasoning, LLMs might still “invent” connections between facts.
- Explainability: It’s often hard to trace why an LLM arrived at a particular answer when given amorphous text chunks.
GraphRAG addresses these challenges by replacing or augmenting the vector store with a knowledge graph. A knowledge graph stores information as a network of interconnected entities (nodes) and relationships (edges). This structured representation offers several advantages:
- Explicit Relationships: Entities like
Person,Project, andSkillare connected by explicit relationships likeWORKED_ON,HAS_SKILL,CONTRIBUTED_TO, which are directly queryable. - Contextual Retrieval: Instead of just text chunks, GraphRAG can retrieve subgraphs — interconnected networks of facts — that directly answer or provide context for a query.
- Enhanced Reasoning: LLMs can perform better logical inference when presented with structured facts and their relationships, significantly improving performance on complex questions.
- Explainability: The path or subgraph used to construct an answer can be visualized and presented, offering inherent explainability.
By transforming unstructured data into a rich, semantic graph, GraphRAG empowers LLMs to navigate a landscape of interconnected knowledge, leading to more accurate, relevant, and transparent responses.
Now that we understand the ‘why,’ let’s prepare our environment for building this powerful system.
Setting Up Your GraphRAG Environment
Before we dive into building our knowledge graph and integrating it with an LLM, we need to set up our development environment. We’ll primarily use Python for this tutorial.
1. Prerequisites
Ensure you have Python 3.9+ and pip installed.
2. Install Necessary Libraries
We’ll need libraries for interacting with LLMs (LangChain), a graph database driver (Neo4j), and potentially a local LLM or API client.
1
pip install langchain langchain-community langchain-openai neo4j tiktoken python-dotenv
langchain: The framework for building LLM applications.langchain-community: Contains various integrations, including graph database connectors.langchain-openai: For connecting to OpenAI’s API (you can substitute with other LLM providers).neo4j: The official Neo4j Python driver.tiktoken: For token counting with OpenAI models.python-dotenv: To manage environment variables securely.
3. Set Up a Graph Database (Neo4j)
For this tutorial, we’ll use Neo4j, a popular graph database. You have a few options:
Option A: Neo4j AuraDB (Cloud Service)
This is the easiest for production or cloud-based development. Sign up for a free tier at Neo4j AuraDB. Once created, you’ll get connection details (URI, username, password).
Option B: Local Docker Container
For local development, Docker is highly recommended.
1
docker run --name neo4j-graphrag -p 7474:7474 -p 7687:7687 -e NEO4J_AUTH=neo4j/password -e NEO4J_apoc_export_file_enabled=true -e NEO4J_apoc_import_file_enabled=true -e NEO4J_apoc_import_file_use__neo4j__config=true -e NEO4J_dbms_security_procedures_unrestricted=apoc.* -e NEO4JLABS_PLUGINS='["apoc", "graph-data-science"]' neo4j:5.16
This command starts a Neo4j 5.x container with APOC and GDS plugins, which are useful for various graph operations.
neo4j/password: Sets the username toneo4jand password topassword. Remember to change this in a production environment.- You can access the Neo4j Browser at
http://localhost:7474.
4. Environment Variables
Create a .env file in your project root to store sensitive API keys and database credentials.
OPENAI_API_KEY="your_openai_api_key_here"
NEO4J_URI="bolt://localhost:7687" # or your AuraDB URI
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="password"
Then, load them in your Python script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
from dotenv import load_dotenv
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
neo4j_uri = os.getenv("NEO4J_URI")
neo4j_username = os.getenv("NEO4J_USERNAME")
neo4j_password = os.getenv("NEO4J_PASSWORD")
if not all([openai_api_key, neo4j_uri, neo4j_username, neo4j_password]):
raise ValueError("Missing one or more environment variables. Check your .env file.")
print("Environment variables loaded successfully.")
With our environment ready, the next crucial step is getting our data into a graph format that Neo4j can understand.
Data Ingestion and Knowledge Graph Construction
The heart of GraphRAG lies in transforming unstructured text into a structured knowledge graph. This process typically involves identifying entities (nouns, concepts) and the relationships between them (verbs, predicates).
1. Choose a Sample Dataset
For this tutorial, let’s consider a hypothetical dataset of internal company project documentation. Each document describes projects, team members, technologies used, and project outcomes.
Example Document Snippets:
1
2
3
Document 1: "Project Phoenix, led by Alice Johnson, aimed to develop a new AI-powered recommendation engine. Bob Smith and Carol White were key contributors. It utilized Python and TensorFlow, and resulted in a 15% increase in user engagement."
Document 2: "The Atlas Initiative, overseen by David Lee, focused on migrating our legacy systems to a cloud-native architecture using Kubernetes and Go. Alice Johnson also consulted on its security aspects. This project achieved a 20% cost reduction."
2. Extract Entities and Relationships
We can use LLMs or specialized NLP tools to extract structured data from these snippets. LangChain provides excellent tools for this.
Let’s define a schema for our knowledge graph. We might want entities like Person, Project, Technology, Outcome, and relationships like LED_BY, CONTRIBUTED_TO, USED_TECHNOLOGY, ACHIEVED_OUTCOME.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from langchain_openai import ChatOpenAI
from langchain.chains import create_tagging_chain_pydantic
from langchain.graphs import Neo4jGraph
from pydantic import BaseModel, Field
from typing import List, Optional
# Initialize LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0, api_key=openai_api_key)
# Connect to Neo4j
graph = Neo4jGraph(url=neo4j_uri, username=neo4j_username, password=neo4j_password)
# Define our knowledge graph schema for extraction
class Person(BaseModel):
name: str = Field(description="Name of the person")
class Project(BaseModel):
name: str = Field(description="Name of the project")
description: Optional[str] = Field(description="Brief description of the project")
class Technology(BaseModel):
name: str = Field(description="Name of the technology or tool")
class Outcome(BaseModel):
description: str = Field(description="Description of the project outcome")
class Relationship(BaseModel):
source_entity_type: str
source_entity_name: str
relation_type: str
target_entity_type: str
target_entity_name: str
class KnowledgeGraphSchema(BaseModel):
people: List[Person] = Field(default_factory=list)
projects: List[Project] = Field(default_factory=list)
technologies: List[Technology] = Field(default_factory=list)
outcomes: List[Outcome] = Field(default_factory=list)
relationships: List[Relationship] = Field(default_factory=list)
# Create a tagging chain for extracting structured data
# Note: LangChain offers more advanced GraphDocument and entity extraction tools,
# but this demonstrates a simpler Pydantic-based approach.
tagging_chain = create_tagging_chain_pydantic(KnowledgeGraphSchema, llm)
# Example text
text1 = "Project Phoenix, led by Alice Johnson, aimed to develop a new AI-powered recommendation engine. Bob Smith and Carol White were key contributors. It utilized Python and TensorFlow, and resulted in a 15% increase in user engagement."
text2 = "The Atlas Initiative, overseen by David Lee, focused on migrating our legacy systems to a cloud-native architecture using Kubernetes and Go. Alice Johnson also consulted on its security aspects. This project achieved a 20% cost reduction."
# Process text and extract entities/relationships
extracted_data1 = tagging_chain.invoke({"input": text1})["parsed"]
extracted_data2 = tagging_chain.invoke({"input": text2})["parsed"]
# You would typically loop through many documents and aggregate
print("Extracted from Document 1:", extracted_data1)
print("\nExtracted from Document 2:", extracted_data2)
The output for extracted_data1 might look something like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Extracted from Document 1: KnowledgeGraphSchema(
people=[Person(name='Alice Johnson'), Person(name='Bob Smith'), Person(name='Carol White')],
projects=[Project(name='Project Phoenix', description='new AI-powered recommendation engine')],
technologies=[Technology(name='Python'), Technology(name='TensorFlow')],
outcomes=[Outcome(description='15% increase in user engagement')],
relationships=[
Relationship(source_entity_type='Project', source_entity_name='Project Phoenix', relation_type='LED_BY', target_entity_type='Person', target_entity_name='Alice Johnson'),
Relationship(source_entity_type='Person', source_entity_name='Bob Smith', relation_type='CONTRIBUTED_TO', target_entity_type='Project', target_entity_name='Project Phoenix'),
Relationship(source_entity_type='Person', source_entity_name='Carol White', relation_type='CONTRIBUTED_TO', target_entity_type='Project', target_entity_name='Project Phoenix'),
Relationship(source_entity_type='Project', source_entity_name='Project Phoenix', relation_type='USED_TECHNOLOGY', target_entity_type='Technology', target_entity_name='Python'),
Relationship(source_entity_type='Project', source_entity_name='Project Phoenix', relation_type='USED_TECHNOLOGY', target_entity_type='Technology', target_entity_name='TensorFlow'),
Relationship(source_entity_type='Project', source_entity_name='Project Phoenix', relation_type='RESULTED_IN', target_entity_type='Outcome', target_entity_name='15% increase in user engagement')
]
)
3. Loading into the Graph Database
Now, we’ll convert the extracted Pydantic objects into Cypher queries to create nodes and relationships in Neo4j.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def ingest_extracted_data(extracted_data: KnowledgeGraphSchema, graph: Neo4jGraph):
for person in extracted_data.people:
graph.query(f"MERGE (p:Person {{name: '{person.name}'}})")
for project in extracted_data.projects:
graph.query(f"MERGE (pr:Project {{name: '{project.name}'}}) SET pr.description = '{project.description}'")
for tech in extracted_data.technologies:
graph.query(f"MERGE (t:Technology {{name: '{tech.name}'}})")
for outcome in extracted_data.outcomes:
graph.query(f"MERGE (o:Outcome {{description: '{outcome.description}'}})")
for rel in extracted_data.relationships:
# Example for a relationship: (Source)-[RELATION]->(Target)
# We need to ensure the nodes exist before creating the relationship.
# This example uses a simplified merge pattern.
source_label = rel.source_entity_type
target_label = rel.target_entity_type
relation_type = rel.relation_type
# Construct the Cypher query carefully to handle different entity types
cypher_query = f"""
MATCH (s:{source_label}) WHERE s.name = '{rel.source_entity_name}' OR s.description = '{rel.source_entity_name}'
MATCH (t:{target_label}) WHERE t.name = '{rel.target_entity_name}' OR t.description = '{rel.target_entity_name}'
MERGE (s)-[:{relation_type}]->(t)
"""
# Note: The above MATCH for name/description is a simplification.
# In a real system, you'd use a unique ID or more robust matching.
graph.query(cypher_query)
print(f"Ingested {len(extracted_data.relationships)} relationships.")
# Ingest data
ingest_extracted_data(extracted_data1, graph)
ingest_extracted_data(extracted_data2, graph)
# Verify by running a simple query
# print(graph.query("MATCH (p:Person)-[r]->(pr:Project) RETURN p.name, type(r), pr.name LIMIT 5"))
After running this, your Neo4j database will contain a network of interconnected nodes representing people, projects, technologies, and outcomes, along with their explicit relationships.
Once our knowledge graph is populated, we can leverage its power for intelligent retrieval, moving beyond simple keyword matching to contextual graph traversal.
Implementing Graph-Aware Retrieval
With our knowledge graph populated, the next step is to retrieve relevant information based on user queries. Graph-aware retrieval goes beyond vector similarity to find connected facts and paths within the graph that directly address a question.
LangChain offers Neo4jGraph as a retriever, which can interpret a natural language query and translate it into Cypher queries to traverse the graph.
1. Connecting LangChain to the Neo4j Graph
We’ve already initialized Neo4jGraph in the previous section. Let’s make it more explicit as a retriever:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
# Re-initialize graph if not already
graph = Neo4jGraph(url=neo4j_uri, username=neo4j_username, password=neo4j_password)
# Define the schema of the graph to help the LLM generate accurate Cypher queries
# This is crucial for GraphCypherQAChain to understand your graph structure.
# You can auto-generate this using graph.get_schema, but for a clear tutorial,
# we'll use a simplified version based on our ingestion.
graph.refresh_schema() # Get the latest schema from the graph
# The LLM will use this schema to understand which labels and relationships exist.
# Example schema content after refresh:
# Nodes: Person, Project, Technology, Outcome
# Relationships: LED_BY, CONTRIBUTED_TO, USED_TECHNOLOGY, RESULTED_IN, CONSULTED_ON
2. Crafting Graph Retrieval Queries
The GraphCypherQAChain uses an LLM to translate natural language questions into Cypher queries, executes those queries against the Neo4j database, and then uses another LLM call to synthesize the results into a human-readable answer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Initialize the LLM for generating Cypher queries and answering
llm_cypher = ChatOpenAI(model="gpt-4o", temperature=0, api_key=openai_api_key)
# Create the GraphCypherQAChain
cypher_chain = GraphCypherQAChain.from_llm(
graph=graph,
llm=llm_cypher,
verbose=True # Set to True to see the generated Cypher queries and graph responses
)
# Example: Complex Multi-Hop Question
complex_question = "What technologies were used in projects Alice Johnson was involved in, and what outcomes did those projects achieve?"
print(f"\nQuerying GraphRAG for: '{complex_question}'")
response = cypher_chain.invoke({"query": complex_question})
print("\nGraphRAG Response:")
print(response["result"])
When verbose=True, you’ll see the LLM’s generated Cypher query, the raw results from Neo4j, and then the final synthesized answer. A plausible Cypher query generated by the LLM might be:
1
2
3
4
5
MATCH (p:Person)-[:LED_BY|CONTRIBUTED_TO|CONSULTED_ON]->(proj:Project)
WHERE p.name = "Alice Johnson"
MATCH (proj)-[:USED_TECHNOLOGY]->(tech:Technology)
MATCH (proj)-[:RESULTED_IN]->(o:Outcome)
RETURN proj.name, COLLECT(DISTINCT tech.name) AS technologies_used, COLLECT(DISTINCT o.description) AS outcomes_achieved
And the synthesized response from the LLM based on the graph results might be:
1
"Alice Johnson was involved in 'Project Phoenix', which utilized Python and TensorFlow and resulted in a 15% increase in user engagement. She also consulted on 'The Atlas Initiative', which used Kubernetes and Go, leading to a 20% cost reduction."
This clearly demonstrates the power of GraphRAG in answering questions that require traversing multiple relationships. The LLM didn’t “guess”; it used the explicitly retrieved structured information from the graph.
Retrieving context is only half the battle; integrating it effectively with an LLM for final answer generation is the critical next step.
Integrating with Large Language Models for Complex Question Answering
After retrieving relevant subgraphs or structured data, the final step in the GraphRAG pipeline is to present this information to an LLM in a way that allows it to generate accurate, comprehensive, and well-grounded answers.
1. Feeding Graph-Retrieved Context to the LLM
The GraphCypherQAChain we used earlier already handles the integration: it takes the graph query results and feeds them into the LLM for final answer generation. The prompt structure inside GraphCypherQAChain is designed to guide the LLM to synthesize these results.
However, for more custom scenarios, you might retrieve the graph data explicitly and then construct your own prompt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# --- Hypothetical Custom Retrieval (if not using GraphCypherQAChain directly) ---
# For demonstration, let's assume we have raw graph results:
raw_graph_results = """
[
{"project_name": "Project Phoenix", "technologies": ["Python", "TensorFlow"], "outcomes": ["15% increase in user engagement"]},
{"project_name": "Atlas Initiative", "technologies": ["Kubernetes", "Go"], "outcomes": ["20% cost reduction"]}
]
"""
# In a real scenario, this would come from graph.query() or a more sophisticated graph traversal.
# --- End Hypothetical Custom Retrieval ---
# Define a custom prompt template to include the graph context
prompt_template = ChatPromptTemplate.from_messages(
[
("system", "You are an expert assistant for company knowledge. Answer the question based ONLY on the provided context. If the answer is not in the context, state that you don't have enough information."),
("user", "Context from knowledge graph: {graph_context}\n\nQuestion: {question}")
]
)
# Initialize the LLM for final answer generation
llm_answer_gen = ChatOpenAI(model="gpt-4o", temperature=0.2, api_key=openai_api_key)
# Create a RAG chain
custom_rag_chain = (
{"graph_context": lambda x: x["graph_context"], "question": RunnablePassthrough()}
| prompt_template
| llm_answer_gen
| StrOutputParser()
)
# Simulate answering the complex question with custom RAG
question_for_custom_rag = "What technologies were used in projects Alice Johnson was involved in, and what outcomes did those projects achieve?"
print(f"\nAnswering with Custom RAG for: '{question_for_custom_rag}'")
final_answer_custom = custom_rag_chain.invoke({
"graph_context": raw_graph_results, # Use the assumed raw graph results
"question": question_for_custom_rag
})
print("\nCustom RAG Final Answer:")
print(final_answer_custom)
The output from custom_rag_chain should be similar to the GraphCypherQAChain output, demonstrating how the LLM leverages the structured graph_context to formulate a precise answer.
2. Prompt Engineering for GraphRAG
Effective prompt engineering is crucial. When presenting graph data to an LLM, consider:
- Clarity of Context: Format the graph results (e.g., as JSON, bullet points, or natural language summaries) to be easily digestible by the LLM.
- Role-Playing: Instruct the LLM to act as an expert or knowledge base for the given domain.
- Constraints: Explicitly tell the LLM to only use the provided context and avoid external knowledge or hallucinations.
- Question Type: If the LLM needs to perform specific reasoning (e.g., comparison, summarization), include these instructions in the prompt.
3. Evaluating Responses
Evaluating GraphRAG responses involves:
- Factuality: Is the answer consistent with the retrieved graph data?
- Completeness: Does it fully address all parts of the complex question?
- Relevance: Is all information in the answer relevant to the question?
- Coherence: Is the answer well-structured and easy to understand?
- Explainability: Can you trace the answer back to specific nodes and relationships in the graph?
Tools like Ragas can help automate some aspects of RAG evaluation, often requiring ground truth answers or human annotations for complex queries. The inherent structure of knowledge graphs, however, often simplifies manual verification.
To solidify your understanding, let’s look at common questions developers encounter.
Frequently Asked Questions (FAQ)
Further Reading
- LangChain Documentation on Graph Integrations: Dive deeper into LangChain’s capabilities for interacting with various graph databases and building graph-aware chains. LangChain Graph Integrations
- Neo4j Developer Resources: Explore comprehensive guides, tutorials, and documentation for Neo4j, including advanced Cypher queries and data modeling. Neo4j Developer Resources
- The “Knowledge Graphs and LLMs” Paper Series: For a deeper academic understanding of the synergy between knowledge graphs and large language models, search for recent research papers on the topic.
Conclusion
This GraphRAG tutorial has guided you through the critical steps of building an advanced RAG system that leverages the power of knowledge graphs. By transforming unstructured data into an interconnected network of facts, we’ve enabled LLMs to perform superior knowledge graph retrieval, tackling complex, multi-hop questions with unprecedented accuracy and explainability. From setting up your environment and constructing the graph to implementing intelligent retrieval and integrating with LLMs, you now have a solid foundation to build sophisticated AI applications.
The ability to provide LLMs with structured, contextualized information is a game-changer for enterprise knowledge management, scientific discovery, and decision support systems. As LLMs continue to evolve, the combination of their generative power with the precise, structured reasoning capabilities of knowledge graphs will undoubtedly lead to even more intelligent and reliable AI systems.
Ready to build more robust AI solutions? Explore our services for custom AI/ML development and advanced RAG implementations. Contact CodeCrux Today!