Les systèmes de génération augmentée par récupération (RAG) traditionnels peinent souvent avec les requêtes multi-sauts. Ce sont des questions qui nécessitent de synthétiser des informations provenant de plusieurs sources distinctes ou étapes de raisonnement. Par exemple, demander « Quel était le premier produit lancé par l'entreprise fondée par l'auteur de Python ? » nécessite d'abord d'identifier l'auteur (Guido van Rossum), puis de trouver l'entreprise qu'il a fondée (bien qu'il ait travaillé au Cwi, la question pourrait faire référence à une startup spécifique qu'il a rejointe ou quittée plus tard), et enfin de récupérer les données de lancement du produit. Une seule étape de récupération fournit rarement le contexte complet nécessaire pour combler ces lacunes.
Le RAG auto-affiné adresse cette limitation en introduisant une boucle itérative où le modèle de langage (LLM) agit non seulement comme un générateur, mais aussi comme un planificateur et un critique. Il évalue si le contexte récupéré est suffisant pour répondre à la requête. Sinon, il génère de nouvelles requêtes de recherche plus ciblées basées sur la compréhension partielle actuelle, récupère de nouveaux documents et répète le processus jusqu'à ce qu'il puisse produire une réponse fiable ou atteigne la limite maximale d'itérations.
L'architecture de la boucle itérative
La logique centrale d'un système RAG auto-affiné repose sur trois étapes au sein de chaque itération :
- Décomposition et évaluation de la requête : Le LLM reçoit la question originale et le contexte actuel (fragments récupérés). Il détermine si le contexte contient suffisamment d'informations pour répondre définitivement à la question.
- Génération d'une nouvelle requête : Si le contexte est insuffisant, le LLM identifie l'information manquante et formule une requête de recherche spécifique pour la récupérer.
- Récupération et accumulation : La nouvelle requête est envoyée à la base de données vectorielle. Les résultats sont ajoutés au contexte existant, et la boucle se répète.
Implémentation en Python
Ci-dessous se trouve une implémentation simplifiée utilisant les concepts de LangChain pour démontrer le flux. Cet exemple suppose que vous avez initialisé un `vectorstore` et un `llm`.
from langchain.llms import LLM
from langchain.vectorstores import VectorStore
from langchain.chains import RetrievalQA
class SelfRefiningRAG:
def __init__(self, llm: LLM, vectorstore: VectorStore, max_iterations: int = 3):
self.llm = llm
self.vectorstore = vectorstore
self.max_iterations = max_iterations
def retrieve(self, query: str) -> list:
# Recherche de similarité vectorielle standard
return self.vectorstore.similarity_search(query, k=3)
def is_sufficient(self, question: str, context: list) -> bool:
prompt = f"""
Question: {question}
Contexte: {contexte}
Le contexte contient-il des informations suffisantes pour répondre à la question ?
Répondez uniquement par "Oui" ou "Non".
"""
response = self.llm(prompt)
return response.strip().lower() == "oui"
def generate_next_query(self, question: str, context: list) -> str:
prompt = f"""
Question originale: {question}
Contexte actuel: {contexte}
Le contexte actuel n'est pas suffisant pour répondre à la question.
Identifiez l'information manquante et générez une seule requête de recherche
pour récupérer cette information manquante.
Sortez uniquement la requête de recherche.
"""
return self.llm(prompt).strip()
def run(self, question: str) -> str:
context_docs = []
for i in range(self.max_iterations):
# 1. Récupération
if not context_docs:
# La première itération utilise la question originale
context_docs = self.retrieve(question)
else:
# Les itérations suivantes utilisent une requête affinée
new_query = self.generate_next_query(question, context_docs)
new_docs = self.retrieve(new_query)
context_docs.extend(new_docs)
# 2. Évaluation
if self.is_sufficient(question, [d.page_content for d in context_docs]):
break
# 3. Génération finale
final_context = "\n\n".join([d.page_content for d in context_docs])
final_prompt = f"""
Contexte: {final_context}
Question: {question}
Réponse:
"""
return self.llm(final_prompt)
Optimisation pour l'efficacité et la qualité
Bien que la logique ci-dessus soit fonctionnelle, les systèmes de production nécessitent des optimisations pour éviter les boucles infinies et réduire la latence.
1. Déduplication du contexte
La récupération itérative récupère souvent les mêmes documents plusieurs fois. Implémentez une étape de déduplication (par exemple, en utilisant l'empreinte numérique de l'ID du document ou la similarité du contenu) avant d'ajouter de nouveaux résultats à context_docs. Cela empêche le prompt de gonfler inutilement.
2. Critères d'arrêt anticipé
La vérification is_sufficient est coûteuse (elle nécessite un appel LLM). Vous pouvez l'optimiser en utilisant d'abord une heuristique moins coûteuse (par exemple, le chevauchement de mots-clés) ou en limitant l'évaluation par le LLM aux cas où les documents récupérés ont un score de pertinence inférieur à un certain seuil.
3. Sortie structurée pour les requêtes
Au lieu de requêtes suivantes en texte libre, utilisez une sortie structurée (JSON) pour vous assurer que le LLM fournit un terme de recherche clair et spécifique. Cela améliore la précision de la recherche vectorielle dans les itérations suivantes.
Exemple pratique : Raisonnement multi-sauts
Considérez la requête : « Qui était le PDG de l'entreprise qui a acquis la startup technologique fondée par l'auteur de React ? »
- Itération 1 :
- Récupération : Recherche de « auteur de React » -> Trouve « Jordan Walke ».
- Évaluation : Non, nous ne connaissons ni la startup ni le PDG.
- Nouvelle requête : « Quelle startup Jordan Walke a-t-il fondée ? »
- Itération 2 :
- Récupération : Trouve « Jordan Walke a fondé 8080 Labs ».
- Évaluation : Non, nous ne savons pas qui l'a acquise ni qui est le PDG actuel.
- Nouvelle requête : « Qui a acquis 8080 Labs et qui est leur PDG ? »
- Itération 3 :
- Récupération : Trouve que 8080 Labs a été acquise par Facebook (Meta). Récupère le PDG de Meta (Mark Zuckerberg).
- Évaluation : Oui, informations suffisantes disponibles.
- Réponse finale : « Mark Zuckerberg est le PDG de Meta, qui a acquis 8080 Labs. »
Conclusion
Le RAG auto-affiné transforme la récupération statique en un processus dynamique et agentique. En permettant au LLM d'affiner itérativement sa compréhension et de demander des pièces d'information spécifiques manquantes, vous pouvez améliorer considérablement la précision sur les requêtes complexes multi-sauts. Cependant, cela se fait au prix d'une latence accrue et de coûts d'API plus élevés en raison de multiples appels LLM et de récupération. Utilisez ce motif avec discernement, en le réservant aux requêtes à haute valeur où la profondeur du raisonnement justifie la surcharge. Pour les tâches plus simples, un RAG en un seul passage reste le choix le plus efficace.