Accueil / Articles

Cinq architectures RAG et les situations dans lesquelles chacune convient en environnement de production.

Une présentation pratique des systèmes RAG naïfs, hybrides, graphiques, avancés et agents utilisant LlamaIndex et LangGraph, abordant les modes de défaillance, les coûts et la manière de les combiner.

Éditeur : REACTAPP.TOP19 min de lecture4457 mots

Sources: The 5 RAG Architectures and Exactly When to Use Each One in Production

Le terme « RAG » englobe désormais une famille d’architectures très différentes, chacune conçue pour un type spécifique de problème de récupération d’informations. Choisir la mauvaise architecture n’est pas seulement une question de performance : cela peut faire la différence entre un système qui fonctionne correctement et un autre qui fournit aux utilisateurs, à grande échelle, des réponses erronées mais présentées comme fiables.

Ce document constitue une carte de toute cette famille d’architectures. L’objectif est de pouvoir, face à n’importe quel problème de récupération, déterminer quelle architecture est appropriée et savoir comment la construire en utilisant LangGraph pour l’orchestration et LlamaIndex pour la couche de données. Les cinq architectures, du plus simple au plus complexe, sont : naïve, hybride, graphique, avancée et agente. Elles ne sont pas des concurrentes ; ce sont plutôt des couches ajoutées progressivement au fur et à mesure que les besoins s’intensifient, et la plupart des systèmes en production combinent au moins deux d’entre elles.

Le problème commun

En réalité, chaque configuration RAG vise à résoudre un seul problème : un modèle de langage a besoin de faits qu’il n’a jamais vus pendant son entraînement, fournis précisément au moment où il répond, et présentés de manière à lui permettre de raisonner à leur sujet.

Les modèles cessent d’apprendre à une date donnée. Ils n’ont jamais lu la wiki interne d’une entreprise, un document de spécifications ou tout autre contenu publié après la fin de l’entraînement. Le réglage fin des modèles sur de tels contenus est lent et coûteux, et le modèle ainsi ajusté devient rapidement obsolète dès que les documents changent.

RAG contourne tous ces problèmes. Au lieu d’enseigner de nouveaux faits au modèle, le système récupère les connaissances pertinentes au moment de la requête et les intégre dans le prompt en tant que contexte. Le modèle n’a jamais besoin de mémoriser quoi que ce soit, il doit simplement les lire lorsque c’est nécessaire. Les cinq architectures présentées ci-dessous représentent cinq solutions pour effectuer de bonnes recherches, chacune adaptée à un problème différent.

Architecture 1 : RAG naïf

Naive RAG constitue le point de départ, et de nombreux travaux réels n’ont jamais besoin de plus : des bots de politique interne, des assistants FAQ, des recherches de documentation, des outils d’intégration. Malgré son nom, il s’agit d’un modèle mature qui fonctionne à grande échelle. Vous chargez les documents, créez un VectorStoreIndex LlamaIndex, exposez son moteur de recherche en tant que @tool, puis attribuez cet outil à un agent LangGraph.

Ses cinq étapes séquentielles correspondent directement à ce que LlamaIndex propose par défaut. Les quatre premières ont lieu lors de la création de l’index, tandis que la cinquième a lieu pour chaque question :

  Stages 1–4: Indexing time (run once)
  Stage 5:    Query time (run on every user question)

La distinction la plus importante dans tout RAG concerne le temps d’indexation par rapport au temps de requête. Le coût d’incorporation est payé une seule fois, au préalable. Lors de la requête, le système ne paie que pour une recherche de similarité et une seule appel à un LLM.

Pourquoi cela fonctionne

La question d’un utilisateur est incorporée dans le même espace vectoriel que les fragments de documents, et ce sont les fragments dont les vecteurs se trouvent géométriquement les plus proches du vecteur de la question qui sont retournés. L’hypothèse de base est que la proximité dans l’espace vectoriel reflète la proximité dans le sens. Pour des corpus factuels et bien structurés, cette hypothèse se révèle étonnamment fiable.

Le code

Un modèle complet suit, couvrant les importations et la configuration, l’état, une base de connaissances persistante, l’outil, les nœuds, le routage et l’assemblage du graphe :

# ============================================================
# NAIVE RAG — COMPLETE TEMPLATE
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
    Settings, SimpleDirectoryReader, VectorStoreIndex,
    StorageContext, load_index_from_storage
)
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
# LangGraph's reasoning model
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex's internal model - separate from the above
# These two model configs do NOT share state or conversation history
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class State(MessagesState):
    pass  # messages list inherited; extend if you need more
