Advanced

Agentic Knowledge Graphs

Training created by Gihad Sohsah — AI Tech Lead & Entrepreneur.

Table of Contents


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 memoryStructured reasoning
No explicit linksExplicitly related facts
Unreliable multi-step logicTraceable reasoning paths
Unexplainable decisionsEmerging Understanding
Inconsistent resultsMulti-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:

  1. Entities — things or objects that interest us in our data
  2. Relationships — describe how these entities connect to each other
  3. Ontologies — define which relationships are allowed in the domain
  4. 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:

EntityType
Customer_4759Customer
LaptopProduct
Overheating IssueIssue
Latest UpdateCause
Patch 1.2Resolution

Step 2 — Identifying relationships:

SubjectRelationshipObject
Customer_4759POSTPONMENTSOverheating Issue
Overheating IssueAFFECTSLaptop
Overheating IssueCAUSED_BYLatest Update
Overheating IssueRESOLVED_BYPatch 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 Customer can REPORTS an Issue
  • The schema ensures that each Customer node contains a customer_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:

  1. Keyword Document Search — quickly retrieves documents using exact keywords
  2. 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

MethodWhat it bringsLimitation
Document RetrievalLiteral, quick and precise reminderLimited to exact words, no understanding of meaning
Vector RetrievalSemantic understandingDoes not prove connections, no causation
Knowledge GraphReasoning, traceability, causality, explainabilityRequires 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

  1. Identification of recurring patterns — they help agents identify recurring patterns across a large collection of information

  2. Evidence-based actions — knowledge graphs allow agents to suggest reliable actions based on evidence (not just retrieving similar examples), following a reasoning path

  3. 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 model
  • ast — to securely parse model output
  • networkx — to build the graph structure
  • matplotlib — 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.2 and update 1.2
  • The problem is described as battery dying fast, battery drain and fast discharge
  • The product is listed as Laptop Model X and Model 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:

  1. Anchoring — anchoring on what you already know
  2. Traversal — traverse relationships to extend across the graph
  3. Constraints — filters, limits and ordering to return only the most relevant structure

Common systems and their use

CategoryToolsQuery languageTypical usage
PropertyGraph DBNeo4j, TigerGraphCypherConversational agents, recommendation, fraud
RDF TriplestoreGraphDB, BlazegraphSPARQLMedical data, semantic web, multi-sources
HybridGraph Store + Vector IndexMixedCombination 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

ComponentStrengthLimitation
LLMTransform information into clear explanationNot a reliable source of truth on its own
Knowledge GraphStore explicit entities and relationshipsDoes not generate natural language explanations
CombinationClarity + proof, traceable answersRequires 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_subgraph and a reasoning_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 RetrievalGraph Retrieval
StrengthAmbiguous and variable language, high recallExplicit links, multi-hop reasoning, explainability
WeaknessDoes not prove connections, cannot justifyMay miss context if incomplete graph
Trade-offRelevance ≠ CorrectionCoverage 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:

  1. Audit the data — assess the completeness of the graph, measure whether tickets are repeatable patterns or highly variable descriptions
  2. Align with operations — define what “bad recommendation” means in this environment, and the cost of missing related cases
  3. 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

StepWhat we did
ExtractTransform language into triples via LLM + prompt ontology
CanonicalizationNormalize entities to avoid duplicates
MappingAlign each fact with schema + ontology
StorageChoose LPG (Neo4j) or RDF depending on the use case
QueryRetrieve subgraphs (not lines)
IntegrationGraph = mandatory tool consulted BEFORE responding
Hybrid RetrievalVectors 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:

ChallengeDescriptionConsequence if ignored
ScaleThousands of tickets, multiple products and periodsPerformance degradation
PerformanceFast responses for many users simultaneouslyDegraded user experience
GovernanceSchema consistency, access controlCorrupted data, GDPR violations
ObservabilityLogs, metrics, drift detectionUndetected 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

