Most RAG systems treat knowledge as a bag of document chunks. Knowledge graphs let AI reason *across* facts — following chains of relationships the way a human expert does. This lesson teaches you the fundamentals from scratch, with working Python code and clear connections to modern RAG and agent architectures.

Imagine you're building a customer support AI for a large software company. A user asks: "Why does the export feature break when I'm using the Enterprise plan with SSO enabled?" To answer that, your system needs to know that export is a feature, that it depends on an authentication module, that SSO is a type of authentication, and that Enterprise plan activates SSO by default — all in a single reasoning chain. A flat list of documentation chunks won't get you there. Neither will a simple keyword search. What you need is a structure that captures relationships between pieces of knowledge, not just the knowledge itself.
That structure is a knowledge graph. And once you understand how they work, you'll see why they're becoming a critical layer in modern RAG systems and AI agent architectures. They give your AI the ability to reason across connected facts — to "walk" a chain of relationships the way a human expert does when they think through a problem.
By the end of this lesson, you'll understand knowledge graphs deeply enough to build a small one from scratch, query it meaningfully, and see exactly where it plugs into a RAG or agent pipeline.
What you'll learn:
You should be comfortable reading Python code and have a general sense of what RAG (Retrieval-Augmented Generation) means — even if you haven't built one yet. If you're brand new to RAG, it's worth reading RAG Fundamentals: Build Your First Retrieval-Augmented Generation System before diving into this lesson. No graph database experience is required.
A knowledge graph is a data structure that represents information as a network of entities and the relationships between them. That's the formal definition — but let's build real intuition for it.
Think about how you actually know something. You don't just know isolated facts. You know that aspirin is a drug, that it treats headaches, that it inhibits COX-2 enzymes, that COX-2 enzymes are part of the inflammation pathway. Your knowledge is a web of connected facts, not a list of bullet points.
A knowledge graph captures exactly that kind of connected, structured knowledge. It has three fundamental building blocks:
Nodes (also called vertices or entities) represent things — people, places, products, concepts, documents, events. In a software support graph, nodes might include: ExportFeature, SSOModule, Enterpriseplan, AuthenticationService, Bug#4521.
Edges (also called relationships or links) represent connections between nodes. An edge always connects exactly two nodes. The ExportFeature depends_on AuthenticationService. Bug#4521 affects ExportFeature. These aren't arbitrary — each edge has a relationship type that describes the nature of the connection.
Properties are key-value attributes attached to nodes or edges. A node representing a bug might have properties like severity: "critical", reported_date: "2024-01-15", status: "open". An edge might carry properties too — for instance, depends_on might have a strength: "hard" property to distinguish mandatory from optional dependencies.
Key insight: The power of a knowledge graph isn't in storing more data — it's in making the meaning of data explicit and machine-readable. When you say
Bug#4521 → affects → ExportFeature, you're not just storing two facts; you're storing their relationship, which enables reasoning.
Together, these three elements let you represent a triple: Subject → Predicate → Object. This triple pattern is the atom of knowledge graph data. Everything in a knowledge graph can be decomposed into triples:
SSOModule → is_a → AuthenticationMethodEnterpriseПлан → enables → SSOModuleExportFeature → depends_on → SSOModuleString enough triples together and you have a graph that a machine — or an AI — can traverse to answer complex questions.
Before we build one, it's worth being precise about what makes knowledge graphs distinct from the tools you're probably already familiar with.
A relational database stores data in tables with fixed schemas. If you want to query relationships, you use JOINs — but JOIN logic has to be written by a human who knows in advance which tables to connect. The schema is rigid: adding a new type of relationship often means altering table structures.
A knowledge graph is schema-flexible. You can add new relationship types without restructuring existing data. You can also traverse arbitrary depths of connection without writing new JOIN logic — the graph structure handles that naturally.
A vector store (like the kind used in standard RAG pipelines) stores text as numerical embeddings and retrieves documents by semantic similarity. It's excellent at finding relevant chunks — passages that are semantically close to a query. But it doesn't know that two chunks are about the same entity, or that one fact contradicts another, or that ExportFeature is part of ProductSuite.
Note: Vector stores answer "what text is similar to this query?" Knowledge graphs answer "what is related to this entity, and how?" These are complementary questions — which is exactly why combining them in Graph RAG is so powerful.
Understanding how text becomes vectors for semantic search is still valuable context here, because in practice you'll often use both representations: vectors for initial retrieval, graphs for structured reasoning.
You could represent relationships in a Python dictionary, and for simple cases you might. But dictionaries don't naturally support bidirectional traversal, don't encode relationship types, and become unwieldy the moment you need to ask "find all entities connected to X through any path of length 3."
Let's make this concrete. We'll build a small knowledge graph representing a software product's support domain — the same scenario from the introduction. We'll use Python's networkx library, which is the standard general-purpose graph library and requires no external database.
pip install networkx
import networkx as nx
# Create a directed graph — direction matters in knowledge graphs
# "ExportFeature depends_on SSOModule" is different from the reverse
G = nx.DiGraph()
# Add nodes with properties (attributes)
G.add_node("ExportFeature", type="Feature", status="active", version_introduced="2.1")
G.add_node("SSOModule", type="AuthModule", protocol="SAML2", status="active")
G.add_node("EnterprisePlan", type="SubscriptionTier", price_usd=499)
G.add_node("AuthenticationService", type="Service", status="active")
G.add_node("Bug4521", type="Bug", severity="critical", status="open", reported="2024-01-15")
G.add_node("StandardPlan", type="SubscriptionTier", price_usd=99)
G.add_node("BasicExport", type="Feature", status="active")
# Add edges with relationship types as an attribute
# Syntax: add_edge(source, target, relationship=..., **other_properties)
G.add_edge("ExportFeature", "AuthenticationService", relationship="depends_on", strength="hard")
G.add_edge("SSOModule", "AuthenticationService", relationship="is_a", strength="hard")
G.add_edge("EnterprisePlan", "SSOModule", relationship="enables", default=True)
G.add_edge("EnterprisePlan", "ExportFeature", relationship="includes")
G.add_edge("StandardPlan", "BasicExport", relationship="includes")
G.add_edge("Bug4521", "ExportFeature", relationship="affects")
G.add_edge("Bug4521", "SSOModule", relationship="triggered_by")
Now let's ask some questions that would be hard or impossible to answer with a flat document store.
def get_neighbors_by_relationship(graph, node, relationship):
"""Find all nodes connected to `node` via a specific relationship type."""
results = []
for source, target, data in graph.edges(data=True):
if source == node and data.get("relationship") == relationship:
results.append((target, data))
return results
# What does ExportFeature depend on?
deps = get_neighbors_by_relationship(G, "ExportFeature", "depends_on")
print("ExportFeature depends on:", deps)
# Output: ExportFeature depends on: [('AuthenticationService', {'relationship': 'depends_on', 'strength': 'hard'})]
# What does EnterprisePlan enable or include?
for rel in ["enables", "includes"]:
results = get_neighbors_by_relationship(G, "EnterprisePlan", rel)
print(f"EnterprisePlan {rel}:", [r[0] for r in results])
# Output:
# EnterprisePlan enables: ['SSOModule']
# EnterprisePlan includes: ['ExportFeature']
Here's where knowledge graphs shine. Let's trace the chain: if a user is on EnterprisePlan and ExportFeature is broken, what's the root cause?
def find_paths(graph, source, target, max_depth=4):
"""Find all simple paths between two nodes up to a given depth."""
try:
paths = list(nx.all_simple_paths(graph, source, target, cutoff=max_depth))
return paths
except nx.NodeNotFound as e:
return []
# Can we trace a path from EnterprisePlan to Bug4521?
# This represents: "How does Enterprise Plan connect to this bug?"
paths = find_paths(G, "EnterprisePlan", "Bug4521", max_depth=5)
# Paths go the wrong direction — let's check reverse
# We want: Bug4521 → affects → ExportFeature ← includes ← EnterprisePlan
# Let's query: what bugs affect features included in EnterprisePlan?
def find_bugs_affecting_plan_features(graph, plan_node):
"""Multi-hop query: bugs that affect features included in a given plan."""
# Step 1: find features included in the plan
included_features = [
target for source, target, data in graph.edges(data=True)
if source == plan_node and data.get("relationship") == "includes"
]
# Step 2: find bugs that affect those features
relevant_bugs = []
for source, target, data in graph.edges(data=True):
if data.get("relationship") == "affects" and target in included_features:
bug_props = graph.nodes[source]
relevant_bugs.append({
"bug": source,
"affects": target,
"severity": bug_props.get("severity"),
"status": bug_props.get("status")
})
return relevant_bugs
bugs = find_bugs_affecting_plan_features(G, "EnterprisePlan")
print("Bugs affecting Enterprise Plan features:", bugs)
# Output: [{'bug': 'Bug4521', 'affects': 'ExportFeature', 'severity': 'critical', 'status': 'open'}]
That last function does what no vector similarity search can do in one step: it reasons through the graph structure — plan → features → bugs — to surface a meaningful, structured answer.
Tip: When building your first knowledge graph, start with your most important entity types and only 4-5 relationship types. Resist the urge to model everything at once. A sparse graph with meaningful relationships is more useful than a dense graph with vague ones.
Schema design is where most beginners struggle. Here's a mental framework that works.
List the main categories of things in your domain. In a medical knowledge graph, you might have: Drug, Condition, Symptom, Gene, Pathway, ClinicalTrial. In a corporate knowledge base: Person, Team, Project, Document, Decision, System.
Resist making entities too granular too early. "Software Feature" is probably one entity type, not ten.
For each pair of entity types that can be meaningfully connected, define named relationship types. Good relationship names are:
triggers, inhibits, authored_by — not just related_toWarning: The single most common knowledge graph mistake is using vague, catch-all relationships like
related_toorconnected_with. These destroy the value of the graph — you can't reason with "A is related to B." Be explicit: how is A related to B?
Only add properties that you'll actually filter or display. Every property you add is data you need to maintain. A good rule: if you'd never query "find all nodes where X > Y," you probably don't need X as a property.
Now let's zoom out and connect this foundational knowledge to the systems you're building.
Standard RAG works like this: embed a query, find similar document chunks, pass them to an LLM. This works well for "find me documents about topic X." It struggles with questions that require connecting multiple facts — the multi-hop problem.
Knowledge graphs solve this by providing a structured retrieval layer. In a Graph RAG architecture, you might:
The result is context that's already organized around the correct entities and their relationships, which leads to dramatically more accurate answers on complex questions. This approach is covered in depth in Graph RAG: Building Knowledge Graph-Enhanced Retrieval Pipelines for Complex Multi-Hop Queries.
You can also combine this with query routing strategies — simple factual questions go to vector retrieval, complex relational questions get routed to graph traversal.
Key insight: Knowledge graphs and vector stores aren't competitors — they're complements. Vectors are great for finding relevant content. Graphs are great for reasoning about structure. The most capable RAG systems use both.
AI agents need to remember things across multiple turns and multiple tool calls. A knowledge graph is an excellent structure for long-term agent memory — it lets the agent store facts about the world and relationships between them, then query that memory with precision.
Imagine an agent helping a user plan a complex data migration. The agent can build up a knowledge graph of: which tables depend on which, which transformations have been applied, which steps have been validated, which stakeholders approved which components. Each time the agent takes a new step, it adds to and queries this graph, rather than trying to stuff everything into the context window.
This connects to the broader topic of memory architectures in AI agents — knowledge graphs serve as a form of structured long-term memory that's both readable and writable by the agent itself.
Let's build a knowledge graph for a simplified HR domain and write queries that would power a real AI assistant.
The scenario: You're building an AI assistant for an HR department. The assistant needs to answer questions like "Who should I contact about benefits for remote employees?" or "Which teams are working on the same project?"
import networkx as nx
# Build the graph
hr = nx.DiGraph()
# Nodes
hr.add_node("Alice", type="Person", role="HR_Manager", location="remote")
hr.add_node("Bob", type="Person", role="Engineer", location="office")
hr.add_node("Carol", type="Person", role="Engineer", location="remote")
hr.add_node("DataTeam", type="Team", size=8)
hr.add_node("PlatformTeam", type="Team", size=12)
hr.add_node("Project_Atlas", type="Project", status="active", budget_usd=250000)
hr.add_node("RemoteBenefitsPolicy", type="Policy", last_updated="2024-03-01")
hr.add_node("HealthInsurancePlan", type="Benefit", provider="BlueCross")
# Edges
hr.add_edge("Alice", "DataTeam", relationship="manages")
hr.add_edge("Bob", "PlatformTeam", relationship="member_of")
hr.add_edge("Carol", "DataTeam", relationship="member_of")
hr.add_edge("DataTeam", "Project_Atlas", relationship="works_on")
hr.add_edge("PlatformTeam", "Project_Atlas", relationship="works_on")
hr.add_edge("Alice", "RemoteBenefitsPolicy", relationship="owns")
hr.add_edge("RemoteBenefitsPolicy", "HealthInsurancePlan", relationship="includes")
# Query 1: Who owns the Remote Benefits Policy?
def find_policy_owner(graph, policy_name):
owners = []
for source, target, data in graph.edges(data=True):
if target == policy_name and data.get("relationship") == "owns":
owners.append({
"person": source,
"role": graph.nodes[source].get("role")
})
return owners
print("RemoteBenefitsPolicy owners:", find_policy_owner(hr, "RemoteBenefitsPolicy"))
# Query 2: Which teams are collaborating on the same project?
def find_collaborating_teams(graph, project):
teams = [
source for source, target, data in graph.edges(data=True)
if target == project and data.get("relationship") == "works_on"
and graph.nodes[source].get("type") == "Team"
]
return teams
print("Teams on Project_Atlas:", find_collaborating_teams(hr, "Project_Atlas"))
# Query 3: Multi-hop — what benefits does a remote employee get?
# Remote person → policy that covers remote → benefits included
def get_remote_benefits(graph):
remote_policies = [
target for source, target, data in graph.edges(data=True)
if data.get("relationship") == "owns"
and "Remote" in target
]
benefits = []
for policy in remote_policies:
for source, target, data in graph.edges(data=True):
if source == policy and data.get("relationship") == "includes":
benefits.append({"policy": policy, "benefit": target})
return benefits
print("Remote benefits:", get_remote_benefits(hr))
Your challenge: Extend this graph to include a Location node type, connect people to locations via a works_from relationship, and write a query that returns all people who work remotely and are on teams working on active projects.
Mistake 1: Over-normalizing your graph You don't need a node for every possible concept. If "department" only appears as a property of Team, keep it as a property. Only promote something to a node when you need to traverse relationships through it.
Mistake 2: Using undirected graphs when direction matters
AuthorA wrote DocumentB is not the same as DocumentB wrote AuthorA. Always ask: does the direction of this relationship carry meaning? For most knowledge domains, the answer is yes — use nx.DiGraph().
Mistake 3: Designing for one query New learners often build a graph optimized for the one query they have in mind today. Sketch out 8-10 different questions the system should answer before you settle on your schema. The schema should serve the full range of questions.
Mistake 4: Forgetting to handle missing nodes in traversals Real data is messy. An edge might reference a node that hasn't been added yet. Always wrap traversal code in try/except blocks and validate node existence before querying properties.
# Safe property access
def safe_get(graph, node, prop, default=None):
if graph.has_node(node):
return graph.nodes[node].get(prop, default)
return default
Mistake 5: Treating a knowledge graph as a replacement for everything Knowledge graphs are powerful for structured, relational reasoning. They're not a replacement for vector search (which excels at semantic similarity) or relational databases (which excel at aggregations and transactions). Think of them as a new layer in your architecture, not a replacement for what you already have.
Warning: If you find yourself storing large blobs of text as node properties (like full document content), stop. That content belongs in a vector store or document store, with the knowledge graph holding only a reference (like a document ID) and structured metadata. Mixing them leads to a graph that's neither a good graph nor a good document store.
You now understand the three building blocks of a knowledge graph — nodes, edges, and relationship types — and why they matter. The core insight is this: knowledge graphs make the meaning of relationships machine-readable, enabling multi-hop reasoning that flat document stores and vector databases simply can't do.
Here's what you covered:
networkx gives you a solid foundation for building and querying graphs in Python without any infrastructureThe natural next step from here is seeing how knowledge graphs get integrated into actual retrieval systems. When you're ready, Graph RAG: Building Knowledge Graph-Enhanced Retrieval Pipelines for Complex Multi-Hop Queries takes everything you've learned here and wires it into a real RAG pipeline with entity extraction, graph traversal, and LLM synthesis.
If you're building agents rather than RAG systems, Building Multi-Step AI Agents with Planning and Memory shows you how to use structured memory — including graph-based memory — to enable agents that reason across multiple steps and multiple sessions.
The patterns you've learned in this lesson are foundational. Every sophisticated RAG system or intelligent agent you'll build from here will benefit from thinking in terms of entities, relationships, and traversal.