# ── MODULE 3: KNOWLEDGE BASE (Build once, persist, reload) ───
PERSIST_DIR = "./storage/naive"
if os.path.exists(PERSIST_DIR):
    storage_context = StorageContext.from_defaults(persist_dir=PERSIST_DIR)
    index = load_index_from_storage(storage_context)
else:
    documents = SimpleDirectoryReader("./data").load_data()
    index = VectorStoreIndex.from_documents(documents, show_progress=True)
    index.storage_context.persist(persist_dir=PERSIST_DIR)
query_engine = index.as_query_engine(similarity_top_k=3)
# ── MODULE 3 (cont.): TOOL ───────────────────────────────────
@tool
def search_knowledge_base(query: str) -> str:
    """Search internal company documents for policies, product specs,
    and procedures. Use this for any question requiring domain-specific
    knowledge rather than general world knowledge."""
    response = query_engine.query(query)
    return str(response)
tools = [search_knowledge_base]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: State) -> dict:
    system_prompt = SystemMessage(content=(
        "You are a helpful assistant with access to an internal knowledge base. "
        "Use search_knowledge_base for company-specific questions. "
        "Answer general questions directly."
    ))
    response = llm_with_tools.invoke([system_prompt] + state["messages"])
    return {"messages": [response]}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
    {"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())

Où cela échoue

Le RAG naïf repose sur une hypothèse unique, et cette hypothèse échoue dans deux situations courantes.

La première concerne un manque de correspondance au niveau du vocabulaire. Un utilisateur demande les conditions SLA applicables aux clients de niveau 1, mais le document promet un temps de disponibilité de 99,9 % aux clients de niveau Gold. Les termes SLA, niveau 1 et niveau Gold sont liés mais ne correspondent pas exactement, ce qui peut amener le système à classer ce fragment de manière trop basse, faisant ainsi passer à côté de la réponse.

La seconde situation concerne les questions relationnelles. Pour savoir quels produits sont affectés en cas de panne d’un fournisseur donné, il est nécessaire de suivre une série de relations à travers plusieurs documents. Aucun fragment isolé ne contient la réponse, et le RAG naïf renvoie des extraits provenant de documents différents sans moyen de les relier entre eux.

Ces deux modes d’échec sont ceux que les deux architectures suivantes visent à résoudre.

Architecture 2 : RAG hybride

Le RAG hybride reflète ce que les professionnels ont appris en pratique : la similarité sémantique et la correspondance exacte des mots-clés sont des indicateurs complémentaires de pertinence, et aucun d’eux seul ne suffit.

La récupération dense, qui correspond à la recherche vectorielle, gère bien les paraphrases : si l’on pose une question sur un SLA, elle peut encore trouver le passage concernant le temps de disponibilité de niveau Gold. En revanche, elle échoue avec les noms propres, les codes de produits, les termes cliniques, les citations juridiques et d’autres expressions spécialisées où la chaîne de caractères exacte porte le sens.

La récupération sparse (BM25 ou recherche par mots-clés) est son opposé. Elle excelle dans la correspondance exacte des termes et trouvera toujours « SKU-4829 » lors d’une recherche sur « SKU-4829 », mais elle ne comprend pas l’équivalence sémantique, donc elle ne trouvera pas « garantie de temps de disponibilité » pour « SLA ».

Le RAG hybride effectue les deux recherches en parallèle, puis utilise un réclassificateur pour transformer les résultats fusionnés en une seule liste classée. Le réclassificateur est l’élément clé. Les modèles d’incorporation comparent une requête et un fragment de manière indépendante, tandis qu’un réclassificateur à codage croisé lit la requête et chaque candidat ensemble pour produire directement un score indiquant leur relation. Il est plus lent que la recherche par similarité, mais il ne traite que l’ensemble de candidats déjà réduit, ce qui permet de garder la latence gérable.

Le code

La version LlamaIndex combine un récupérateur dense, un récupérateur BM25 et une fusion de rangs réciproques :

# ============================================================
# HYBRID RAG — LLAMAINDEX IMPLEMENTATION
# pip install llama-index-retrievers-bm25 rank-bm25
# ============================================================

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from langchain_core.tools import tool
# Build document store and index as before
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── DENSE RETRIEVER (vector similarity) ──────────────────────
vector_retriever = VectorIndexRetriever(
    index=index,
    similarity_top_k=5,  # Fetch more candidates pre-rerank
)
# ── SPARSE RETRIEVER (BM25 keyword match) ────────────────────
# BM25Retriever builds directly from the index's nodes
bm25_retriever = BM25Retriever.from_defaults(
    index=index,
    similarity_top_k=5,
)
# ── FUSION: Merge both retrievers with Reciprocal Rank Fusion ─
# QueryFusionRetriever is LlamaIndex's built-in hybrid combiner
# mode="reciprocal_rerank" implements the RRF algorithm:
# it combines ranked lists without needing score calibration
hybrid_retriever = QueryFusionRetriever(
    retrievers=[vector_retriever, bm25_retriever],
    similarity_top_k=3,        # Final top-k after fusion
    num_queries=1,             # No query expansion in this mode
    mode="reciprocal_rerank",  # The fusion algorithm
    use_async=True,            # Run both retrievers in parallel
)
# Build a QueryEngine on top of the hybrid retriever
hybrid_query_engine = RetrieverQueryEngine.from_args(
    retriever=hybrid_retriever,
)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_hybrid(query: str) -> str:
    """Search internal knowledge base using both semantic similarity
    and keyword matching. More precise than pure vector search,
    especially for technical terms, product codes, and exact names."""
    response = hybrid_query_engine.query(query)
    return str(response)
# The LangGraph graph structure is identical to Naive RAG.
# You are only swapping the tool. Everything from Module 4 onward
# stays exactly the same.

Taille des blocs dans la recherche hybride

Avec la recherche hybride, on indexe généralement des blocs plus petits que pour une recherche vectorielle classique. BM25 perçoit un signal plus fort lorsqu’un terme se répète dans un bloc court qu’en un bloc long. Une première configuration raisonnable consiste à utiliser environ 256 tokens avec 30 tokens d’overlap une fois BM25 intégré au pipeline.

Quand la recherche hybride est appropriée

Utilisez-la chaque fois que les documents combinent du texte libre avec de la terminologie structurée, ce qui couvre presque tous les cas d’usage professionnels importants. Parmi les exemples, on trouve l’examen juridique, où les formules de citation doivent correspondre exactement, les dossiers médicaux contenant des noms de médicaments précis, des codes de dosage et des codes ICD, l’analyse financière avec des symboles boursiers et des identifiants de clauses, ainsi que la documentation technique avec des codes d’erreur, des noms de méthodes API et des numéros de version.

Architecture 3 : graph RAG

Graph RAG redéfinit l’unité de connaissance. Dans les approches RAG naïves et hybrides, un document n’est qu’un texte et la récupération sélectionne les passages qui ressemblent le plus à la requête. Ici, les documents deviennent des entités reliées par des relations, et la récupération suit les chemins au sein de ce réseau.

Considérez la question de savoir quels clients d’entreprise seraient affectés si le module d’authentification obsolète était retiré. Un système naïf chercherait des extraits mentionnant à la fois « clients d’entreprise » et « module d’authentification » et en trouverait peut-être quelques-uns. Ce qui est vraiment nécessaire, c’est une chaîne : du module aux produits qui en dépendent, de ces produits aux clients qui les utilisent. Aucun extrait isolé ne contient cette réponse, qui émerge de la structure du graphe de connaissances. Graph RAG vise précisément cette situation, que la tâche consiste à sauter d’un lien à l’autre, à tracer une relation ou à répondre à une question nécessitant des faits provenant de parties éloignées du corpus.

Comment l’index est construit

Au lieu de diviser les documents en fragments et d’incorporer ces fragments, Graph RAG commence par une étape d’extraction d’entités sur chaque document. Il identifie les entités nommées (produits, personnes, organisations, concepts) ainsi que les relations entre elles, telles que « dépend de », « est un client de », « a été écrit par » ou « a été remplacé par ». Ceux-ci deviennent les nœuds et les arêtes d’un graphe de connaissances. Les algorithmes de clustering de graphes regroupent ensuite les entités étroitement liées en communautés, et un LLM rédige un résumé pour chaque communauté. Lors d’une requête, le système recherche les résumés des communautés et parcourt le graphe pour atteindre les entités pertinentes.

Le code

L’index de graphe de propriétés de LlamaIndex gère le pipeline d’extraction :

# ============================================================
# GRAPH RAG — LLAMAINDEX PROPERTY GRAPH INDEX
# pip install llama-index-core
# ============================================================
from llama_index.core import SimpleDirectoryReader, PropertyGraphIndex
from llama_index.core.indices.property_graph import (
    ImplicitPathExtractor,
    SimpleLLMPathExtractor,
)
from langchain_core.tools import tool
# Load documents
documents = SimpleDirectoryReader("./data").load_data()
# ── BUILD THE PROPERTY GRAPH INDEX ───────────────────────────
# LlamaIndex's PropertyGraphIndex handles the full extraction pipeline.
# SimpleLLMPathExtractor uses an LLM to extract (subject, relation, object)
# triples from each chunk - these become graph edges.
# ImplicitPathExtractor uses fast heuristics (cheaper, less precise).
index = PropertyGraphIndex.from_documents(
    documents,
    kg_extractors=[
        # LLM-based extraction: higher quality, higher cost
        SimpleLLMPathExtractor(
            llm=Settings.llm,
            max_paths_per_chunk=10,
        ),
        # Heuristic extraction: fast fallback
        ImplicitPathExtractor(),
    ],
    show_progress=True,
)
# ── CREATE A GRAPH-AWARE RETRIEVER ────────────────────────────
# This retriever traverses the graph rather than searching flat vectors.
kg_retriever = index.as_retriever(
    include_text=True,       # Include surrounding text with each entity
    retriever_mode="hybrid", # Combines keyword + embedding on the graph
    similarity_top_k=3,
)
from llama_index.core.query_engine import RetrieverQueryEngine
graph_query_engine = RetrieverQueryEngine.from_args(retriever=kg_retriever)
# ── LANGGRAPH TOOL ────────────────────────────────────────────
@tool
def search_knowledge_graph(query: str) -> str:
    """Search the knowledge graph for questions involving relationships
    between entities - dependencies, organizational hierarchies, impact
    analysis, and multi-hop reasoning across connected information.
    Use this when the answer requires tracing a chain of relationships
    rather than finding a single relevant document."""
    response = graph_query_engine.query(query)
    return str(response)
# ── COMBINING WITH NAIVE RAG: DUAL-TOOL AGENT ─────────────────
# In practice, Graph RAG and Naive RAG are often combined.
# The agent's LLM decides which tool fits the query.
@tool
def search_knowledge_base(query: str) -> str:
    """Search for factual information in company documents.
    Best for direct questions with answers in a single document."""
    response = query_engine.query(query)  # The naive query engine
    return str(response)
# Give the LangGraph agent BOTH tools.
# It will route to the right one based on the question type.
tools = [search_knowledge_base, search_knowledge_graph]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# Graph assembly remains identical - only the tool list changes.

Avertissement concernant les coûts

L’extraction d’entités effectue une requête vers un LLM pour chaque morceau de texte, ce qui signifie que pour un grand ensemble de documents, des milliers de requêtes sont nécessaires lors de l’indexation. Cela est coûteux et lent par conception : le prix payé permet d’obtenir une structure de données bien plus riche, une seule fois. Un index graphique RAG ne doit jamais être reconstruit à chaque démarrage. Il suffit de conserver le graphique et de le recharger, exactement comme pour la version traditionnelle.

Quand le choisir

Choisissez le graphique RAG lorsque les questions exigent de suivre des chaînes de relations. Les cas typiques sont l’analyse de conformité et des risques (quelles procédures sont affectées par une réglementation donnée), l’intelligence de la chaîne d’approvisionnement (quels produits dépendent d’un fournisseur), les connaissances organisationnelles (qui possède quoi et comment ces chaînes de propriété se connectent) ainsi que la cartographie des dépendances logicielles (ce qui cesse de fonctionner si un module est supprimé).

Architecture 4 : RAG avancé

Le RAG avancé n’est pas une technique nouvelle unique. Il s’agit d’un ensemble structuré d’améliorations ajoutées à n’importe quelle méthode de récupération de base que vous possédez déjà. Le RAG naïf se fie à la première tentative de récupération, tandis que le RAG avancé la remet en question, la affine et la valide. Ces améliorations se répartissent en trois groupes correspondant à différents étapes du processus : avant la récupération, pendant le traitement de la requête, et après la récupération.

Avant la récupération : réécriture de la requête et HyDE

Ce que tape un utilisateur n’est que rarement la meilleure requête de recherche. Une question informelle concernant la politique de retour pour les clients entreprises est tout à fait naturelle pour un être humain, mais elle produit de moins bons résultats que des expressions comme "procédures de retour et d’indemnisation pour les clients entreprises". La réécriture de la requête permet à l’LLM de transformer la question en une meilleure requête de recherche avant qu’elle n’atteigne l’index.

HyDE, abréviation de Hypothetical Document Embedding, emprunte une autre approche. Au lieu de rechercher à partir de la question, il demande au LLM d’écrire un document hypothétique qui y répondrait, puis il incorpore ce document dans la recherche. L’idée est que le texte ayant la forme d’une réponse se trouve plus proche, dans l’espace vectoriel, d’autres textes ayant également la forme d’une réponse que le texte ayant la forme d’une question.

Décomposition de la requête

Les questions à plusieurs étapes contournent les limites du RAG naïf, car elles nécessitent plusieurs recherches. Le RAG avancé les décompose d’abord. Demander de comparer la politique de remboursement pour les clients entreprises et les clients de détail et de résumer les principales différences revient en réalité à trois tâches : récupérer la politique des entreprises, récupérer la politique des détails, puis les comparer. La décomposition permet de diviser la requête en sous-requêtes, de procéder à des recherches pour chacune d’elles et de fusionner les résultats avant de les synthétiser.

Après la récupération : réclassement et CRAG

Le réclassement, déjà abordé dans le cadre de la recherche hybride, revient en jeu à ce stade : un encodeur croisé réévalue tous les fragments récupérés, même si la récupération s’est faite de manière purement dense.

CRAG (Corrective RAG) est la technique post-récupération la plus sophistiquée. Après la récupération, elle effectue une vérification rapide de pertinence des fragments. S’ils semblent insuffisants, elle se tourne vers une autre source, comme une recherche sur le web ou un index plus vaste, afin que le LLM ne soit pas contraint de répondre à partir d’un contexte faible.

Le code

Une implémentation réunit plusieurs de ces techniques :

# ============================================================
# ADVANCED RAG — MULTI-TECHNIQUE IMPLEMENTATION
# ============================================================

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex
from llama_index.core.query_engine import (
    SubQuestionQueryEngine,    # Handles query decomposition
    RetrieverQueryEngine,
)
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.core.postprocessor import SentenceTransformerRerank
from langchain_core.tools import tool
documents = SimpleDirectoryReader("./data").load_data()
index = VectorStoreIndex.from_documents(documents)
# ── RERANKER: Post-retrieval cross-encoder scoring ────────────
# Fetch 8 candidates, rerank down to 3
# This is the most impactful single improvement in Advanced RAG
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-2-v2",
    top_n=3,
)
# ── RETRIEVER WITH RERANKING ──────────────────────────────────
base_retriever = index.as_retriever(similarity_top_k=8)
reranked_engine = RetrieverQueryEngine.from_args(
    retriever=base_retriever,
    node_postprocessors=[reranker],  # Applied after retrieval
)
# ── QUERY DECOMPOSITION with SubQuestionQueryEngine ───────────
# Wrap the base engine as a "tool" that the decomposer can call
engine_tools = [
    QueryEngineTool(
        query_engine=reranked_engine,
        metadata=ToolMetadata(
            name="company_knowledge_base",
            description=(
                "Searches company documents for policies, procedures, "
                "and product information."
            ),
        ),
    )
]
# SubQuestionQueryEngine decomposes complex questions into
# sub-questions, runs each against the available tools,
# and synthesizes a final answer from all sub-answers
decomposed_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=engine_tools,
    use_async=True,  # Sub-queries run in parallel when possible
)
# ── HYDE: Hypothetical Document Embedding ─────────────────────
from llama_index.core.indices.query.query_transform.base import (
    HyDEQueryTransform,
)
from llama_index.core.query_engine import TransformQueryEngine
hyde_transform = HyDEQueryTransform(include_original=True)
hyde_engine = TransformQueryEngine(
    query_engine=reranked_engine,
    query_transform=hyde_transform,
)
# ── LANGGRAPH TOOLS: Different engines for different patterns ──
@tool
def search_with_decomposition(query: str) -> str:
    """Search for answers to complex questions that may require
    combining information from multiple documents. Automatically
    breaks the question into sub-questions and merges the results.
    Best for comparison questions, multi-part questions, and anything
    requiring synthesis across different topics."""
    response = decomposed_engine.query(query)
    return str(response)
@tool
def search_with_hyde(query: str) -> str:
    """Search the knowledge base using hypothetical document embedding.
    More effective than standard search for abstract or exploratory
    questions where the exact terminology in the answer differs from
    the terminology in the question."""
    response = hyde_engine.query(query)
    return str(response)
# The LangGraph agent now has specialized retrieval tools
# and its LLM decides which retrieval strategy each question needs.
tools = [search_with_decomposition, search_with_hyde]

La règle à suivre

Évitez de tout combiner en même temps. Choisissez la modification la plus susceptible de résoudre le problème que vous observez réellement. La réclassification est généralement la meilleure première étape, suivie de la décomposition ; HyDE constitue une troisième option valable pour les collections abstraites ou conceptuelles. Avancez progressivement et vérifiez la mémorisation après chaque étape.

Architecture 5 : RAG agent

Le RAG agent va au-delà d’une simple amélioration de la récupération des informations : il confie les décisions de récupération à un autre élément. Les quatre architectures précédentes sont des pipelines, des séquences fixes qui se comportent de la même manière à chaque exécution. Ici, la récupération devient un cycle dans lequel un agent LLM choisit ce à rechercher, évalue les résultats, décide s’il faut rechercher à nouveau, s’arrête lorsqu’il a suffisamment d’informations, ou conclut que la question n’a pas de réponse.

