Aller au contenu
Développement medium

RAG agentique : un retrieval qui se corrige

11 min de lecture

logo python

Un RAG classique fait une seule recherche, puis répond. Si cette recherche est mauvaise — question ambiguë, mal formulée —, la réponse l'est aussi, et rien ne le rattrape. Le RAG agentique brise ce schéma figé : il transforme le retrieval en une boucle qui se corrige. Un graphe LangGraph cherche, évalue si le contexte récupéré suffit, et sinon reformule la question pour relancer une recherche — jusqu'à un contexte jugé suffisant ou un plafond atteint. Ce guide construit ce graphe nœud par nœud, et explique quand le RAG agentique vaut son surcoût. Public visé : développeur qui maîtrise le RAG de base et LangGraph, et veut un retrieval robuste.

  • Pourquoi un retrieval unique est un point de fragilité.
  • Modéliser un RAG comme une boucle avec LangGraph.
  • Faire évaluer par un nœud LLM si le contexte suffit.
  • Reformuler la question pour relancer une recherche.
  • Borner la boucle et savoir quand le RAG agentique vaut son coût.

Le RAG des guides précédents est linéaire : on vectorise la question, on cherche, on injecte le contexte, on répond. Une chaîne, un seul passage.

Cette linéarité a un défaut. Si la recherche échoue — la question est vague, formulée autrement que les documents, ou trop large —, le contexte récupéré est mauvais, et le RAG répond mal sans le savoir. Aucune étape ne vérifie que le contexte tient la route avant de générer.

Le RAG agentique ajoute cette vérification, et la transforme en boucle. Au lieu de « cherche puis réponds », le schéma devient « cherche, juge, et si ce n'est pas bon, réessaie autrement ». Le retrieval gagne une capacité d'auto-correction — exactement ce qu'apporte un agent par rapport à un script.

Une boucle qui se corrige se modélise mal avec une fonction linéaire — mais très bien avec un graphe d'états LangGraph. On reprend le modèle des guides sur les agents : un état partagé, des nœuds qui le transforment, des arêtes qui décident du parcours.

L'état porte tout ce que la boucle manipule :

from typing_extensions import TypedDict
class EtatRag(TypedDict):
question: str # la question d'origine, jamais modifiée
requete: str # la requête courante, éventuellement reformulée
contexte: list[str] # les chunks récupérés
suffisant: bool # le contexte permet-il de répondre ?
tentatives: int # nombre de recherches effectuées
reponse: str # la réponse finale

La distinction entre question et requete est centrale. La question est ce que l'utilisateur a demandé — elle ne change jamais. La requete est ce avec quoi on cherche — elle, peut être reformulée. Garder les deux séparées permet de reformuler sans perdre l'intention initiale.

Quatre nœuds composent le graphe. rechercher récupère les chunks les plus proches de la requete courante, et incrémente le compteur de tentatives.

def rechercher(etat: EtatRag) -> dict:
"""Récupère les chunks les plus proches de la requête courante."""
v_requete = EMBED.embed_query(etat.get("requete") or etat["question"])
# ... classement par similarité ...
return {
"contexte": [CORPUS[i] for i in classes[:2]],
"tentatives": etat.get("tentatives", 0) + 1,
}

evaluer est le cœur du dispositif : un nœud LLM qui juge si le contexte récupéré suffit. Il utilise une sortie structurée — un booléen — pour produire un verdict exploitable par le graphe.

class Evaluation(BaseModel):
suffisant: bool = Field(
description="Vrai si le contexte permet de répondre à la question"
)
def evaluer(etat: EtatRag) -> dict:
"""Juge si le contexte récupéré suffit à répondre à la question."""
verdict = LLM.with_structured_output(Evaluation).invoke([
("system", "Tu juges si un contexte suffit à répondre à une "
"question. Le contexte contient plusieurs passages, dont certains "
"peuvent être hors sujet — c'est normal. Réponds suffisant=true "
"si AU MOINS UN passage contient l'information pour répondre."),
("human", f"Question : {etat['question']}\n\nContexte :\n"
+ "\n".join(f"- {c}" for c in etat["contexte"])),
])
return {"suffisant": verdict.suffisant}

reformuler entre en jeu quand le contexte est jugé insuffisant : il demande au modèle de réécrire la question autrement, pour donner une autre chance à la recherche. repondre, enfin, génère la réponse à partir du contexte retenu.

Reste à câbler le parcours. Après evaluer, une fonction de routage décide : reformuler et rechercher encore, ou répondre maintenant.

MAX_TENTATIVES = 3
def router(etat: EtatRag) -> Literal["reformuler", "repondre"]:
"""Décide : reformuler et chercher encore, ou répondre maintenant."""
if etat["suffisant"] or etat["tentatives"] >= MAX_TENTATIVES:
return "repondre"
return "reformuler"

Deux conditions mènent à repondre. Le contexte est suffisant : inutile de chercher plus. Ou bien le plafond de tentatives est atteint : on répond avec ce qu'on a, plutôt que de boucler sans fin. Ce MAX_TENTATIVES est obligatoire — sans lui, une question impossible ferait tourner le graphe indéfiniment.

Le graphe se construit autour de cette logique : reformuler renvoie vers rechercher, formant la boucle de correction.

constructeur.add_edge(START, "rechercher")
constructeur.add_edge("rechercher", "evaluer")
constructeur.add_conditional_edges("evaluer", router, ["reformuler", "repondre"])
constructeur.add_edge("reformuler", "rechercher") # la boucle
constructeur.add_edge("repondre", END)

À l'exécution, sur une question dont le premier retrieval suffit, le graphe converge immédiatement :

Recherches effectuées : 1
Contexte suffisant : True
Réponse : Pour garder les données d'un conteneur Docker, vous pouvez
utiliser un volume...

Une recherche, une évaluation positive, une réponse. Sur une question plus difficile, le graphe boucle — reformule, recherche à nouveau — jusqu'à converger ou atteindre le plafond. Le parcours s'adapte à la difficulté de la question.

Le RAG agentique est plus robuste — mais il coûte plus cher. Chaque tour de boucle ajoute un appel d'évaluation, parfois un appel de reformulation, et une recherche de plus. Une question qui boucle trois fois consomme trois fois le travail d'un RAG classique.

Ce surcoût se justifie sur les questions complexes ou mal formulées — là où une recherche unique a de bonnes chances d'échouer, et où une seconde tentative reformulée change tout. Il ne se justifie pas sur des questions simples et directes : la boucle s'y déclenche pour rien, et alourdit la réponse.

La bonne approche est graduée. On commence par un RAG classique. Si la mesure — le recall, vu dans le guide sur le RAG en production — montre un retrieval qui échoue sur une part notable des questions, alors le RAG agentique devient pertinent. C'est la même règle que pour la recherche hybride ou le re-ranking : on mesure, puis on décide.

SymptômeCause probableSolution
La boucle ne s'arrête jamaisMAX_TENTATIVES absent ou non testéVérifier la condition d'arrêt du routeur
GraphRecursionErrorBoucle non bornéeBorner les tentatives, vérifier le routeur
L'évaluateur rejette un bon contexteConsigne trop strictePréciser qu'un passage pertinent suffit
La reformulation n'améliore rienQuestion déjà optimaleNormal : le plafond fera répondre
Coût élevé sur questions simplesRAG agentique appliqué partoutLe réserver aux questions difficiles
  • Un RAG linéaire échoue en silence quand sa recherche unique rate.
  • Le RAG agentique transforme le retrieval en boucle qui se corrige.
  • Un graphe LangGraph modélise cette boucle : nœuds rechercher, évaluer, reformuler, répondre.
  • Le nœud evaluer juge la suffisance du contexte — sa consigne doit accepter un contexte partiellement pertinent.
  • Un plafond de tentatives est obligatoire pour borner la boucle.
  • Le RAG agentique coûte plus : on le réserve aux questions difficiles, sur la foi d'une mesure.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn