GraphRAG en pratique : enrichir votre RAG avec des graphes de connaissances
Vous avez mis en place un pipeline RAG classique : chunking, embeddings, recherche vectorielle, génération. Les résultats sont corrects sur des questions factuelles simples. Mais dès qu’un utilisateur pose une question qui exige de relier plusieurs documents entre eux — “Quels fournisseurs sont liés à des incidents qualité sur la ligne de production X ?” — votre système hallucine ou renvoie des fragments sans cohérence. Le problème n’est pas votre LLM. C’est votre méthode de retrieval : la recherche vectorielle traite chaque chunk comme une île isolée. Elle ignore les relations entre entités. GraphRAG résout ce problème en ajoutant une couche de graphe de connaissances à votre pipeline RAG. Dans cet article, on construit un pipeline GraphRAG fonctionnel en Python, de l’extraction d’entités à la génération augmentée par le graphe. Vous repartirez avec du code testable et une architecture claire pour vos projets d’IA générative.
RAG classique vs GraphRAG : pourquoi le vecteur seul ne suffit pas
Un pipeline RAG standard repose sur une hypothèse forte : la réponse à une question se trouve dans un seul chunk ou dans quelques chunks sémantiquement proches. Cette hypothèse tient pour des questions de type “Quelle est la politique de retour ?” mais s’effondre sur trois catégories de requêtes :
- Questions multi-hop : la réponse nécessite de chaîner des informations issues de documents différents.
- Questions de synthèse : “Résume les tendances principales de notre base documentaire” — aucun chunk isolé ne contient la réponse.
- Questions relationnelles : “Quels clients sont aussi partenaires de nos concurrents ?” — la réponse est dans les liens entre entités, pas dans le texte brut.
GraphRAG introduit un knowledge graph comme structure intermédiaire. Au lieu de chercher uniquement par similarité vectorielle, le système peut traverser des relations typées (FOURNIT, EMPLOIE, PRODUIT) pour assembler un contexte riche et structuré. Le LLM reçoit alors non pas une liste de chunks décorrélés, mais un sous-graphe contextuel qui encode les relations pertinentes. Si vous hésitez encore entre RAG et fine-tuning pour votre cas d’usage, consultez notre comparatif des architectures.
Architecture d’un pipeline GraphRAG
Un pipeline GraphRAG se décompose en quatre étapes :
- Extraction d’entités et de relations : un LLM parse vos documents et extrait des triplets (sujet, relation, objet).
- Construction du graphe : les triplets sont stockés dans une base orientée graphe (Neo4j, NetworkX pour le prototypage).
- Retrieval hybride : à la requête, on combine recherche vectorielle classique (sur les chunks) et traversée du graphe (sur les entités détectées dans la question).
- Génération augmentée : le contexte fusionné (chunks + sous-graphe) est injecté dans le prompt du LLM.
Voici le schéma des dépendances que nous allons installer :
```bash pip install openai networkx langchain langchain-openai langchain-community chromadb tiktoken ```
Étape 1 : extraire entités et relations avec un LLM
L’extraction d’entités-relations est le cœur de GraphRAG. On utilise un LLM avec un prompt structuré pour produire des triplets JSON à partir de texte brut. Cette approche est bien plus flexible qu’un NER classique car elle capture des relations sémantiques riches sans entraînement supervisé.
Définissez d’abord le prompt d’extraction :
```python from openai import OpenAI import json client = OpenAI() EXTRACTION_PROMPT = """Tu es un expert en extraction d’information. À partir du texte fourni, extrais toutes les entités et relations sous forme de triplets. Retourne un JSON avec la structure suivante : { “entities”: [{“name”: ”…”, “type”: ”…”}], “relations”: [{“source”: ”…”, “relation”: ”…”, “target”: ”…”}] } Types d’entités possibles : PERSONNE, ORGANISATION, PRODUIT, TECHNOLOGIE, LIEU, CONCEPT. Types de relations possibles : UTILISE, PRODUIT, EMPLOIE, PARTENAIRE_DE, SITUE_A, FAIT_PARTIE_DE, CONCURRENCE. Texte : {text} """ def extract_triplets(text: str) -> dict: response = client.chat.completions.create( model=“gpt-4o”, messages=[ {“role”: “system”, “content”: “Tu extrais des entités et relations en JSON.”}, {“role”: “user”, “content”: EXTRACTION_PROMPT.format(text=text)} ], response_format={“type”: “json_object”}, temperature=0 ) return json.loads(response.choices[0].message.content) ```
Testez sur un document exemple :
```python sample_text = """ Acme Corp utilise la plateforme DataFlow pour ses analyses prédictives. DataFlow est développé par TechVision, une startup basée à Lyon. Acme Corp est partenaire de GlobalRetail sur le projet LogiChain. """ result = extract_triplets(sample_text) print(json.dumps(result, indent=2, ensure_ascii=False)) ```
Sortie attendue :
```json { “entities”: [ {“name”: “Acme Corp”, “type”: “ORGANISATION”}, {“name”: “DataFlow”, “type”: “PRODUIT”}, {“name”: “TechVision”, “type”: “ORGANISATION”}, {“name”: “Lyon”, “type”: “LIEU”}, {“name”: “GlobalRetail”, “type”: “ORGANISATION”}, {“name”: “LogiChain”, “type”: “PRODUIT”} ], “relations”: [ {“source”: “Acme Corp”, “relation”: “UTILISE”, “target”: “DataFlow”}, {“source”: “TechVision”, “relation”: “PRODUIT”, “target”: “DataFlow”}, {“source”: “TechVision”, “relation”: “SITUE_A”, “target”: “Lyon”}, {“source”: “Acme Corp”, “relation”: “PARTENAIRE_DE”, “target”: “GlobalRetail”} ] } ```
Pour un corpus volumineux, pensez à optimiser votre stratégie de chunking en amont : des chunks trop longs diluent les entités, des chunks trop courts cassent les relations inter-phrases.
Étape 2 : construire le knowledge graph avec NetworkX
Pour le prototypage, NetworkX suffit largement. En production, vous migrerez vers Neo4j ou Amazon Neptune. Construisons le graphe à partir des triplets extraits :
```python import networkx as nx class KnowledgeGraph: def __init__(self): self.graph = nx.DiGraph() def add_triplets(self, extraction: dict, source_doc: str = ""): for entity in extraction.get(“entities”, []): self.graph.add_node( entity[“name”], type=entity[“type”], source=source_doc ) for rel in extraction.get(“relations”, []): self.graph.add_edge( rel[“source”], rel[“target”], relation=rel[“relation”], source=source_doc ) def get_subgraph(self, entity: str, depth: int = 2) -> list: """Récupère le sous-graphe autour d’une entité jusqu’à N niveaux.""" if entity not in self.graph: return [] paths = [] visited = set() queue = [(entity, 0)] while queue: node, d = queue.pop(0) if d >= depth or node in visited: continue visited.add(node) for _, target, data in self.graph.out_edges(node, data=True): paths.append(f”{node} —[{data[‘relation’]}]—> {target}”) queue.append((target, d + 1)) for source, _, data in self.graph.in_edges(node, data=True): paths.append(f”{source} —[{data[‘relation’]}]—> {node}”) queue.append((source, d + 1)) return paths def stats(self) -> dict: return { “nodes”: self.graph.number_of_nodes(), “edges”: self.graph.number_of_edges(), “components”: nx.number_weakly_connected_components(self.graph) } # Construction kg = KnowledgeGraph() kg.add_triplets(result, source_doc=“doc_001.txt”) print(kg.stats()) # {‘nodes’: 6, ‘edges’: 4, ‘components’: 1} ```
La méthode get_subgraph est clé : elle effectue un parcours BFS autour d’une entité et retourne les triplets sous forme textuelle, prêts à être injectés dans un prompt. C’est le mécanisme qui permet de croiser données structurées et non structurées au moment du retrieval.
Étape 3 : retrieval hybride — vecteurs + graphe
Le retrieval hybride est ce qui distingue GraphRAG d’un RAG classique enrichi. On combine deux sources de contexte : les chunks retrouvés par similarité vectorielle et les triplets extraits du graphe autour des entités détectées dans la question.
```python from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.schema import Document # --- Indexation vectorielle des chunks --- documents = [ Document( page_content=“Acme Corp utilise la plateforme DataFlow pour ses analyses prédictives.”, metadata={“source”: “doc_001.txt”, “chunk_id”: 0} ), Document( page_content=“DataFlow est développé par TechVision, une startup basée à Lyon.”, metadata={“source”: “doc_001.txt”, “chunk_id”: 1} ), Document( page_content=“Acme Corp est partenaire de GlobalRetail sur le projet LogiChain.”, metadata={“source”: “doc_001.txt”, “chunk_id”: 2} ), ] embeddings = OpenAIEmbeddings(model=“text-embedding-3-small”) vectorstore = Chroma.from_documents(documents, embeddings) # --- Extraction d’entités depuis la question --- def extract_question_entities(question: str) -> list: response = client.chat.completions.create( model=“gpt-4o-mini”, messages=[ {“role”: “system”, “content”: “Extrais les noms d’entités de la question. Retourne un JSON: {\“entities\”: […]}”}, {“role”: “user”, “content”: question} ], response_format={“type”: “json_object”}, temperature=0 ) return json.loads(response.choices[0].message.content).get(“entities”, []) # --- Retrieval hybride --- def hybrid_retrieve(question: str, kg: KnowledgeGraph, vectorstore, k: int = 3) -> dict: # 1. Retrieval vectoriel vector_results = vectorstore.similarity_search(question, k=k) vector_context = “\n”.join([doc.page_content for doc in vector_results]) # 2. Retrieval par graphe entities = extract_question_entities(question) graph_context_parts = [] for entity in entities: triplets = kg.get_subgraph(entity, depth=2) graph_context_parts.extend(triplets) graph_context = “\n”.join(list(set(graph_context_parts))) return { “vector_context”: vector_context, “graph_context”: graph_context, “entities_found”: entities } # Test question = “Qui développe la plateforme utilisée par Acme Corp ?” context = hybrid_retrieve(question, kg, vectorstore) print(“Entités détectées :”, context[“entities_found”]) print(“Contexte graphe :\n”, context[“graph_context”]) ```
Sortie attendue :
```text Entités détectées : [‘Acme Corp’] Contexte graphe : Acme Corp —[UTILISE]—> DataFlow Acme Corp —[PARTENAIRE_DE]—> GlobalRetail TechVision —[PRODUIT]—> DataFlow ```
Remarquez : le graphe fournit immédiatement le chaînage Acme Corp → DataFlow → TechVision. Un RAG vectoriel pur aurait pu retrouver les chunks pertinents, mais sans garantie d’assembler la chaîne causale complète — surtout quand les informations sont dispersées dans des documents distincts.
Étape 4 : génération augmentée par le graphe
La dernière étape fusionne les deux contextes dans un prompt structuré pour le LLM :
```python def graphrag_answer(question: str, kg: KnowledgeGraph, vectorstore) -> str: context = hybrid_retrieve(question, kg, vectorstore) prompt = f"""Réponds à la question en utilisant les deux sources de contexte ci-dessous. Le contexte vectoriel contient des extraits de documents. Le contexte graphe contient des relations structurées entre entités. Privilégie les informations du graphe pour les questions relationnelles. ## Contexte vectoriel {context[‘vector_context’]} ## Contexte graphe (triplets entité-relation-entité) {context[‘graph_context’]} ## Question {question} ## Réponse """ response = client.chat.completions.create( model=“gpt-4o”, messages=[{“role”: “user”, “content”: prompt}], temperature=0 ) return response.choices[0].message.content answer = graphrag_answer(“Qui développe la plateforme utilisée par Acme Corp ?”, kg, vectorstore) print(answer) # TechVision développe DataFlow, la plateforme utilisée par Acme Corp # pour ses analyses prédictives. TechVision est une startup basée à Lyon. ```
Aller plus loin : industrialiser votre GraphRAG
Évaluation du pipeline
Un pipeline GraphRAG introduit de nouvelles dimensions à évaluer au-delà de la pertinence classique : la qualité de l’extraction d’entités (precision/recall des triplets), la couverture du graphe et la fidélité des réponses multi-hop. Consultez notre guide sur l’évaluation RAG et la réduction des hallucinations pour mettre en place des métriques adaptées.
Passage à Neo4j pour la production
NetworkX fonctionne en mémoire et ne tient pas la charge sur des graphes de plus de 100 000 nœuds. Pour la production, utilisez Neo4j avec le driver Python :
```python from neo4j import GraphDatabase driver = GraphDatabase.driver(“bolt://localhost:7687”, auth=(“neo4j”, “password”)) def insert_triplet(tx, source: str, relation: str, target: str): tx.run( “MERGE (a:Entity {name: $source}) ” “MERGE (b:Entity {name: $target}) ” “MERGE (a)-[r:RELATION {type: $relation}]->(b)”, source=source, relation=relation, target=target ) with driver.session() as session: for rel in result[“relations”]: session.execute_write(insert_triplet, rel[“source”], rel[“relation”], rel[“target”]) ```
Gouvernance et qualité des données
GraphRAG rend explicite la structure de vos connaissances. C’est l’occasion d’adopter une approche Data Product : traitez votre knowledge graph comme un produit avec des SLAs sur la fraîcheur des données, la couverture des entités et la qualité des relations extraites. Sans cette rigueur, le graphe devient une source de bruit plutôt qu’un levier de précision.
Pour les cas où vos utilisateurs ont besoin d’interroger des données structurées en parallèle du graphe, combinez GraphRAG avec une couche Text-to-SQL pour couvrir l’ensemble du spectre analytique.
GraphRAG transforme votre RAG d’un moteur de recherche documentaire en un véritable système de raisonnement sur vos données. En ajoutant un graphe de connaissances à votre pipeline, vous débloquez les questions multi-hop, les synthèses transversales et les requêtes relationnelles que le RAG vectoriel seul ne peut pas traiter. Le code présenté ici constitue une base fonctionnelle — l’étape suivante est de l’adapter à vos données métier. Chez Flowt, nos équipes Data Science accompagnent les PME et ETI dans la conception et l’industrialisation de pipelines GraphRAG sur-mesure. Parlons de votre projet.