C’est précisément à cela que LangGraph a été conçu. Le nœud d’agent, le nœud d’outil et le bord conditionnel forment déjà une structure de base pour un RAG agent. Ce qui change, c’est la richesse de l’ensemble d’outils et la complexité de la logique de routage qui en découle.

Plusieurs outils de récupération en même temps

La véritable force du RAG agent dans LangGraph réside dans le fait que l’agent peut recevoir simultanément toutes les stratégies de récupération décrites ci-dessus, et son LLM choisit l’outil adapté à chaque sous-question. Cette structure permet à un seul agent d’accéder aux cinq architectures disponibles :

# ============================================================
# AGENTIC RAG — COMPLETE MULTI-TOOL TEMPLATE
# This is the full production scaffold: all five architectures
# available to a single agent, which selects dynamically.
# ============================================================

# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import Literal, Annotated
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.memory import MemorySaver
from llama_index.core import (
    Settings, SimpleDirectoryReader, VectorStoreIndex,
    StorageContext, load_index_from_storage, PropertyGraphIndex
)
from llama_index.core.indices.property_graph import SimpleLLMPathExtractor
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.query_engine import RetrieverQueryEngine, SubQuestionQueryEngine
from llama_index.core.tools import QueryEngineTool, ToolMetadata
from llama_index.llms.openai import OpenAI as LlamaOpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever, VectorIndexRetriever
# The agent's main reasoning model (LangGraph)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# LlamaIndex internal configuration
Settings.llm = LlamaOpenAI(model="gpt-4o-mini", temperature=0.1)
Settings.embed_model = OpenAIEmbedding(model="text-embedding-3-small")
Settings.chunk_size = 512
Settings.chunk_overlap = 50
# ── MODULE 2: STATE ──────────────────────────────────────────
class AgentState(MessagesState):
    # Extend with retrieval metadata if you need observability
    retrieval_count: int = 0  # Track how many retrieval calls were made
