Retrieval-Augmented Generation (RAG)

RAG auto-affiné : Boucles itératives pour les requêtes multi-sauts complexes

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 :

  1. 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.
  2. 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.
  3. 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 ? »

  1. 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 ? »
  2. 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 ? »
  3. 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.

Share: