Table of Contents
-
Building and Integrating Agent driven Knowledge Graphs — 33m 5s
-
From foundations to function: how agents build Knowledge Graphs
-
Frameworks, Tools, and Best Practices for Agentic Knowledge Graphs — 24m 41s
1. Foundations of Knowledge Graphs for Agents
1.1 Why agents need structured reasoning
Welcome to the “Foundations of Knowledge Graphs for Agents” module. In this course, you will learn how knowledge graphs give AI agents the structures they need to reason, explain their decisions, and operate reliably in real-world systems.
When agents generate responses, they often appear confident, but the reasoning is hidden, fragmented, or even absent. It’s not just a communication problem — it’s a reliability problem. Knowledge graphs answer this by giving agents a structured way to connect information, trace relationships and make their decisions transparent.
The fundamental problem: fluency versus comprehension
Large Language Models (LLMs) are excellent for producing fluent language, but fluency alone does not guarantee reasoning. If you’ve ever asked the same question twice and received different answers, you’ve already experienced the downside of not having a transparent reasoning path — leading to inconsistent answers for similar questions. Fluency may sound compelling, but understanding and reasoning is another matter.
Introducing Orion, our support agent
Before diving into knowledge graphs, here is Orion, our support agent. Orion does a good job of answering tickets and finding relevant information, but like many LLM-based agents, it struggles to explain why it reached a conclusion or how different pieces of information actually connect. This is where our role as AI engineers comes in: upgrading Orion by giving it structure, context, and the ability to reason transparently. In the end, Orion won’t just answer questions — he’ll understand how his knowledge fits together and be able to clearly explain his reasoning.
Unstructured memory vs. structured reasoning
To understand what Orion is missing, we need to look at how most agents store information today. They rely on unstructured memory: raw text and embeddings stored without explicit links. Nothing is truly connected. There is no traceable logic or clear path of reasoning that one can follow.
| Unstructured memory | Structured reasoning |
|---|---|
| No explicit links | Explicitly related facts |
| Unreliable multi-step logic | Traceable reasoning paths |
| Unexplainable decisions | Emerging Understanding |
| Inconsistent results | Multi-step logic possible |
Key finding: Agents need structure to reason, and knowledge graphs provide that structure.
1.2 What is a Knowledge Graph?
A knowledge graph is much more than a database — it is a map of meaning. Also known as a semantic network, it represents a network of real-world entities (objects, events, situations or concepts) and illustrates the relationships between them.
In the world of Orion
In the world of Orion, support tickets naturally revolve around a small set of entities:
- Customers → report problems
- Products → are used by customers
- Problems → affect products
- Resolutions → resolve issues
A knowledge graph allows all of these elements to exist within a single connected structure.
The key transformation
This is where an essential transformation occurs: isolated facts (like tickets or isolated sentences) become connected knowledge. Relationships between entities explicitly represent meaning, transforming raw data into a network that the agent can reason about.
Once the information is connected, the patterns become visible. If multiple customers report the same issue for the same product, these connections stand out immediately in the graph. This type of visibility is simply not possible with plain text or embeddings alone.
The essence of a knowledge graph: It transforms fragmented data into a connected structure on which an agent can reason.
1.3 Entities, Relations, Ontologies and Schemas
Each knowledge graph has four fundamental building blocks:
- Entities — things or objects that interest us in our data
- Relationships — describe how these entities connect to each other
- Ontologies — define which relationships are allowed in the domain
- Schemas — enforcing consistency by specifying required fields and structure
Together, these four elements give a knowledge graph its structure and meaning.
The four building blocks
1. Entities
These are the things the agent cares about. In Orion’s domain, this includes customers, products, issues, and resolutions. These entities form the nodes of the graph.
2. Relationships
Once the entities are defined, the next step is to connect them. Relationships describe how these entities interact: who reported what, what caused the problem, and how it was resolved. Without relationships, entities exist in isolation and have no context.
3. Ontologies
Once relationships exist, they need rules. Ontologies define which connections are allowed, encoding the logic of the domain:
- Only customers can report issues
- Issues may affect products
- A product obviously cannot report a problem
This prevents invalid or nonsense connections.
4. Diagrams
Once the rules are defined, they must be enforced. Schemas provide consistency at the data level by defining the fields and structure required for each node and relationship. Together, ontologies and schemas keep the graph valid and trustworthy.
To avoid ambiguity: The ontology defines meaningful relationships in the domain. The schema enforces the structural rules that keep the graph valid.
Concrete example: from support ticket to graph
Here’s how it all fits together. We start with two pieces of raw text:
- User reports: “My laptop is overheating after the last update.”
- The technician adds: “Resolved with Patch 1.2.”
Step 1 — Extracting entities:
| Entity | Type |
|---|---|
| Customer_4759 | Customer |
| Laptop | Product |
| Overheating Issue | Issue |
| Latest Update | Cause |
| Patch 1.2 | Resolution |
Step 2 — Identifying relationships:
| Subject | Relationship | Object |
|---|---|---|
| Customer_4759 | POSTPONMENTS | Overheating Issue |
| Overheating Issue | AFFECTS | Laptop |
| Overheating Issue | CAUSED_BY | Latest Update |
| Overheating Issue | RESOLVED_BY | Patch 1.2 |
Step 3 — Graph representation:
Customer_4759 ──REPORTS──▶ Overheating Issue ──AFFECTS──▶ Laptop
│
CAUSED_BY │ RESOLVED_BY
│
Latest Update Patch 1.2
Result: This simple diagram shows how a single support ticket transforms into a connected set of entities and relationships. The ontology and the schema guide and validate this structure:
- The ontology enforces that only a
CustomercanREPORTSanIssue - The schema ensures that each
Customernode contains acustomer_id
Key point: Knowledge graphs give agents a way to transform raw, unstructured text into structured, connected meaning. Ontologies define which connections are allowed, and schemas enforce the required structure and fields. Together, they provide agents with a reliable framework for reasoning and making consistent decisions.
1.4 Comparison: Graph, Vector and Document Retrieval
So far, we have seen how knowledge graphs structure information. Now, let’s compare this approach with the two ways Orion already retrieves information: document search, vector search, and graph reasoning.
Current Orion retrieval methods
Orion already uses two retrieval methods:
- Keyword Document Search — quickly retrieves documents using exact keywords
- Semantic Vector Search — finds semantically similar issues, even when customers describe things differently (overheating, high temperature, thermal issue)
But Orion still misses the big picture. He can retrieve information, but he can’t explain why.
Detailed comparison
Document Retrieval
This is Orion’s most literal memory. If a support ticket contains the word “overheating”, Orion can instantly pull all old tickets with that same word. It’s fast and accurate, making it great for quick searches. But it’s also very limited: if the wording changes, or if the meaning is implicit rather than explicit, the literature search simply can’t see it.
Vector Retrieval
To go beyond exact words, Orion also uses vector search. Here, tickets are converted to embeddings, and Orion searches for semantic similarity rather than keywords. This means it can connect phrases like “device gets hot” and “thermal problem” even when the word “overheating” never appears. This gives Orion a much better sense of significance.
But something crucial is still missing: Orion knows that two issues are related, but it doesn’t know why they are related or why they are connected in a larger context.
Knowledge Graph Reasoning
This is where knowledge graphs fundamentally change what our agent can do. Instead of just retrieving documents, Orion can now track relationships between entities. He can see that:
- A product is linked to an overheating problem
- The problem was caused by the latest update
- It was resolved by Patch 1.2
For the first time, Orion does more than match text or meaning — it sees structure. This structure gives it something that retrieval alone can never provide: a clear reasoning path. Because cause and effect are explicit, Orion becomes more consistent between similar cases.
Comparison table
| Method | What it brings | Limitation |
|---|---|---|
| Document Retrieval | Literal, quick and precise reminder | Limited to exact words, no understanding of meaning |
| Vector Retrieval | Semantic understanding | Does not prove connections, no causation |
| Knowledge Graph | Reasoning, traceability, causality, explainability | Requires construction and maintenance |
Full progress
Document Retrieval → rappel littéral
Vector Retrieval → compréhension sémantique
Knowledge Graph → raisonnement + traçabilité de la cause vers l'effet
By combining the three — literature search for precision, vector search for recall, and knowledge graphs for reasoning and traceability — Orion becomes a fundamentally different type of agent.
Key Point: Recovery alone is only the beginning. Knowledge graphs unlock the layer of reasoning that makes an agent transparent, trustworthy and explainable.
1.5 Where Knowledge Graphs make agents more reliable
Let’s now focus on why this structure is so important in practice, especially when agents are used in environments where decisions need to be justified, not just generated.
The black box problem
Imagine this scenario: A key stakeholder in the company — perhaps from compliance or operations — asks: “Can you explain why the model made this decision?” And suddenly the room gets quieter, not because the model is wrong, but because the system can’t clearly explain itself.
In many machine learning systems today, predictions exist, but the reasoning behind them is either opaque or completely inaccessible. This lack becomes particularly risky in sensitive areas:
- Medical diagnostics: models can’t just produce a result — doctors need to understand the reasoning behind
- Financial compliance: every marked transaction must be traceable
Retrieval systems can surface related information, but they cannot explain why something was marked or how different factors led to that decision.
In high-stakes, regulated environments, black-box outputs are not enough. What is required is a visible chain of reasoning — and this is exactly where knowledge graphs become a requirement, not just an enhancement.
Managing massive unstructured information
In many agent systems, they deal with massive amounts of unstructured information: research articles, files, logs, incident reports or free-form notes. Without a structured layer to connect these elements, the agent sees each item in isolation and cannot fully understand how they relate.
Three Ways Knowledge Graphs Change Agents
-
Identification of recurring patterns — they help agents identify recurring patterns across a large collection of information
-
Evidence-based actions — knowledge graphs allow agents to suggest reliable actions based on evidence (not just retrieving similar examples), following a reasoning path
-
Decision explainability — when a human reviewer asks “Why did you choose that?”, the agent can show the connected entities and relationships behind its reasoning
This level of transparency is particularly crucial in high-stakes areas: health, finance, security engineering and scientific research.
Important conclusion: Knowledge graphs are not just an improvement. In many areas, they are a requirement. Whenever an agent needs to reason clearly, justify decisions, or pass regulatory measures, knowledge graphs provide the structure that makes this possible.
1.6 Summary and Reflection
Before we go any further, it’s a good time to pause and reflect.
What we covered
We started by defining knowledge graphs as structured networks of entities and relationships. Next, we explored ontologies and schemas, the semantic layer that enforces coherence and meaning. Together, these elements transform isolated information into connected and explainable knowledge.
We also compared knowledge graphs to traditional retrieval approaches:
- Document retrieval finds exact words — fast, but literal, with no understanding of meaning or structure
- Vector recovery finds similar meaning — but not relationships or causality
- Only knowledge graphs expose causality — showing how and why information is connected
The red thread example summarized
Starting with a simple support ticket — “My laptop is overheating after the last update” — we extracted the basic entities: the customer, the product, the problem, the cause and the resolution. The agent then links these entities:
- The client reports the problem
- The problem is caused by the latest update
- It affects the product
- It is resolved by Patch 1.2
When these entities and relationships are placed in a graph, we no longer have isolated sentences — we have a connected view of the problem.
Below, the ontology and schema ensure consistency:
- The ontology defines which relationship directions are allowed
- The schema ensures that each node contains the required information
This is what transforms plain text into something an agent can actually reason about.
Key point of this module: Knowledge graphs provide the reasoning structure that agents need. With this mental model in place, you are ready to start building agentic knowledge graphs that are reliable, transparent and explainable.
2. Building and Integrating Agent driven Knowledge Graphs
2.1 From foundations to function: how agents build Knowledge Graphs
In this module, we move from fundamental ideas to the practical process showing how agents can actually build their own knowledge graphs from the data they receive.
So far, we understand what a knowledge graph is: entities, relationships and structured connections. But Orion, our support agent, still doesn’t know how to build one. The next challenge is to take unstructured support tickets and turn them into structured knowledge. This means:
- Extract important facts from plain text
- Organize them coherently
- Represent them within a knowledge graph
This is where we move from concept to construction.
The simple agentic pipeline
To help Orion reason efficiently, we design a simple agentic pipeline:
Texte brut Knowledge Graph
"My laptop overheats → [Agent LLM] → Nœuds + Arêtes
after the latest update" Extraction + Structure
Entités/Rels → Raisonnement
It starts with plain text and from there the agent extracts entities and relationships. Once these facts are structured, they are mapped into a knowledge graph. This graph gives the agent nodes, edges, and structure — something it can actually reason about.
2.2 Demo: Extracting entities and relationships from text
This demo shows how to programmatically transform raw text into a knowledge graph. It uses Google Colab (Jupyter Notebook in the cloud) with some lightweight libraries.
Required Libraries
# Installation des dépendances
# pip install openai networkx matplotlib
import openai
import ast
import networkx as nx
import matplotlib.pyplot as plt
openai— to interact with the language modelast— to securely parse model outputnetworkx— to build the graph structurematplotlib— to visualize the graph These packages allow you to go from plain text to a functional mini knowledge graph — just like an agent would do.
Example support ticket
# Ticket de support exemple — type d'input réel que l'agent reçoit
ticket = """
My laptop overheats after the latest update.
Support note: Fixed using Patch 1.2.
"""
Prompt aligned with ontology and schema
# Prompt structuré — construit directement sur l'ontologie et le schema
prompt = f"""
Extract entities and relationships from the following support ticket.
Allowed entity types: Customer, Product, Issue, Cause, Resolution
Allowed relationship labels: REPORTS, AFFECTS, CAUSED_BY, RESOLVED_BY
Return ONLY a Python list of triplets in this exact format:
[("subject", "RELATION", "object"), ...]
Text: {ticket}
"""
This prompt is built directly on the ontology and schema that we designed, so that the agent knows exactly how to interpret the text. We define the allowed entity types, relationship labels, and the Python format of precise triples that the model should return. With this schema-aligned prompt, the agent can reliably transform raw text into structured triples that match our knowledge graph design.
OpenAI model call
client = openai.OpenAI(api_key="YOUR_API_KEY")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0
)
raw_output = response.choices[0].message.content
print("Réponse du modèle:")
print(raw_output)
This is where unstructured text is transformed into structured meaning.
Parsing triples
# Transformer la sortie texte du modèle en objets Python réels
# ast.literal_eval parse de façon sécurisée sans exécuter de code arbitraire
triplets = ast.literal_eval(raw_output)
print("Triplets parsés:", triplets)
# Exemple de sortie attendue :
# [
# ("Customer_4759", "REPORTS", "Overheating Issue"),
# ("Overheating Issue", "AFFECTS", "Laptop"),
# ("Overheating Issue", "CAUSED_BY", "Latest Update"),
# ("Overheating Issue", "RESOLVED_BY", "Patch 1.2")
# ]
This line confirms that we now have a clean Python list of triples ready to use.
Building the Knowledge Graph with NetworkX
# Créer un graphe dirigé — les relations ont une directionalité claire
G = nx.DiGraph()
# Ajouter nœuds et arêtes pour chaque triplet
for subject, relation, obj in triplets:
G.add_node(subject)
G.add_node(obj)
G.add_edge(subject, obj, label=relation)
print(f"Nœuds: {list(G.nodes())}")
print(f"Arêtes: {list(G.edges(data=True))}")
NetworkX automatically handles duplicates — each unique entity appears only once, and labeled edges capture the meaning of the relationships. At the end of this step, we transformed the extracted triples into a fully structured graph that the agent can reason about.
Visualization of the graph
def visualize_graph(G, title="Knowledge Graph"):
plt.figure(figsize=(12, 8))
pos = nx.spring_layout(G, k=2, seed=42)
# Dessiner les nœuds
nx.draw_networkx_nodes(G, pos, node_size=3000,
node_color='lightblue', alpha=0.9)
# Labels des nœuds
nx.draw_networkx_labels(G, pos, font_size=10, font_weight='bold')
# Arêtes
nx.draw_networkx_edges(G, pos, edge_color='gray',
arrows=True, arrowsize=20,
connectionstyle='arc3,rad=0.1')
# Labels des relations
edge_labels = nx.get_edge_attributes(G, 'label')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels,
font_size=9, font_color='darkred')
plt.title(title)
plt.axis('off')
plt.tight_layout()
plt.show()
visualize_graph(G, "Knowledge Graph — Ticket de Support")
The visualized graph clearly shows the entities defined in our schema—the client, the overheating problem, the laptop, the last update, and the resolution—all linked by the relationships extracted by the model. Because the structure follows the pattern we designed, the connections are easy to interpret.
Demo Summary: We have seen how plain text can be transformed into structured knowledge. We extracted entities and relationships, transformed them into a directed graph, and visualized the connections — the same process that agents use to build and extend their knowledge graphs in real-world scenarios.
2.3 Map natural language to graph representations
Now we take the next step: mapping these facts into a graph structure that remains consistent as it grows. This is where the schemas and labels of controlled relationships cease to be optional and become the difference between a graph that reasons and a graph that drifts.
The importance of schema
A schema gives Orion a stable form of knowledge. Instead of a random set of nodes, we align each extracted fact to a predictable pattern: Customer → Issue → Product, connected by consistent relationship labels.
Structure vs. Attributes
triplets (nodes and edges) capture structure. But the attributes carry the details. When we add severity, dates, versions and ticket IDs, the agent can filter, compare and reason with constraints, not just connections.
# Exemple de nœud enrichi avec attributs
node_issue = {
"id": "issue_overheating_001",
"type": "Issue",
"label": "Overheating Issue",
"severity": "high",
"created_date": "2024-01-15",
"ticket_id": "TKT-4759",
"product_version": "v1.2"
}
# L'agent peut maintenant filtrer : "tous les problèmes HIGH severity depuis janvier 2024"
Ontology as grammar
Ontology is the grammar that prevents nonsense graphs. It restricts what can connect to what and in what direction. If everything can connect to everything, the agent cannot trust the structure. Ontology rules keep the extraction grounded in the domain.
# Règles d'ontologie codifiées
ONTOLOGY_RULES = {
"REPORTS": {"subjects": ["Customer"], "objects": ["Issue"]},
"AFFECTS": {"subjects": ["Issue"], "objects": ["Product"]},
"CAUSED_BY": {"subjects": ["Issue"], "objects": ["Cause"]},
"RESOLVED_BY": {"subjects": ["Issue"], "objects": ["Resolution"]},
}
def validate_triplet(subj_type: str, relation: str, obj_type: str) -> bool:
"""Valide qu'un triplet est conforme à l'ontologie."""
if relation not in ONTOLOGY_RULES:
return False
rules = ONTOLOGY_RULES[relation]
return (subj_type in rules["subjects"] and
obj_type in rules["objects"])
# Exemple :
# validate_triplet("Customer", "REPORTS", "Issue") → True
# validate_triplet("Product", "REPORTS", "Issue") → False (produit ne peut pas signaler)
2.4 Incremental graph construction pipelines
In production, the Orion agent will not build a graph once. It will build it continuously, every time a new ticket arrives, without rebuilding from scratch. So we need an updating strategy that can add new knowledge, link it correctly and avoid duplication.
The problem with a naive approach
In real support data, new tickets don’t just add information — they add variation. If we naively add everything, we create duplicates and the graph becomes noisy quickly.
When evidence is divided between multiple nodes, agents cannot see the complete pattern. Queries return partial results, and even if the correct solution exists somewhere in the graph, it is more difficult to recover and defend.
Simple objective: Make the graph grow without drifting, reusing the same entities and adding only what is truly new.
The incremental update pattern
Pour chaque nouveau ticket :
1. Extraire entités et relations
2. Résoudre les doublons (normaliser les IDs → réutiliser les mêmes entités)
3. Upsert des nœuds (réutiliser si existant, créer si nouveau)
4. Connecter les relations
5. Attacher les attributs (temps, sévérité, version)
Basic pattern: Merge first, then connect.
def incremental_graph_update(G: nx.DiGraph,
new_triplets: list,
canon_map: dict = None) -> nx.DiGraph:
"""
Mise à jour incrémentale du graphe sans doublons.
Args:
G: graphe NetworkX existant
new_triplets: liste de nouveaux triplets [(subj, rel, obj), ...]
canon_map: dictionnaire de canonicalisation des entités
Returns:
G mis à jour
"""
for subj, rel, obj in new_triplets:
# Étape 1 : Normaliser les IDs d'entités
if canon_map:
subj = canon_map.get(subj.lower(), subj)
obj = canon_map.get(obj.lower(), obj)
# Étape 2 : Upsert des nœuds (réutiliser si existant)
if subj not in G.nodes:
G.add_node(subj)
if obj not in G.nodes:
G.add_node(obj)
# Étape 3 : Connecter les relations
G.add_edge(subj, obj, label=rel)
return G
Each time a new ticket arrives, Orion first normalizes the entity IDs — v1.2 and update 1.2 resolve to the same key. Next, we upsert the nodes. If the product or problem already exists, we reuse it. Otherwise, we create it. Only after that we connect the relationships.
The loop is key. Run the pipeline again and you don’t get duplicates — you get a stable graph that continues to accumulate evidence.
2.5 Demo: Update graph without duplicates
In this demo, we add a small batch of new text to the graph and focus on a practical problem: avoiding duplicate nodes. The key idea is simple: reuse the same entities so that evidence accumulates instead of fragmenting.
Phase 1: Build a base graph and naively update it to see the duplicates problem.
Phase 2: Introduce canonicalization, update again and confirm that NetworkX reuses nodes when IDs match.
Ticket data with realistic variations
# Tickets avec variations — exactement comme dans des données de support réelles
raw_tickets = [
"Customer_001 reports battery dying fast on Laptop Model X. Fixed by v1.2.",
"Customer_002 reports battery drain on Model X. Resolved by update 1.2.",
"Customer_003 reports fast discharge on Laptop Model X. Not yet resolved."
]
Notice the variations:
- Same version appears as
v1.2andupdate 1.2 - The problem is described as
battery dying fast,battery drainandfast discharge - The product is listed as
Laptop Model XandModel X
This is realistic support data, and this is exactly where the duplicates are coming from.
Triple Extraction Helper
def extract_triplets_from_ticket(ticket_text: str) -> list:
"""Extrait des triplets d'un ticket de support via LLM."""
prompt = f"""
Extract entities and relationships from this support ticket.
Entity types: Customer, Issue, Product, Resolution
Relationships: REPORTS, AFFECTS, RESOLVED_BY
Return ONLY a Python list of (subject, relation, object) triplets.
Text: {ticket_text}
"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0
)
return ast.literal_eval(response.choices[0].message.content)
Even with a schema, the extracted feature strings can still vary — and these strings become node identifiers if we don’t normalize them.
Naive update — the visible problem
def build_naive_graph(all_triplets_lists: list) -> nx.DiGraph:
"""Construction naïve : produit des doublons car identité = string brute."""
G = nx.DiGraph()
for triplets in all_triplets_lists:
for subj, rel, obj in triplets:
G.add_node(subj)
G.add_node(obj)
G.add_edge(subj, obj, label=rel)
return G
# Extraire triplets de chaque ticket
all_triplets = [extract_triplets_from_ticket(t) for t in raw_tickets]
# Construire graphe naïf
naive_G = build_naive_graph(all_triplets)
print(f"Naïf — Nœuds: {naive_G.number_of_nodes()}, Arêtes: {naive_G.number_of_edges()}")
# → Plusieurs nœuds pour le même concept réel !
In the naive graph, similar nodes appear for the same real concept. Once that happens, the evidence splits — different tickets attach to different duplicates, and the graph becomes noisy very quickly.
Canonicalization — the solution
# Dictionnaire de synonymes pour la canonicalisation
# En production : peut être amélioré avec similarité d'embeddings
# ou LLM pour proposer des labels canoniques pour de nouveaux variants
CANON_MAP = {
"v1.2": "Patch 1.2",
"update 1.2": "Patch 1.2",
"patch 1.2": "Patch 1.2",
"laptop model x": "Laptop Model X",
"model x": "Laptop Model X",
"battery dying fast": "Battery Drain",
"battery drain": "Battery Drain",
"fast discharge": "Battery Drain",
}
def canonicalize(entity: str) -> str:
"""Normalise une entité vers sa forme canonique."""
return CANON_MAP.get(entity.lower().strip(), entity)
def build_canonical_graph(all_triplets_lists: list) -> nx.DiGraph:
"""Construction avec canonicalisation — les doublons s'effondrent."""
G = nx.DiGraph()
for triplets in all_triplets_lists:
for subj, rel, obj in triplets:
# Canonicaliser AVANT d'ajouter au graphe
canon_subj = canonicalize(subj)
canon_obj = canonicalize(obj)
G.add_node(canon_subj)
G.add_node(canon_obj)
G.add_edge(canon_subj, canon_obj, label=rel)
return G
# Construire graphe canonique
canonical_G = build_canonical_graph(all_triplets)
print(f"Canonique — Nœuds: {canonical_G.number_of_nodes()}, Arêtes: {canonical_G.number_of_edges()}")
# → Moins de nœuds, mais connectivité plus forte !
Enhancement Check
def compare_graphs(naive_G: nx.DiGraph, canonical_G: nx.DiGraph):
"""Compare les deux graphes pour confirmer l'amélioration."""
print("=== Graphe naïf ===")
print(f" Nœuds ({naive_G.number_of_nodes()}): {sorted(naive_G.nodes())}")
print("\n=== Graphe canonique ===")
print(f" Nœuds ({canonical_G.number_of_nodes()}): {sorted(canonical_G.nodes())}")
reduction = naive_G.number_of_nodes() - canonical_G.number_of_nodes()
print(f"\n Réduction: {reduction} doublons éliminés")
compare_graphs(naive_G, canonical_G)
Fundamental lesson: Safe growth of a graph is essentially a problem of naming. If entities resolve to consistent identifiers, node reuse occurs naturally, and the evidence becomes stronger over time. Updates should always strengthen the evidence — let the graph reuse nodes automatically.
2.6 Choose graph storage and query options
In production, when the graph starts to grow, we need a storage layer that can scale, support fast queries, and maintain consistency. The choice of storage stops being an implementation detail and becomes a design decision.
Production requirements
As Orion grows beyond the prototype, it must:
- Scale to thousands of tickets without degrading performance
- Manage production concerns: concurrent access, indexing and sustained load
- Persist securely over time with predictable behavior
This is why tools like NetworkX are great for learning and experimentation, but not sufficient for production systems.
The two graph storage models
Labeled Property Graph (LPG)
Designed for connected reasoning. Represents entities as nodes with properties, relationships as edges (also with properties). This is a strong choice when your environment requires operational proof paths and intensive query traversal.
Resource Description Framework (RDF) / Triple Store
Represents knowledge as subject-predicate-object statements. Closely aligns with ontologies and formal semantics. This is a strong choice when your domain is standards-driven and you want strict semantic consistency and interoperability between systems.
The three building blocks of a graph query
Graph queries are not about extracting rows from a table. They concern the recovery of a pattern — connected entities and relationships between them. The output is often a subgraph, not a list.
All graph queries can be broken down into three blocks:
- Anchoring — anchoring on what you already know
- Traversal — traverse relationships to extend across the graph
- Constraints — filters, limits and ordering to return only the most relevant structure
Common systems and their use
| Category | Tools | Query language | Typical usage |
|---|---|---|---|
| PropertyGraph DB | Neo4j, TigerGraph | Cypher | Conversational agents, recommendation, fraud |
| RDF Triplestore | GraphDB, Blazegraph | SPARQL | Medical data, semantic web, multi-sources |
| Hybrid | Graph Store + Vector Index | Mixed | Combination structure + semantic recall |
Selection Guide
- Choose a property graph when you are building traversal-intensive applications and the schema must remain flexible
- Choose a triplestore RDF when your priority is semantics aligned with ontology and interoperability
- Add a vector layer when semantic recall becomes a bottleneck (like Orion tickets, where the same problem is described in many different ways)
Syntax comparison: Cypher vs SPARQL
The same intention expressed in both systems:
-- Cypher (Neo4j) : trouver les clients ayant signalé "Battery Drain"
MATCH (c:Customer)-[:REPORTS]->(i:Issue)
WHERE i.title = "Battery Drain"
RETURN c.name AS customer, i.title AS issue
# SPARQL (RDF Triplestore) : même intention
SELECT ?customer WHERE {
?customer rdf:type :Customer .
?issue rdf:type :Issue .
?issue rdfs:label "Battery Drain" .
?customer :reports ?issue .
}
The objective is identical in both cases: recover the connected structure which corresponds to a constraint. The syntax differs because the underlying storage model is different — property graphs express labeled nodes and relationships, while RDF expresses subject-predicate-object triples.
Design decision: If you care most about fast traversal over nodes and relationships with a flexible schema, an LPG is generally the pragmatic choice. If you care most about strict meaning, interoperability, and semantic reasoning, RDF is designed for that.
2.7 Integrate graphs with LLM Agents
Now let’s focus on integration: how Orion connects an LLM to a graph so that answers are rooted in evidence, not generated by guessing.
What each component provides
| Component | Strength | Limitation |
|---|---|---|
| LLM | Transform information into clear explanation | Not a reliable source of truth on its own |
| Knowledge Graph | Store explicit entities and relationships | Does not generate natural language explanations |
| Combination | Clarity + proof, traceable answers | Requires careful design |
When we integrate the two, Orion can answer with both clarity and proof, because the answer is anchored in a traceable path through the graph.
The graph as a tool — a strict contract
When we say “graph as tool”, we mean a strict contract:
# Définition de l'outil de graphe pour l'agent
def graph_query_tool(
intent: str,
key_entities: list[str],
constraints: dict = None
) -> dict:
"""
CONTRAT : L'agent DOIT appeler cet outil avant de répondre.
Retourne:
evidence_subgraph: liste de connexions prouvant la réponse
reasoning_path: chemin traçable sous forme de string
confidence: score de confiance basé sur la connectivité
"""
cypher = build_cypher_query(intent, key_entities, constraints or {})
results = neo4j_driver.session().run(cypher)
return {
"evidence_subgraph": [(r["from"], r["rel"], r["to"]) for r in results],
"reasoning_path": format_path(results),
"confidence": compute_confidence(results)
}
- Orion does not write response first
- It starts by calling a graph query tool with an intent and key entities
- The graph returns an
evidence_subgraphand areasoning_path - The LLM explains using only what came back
The agent response loop
def orion_answer_loop(user_question: str) -> dict:
"""
Boucle de réponse d'Orion :
Le LLM ne se souvient pas de la vérité — il la récupère depuis le graphe,
puis répond à partir de preuves.
"""
# 1. LLM identifie l'intention et les entités clés
parse_result = llm_extract_intent(user_question)
# 2. Requête OBLIGATOIRE au graphe (avant de générer une réponse)
evidence = graph_query_tool(
intent=parse_result["intent"],
key_entities=parse_result["entities"],
constraints=parse_result["constraints"]
)
# 3. LLM génère une réponse à partir des preuves UNIQUEMENT
# (ne peut pas inventer d'information non présente dans evidence)
answer = llm_generate_answer(
question=user_question,
evidence=evidence["evidence_subgraph"],
reasoning_path=evidence["reasoning_path"]
)
return {
"answer": answer,
"evidence": evidence["evidence_subgraph"],
"reasoning_path": evidence["reasoning_path"],
"is_grounded": True
}
Concrete example — causal question
“Is Update v1.2 causing overheating on Laptop Model X?”
Orion queries the graph for important connections. The return path links:
Update v1.2 ──CAUSED_BY──▶ Overheating Issue ──AFFECTS──▶ Laptop Model X
│
RESOLVED_BY
│
Patch 1.2
Now the LLM doesn’t have to guess. He translates this path into a recommendation and cites the path as justification.
Transformed role of the LLM: It is no longer the source of truth — it is the interface. The graph provides the evidence, and the LLM transforms that evidence into a clear, auditable answer.
2.8 Hybrid Retrieval: Vector and Graph
In real systems, you rarely choose between vectors and graphs. You orchestrate them. Vectors are excellent for recall, finding relevant things even when the wording changes. Graphs are great for reasoning, showing how and why things connect.
The fundamental tension
| Vector Retrieval | Graph Retrieval | |
|---|---|---|
| Strength | Ambiguous and variable language, high recall | Explicit links, multi-hop reasoning, explainability |
| Weakness | Does not prove connections, cannot justify | May miss context if incomplete graph |
| Trade-off | Relevance ≠ Correction | Coverage limited by the completeness of the graph |
This is the voltage. Vectors give the recall, graphs give the proof, and hybrid retrieval is the way to get the best of both worlds.
The two patterns of Hybrid Retrieval
Pattern 1: Routing
The agent looks at the question and chooses the best recovery tool for the job.
from enum import Enum
class RetrievalStrategy(Enum):
VECTOR = "vector"
GRAPH = "graph"
def classify_question(question: str) -> RetrievalStrategy:
"""Détermine la meilleure stratégie selon la nature de la question."""
# Questions floues/ambiguës → vecteurs pour maximiser le rappel
fuzzy_keywords = ["similar", "like", "related", "about", "mention"]
proof_keywords = ["why", "caused", "resolve", "explain", "trace", "how"]
question_lower = question.lower()
if any(kw in question_lower for kw in proof_keywords):
return RetrievalStrategy.GRAPH
elif any(kw in question_lower for kw in fuzzy_keywords):
return RetrievalStrategy.VECTOR
else:
return RetrievalStrategy.GRAPH # Défaut : graphe pour l'explicabilité
def hybrid_routing(question: str) -> dict:
"""Pattern de routage : un seul outil selon la classification."""
strategy = classify_question(question)
if strategy == RetrievalStrategy.VECTOR:
results = vector_search(question)
return {"source": "vector", "results": results}
else:
results = graph_query(question)
return {"source": "graph", "results": results,
"reasoning_path": results.get("path")}
Pattern 2: Fusion
Instead of choosing, the agent uses both and combines the results.
def hybrid_fusion(question: str) -> dict:
"""
Pattern de fusion : vecteurs pour la couverture + graphe pour la preuve.
Idéal quand le graphe est encore en croissance ou incomplet.
"""
# Étape 1 : Vecteurs pour une large couverture sémantique
vector_candidates = vector_search(question, top_k=20)
# Étape 2 : Extraire les entités des candidats vectoriels
candidate_entities = extract_entities_from_results(vector_candidates)
# Étape 3 : Graphe pour vérifier les connexions et fournir la structure
graph_evidence = graph_query(
entities=candidate_entities,
question=question
)
# Étape 4 : Combiner — rappel des vecteurs + preuve du graphe
return {
"vector_candidates": vector_candidates,
"graph_evidence": graph_evidence["subgraph"],
"reasoning_path": graph_evidence["path"],
"source": "hybrid_fusion",
"confidence": max(
graph_evidence.get("confidence", 0),
len(vector_candidates) / 20 # rappel normalisé
)
}
Decision rule
ROUTER quand :
• Un outil est clairement suffisant
• La vitesse compte
• Le graphe est fiable ET les tickets sont majoritairement répétables
FUSER quand :
• Vous avez besoin à la fois de couverture ET de preuve
• Le graphe est encore en croissance ou incomplet
• Une proportion significative des tickets est floue/langage-heavy
• Le coût de manquer un cas est élevé
Policy for Orion specifically
To choose the right approach for Orion:
- Audit the data — assess the completeness of the graph, measure whether tickets are repeatable patterns or highly variable descriptions
- Align with operations — define what “bad recommendation” means in this environment, and the cost of missing related cases
- Choose policy based on nature of data AND business needs
Key point: There is no theoretical preference. It’s a data-driven product and operations decision. In some advanced applications, AI engineers orchestrate both: routing under normal conditions and switching to fusion when the data and risk warrant it.
2.9 Summary and Reflection
In this module we moved from concept to construction.
The three phases covered
1. Construction
The build process starts with raw texts like natural language support tickets. The agent extracts entities and relationships from this text. These facts are not stored arbitrarily — they are mapped into a schema-aligned graph structure. The ontology constrains what can connect, and the schema enforces consistency as the graph grows. This step transforms the language into a structure that the agent can trust.
2. Incremental growth
Graphs are not built once — they grow continuously. Incremental building ensures that new data reinforces the existing structure instead of fragmenting it. When evidence accumulates across shared entities, the agent can reason, follow causal paths, and identify patterns between cases. Self-growth is what makes reasoning possible.
3. Storage, Query, Integration
Once the graph exists, it becomes usable through storage and queries. In practice, you choose an LPG or RDF backend and query for a structure, not rows. The output is a subgraph that corresponds to a pattern. Then the graph becomes a tool call — queried first, which retrieves a path of evidence, and which responds only from that evidence.
The blueprint of this module
| Step | What we did |
|---|---|
| Extract | Transform language into triples via LLM + prompt ontology |
| Canonicalization | Normalize entities to avoid duplicates |
| Mapping | Align each fact with schema + ontology |
| Storage | Choose LPG (Neo4j) or RDF depending on the use case |
| Query | Retrieve subgraphs (not lines) |
| Integration | Graph = mandatory tool consulted BEFORE responding |
| Hybrid Retrieval | Vectors for recall, graph for proof |
Key point of this module: You now have a clear blueprint for building agentic knowledge graphs. You’ve seen how plain text becomes structured graph updates, how graphs grow incrementally without fragmenting evidence, and how the graph is used as a proof tool that supports transparent reasoning. This module moves agents toward an executable workflow.
3. Frameworks, Tools, and Best Practices for Agentic Knowledge Graphs
3.1 Production Challenges for Agentic Knowledge Graphs
This module focuses on moving from a working prototype to a production-ready knowledge graph pipeline. The objective is to understand what changes when an agentic knowledge graph goes from a small example to a real system used by several teams.
The current state of Orion
Orion now has a basic knowledge graph. It can extract key facts from support tickets and connect customers, products, issues and resolutions. In other words, Orion can build a small knowledge graph from text and even use it to answer simple questions. This was a big step — it gave Orion some capacity for structured reasoning.
The new challenge is to move from a small prototype graph to a complete production system.
The new challenges of production
Production systems introduce constraints that prototypes can ignore:
| Challenge | Description | Consequence if ignored |
|---|---|---|
| Scale | Thousands of tickets, multiple products and periods | Performance degradation |
| Performance | Fast responses for many users simultaneously | Degraded user experience |
| Governance | Schema consistency, access control | Corrupted data, GDPR violations |
| Observability | Logs, metrics, drift detection | Undetected silent errors |
Each becomes a failure mode if not designed in from the start.
Production pipeline architecture
┌─────────────────────────────────────────┐
│ Pipeline de Production Orion │
└─────────────────────────────────────────┘
Tickets Ingestion Extraction Graph Store Query Layer
entrants ──────▶ (streaming) ──▶ (LLM) ──▶ (Neo4j/RDF) ──▶ (Cypher)
│
Evidence +
Reasoning Path
┌──────────────── COUCHE DE GOUVERNANCE ────────────────┐
│ Schema Validation │ Versioning │ Metrics │ Drift Det. │
└───────────────────────────────────────────────────────┘
The governance layer is critical:
- Schema validation — ensure the graph remains consistent
- Versioning — evolve the structure safely
- Metrics, logging and drift detection — detect silent errors before they cause failures
Key Point: Preparing for production isn’t just about handling more data — it’s about keeping the structure consistent, queries fast, and graph quality measurable over time.
3.2 Overview of frameworks and tools
Most agentic knowledge graph stacks fall into four families, each with a different philosophy.
The four families of tools
1. Property Graphs — Neo4j
Represent knowledge as nodes with properties and relationships. Use the Cypher query language for expressive traversals. Optimized for practical applications like recommender systems, chatbots, and structured retrieval. Developer-friendly and great for rapid iterations.
2. RDF Graph Databases / Triplestores
Store subject-predicate-object triples. Focus on semantics and interoperability using SPARQL to query linked data. Excellent when data comes from different sources using shared schemas, such as linking company knowledge with the public semantic web.
3. DBType
Enforces a strong schema and allows logical inference through rules. Well suited for use cases involving complex hierarchies, deep relationships and structured reasoning. Can automatically infer new facts based on strict schema and rules.
4. Custom Pipelines
Trade completeness for flexibility. Can use Python tools like NetworkX and in-memory structures. Useful for rapid prototyping or niche systems, but governance and consistency become entirely the responsibility of the team.
Correspondence of tools ↔ use cases
| Framework | Typical use cases |
|---|---|
| Neo4j | Conversational AI, recommendation systems, customer graphs, fraud detection |
| RDF Triplestores | Medical/genetic data, multi-source knowledge bases, semantic web |
| DBType | Biomedical research, complex fraud detection, legal reasoning |
| Custom Pipeline | Prototyping, niche systems, cases where flexibility > robustness |
Integration paths per system
# Neo4j : driver direct + intégration LangChain
from neo4j import GraphDatabase
from langchain_community.graphs import Neo4jGraph
graph = Neo4jGraph(url="bolt://localhost:7687",
username="neo4j", password="password")
# LangChain peut générer automatiquement des requêtes Cypher
# ──────────────────────────────────
# RDF : via RDFLib ou endpoints SPARQL
from rdflib import Graph, URIRef, Literal
g = Graph()
g.parse("knowledge_graph.ttl", format="turtle")
results = g.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o . }")
# ──────────────────────────────────
# TypeDB : client API avec TypeQL
from typedb.client import TypeDB, SessionType, TransactionType
with TypeDB.core_client("localhost:1729") as client:
with client.session("support_kg", SessionType.DATA) as session:
with session.transaction(TransactionType.READ) as tx:
results = tx.query().match("match $x isa Issue; get;")
Key point: The choice of tool is also a choice of reasoning. The best framework is one that matches how your agent should query, traverse, and explain.
3.3 Comparison of frameworks for agentic reasoning
Different graph frameworks offer different strengths, but not all support reasoning in the same way.
Four comparison criteria
- Integration — does the framework connect cleanly to the agent stack?
- Scalability — can it grow with data, traffic and updates?
- Query performance — does it retrieve answers efficiently, especially for multi-hop paths?
- Suitability for reasoning — does its structure align with how the agent should explain?
Complete comparison table
| Criterion | Neo4j | RDF/SPARQL | DBType | Custom Pipeline |
|---|---|---|---|---|
| Integration | Fort — LangChain adapters, Python driver | Via SPARQL libraries or endpoints | Clean API Client + TypeQL | Free design by the team |
| Scalability | Moderate to large graph sizes | Multi-source linked data | Large rule-driven graphs | If designed carefully |
| Performance | Fast for traversing patterns | Semantic richness at the expense of speed | Adds reasoning to queries | Fast if disciplined structure |
| Reasoning | Flexible but informal | Aligned with ontologies | Logical force + inference | Full control, correction on charge |
What Orion Needs
In our system, Orion needs:
- Move quickly through complex relationships (texts, products, issues)
- Return responses that include path taken
- Working with a schema that will evolve over time
- Generate queries via language, not fixed code
This makes path transparency, integration simplicity, and schema flexibility core requirements.
Key point: The best framework is not the most powerful — it is the one that makes reasoning easiest to recover and justify. This is what supports well-founded responses at scale.
3.4 Demo: Building a Simple Knowledge Graph Pipeline with Neo4j
In this demo, we build a complete mini pipeline that transforms Orion support ticket data into a structured agentic knowledge graph using Neo4j.
Installation and connection
# pip install neo4j
from neo4j import GraphDatabase
# Connexion à Neo4j (instance locale)
URI = "bolt://localhost:7687"
AUTH = ("neo4j", "your_password")
driver = GraphDatabase.driver(URI, auth=AUTH)
# Test de connexion
with driver.session() as session:
result = session.run("RETURN 'Connection OK' AS message")
print(result.single()["message"]) # → Connection OK
Definition of normalized triples
# Triplets normalisés et alignés sur le schema
# Chacun encode une relation sujet-prédicat-objet
triplets = [
("Customer_2345", "REPORTS", "Battery Drain"),
("Battery Drain", "AFFECTS", "Laptop Model X"),
("Battery Drain", "RESOLVED_BY","Patch 1.21"),
("Customer_3678", "REPORTS", "Battery Drain"), # même problème
("Customer_4891", "REPORTS", "Battery Drain"), # même problème
]
Node-level metadata
# Attributs enrichis pour chaque nœud
# Aident l'agent à filtrer, raisonner et expliquer
metadata = {
"Customer_2345": {"customer_id": "2345", "ticket_date": "2024-01-15"},
"Battery Drain": {"severity": "high", "category": "hardware"},
"Laptop Model X": {"product_version": "3.0", "product_line": "ProSeries"},
"Patch 1.21": {"release_date": "2024-01-20", "patch_type": "hotfix"},
}
Ingestion in Neo4j with MERGE pattern (upsert)
def ingest_triplets_to_neo4j(driver, triplets, metadata):
"""
Ingère les triplets dans Neo4j en utilisant MERGE.
MERGE = créer si absent, trouver si existant (upsert).
Clé : vous pouvez relancer ce pipeline et le graphe ne se dupliquera pas.
"""
with driver.session() as session:
# Passe 1 : Créer nœuds et arêtes via MERGE
for subj, rel, obj in triplets:
session.run(
f"""
MERGE (s:Entity {{name: $subj}})
MERGE (o:Entity {{name: $obj}})
MERGE (s)-[r:{rel}]->(o)
""",
subj=subj, obj=obj
)
# Passe 2 : Enrichir les nœuds avec métadonnées
for node_name, attrs in metadata.items():
set_clause = ", ".join([f"n.{k} = ${k}" for k in attrs])
session.run(
f"MATCH (n:Entity {{name: $name}}) SET {set_clause}",
name=node_name, **attrs
)
print("Graphe ingéré avec succès dans Neo4j")
# Lancer l'ingestion
ingest_triplets_to_neo4j(driver, triplets, metadata)
Resulting structure and reasoning value
Graphe après ingestion :
Customer_2345 ──REPORTS──┐
Customer_3678 ──REPORTS──├──▶ Battery Drain ──AFFECTS──▶ Laptop Model X
Customer_4891 ──REPORTS──┘ │
└──RESOLVED_BY──▶ Patch 1.21
(cas 3678 et 4891 pas encore résolus — en rouge)
Even if a ticket is not directly resolved, the agent can still reason from the structure. If another case on the same issue and product has been resolved, the agent may cite it.
This is where the value is created: reason about the entire graph, not just about isolated records.
3.5 Introduction to Graph Querying Approaches
Now that we have built a structured graph, we need to query it efficiently.
The two query paradigms
┌─────────────────────────────────────────────────────────────┐
│ Paradigmes de requête │
├──────────────────────────┬──────────────────────────────────┤
│ Langages déclaratifs │ API / Outils (recommandé) │
│ (Cypher, SPARQL) │ (Tool-based pattern) │
├──────────────────────────┼──────────────────────────────────┤
│ • LLM génère les queries │ • LLM invoque des outils │
│ • Flexible │ • Outils gèrent la logique │
│ • Risque d'hallucination │ • Plus robuste et debuggable │
│ sur les schemas │ • Moins sujet aux hallucinations │
└──────────────────────────┴──────────────────────────────────┘
Recommended pattern: query wrapping tools
In practice, most systems do not rely on LLM to write raw queries. Instead, they wrap common graph access patterns into callable tools or APIs. The agent simply invokes a function.
# Pattern recommandé — l'agent appelle des fonctions, pas du Cypher brut
from langchain.tools import Tool
def find_issue_resolution(issue_name: str) -> str:
"""Trouve la résolution pour un problème donné. Input: nom du problème."""
with driver.session() as session:
result = session.run(
"""
MATCH (i:Entity {name: $issue})-[:RESOLVED_BY]->(r:Entity)
RETURN r.name AS resolution, i.severity AS severity,
r.release_date AS date
""",
issue=issue_name
)
records = [rec.data() for rec in result]
if records:
return f"Résolu par: {records[0]['resolution']} (date: {records[0]['date']})"
return f"Aucune résolution trouvée pour: {issue_name}"
def find_related_issues(product_name: str) -> str:
"""Trouve tous les problèmes liés à un produit. Input: nom du produit."""
with driver.session() as session:
result = session.run(
"""
MATCH (i:Entity)-[:AFFECTS]->(p:Entity {name: $product})
OPTIONAL MATCH (i)-[:RESOLVED_BY]->(r:Entity)
RETURN i.name AS issue, r.name AS resolution,
i.severity AS severity
ORDER BY i.severity DESC
""",
product=product_name
)
return [rec.data() for rec in result]
# Définir les outils pour l'agent
graph_tools = [
Tool(
name="find_issue_resolution",
func=find_issue_resolution,
description="Trouve si et comment un problème a été résolu. "
"Input: nom exact du problème (string)"
),
Tool(
name="find_related_issues",
func=find_related_issues,
description="Trouve tous les problèmes liés à un produit. "
"Input: nom exact du produit (string)"
)
]
This pattern is common in modern agent frameworks like LangChain. It’s more robust, debuggable, easier to control, and keeps complex query logic out of the template prompt.
Performance optimization
-- Créer des index sur les propriétés fréquemment interrogées
CREATE INDEX entity_name FOR (n:Entity) ON (n.name);
CREATE INDEX issue_severity FOR (n:Entity) ON (n.severity);
CREATE INDEX ticket_date FOR (n:Entity) ON (n.ticket_date);
- Indexing key properties (Client ID, Issue ID) speeds up recovery
- Filters reduce search space
- Traversal limits prevent overly complex queries
- Caching frequent results keeps response times low
Summary: Giving our agent the ability to query intelligently means it can respond intelligently. By leveraging structured queries — whether in Cypher, SPARQL or via an API — our agent can extract exactly the facts it needs from its knowledge graph quickly and reliably. It is the synergy between graphs and LLMs that makes agents both intelligent and trustworthy.
3.6 Governance and maintenance: making graphs production ready
So far we have focused on how to construct and query graphs. Now we look at what keeps the graph healthy over time: governance.
Queries answer the what, but governance answers the how we keep it reliable.
The four pillars of governance
1. Schema versioning
Controls the evolution of field names, entity types, and relationships. Without it, changes could silently break queries or misalign new data.
2. Entity resolution
Different tickets may mention the same problem in different terms: “laptop overheating” or “thermal shutdown”. Agents should merge these variants into a node using fuzzy matching or known synonyms. Without this step, reports would be fragmented.
3. Consistency checks
If a ticket refers to a resolution that does not exist, or if a node suddenly violates type rules, the system marks this as drift.
4. Access control and audit logging
Not all data is equally visible. Our agent tracks who accessed what and filters the results to ensure the graph supports privacy.
Entity resolution in practice
In the Orion graph, the same problem could be reported in many ways:
| Term used | → | Canonical term |
|---|---|---|
| ”laptop overheating” | → | “Overheating Issue” |
| “thermal shutdown” | → | “Overheating Issue” |
| “heat warning system” | → | “Overheating Issue” |
from rapidfuzz import fuzz # pip install rapidfuzz
class EntityResolver:
"""Résoudre les variants d'entités vers des nœuds canoniques."""
def __init__(self, canonical_entities: dict):
"""
canonical_entities: {canonical_name: [liste de variants connus]}
Exemple: {"Overheating Issue": ["laptop overheating",
"thermal shutdown",
"system heat warning"]}
"""
self.canonical_entities = canonical_entities
def resolve(self, entity: str, threshold: int = 80) -> str:
"""
Résout une entité vers sa forme canonique.
1. Exact match
2. Recherche dans les variants connus
3. Similarité floue comme fallback
"""
entity_lower = entity.lower().strip()
# 1. Exact match sur les canoniques
for canonical in self.canonical_entities:
if entity_lower == canonical.lower():
return canonical
# 2. Recherche dans les variants connus
for canonical, variants in self.canonical_entities.items():
if entity_lower in [v.lower() for v in variants]:
return canonical
# 3. Similarité floue
best_match, best_score = None, 0
for canonical in self.canonical_entities:
score = fuzz.ratio(entity_lower, canonical.lower())
if score > best_score:
best_score, best_match = score, canonical
return best_match if best_score >= threshold else entity
Schema evolution timeline with governance
Version 1.0 (initial)
├── Entités: Customer, Issue, Product
├── Ingestion: version tagging enforced (v1.0)
└── Requêtes: stables et version-aware
Version 1.1 (ajout)
├── Nouveau type: Resolution
├── Nouvelle relation: Issue -[RESOLVED_BY]→ Resolution
└── Validation: seul Issue peut lier à Resolution
Version 2.0 (refactoring)
├── Renommage: related_to → has_variant, has_model
├── Monitoring: détecter les utilisations résiduelles de l'ancien nom
└── Nettoyage: flags pour corriger les incohérences
With each release, governance practices keep the graph stable. The key point: schema evolution is healthy, but only when it is accompanied by controls.
Graph health monitoring dashboard
A healthy graph is not only well structured — it is monitored over time:
def compute_graph_health_metrics(driver) -> dict:
"""Calcule les métriques de santé du graphe pour le monitoring."""
with driver.session() as session:
# Nœuds orphelins (sans aucune relation)
orphan_count = session.run("""
MATCH (n:Entity) WHERE NOT (n)-[]-()
RETURN count(n) AS count
""").single()["count"]
# Distribution des nœuds par type
type_distribution = session.run("""
MATCH (n:Entity)
RETURN n.type AS entity_type, count(n) AS count
ORDER BY count DESC
""").data()
# Problèmes sans résolution (tickets ouverts)
unresolved_issues = session.run("""
MATCH (i:Entity {type: 'Issue'})
WHERE NOT (i)-[:RESOLVED_BY]->()
RETURN count(i) AS count
""").single()["count"]
# Latence des requêtes (exemple simplifié)
import time
start = time.time()
session.run("MATCH (n:Entity) RETURN count(n)").single()
query_latency_ms = (time.time() - start) * 1000
return {
"orphan_nodes": orphan_count,
"type_distribution": type_distribution,
"unresolved_issues": unresolved_issues,
"query_latency_ms": round(query_latency_ms, 2)
}
# Utilisation
metrics = compute_graph_health_metrics(driver)
print(f"Nœuds orphelins: {metrics['orphan_nodes']}")
print(f"Problèmes non résolus: {metrics['unresolved_issues']}")
print(f"Latence requête: {metrics['query_latency_ms']}ms")
| Metric | What it indicates |
|---|---|
| Node Growth by Type | Usage patterns; rapid growth = change in intake or load |
| Orphan node rate | Disconnected data, broken pipelines, or schema misalignments |
| Query latency | Gradual slowdown → indexing issues or query inefficiencies |
| Last index update | Obsolete indexes → stale reads or missed relationships |
| Schema drift alerts | Relations/properties renamed but still appearing in data |
Access control and audit
# Exemple de contrôle d'accès basé sur les rôles
ACCESS_RULES = {
"support_agent": {
"can_access_types": ["Customer", "Issue", "Product", "Resolution"],
"cannot_access_types": ["InternalNote", "DebugNode"],
"can_override_resolution": False
},
"admin": {
"can_access_types": ["*"], # Tout
"can_override_resolution": True
},
"compliance_auditor": {
"can_access_types": ["Issue", "Resolution", "AuditLog"],
"pii_masked": True # PII masquée
}
}
def apply_access_filter(cypher_query: str, user_role: str) -> str:
"""Ajoute des filtres d'accès à une requête Cypher."""
rules = ACCESS_RULES.get(user_role, {})
excluded = rules.get("cannot_access_types", [])
if excluded:
filter_clause = " AND ".join(
[f"NOT n.type = '{t}'" for t in excluded]
)
return cypher_query + f" WHERE {filter_clause}"
return cypher_query
# Logging d'audit
import logging
audit_logger = logging.getLogger("graph_audit")
def audited_query(query: str, user_id: str, user_role: str):
"""Execute une requête avec logging d'audit complet."""
import datetime
audit_logger.info({
"timestamp": datetime.datetime.utcnow().isoformat(),
"user_id": user_id,
"user_role": user_role,
"query": query[:200], # Tronquer pour le log
"action": "graph_query"
})
filtered_query = apply_access_filter(query, user_role)
return driver.session().run(filtered_query).data()
Conclusion on governance: Without governance, graphs drift, break or lose trust. With it, they stay clean, consistent, and ready for reasoning — and that’s what turns a passive memory graph into active infrastructure.
3.7 Demo: Graphs for multi-hop reasoning in agentic workflows
Agents don’t just answer direct questions. They navigate knowledge to construct explanations. In this demo, we will see how a graph allows an agent to trace a multi-step path.
The starting question
“Has the issue battery drain been resolved for Laptop Model X?”
To answer this, the system must reason through several hops in the graph:
- Starting from the product (
Laptop Model X) - Find the related problem (
Battery Drain) - Check if a resolution exists
This is multi-hop reasoning — impossible with a simple documentary or vector search.
Connecting and extracting the schema
from langchain_community.graphs import Neo4jGraph
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
# Connexion programmatique au graphe
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="your_password"
)
# Extraire le schema du graphe SANS hypothèses hard-codées
# Le schema décrit les types de nœuds, propriétés et relations valides
schema = graph.get_schema
print("Schema du graphe:")
print(schema)
# Sortie typique :
# Node properties:
# Entity {name: STRING, type: STRING, severity: STRING, ticket_date: DATE,
# customer_id: STRING, release_date: DATE}
# Relationships:
# (:Entity)-[:REPORTS]->(:Entity)
# (:Entity)-[:AFFECTS]->(:Entity)
# (:Entity)-[:RESOLVED_BY]->(:Entity)
Construction of the chain
# Créer le LLM
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# Créer la chain GraphCypherQAChain
# Le LLM génère la requête Cypher basée sur le schema
chain = GraphCypherQAChain.from_llm(
llm=llm,
graph=graph,
verbose=True, # Afficher la requête Cypher générée
return_intermediate_steps=True
)
Response and test function
def answer_query(question: str) -> dict:
"""
Prend une question en langage naturel, génère une requête Cypher,
exécute sur le graphe, et retourne une réponse fondée en preuves.
"""
result = chain.invoke({"query": question})
return result
# Test avec la question originale
question = "Has the issue battery drain been resolved for Laptop Model X?"
result = answer_query(question)
print("Réponse:", result["result"])
print("\nÉtapes intermédiaires (requête générée):")
for step in result.get("intermediate_steps", []):
if "query" in step:
print(f"Cypher: {step['query']}")
The Cypher query generated typically:
MATCH (p:Entity {name: 'Laptop Model X'})<-[:AFFECTS]-(i:Entity {name: 'Battery Drain'})
OPTIONAL MATCH (i)-[:RESOLVED_BY]->(r:Entity)
RETURN i.name AS issue, r.name AS resolution,
i.severity AS severity, r.release_date AS patch_date
Answer anchored in graph proofs
def generate_grounded_response(question: str, graph_evidence: list) -> str:
"""
Prend le résultat structuré du graphe et demande au LLM de l'expliquer.
Le LLM n'ajoute PAS d'information absente dans les preuves.
"""
prompt = f"""
Based ONLY on the following graph evidence, answer the question.
Do NOT add any information not present in the evidence below.
If the evidence is insufficient, say so.
Question: {question}
Graph Evidence: {graph_evidence}
Provide a factual answer citing the evidence path.
"""
return llm.invoke(prompt).content
# La réponse finale est entièrement fondée dans les preuves du graphe
# Exemple : "Yes, Battery Drain has been resolved for Laptop Model X.
# The issue was fixed by Patch 1.21 (released 2024-01-20)."
Thinking: Two Query Strategies
At this point, it is important to pause and think about the approach:
Strategy 1 — LLM generates Cypher/SPARQL directly:
- ✅ Simple and flexible
- ❌ Susceptible to hallucinations when patterns grow
- ❌ Difficult to debug in production
Strategy 2 (recommended) — Tools/APIs wrapping queries:
- ✅ Validation of inputs and enforcement of constraints
- ✅ Structured and predictable outputs
- ✅ The LLM chooses which tool to call, not how to construct the query
- ✅ Model of modern agent frameworks (LangChain, LlamaIndex)
# Production-ready : agent avec outils de graphe prédéfinis
from langchain.agents import initialize_agent, AgentType
agent = initialize_agent(
tools=graph_tools, # Outils définis dans la section 3.5
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
# L'agent choisit le bon outil et obtient des résultats fiables
response = agent.run(
"Has battery drain been resolved for Laptop Model X? "
"Show the full resolution path."
)
print(response)
3.8 Conclusion: From prototype to production
Let’s step back and think about what it takes to move from a prototype graph to a scalable, explainable, production-ready system.
What we covered in this module
This module walked the journey from idea to infrastructure:
-
Building scalable graphs — select the right frameworks based on suitability for reasoning, performance, integration and scale
-
Transparent multi-hop reasoning — use structured proof paths for informed agent responses
-
Governance and monitoring — integrated into each step, not added after the fact
You are now able to:
- Choose the right framework for your domain
- Design and build structured graph pipelines for agentic reasoning
- Build dashboards and design metrics to maintain schema consistency and graph health
- Ensuring governance for explainability and trust
The Truth About Production Readiness
Production readiness isn’t about managing more data — it’s managing it responsibly:
- A governance structure that ensures schema consistency over time
- Queries that remain fast as the graph grows
- metrics that allow you to monitor the health of the graph
These foundations transform a working prototype into a reliable system.
Final thought question: Now that you’ve seen the trade-offs between RDF, Neo4j, and hybrid models, and how governance shapes production, return to your own agent goals. Which framework gives you the structure and explainability you need?
4. Summary and Key Points
Glossary of fundamental concepts
| Concept | Definition |
|---|---|
| Knowledge Graph | Structured network of entities (nodes) and relationships (edges) representing domain knowledge |
| Entity | Node representing a real-world object: Customer, Product, Issue, Resolution |
| Relationship | Directed edge between nodes: REPORTS, AFFECTS, CAUSED_BY, RESOLVED_BY |
| Triplet | Base unit: (subject, relationship, object) = (Customer, REPORTS, Issue) |
| Ontology | Rules defining which connections are allowed in the domain |
| Diagram | Structure enforced to ensure data consistency |
| Subgraph | Connected subset of nodes and edges returned by a query |
| Reasoning Path | Traceable chain of connections followed by the agent to reach a conclusion |
| Canonicalization | Normalizing entities to a standard form to avoid duplicates |
| Multi-hop Reasoning | Reasoning that traverses multiple nodes and edges to answer a question |
| Evidence Subgraph | Proof returned by the graph anchoring the agent’s response |
| LPG | Labeled Property Graph — graph model with properties on nodes and edges |
| RDF | Resource Description Framework — subject-predicate-object semantic model |
| Cypher | Declarative Query Language for Neo4j |
| SPARQL | Query language for RDF triplestores |
| Upsert | Create if absent, update if existing — fundamental pattern for incremental construction |
Agent Orion Evolution
Version 0 : LLM seul
→ Réponses fluides mais non justifiables, incohérentes
Version 1 : + Document Retrieval
→ Peut trouver des documents pertinents par mots-clés exacts
Version 2 : + Vector Retrieval (embeddings)
→ Peut trouver des problèmes similaires sémantiquement
Version 3 : + Knowledge Graph
→ Peut tracer les causes, dépendances et résolutions
→ Peut expliquer son raisonnement avec des preuves
→ Cohérent, transparent, auditable
Final comparison of retrieval approaches
| Document Retrieval | Vector Retrieval | Knowledge Graph | |
|---|---|---|---|
| Mechanism | Exact keywords | Semantic similarity | Explicit relationships |
| Strength | Precision, speed | Semantic recall | Reasoning, causality |
| Weakness | Rigid wording | No proof | Requires construction |
| Output | Documents | Similar candidates | Subgraph + path |
| Explainability | Low | Low | High |
Comparison of storage frameworks
| NetworkX | Neo4j | RDF/SPARQL | DBType | |
|---|---|---|---|---|
| Usage | Prototype, experimentation | General production | Semantic data | Complex logic |
| Query | Python API | Cypher | SPARQL | TypeQL |
| Persistence | Memory only | Disc (ACID) | Disc | Disc |
| Scale | Limited (RAM) | Moderate to large | Large (multi-source) | Large (rule-driven) |
| LangChain | Python adapter | Native integration | Via RDFLib | API Client |
| Inference | No | No | Limited (OWL) | Yes (rules) |
Checklist for a production system
- Extraction: Ontology-aligned LLM prompts, constrained output format
- Canonicalization: Dictionary of synonyms + fuzzy matching to avoid duplicates
- Incremental construction: Pattern MERGE/upsert, never naive addition
- Ontology validation: Check each triple before insertion
- Choice of backend: LPG (Neo4j) vs RDF depending on the use case
- Indexing: Index to frequently queried properties
- LLM integration: The graph is a MANDATORY tool consulted BEFORE responding
- Hybrid Retrieval: Routing or merger strategy depending on the nature of the questions
- Schema versioning: Each evolution is versioned and tagged
- Entity resolution: Matching system to unify variants
- Monitoring dashboard: Orphan nodes, latency, drift, resolution rate
- Access control: RBAC + PII hidden for LLM tools
- Audit logging: Each access to the graph is recorded (who, what, when)
Training created by Gihad Sohsah — AI Tech Lead & Entrepreneur
Search Terms
agentic · knowledge · graphs · rag · vector · search · embeddings · artificial · intelligence · generative · ai · graph · comparison · production · orion · agents · query · reasoning · retrieval · tools · agent · four · frameworks · schema