# ── MODULE 3: KNOWLEDGE BASES (all built at startup) ──────────
VECTOR_DIR = "./storage/vector"
GRAPH_DIR  = "./storage/graph"
documents  = SimpleDirectoryReader("./data").load_data()
# Vector index (for Naive, Hybrid, Advanced)
if os.path.exists(VECTOR_DIR):
    ctx = StorageContext.from_defaults(persist_dir=VECTOR_DIR)
    vector_index = load_index_from_storage(ctx)
else:
    vector_index = VectorStoreIndex.from_documents(documents, show_progress=True)
    vector_index.storage_context.persist(persist_dir=VECTOR_DIR)
# Property Graph index (for Graph RAG)
if os.path.exists(GRAPH_DIR):
    ctx = StorageContext.from_defaults(persist_dir=GRAPH_DIR)
    graph_index = load_index_from_storage(ctx)
else:
    graph_index = PropertyGraphIndex.from_documents(
        documents,
        kg_extractors=[SimpleLLMPathExtractor(llm=Settings.llm)],
        show_progress=True,
    )
    graph_index.storage_context.persist(persist_dir=GRAPH_DIR)
# ── Set up individual engines ─────────────────────────────────
reranker = SentenceTransformerRerank(
    model="cross-encoder/ms-marco-MiniLM-L-2-v2", top_n=3
)
# Tool 1: Naive / vector search
vector_engine = vector_index.as_query_engine(
    similarity_top_k=3,
    node_postprocessors=[reranker],
)
# Tool 2: Hybrid (vector + BM25)
hybrid_retriever = QueryFusionRetriever(
    retrievers=[
        VectorIndexRetriever(index=vector_index, similarity_top_k=5),
        BM25Retriever.from_defaults(index=vector_index, similarity_top_k=5),
    ],
    similarity_top_k=3,
    mode="reciprocal_rerank",
    use_async=True,
)
hybrid_engine = RetrieverQueryEngine.from_args(
    retriever=hybrid_retriever,
    node_postprocessors=[reranker],
)
# Tool 3: Graph traversal
graph_engine = graph_index.as_query_engine(
    include_text=True,
    retriever_mode="hybrid",
    similarity_top_k=3,
)
# Tool 4: Decomposed (for complex multi-part questions)
decomposed_engine = SubQuestionQueryEngine.from_defaults(
    query_engine_tools=[
        QueryEngineTool(
            query_engine=vector_engine,
            metadata=ToolMetadata(
                name="docs",
                description="Company documents and policies."
            )
        )
    ],
    use_async=True,
)
# ── MODULE 3 (cont.): ALL TOOLS ───────────────────────────────
@tool
def search_documents(query: str) -> str:
    """Search company documents by semantic meaning. Best for
    conceptual questions where the exact wording in the answer
    may differ from the question."""
    return str(vector_engine.query(query))