FrameworkTypical use cases
Neo4jConversational AI, recommendation systems, customer graphs, fraud detection
RDF TriplestoresMedical/genetic data, multi-source knowledge bases, semantic web
DBTypeBiomedical research, complex fraud detection, legal reasoning
Custom PipelinePrototyping, 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

  1. Integration — does the framework connect cleanly to the agent stack?
  2. Scalability — can it grow with data, traffic and updates?
  3. Query performance — does it retrieve answers efficiently, especially for multi-hop paths?
  4. Suitability for reasoning — does its structure align with how the agent should explain?

Complete comparison table

CriterionNeo4jRDF/SPARQLDBTypeCustom Pipeline
IntegrationFort — LangChain adapters, Python driverVia SPARQL libraries or endpointsClean API Client + TypeQLFree design by the team
ScalabilityModerate to large graph sizesMulti-source linked dataLarge rule-driven graphsIf designed carefully
PerformanceFast for traversing patternsSemantic richness at the expense of speedAdds reasoning to queriesFast if disciplined structure
ReasoningFlexible but informalAligned with ontologiesLogical force + inferenceFull 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 │
└──────────────────────────┴──────────────────────────────────┘

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 usedCanonical 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")
MetricWhat it indicates
Node Growth by TypeUsage patterns; rapid growth = change in intake or load
Orphan node rateDisconnected data, broken pipelines, or schema misalignments
Query latencyGradual slowdown → indexing issues or query inefficiencies
Last index updateObsolete indexes → stale reads or missed relationships
Schema drift alertsRelations/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:

  1. Starting from the product (Laptop Model X)
  2. Find the related problem (Battery Drain)
  3. 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:

  1. Building scalable graphs — select the right frameworks based on suitability for reasoning, performance, integration and scale

  2. Transparent multi-hop reasoning — use structured proof paths for informed agent responses

  3. 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

ConceptDefinition
Knowledge GraphStructured network of entities (nodes) and relationships (edges) representing domain knowledge
EntityNode representing a real-world object: Customer, Product, Issue, Resolution
RelationshipDirected edge between nodes: REPORTS, AFFECTS, CAUSED_BY, RESOLVED_BY
TripletBase unit: (subject, relationship, object) = (Customer, REPORTS, Issue)
OntologyRules defining which connections are allowed in the domain
DiagramStructure enforced to ensure data consistency
SubgraphConnected subset of nodes and edges returned by a query
Reasoning PathTraceable chain of connections followed by the agent to reach a conclusion
CanonicalizationNormalizing entities to a standard form to avoid duplicates
Multi-hop ReasoningReasoning that traverses multiple nodes and edges to answer a question
Evidence SubgraphProof returned by the graph anchoring the agent’s response
LPGLabeled Property Graph — graph model with properties on nodes and edges
RDFResource Description Framework — subject-predicate-object semantic model
CypherDeclarative Query Language for Neo4j
SPARQLQuery language for RDF triplestores
UpsertCreate 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 RetrievalVector RetrievalKnowledge Graph
MechanismExact keywordsSemantic similarityExplicit relationships
StrengthPrecision, speedSemantic recallReasoning, causality
WeaknessRigid wordingNo proofRequires construction
OutputDocumentsSimilar candidatesSubgraph + path
ExplainabilityLowLowHigh

Comparison of storage frameworks

NetworkXNeo4jRDF/SPARQLDBType
UsagePrototype, experimentationGeneral productionSemantic dataComplex logic
QueryPython APICypherSPARQLTypeQL
PersistenceMemory onlyDisc (ACID)DiscDisc
ScaleLimited (RAM)Moderate to largeLarge (multi-source)Large (rule-driven)
LangChainPython adapterNative integrationVia RDFLibAPI Client
InferenceNoNoLimited (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

Interested in this course?

Contact us to book it or get a custom training plan for your team.