@tool
def search_exact_terms(query: str) -> str:
    """Search using both keyword and semantic matching. Best when
    the query contains specific terminology, product codes, names,
    or exact phrases that must appear in the result."""
    return str(hybrid_engine.query(query))
@tool
def search_relationships(query: str) -> str:
    """Search the knowledge graph for questions about how things
    connect: dependencies, impact chains, organizational links,
    and multi-hop reasoning. Use when the answer requires tracing
    a relationship across multiple entities."""
    return str(graph_engine.query(query))
@tool
def search_complex_question(query: str) -> str:
    """For multi-part questions requiring synthesis across several
    topics. Automatically decomposes the question into sub-queries,
    retrieves each independently, and combines the results."""
    return str(decomposed_engine.query(query))
tools = [
    search_documents,
    search_exact_terms,
    search_relationships,
    search_complex_question,
]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ──────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
    system_prompt = SystemMessage(content=(
        "You are a precise research assistant with access to four retrieval tools:\n\n"
        "1. search_documents - semantic search over company documents\n"
        "2. search_exact_terms - hybrid semantic + keyword search\n"
        "3. search_relationships - graph traversal for relationship questions\n"
        "4. search_complex_question - decomposed retrieval for multi-part questions\n\n"
        "Think step by step. Use the tool that best fits the question type. "
        "You may call multiple tools if a question has multiple parts. "
        "Only answer when you have retrieved sufficient evidence."
    ))
    response = llm_with_tools.invoke([system_prompt] + state["messages"])
    return {
        "messages": [response],
        "retrieval_count": state.get("retrieval_count", 0),
    }
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
    last = state["messages"][-1]
    if hasattr(last, "tool_calls") and last.tool_calls:
        return "tools"
    return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue,
    {"tools": "tools", "__end__": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=MemorySaver())
# ── MODULE 7: ENTRYPOINT ──────────────────────────────────────
if __name__ == "__main__":
    config = {"configurable": {"thread_id": "agentic-session-001"}}
    print("Agentic RAG ready. Ask anything.\n")
    while True:
        user_text = input("You: ").strip()
        if not user_text or user_text.lower() in ("exit", "quit"):
            break
        response = graph.invoke(
            {"messages": [HumanMessage(content=user_text)]},
            config=config,
        )
        print(f"\nAgent: {response['messages'][-1].content}\n")

Ce que les pipelines ne peuvent pas faire

Ce qui le distingue des autres, c’est la capacité d’auto-correction. Un pipeline ne peut pas se rendre compte qu’il a obtenu le mauvais matériel ; un agent, lui, le peut. Lorsque les premiers résultats sont insuffisants, son étape de raisonnement suivante réécrit la requête avec de nouveaux termes. Lorsqu’une question cache une dépendance, l’agent la détecte en cours de route et obtient plus d’informations. Lorsque la question est vague, il peut interroger l’utilisateur avant de rechercher.

Recourrez à cette architecture lorsque la réponse erronée a de lourdes conséquences, comme dans les domaines du respect des réglementations, du droit, de la finance ou de la médecine, car des étapes de vérification, des seuils de confiance et des points de contrôle impliquant un humain peuvent être intégrés directement dans le graphe de l’agent.

Le prix à payer en termes de latence

Il est impossible de cacher le ralentissement. Les pipelines à outil unique répondent en environ 200 à 500 millisecondes ; un agent effectuant trois appels de récupération peut quant à lui mettre de 8 à 12 secondes, ce qui est trop lent pour le flux principal de nombreuses interfaces en temps réel. Deux approches permettent d’atténuer ce problème : diffuser les étapes intermédiaires afin que les utilisateurs puissent suivre les progrès plutôt que d’attendre en silence, et exécuter en arrière-plan des opérations de récupération par agent pour précharger les éléments susceptibles d’être nécessaires par la suite.

Choix et combinaisons

Chaque problème de récupération a une solution adaptée parmi ces cinq méthodes. Pour prendre une décision approximative : commencez par RAG naïf pour les questions-réponses sur des documents factuels et clairs, passez à une approche hybride lorsque la terminologie est spécialisée ou que des identifiants précis sont essentiels ; optez pour RAG graphique lorsque les réponses dépendent des relations entre documents ; intégrez des techniques avancées lorsque la qualité de la récupération doit être améliorée ; et adoptez un agent lorsque la tâche est ouverte, à haut risque ou nécessite une auto-correction.

Les architectures sont également composables, il n’est donc pas nécessaire de choisir une seule et d’écarter les autres. Le modèle de production le plus courant est une pile. Dans LangGraph, cette pile se traduit directement par la liste des outils. Un agent doté d’un outil naïf, d’un outil hybride, d’un outil graphique et d’un outil de requête décomposée représente précisément cette pile à cinq architectures, l’agent servant en tant que couche supérieure qui sélectionne et orchestre les autres à chaque tour.

Carte de référence des mots-clés

Cette carte énumère les principales classes et paramètres de chaque couche :

NAIVE RAG
SimpleDirectoryReader           Load files into Document objects
VectorStoreIndex.from_documents Build the embed-and-store index
index.as_query_engine()         Full retrieve-and-answer pipeline
index.as_retriever()            Retrieve only (no answer generation)
Settings.chunk_size             Token size per Node
Settings.chunk_overlap          Token overlap between adjacent Nodes

HYBRID RAG
BM25Retriever                   Keyword-based sparse retriever
VectorIndexRetriever            Dense embedding retriever
QueryFusionRetriever            Merges multiple retrievers (RRF algorithm)
SentenceTransformerRerank       Cross-encoder reranker for post-retrieval

GRAPH RAG
PropertyGraphIndex              Builds a knowledge graph from documents
SimpleLLMPathExtractor          LLM-based entity and relation extraction
ImplicitPathExtractor           Heuristic-based entity extraction (fast)

ADVANCED RAG
SubQuestionQueryEngine          Decomposes complex queries into sub-queries
HyDEQueryTransform              Hypothetical Document Embedding transform
TransformQueryEngine            Wraps any engine with a query transform
node_postprocessors             Where rerankers and filters attach

AGENTIC RAG (LANGGRAPH LAYER)
@tool                           The bridge - every LlamaIndex engine becomes
                                a LangGraph tool through this decorator
ToolNode                        Executes whatever tool the agent selects
bind_tools()                    Gives the agent LLM its tool registry
MemorySaver / SqliteSaver       Thread-level memory across turns (Part 2)
interrupt()                     Human approval checkpoint before retrieval (Part 3)

La récupération en tant qu’infrastructure

Ces cinq approches ne représentent pas cinq manières de faire la même chose. Ce sont des solutions à cinq problèmes de récupération différents, dans une progression logique allant du simple au plus sophistiqué.

  • Naive RAG : rapide, peu coûteux, et idéal pour la plupart des questions-réponses sur des documents simples.
  • Hybrid RAG : la solution par défaut appropriée lorsque le vocabulaire est spécialisé.
  • Graph RAG : le choix à retenir lorsque les liens entre faits sont plus importants que n’importe quel document isolé.
  • Advanced RAG : le modèle permettant d’améliorer la précision lorsque le point faible réside dans la qualité de la récupération.
  • Agentic RAG : adapté aux tâches ouvertes et à haut risque où le système doit raisonner de manière autonome.

En utilisant LlamaIndex pour la couche de données et LangGraph pour l’orchestration, ces modèles couvrent presque tous les besoins d’une application de récupération en production.