Mise à l’échelle de l’inférence pour améliorer la RAG multimodale

Mise à l’échelle de l’inférence pour améliorer la RAG multimodale 

Dans le domaine de l'intelligence artificielle (IA), le dimensionnement de l'inférence désigne les techniques qui améliorent les performances des modèles en allouant des ressources informatiques pendant la phase d'inférence (lorsque les modèles génèrent des résultats) plutôt que de s'appuyer sur des jeux de données d'apprentissage plus volumineux ou sur des architectures de modèles. Les grands modèles de langage (LLM) continuent de se développer à la fois dans les paramètres du modèle et à l’échelle du jeu de données, l’optimisation du temps d’inférence et la gestion de la mise à l’échelle du calcul de l’inférence, en particulier sur le matériel GPU, sont devenues des défis centraux pour déployer des systèmes de génération augmentée de récupération multimodaux hautes performances. 

Introduction à la mise à l'échelle de l'inférence

Les récentes avancées en matière de stratégies d'inférence, qui augmentent les ressources informatiques et utilisent des algorithmes complexes au moment des tests, redéfinissent la manière dont les LLM abordent les tâches de raisonnement complexes et fournissent des résultats de meilleure qualité pour diverses modalités d'entrée. La mise à l'échelle des inférences optimise la chaîne de pensée (CoT) en élargissant la profondeur du raisonnement. Cette évolution permet aux modèles de produire des chaînes de pensée plus longues et plus détaillées grâce à des invites itératives ou à une génération en plusieurs étapes. Le dimensionnement de l'inférence peut être exploité pour améliorer le RAG multimodal, en se concentrant sur l'interaction entre la taille des modèles, les budgets informatiques et l'optimisation pratique du temps d'inférence pour les applications du monde réel.

De plus, les lois de mise à l'échelle et les résultats des tests de performance soulignent les compromis entre le pré-entraînement, l'ajustement, les stratégies de temps d'inférence et les algorithmes avancés pour la sélection des résultats. Les modèles de grande taille comme ceux de petite taille bénéficient de la mise à l'échelle de l'inférence, car celle-ci permet également aux systèmes aux ressources limitées de se rapprocher des performances des LLM de pointe. Ce tutoriel démontre l'impact des techniques d'optimisation sur les performances des modèles et offre des conseils pratiques pour équilibrer la précision, la latence et le coût dans les déploiements RAG multimodaux.

Ce tutoriel est conçu pour les développeurs d’intelligence artificielle, les chercheurs et les passionnés qui cherchent à améliorer leurs connaissances en matière de gestion de documents et de techniques avancées de traitement automatique du langage naturel (NLP). Vous apprendrez à exploiter la puissance de la mise à l’échelle de l’inférence pour améliorer le pipeline RAG multimodal créé dans une formule précédente. Bien que ce tutoriel se concentre sur les stratégies d’évolutivité dans les RAG multimodaux spécifiquement axés sur les grands modèles de langage IBM Granite, des principes similaires sont applicables à la plupart des modèles populaires, notamment ceux d’OpenAI (par exemple, GPT-4, GPT-4o, ChatGPT) et de DeepMind.

Ce tutoriel vous guide à travers les processus suivants :

  • Prétraitement des documents : vous apprendrez à gérer des documents provenant de diverses sources, à les analyser et à les transformer en formats utilisables et à les stocker dans des bases de données vectorielles à l’aide de Docling. Docling est un outil open source d’IBM utilisé pour analyser efficacement les formats de documents, comme PDF, DOCX, PPTX, XLSX, images, HTML, AsciiDoc et Markdown. Il exporte ensuite le contenu du document dans des formats lisibles par machine comme Markdown ou JSON. Vous utiliserez un modèle de machine learning (ML) Granite pour générer les descriptions d’image dans les documents. Dans ce tutoriel, Docling téléchargera les documents PDF et les traitera afin que nous puissions obtenir le texte et les images qu’ils contiennent. Dans ce tutoriel, Docling téléchargera les documents PDF et les traitera afin que nous puissions obtenir le texte et les images qu’ils contiennent.

  • Génération augmentée de récupération : comprenez comment connecter des LLM tels que Granite à des bases de connaissances externes pour améliorer les réponses aux requêtes et générer des informations utiles. La RAG est une technique de grand modèle de langage (LLM), qui est utilisée pour connecter les LLM à une base de connaissances d’informations extérieures aux données sur lesquelles le LLM a été entraîné. Cette technique est appliquée aux LLM sans nécessiter d'ajustement. La RAG traditionnelle est limitée aux cas d’utilisation tels que la synthèse de textes et les chatbots.

  • RAG multimodal : découvrez comment le RAG multimodal utilise de grands modèles de langage (MLLM) multimodaux pour traiter des informations à partir de plusieurs types de données. Ces données peuvent ensuite être incluses dans la base de connaissances externe utilisée dans la RAG. Les données multimodales peuvent inclure du texte, des images, de l’audio, de la vidéo et d’autres formes. Dans ce tutoriel, nous utilisons le dernier modèle de vision multimodal d’IBM, Granite 3.2.

  • Mise en œuvre de RAG basé sur la démonstration (DRAG) et de RAG basé sur la démonstration itérative (IterDRAG) : appliquez les techniques de mise à l'échelle de l'inférence de l'article de recherche pour améliorer significativement les performances de RAG lors de l'utilisation d'un contexte long. La méthode DRAG tire parti de l’apprentissage en contexte pour améliorer la performance RAG. En incluant plusieurs exemples RAG comme démonstrations, DRAG aide les modèles à apprendre à localiser des informations pertinentes dans des contextes longs. Contrairement au RAG standard qui peut plafonner avec un plus grand nombre de documents, le DRAG affiche des améliorations linéaires avec l'augmentation de la longueur du contexte. IterDRAG est une extension du DRAG qui traite les requêtes multi-hop complexes en les décomposant en sous-requêtes plus simples. Le multi-hop est un processus dans lequel une requête complexe est décomposée et traitée par des sous- interrogations simples. Chaque sous-interrogation peut nécessiter des informations récupérées et/ou synthétisées à partir de différentes sources. IterDRAG mêle les étapes de récupération et de génération, créant ainsi des chaînes de raisonnement qui comblent les lacunes compositionnelles. Cette approche est particulièrement efficace pour traiter des requêtes complexes dans des contextes longs.

  • LangChain pour l'intégration des workflows : découvrez comment utiliser LangChain pour rationaliser et orchestrer les workflows de traitement et de récupération des documents, permettant ainsi une interaction fluide entre les différents composants du système.

Au cours de ce tutoriel, vous utiliserez également trois technologies de pointe :

  1. Docling : un outil open source utilisé pour analyser et convertir des documents.

  2. Granite: Une gamme de modèles de langage à grande échelle (LLM) de pointe offrant de solides capacités en matière de langage naturel, ainsi qu'un modèle de langage visuel permettant la génération de texte à partir d'images.

  3. LangChain: un cadre puissant, conçu pour simplifier les workflows complexes et intégrer des outils externes de façon fluide, afin de créer des applications alimentées par des modèles de langage.

À la fin de ce tutoriel, vous pourrez accomplir les tâches suivantes :

  • Maîtriser la compétence en prétraitement des documents, le découpage et la compréhension des images.

  • Intégrer des bases de données vectorielles pour améliorer les capacités de récupération.

  • Implémenter DRAG et IterDRAG pour effectuer une récupération de données efficace et précise avec mise à l’échelle de l’inférence.

  • Découvrir par vous-même comment la mise à l'échelle du calcul d'inférence peut entraîner des améliorations quasi linéaires des performances RAG.

Comprendre les défis liés à un long contexte

Les modèles de langage traditionnels ont du mal à gérer les contextes longs pour plusieurs raisons :

  • Les mécanismes d'attention traditionnels tels que les transformateurs évoluent de manière quadratique, ce qui peut nécessiter d'importantes ressources informatiques. 

  • Difficulté à localiser des informations pertinentes dans des séquences très longues. 

  • Difficultés à préserver la cohérence entre des parties distantes de l’entrée. 

  • Exigences informatiques accrues pour le traitement de séquences longues.

Les techniques dans ce tutoriel répondent à ces défis grâce à une allocation stratégique du calcul d’inférence.

Méthodes de mise à l'échelle de l’inférence : DRAG et IterDRAG

DRAG et IterDRAG
DRAG et IterDRAG

Vous trouverez de plus amples informations sur ces deux techniques avancées de mise à l’échelle de l’inférence (DRAG et IterDRAG) dans l'étude «  Inference Scaling for Long-Context Retrieval Augmented Generation »

Ces méthodes démontrent que le calcul d'inférence à l'échelle peut améliorer les performances du RAG de manière quasi linéaire lorsqu'il est alloué de manière optimale, permettant ainsi aux systèmes RAG de mieux exploiter les capacités de contexte long des LLM modernes. Pour cette mise en œuvre, nous utiliserons un modèle IBM Granite capable de traiter différentes modalités. Vous allez créer un système d’IA pour répondre aux requêtes des utilisateurs en temps réel à partir de données non structurées, en appliquant les principes du document.

Prérequis

  • Familiarisez-vous avec la programmation Python.

  • Compréhension de base des LLM, des concepts NLP et de la vision par ordinateur.

Étapes

Assurez-vous que vous exécutez Python 3.10, 3.11 ou 3.12 dans un environnement virtuel nouvellement créé. Notez que vous pouvez également accéder à ce tutoriel sur GitHub

Étape 1 : configurer l’environnement

import sys
assert sys.version_info >= (3, 10) and sys.version_info < (3, 13), "Use Python 3.10, 3.11, or 3.12 to run this notebook."

Étape 2 : installer les dépendances

! pip install "git+https://github.com/ibm-granite-community/utils.git" \
    transformers \
    pillow \
    langchain_community \
    langchain_huggingface \
    langchain_milvus \
    docling \
    replicate

Journalisation

Pour afficher certaines informations de journalisation, nous pouvons configurer le niveau de journalisation INFO.

REMARQUE : vous pouvez ignorer l'exécution de cette cellule.

import logging

logging.basicConfig(level=logging.INFO)

Étape 3 : sélectionner les modèles IA

Charger les modèles Granite

Spécifiez le modèle d'embedding à utiliser pour générer des vecteurs d'embedding de texte. Ici, nous allons utiliser l’un des modèles Granite Embeddings .

Pour utiliser un modèle d'embedding différent, remplacez cette cellule de code par celle de la formule de ce modèle d'embedding.

from langchain_huggingface import HuggingFaceEmbeddings
from transformers import AutoTokenizer

embeddings_model_path = "ibm-granite/granite-embedding-30m-english"
embeddings_model = HuggingFaceEmbeddings(
    model_name=embeddings_model_path,
)
embeddings_tokenizer = AutoTokenizer.from_pretrained(embeddings_model_path)

Spécifiez le MLLM à utiliser pour la compréhension de l’image. Nous utiliserons le modèle de vision Granite.

from ibm_granite_community.notebook_utils import get_env_var
from langchain_community.llms import Replicate
from transformers import AutoProcessor

vision_model_path = "ibm-granite/granite-vision-3.2-2b"
vision_model = Replicate(
    model=vision_model_path,
    replicate_api_token=get_env_var("REPLICATE_API_TOKEN"),
    model_kwargs={
        "max_tokens": embeddings_tokenizer.max_len_single_sentence, # Set the maximum number of tokens to generate as output.
        "min_tokens": 100, # Set the minimum number of tokens to generate as output.
        "temperature": 0.01,
    },
)
vision_processor = AutoProcessor.from_pretrained(vision_model_path)

Spécifiez le modèle de langage à utiliser pour l’opération de génération RAG. Ici, nous utilisons le client Replicate LangChain pour nous connecter à un modèle Granite provenant de l’organisation ibm-granite sur Replicate.

Pour commencer à utiliser Replicate, consultez la rubrique Initiation à Replicate.

Pour vous connecter à un modèle sur un fournisseur autre que Replicate, remplacez cette cellule de code par celle de la formule de composant LLM.

model_path = "ibm-granite/granite-3.3-8b-instruct"
model = Replicate(
    model=model_path,
    replicate_api_token=get_env_var("REPLICATE_API_TOKEN"),
    model_kwargs={
        "max_tokens": 1000, # Set the maximum number of tokens to generate as output.
        "min_tokens": 100, # Set the minimum number of tokens to generate as output.
        "temperature": 0.01
    },
)
tokenizer = AutoTokenizer.from_pretrained(model_path)

Étape 4 : préparer les documents pour la base de données vectorielle avec Docling

from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.base_models import InputFormat
from docling.datamodel.pipeline_options import PdfPipelineOptions

pdf_pipeline_options = PdfPipelineOptions(
    do_ocr=False,
    generate_picture_images=True,
)
format_options = {
    InputFormat.PDF: PdfFormatOption(pipeline_options=pdf_pipeline_options),
}
converter = DocumentConverter(format_options=format_options)

sources = [
    "https://midwestfoodbank.org/images/AR_2020_WEB2.pdf",
]
conversions = { source: converter.convert(source=source).document for source in sources }

Une fois les documents traités, nous traitons ensuite les éléments textuels des documents et les découpons en morceaux de taille appropriée pour le modèle d'embessing que nous utilisons. Une liste de documents LangChain est créée à partir des blocs de texte.

from docling_core.transforms.chunker.hybrid_chunker import HybridChunker
from docling_core.types.doc import DocItem, TableItem
from langchain_core.documents import Document

doc_id = 0
texts: list[Document] = []
for source, docling_document in conversions.items():
    for chunk in HybridChunker(tokenizer=embeddings_tokenizer).chunk(docling_document):
        items: list[DocItem] = chunk.meta.doc_items # type: ignore
        if len(items) == 1 and isinstance(items[0], TableItem):
            continue # we will process tables later
        refs = " ".join(map(lambda item: item.get_ref().cref, items))
        print(refs)
        text = chunk.text
        document = Document(
            page_content=text,
            metadata={
                "doc_id": (doc_id:=doc_id+1),
                "source": source,
                "ref": refs,
            },
        )
        texts.append(document)

print(f"{len(texts)} text document chunks created")

Ensuite, nous traitons tous les tableaux contenus dans les documents. Nous convertissons les données des tableaux au format Markdown afin que le modèle de langage puisse les traiter. Une liste de documents LangChain est générée à partir des rendus Markdown du tableau.

from docling_core.types.doc import DocItemLabel

doc_id = len(texts)
tables: list[Document] = []
for source, docling_document in conversions.items():
    for table in docling_document.tables:
        if table.label in [DocItemLabel.TABLE]:
            ref = table.get_ref().cref
            print(ref)
            text = table.export_to_markdown(docling_document)
            document = Document(
                page_content=text,
                metadata={
                    "doc_id": (doc_id:=doc_id+1),
                    "source": source,
                    "ref": ref
                },
            )
            tables.append(document)


print(f"{len(tables)} table documents created")

Enfin, nous traitons toutes les images des documents. Nous utilisons ici le modèle linguistique visuel pour analyser le contenu des images. Dans cet exemple, nous sommes intéressés par toute information textuelle dans l’image.

Le choix d’un prompt approprié est critique, car il indique les aspects sur lesquels le modèle se concentrera. En voici quelques exemples :

  • Un prompt du type « Donner une description détaillée de ce qui est représenté dans l’image » (utilisé ci-dessous) fournira des informations générales sur tous les éléments visuels.

  • Une prompt du type « Quel texte apparaît dans cette image ? » se concentrerait spécifiquement sur l’extraction de contenu textuel.

  • Un prompt du type « Décrire la visualisation des données dans cette image » serait plus adapté pour les diagrammes et les graphiques.

  • Vous devez expérimenter différents prompts en fonction des types d’images présentes dans vos documents et des informations que vous devez en extraire.

REMARQUE : le traitement des images peut nécessiter un temps de traitement important en fonction du nombre d'images et du service exécutant le modèle de langage visuel.

import base64
import io
import PIL.Image
import PIL.ImageOps

def encode_image(image: PIL.Image.Image, format: str = "png") -> str:
    image = PIL.ImageOps.exif_transpose(image) or image
    image = image.convert("RGB")

    buffer = io.BytesIO()
    image.save(buffer, format)
    encoding = base64.b64encode(buffer.getvalue()).decode("utf-8")
    uri = f"data:image/{format};base64,{encoding}"
    return uri

# Feel free to experiment with this prompt
image_prompt = "Give a detailed description of what is depicted in the image"
conversation = [
    {
        "role": "user",
        "content": [
            {"type": "image"},
            {"type": "text", "text": image_prompt},
        ],
    },
]
vision_prompt = vision_processor.apply_chat_template(
    conversation=conversation,
    add_generation_prompt=True,
)
pictures: list[Document] = []
doc_id = len(texts) + len(tables)
for source, docling_document in conversions.items():
    for picture in docling_document.pictures:
        ref = picture.get_ref().cref
        print(ref)
        image = picture.get_image(docling_document)
        if image:
            text = vision_model.invoke(vision_prompt, image=encode_image(image))
            document = Document(
                page_content=text,
                metadata={
                    "doc_id": (doc_id:=doc_id+1),
                    "source": source,
                    "ref": ref,
                },
            )
            pictures.append(document)

print(f"{len(pictures)} image descriptions created")

Nous pouvons ensuite afficher les documents LangChain créés à partir des documents d’entrée.

import itertools
from docling_core.types.doc import RefItem
from IPython.display import display

# Print all created documents
for document in itertools.chain(texts, tables):
    print(f"Document ID: {document.metadata['doc_id']}")
    print(f"Source: {document.metadata['source']}")
    print(f"Content:\n{document.page_content}")
    print("=" * 80)  # Separator for clarity

for document in pictures:
    print(f"Document ID: {document.metadata['doc_id']}")
    source = document.metadata['source']
    print(f"Source: {source}")
    print(f"Content:\n{document.page_content}")
    docling_document = conversions[source]
    ref = document.metadata['ref']
    picture = RefItem(cref=ref).resolve(docling_document)
    image = picture.get_image(docling_document)
    print("Image:")
    display(image)
    print("=" * 80)  # Separator for clarity

Alimenter la base de données vectorielle

En utilisant le modèle d'embedding, nous chargeons les documents à partir des blocs de texte et du sous-titrage d’image généré dans une base de données vectorielle. La création de cette base de données vectorielle nous permet d’effectuer facilement une recherche de similarités sémantiques dans nos documents.

REMARQUE : le remplissage de la base de données vectorielle peut nécessiter un temps de traitement important en fonction de votre modèle et de votre service d'embedding.

Choisir votre base de données vectorielle

Spécifiez la base de données à utiliser pour stocker et récupérer les vecteurs d’embedding. Pour les besoins de ce tutoriel, nous utiliserons Milvus via Langchain. En tant que base de données vectorielle, Milvus stocke, indexe et gère les embeddings numériques générés par les réseaux neuronaux et divers algorithmes ML.

Pour vous connecter à une base de données vectorielle autre que Milvus, remplacez cette cellule de code par celle de cette formule de base de données vectorielle.

import tempfile
from langchain_core.vectorstores import VectorStore, VectorStoreRetriever
from langchain_milvus import Milvus

db_file = tempfile.NamedTemporaryFile(prefix="vectorstore_", suffix=".db", delete=False).name
print(f"The vector database will be saved to {db_file}")

vector_db: VectorStore = Milvus(
    embedding_function=embeddings_model,
    connection_args={"uri": db_file},
    auto_id=True,
    enable_dynamic_field=True,
    index_params={"index_type": "AUTOINDEX"},
)

Nous ajoutons maintenant tous les documents LangChain pour le texte, les tableaux et les descriptions d'images à la base de données vectorielle.

import itertools

documents = list(itertools.chain(texts, tables, pictures))
ids = vector_db.add_documents(documents)
print(f"{len(ids)} documents added to the vector database")
retriever: VectorStoreRetriever = vector_db.as_retriever(search_kwargs={"k": 10})

Étape 5 : RAG avec Granite

Maintenant que nous avons converti et vectorisé nos documents avec succès, nous pouvons configurer notre pipeline RAG.

Valider la qualité de la récupération

Ici, nous testons la base de données vectorielle en recherchant les blocs contenant des informations pertinentes pour notre requête dans l’espace vectoriel. Nous affichons les documents associés à la description de l’image récupérée.

Cette étape de validation est importante pour garantir le bon fonctionnement de notre système de recherche avant de mettre en place notre pipeline RAG. Nous voulons voir si les documents renvoyés sont pertinents pour notre requête.

N'hésitez pas à formuler d'autres requêtes.

query = "Analyze how Midwest Food Bank's financial efficiency changed during the pandemic by comparing their 2019 and 2020 performance metrics. What specific pandemic adaptations had the greatest impact on their operational capacity, and how did their volunteer management strategy evolve to maintain service levels despite COVID-19 restrictions? Provide specific statistics from the report to support your analysis."
for doc in vector_db.as_retriever().invoke(query):
    print(doc)
    print("=" * 80)  # Separator for clarity

Les documents renvoyés doivent répondre à la requête. Allons-y et créons notre pipeline RAG.

Les documents renvoyés doivent répondre à la requête. Allons-y et créons notre pipeline RAG.

Créer le pipeline RAG pour Granite

Tout d’abord, nous créons les prompts permettant à Granite d’exécuter la requête RAG. Nous utilisons le modèle de chat Granite et fournissons les valeurs d’espace réservé que le pipeline LangChain RAG remplacera.

{context} conservera les blocs récupérés, comme indiqué dans la recherche précédente, et les transmettra au modèle en tant que contexte de document pour répondre à notre question.

Nous construisons ensuite le pipeline RAG en utilisant les modèles de prompt Granite que nous avons créés.

from ibm_granite_community.notebook_utils import escape_f_string
from langchain.prompts import PromptTemplate
from langchain.chains.retrieval import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain

# Create a Granite prompt for question-answering with the retrieved context
prompt = tokenizer.apply_chat_template(
    conversation=[{
        "role": "user",
        "content": "{input}",
    }],
    documents=[{
        "doc_id": "0",
        "text": "{context}",
    }],
    add_generation_prompt=True,
    tokenize=False,
)
prompt_template = PromptTemplate.from_template(template=escape_f_string(prompt, "input", "context"))

# Create a Granite document prompt template to wrap each retrieved document
document_prompt_template = PromptTemplate.from_template(template="""\
<|end_of_text|>
<|start_of_role|>document {{"document_id": "{doc_id}"}}<|end_of_role|>
{page_content}""")
document_separator=""

# Assemble the retrieval-augmented generation chain
combine_docs_chain = create_stuff_documents_chain(
    llm=model,
    prompt=prompt_template,
    document_prompt=document_prompt_template,
    document_separator=document_separator,
)
rag_chain = create_retrieval_chain(
    retriever=retriever,
    combine_docs_chain=combine_docs_chain,
)

Générer une réponse augmentée de récupération à une question

Le pipeline utilise la requête pour localiser les documents de la base de données vectorielle et les utiliser comme contexte pour la requête.

outputs = rag_chain.invoke({"input": query})
print(outputs['answer'])

Limites standard de la RAG et nécessité d'une mise à l'échelle de l'inférence

Bien que l’approche RAG standard fonctionne raisonnablement bien, elle présente plusieurs limites clés lorsqu’il s’agit de traiter un contenu long ou complexe :

  1. Gestion du contexte : lorsque vous traitez de nombreux documents, la RAG standard a du mal à utiliser efficacement tout le contexte disponible.

  2. Qualité de la récupération : en l'absence de conseils sur la manière d'utiliser les informations récupérées, les modèles se concentrent souvent sur les mauvaises parties des documents.

  3. Raisonnement compositionnel : comprendre les requêtes complexes nécessitant un raisonnement à plusieurs étapes est difficile pour la RAG standard.

  4. Performances stagnantes : l'ajout de documents à un RAG standard entraîne souvent une diminution des résultats après un certain seuil.

Les techniques de mise à l’échelle de l’inférence adressent ces limitations en allouant stratégiquement davantage de calcul au moment de l’inférence.

RAG améliorée avec DRAG (RAG basée sur la démonstration)

Nous allons maintenant mettre en œuvre la technique DRAG du document de recherche « Inference Scaling for Long-Context Retrieval Augmented Generation » pour améliorer notre système RAG.

Le modèle DRAG utilise des exemples contextuels pour démontrer au modèle comment extraire et utiliser les informations contenues dans les documents, améliorant ainsi les performances dans les scénarios à contexte long.

Étape 1 : créer des exemples de démonstration contextuelle

Ces exemples proviennent généralement d'un jeu de données sélectionnées avec soin, composé de paires de questions-réponses de haute qualité. À cette fin, nous allons créer des exemples synthétiques qui correspondent au domaine attendu.

Ici, nous définissons une classe de données pour représenter une démonstration individuelle, puis nous créons quelques démonstrations.

from dataclasses import dataclass, field, InitVar
from langchain_core.documents import Document

@dataclass
class DRAG_Demonstration:
    query: str
    answer: str
    retriever: InitVar[VectorStoreRetriever] = field(kw_only=True)
    documents: list[Document] = field(default_factory=list, kw_only=True)

    def __post_init__(self, retriever: VectorStoreRetriever):
        if not self.documents:
            self.documents = retriever.invoke(self.query)

    def __format__(self, format_spec: str) -> str:
        formatted_documents = "\n".join(
            f"Document {i+1}:\n{document.page_content}"
            for i, document in enumerate(self.documents)
        )
        return f"""\
{formatted_documents}
Question: {self.query}
Answer: {self.answer}
"""

def create_enhanced_drag_demonstrations(vector_db: VectorStore) -> list[DRAG_Demonstration]:
    """Create high-quality demonstrations for DRAG technique that showcase effective document analysis"""
    demonstration_retriever: VectorStoreRetriever = vector_db.as_retriever(search_kwargs={"k": 5})
    demonstrations = [
        DRAG_Demonstration(
            query="How did the COVID-19 pandemic impact Midwest Food Bank's operations in 2020?",
            answer="The COVID-19 pandemic significantly impacted Midwest Food Bank's operations in 2020. Despite challenges, MFB remained open and responsive to increased needs. They implemented safety protocols, reduced volunteer numbers for social distancing, and altered their distribution model to allow partner agencies to receive food safely. The pandemic created unprecedented food insecurity, with many people seeking assistance for the first time. MFB distributed 37% more food than in 2019, with a record 179 semi-loads of Disaster Relief family food boxes sent nationwide. The organization also faced supply chain disruptions and food procurement challenges in the early months but continued to find and distribute food. Community, business, and donor support helped fund operations and food purchases. Additionally, MFB began participating in the USDA Farmers to Families Food Box program in May 2020, distributing over $52 million worth of nutritious produce, protein, and dairy products.",
            retriever=demonstration_retriever
        ),
        DRAG_Demonstration(
            query="What role did volunteers play at Midwest Food Bank during 2020, and how were they affected by the pandemic?",
            answer="Volunteers were described as 'the life-blood of the organization' in the 2020 annual report. Despite the pandemic creating safety challenges, volunteers demonstrated courage and dedication by increasing their hours to meet growing needs. MFB implemented safety protocols at each location and limited volunteer group sizes to allow for social distancing. This created a challenge as food needs increased while fewer volunteers were available to help. To address this gap, multiple MFB locations received assistance from the National Guard, who filled vital volunteer positions driving trucks, operating forklifts, and helping with food distributions. In 2020, 17,930 individuals volunteered 300,898 hours of service, equivalent to 150 full-time employees. The volunteer-to-staff ratio was remarkable with 450 volunteers for every 1 paid MFB staff member, highlighting the volunteer-driven nature of the organization during the crisis.",
            retriever=demonstration_retriever
        ),
        DRAG_Demonstration(
            query="How did Midwest Food Bank's international programs perform during 2020, particularly in Haiti and East Africa?",
            answer="In 2020, Midwest Food Bank's international operations in East Africa and Haiti faced unique challenges but continued to serve communities. In East Africa (operated as Kapu Africa), strict lockdowns led to mass hunger, especially in slum areas. Kapu Africa distributed 7.2 million Tender Mercies meals, working with partner ministries to share food in food-insecure slums. A notable outcome was a spiritual awakening among recipients, with many asking why they were receiving help. In Haiti, the pandemic added to existing challenges, closing airports, seaports, factories, and schools. MFB Haiti more than doubled its food shipments to Haiti, delivering over 160 tons of food relief, nearly three-quarters being Tender Mercies meals. As Haitian children primarily receive nourishment from school lunches, MFB Haiti distributed Tender Mercies through faith-based schools and also partnered with over 20 feeding centers serving approximately 1,100 children daily. Nearly 1 million Tender Mercies meals were distributed in Haiti during 2020.",
            retriever=demonstration_retriever
        ),
    ]

    return demonstrations

Étape 2 : formater les démonstrations pour les inclure dans le prompt

Nous formatons ensuite toutes les démonstrations ensemble pour le prompt.

# Format all demonstrations together
demonstrations = create_enhanced_drag_demonstrations(vector_db)

formatted_demonstrations = "\n\n".join(
    f"Example {i+1}:\n{demo}"
    for i, demo in enumerate(demonstrations)
)

Étape 3 : créer le modèle de prompt DRAG

Nous créons ensuite le prompt DRAG pour le modèle qui inclut les exemples de démonstration formatés.

drag_prompt = tokenizer.apply_chat_template(
    conversation=[{
        "role": "user",
        "content": f"""\
Here are examples of effectively extracting information from documents to answer questions.

{formatted_demonstrations}

Follow these examples when answering the user's question:

{{input}}""",
    }],
    documents=[{
        "doc_id": "0",
        "text": "Placeholder{context}",
    }],
    add_generation_prompt=True,
    tokenize=False,
)

# Convert to prompt template
drag_prompt_template = PromptTemplate.from_template(template=escape_f_string(drag_prompt, "input", "context"))

Étape 4 : créer un récupérateur personnalisé qui réorganise les documents

Normalement, la récupération renverra les documents par ordre de similarité, le document le plus similaire étant le premier. Nous définissons un outil de réorganisation pour inverser l'ordre des résultats. La commande affiche désormais le document le plus similaire en dernier, donc près de la fin du prompt.

import typing
from langchain_core.retrievers import BaseRetriever, RetrieverInput, RetrieverOutput
from langchain_core.callbacks.manager import CallbackManagerForRetrieverRun

class ReorderingRetriever(BaseRetriever):
    base_retriever: BaseRetriever

    def _get_relevant_documents(
        self, query: RetrieverInput, *, run_manager: CallbackManagerForRetrieverRun, **kwargs: typing.Any
    ) -> RetrieverOutput:
        docs = self.base_retriever._get_relevant_documents(query, run_manager=run_manager, **kwargs)
        return list(reversed(docs))  # Reverse the order so higher-ranked docs are closer to query in prompt

reordering_retriever = ReorderingRetriever(base_retriever=retriever)

Étape 5 : créer un pipeline DRAG

Nous créons le pipeline pour la requête DRAG en utilisant le modèle d'invite DRAG et le système de réorganisation.

drag_combine_docs_chain = create_stuff_documents_chain(
    llm=model,
    prompt=drag_prompt_template,
    document_prompt=document_prompt_template,
    document_separator=document_separator,
)

drag_chain = create_retrieval_chain(
    retriever=reordering_retriever,
    combine_docs_chain=drag_combine_docs_chain,
)

Étape 6 : générer une réponse améliorée par la DRAG à une question

drag_outputs = drag_chain.invoke({"input": query})
print("\n=== DRAG-Enhanced Answer ===")
print(drag_outputs['answer'])

Excellent, il semble que nous ayons amélioré la réponse en fournissant quelques exemples. Passons maintenant à une technique RAG encore plus approfondie.

Mise en œuvre d’IterDRAG (RAG basée sur la démonstration itérative)

L'IterDRAG complète le DRAG en décomposant les requêtes complexes en sous-requêtes plus simples et en effectuant une recherche entrelacée. Cette approche est particulièrement efficace pour les questions complexes sur plusieurs sauts qui nécessitent l’intégration d’informations provenant de plusieurs sources ou un raisonnement sur plusieurs étapes.
 
Principaux avantages de l'approche itérative :

  • Décompose les questions complexes en éléments gérables.

  • Récupère les informations plus pertinentes pour chaque sous-demande.

  • Elle crée des chaînes de raisonnement explicites.

  • Permet de répondre à des questions qui seraient complexes en une seule étape.

Étape 1 : créer une chaîne de décomposition de requêtes

L'étape de décomposition est essentielle, car elle consiste à décomposer une requête complexe en sous-requêtes plus simples et plus ciblées, auxquelles il est possible de répondre individuellement.

decompose_prompt = tokenizer.apply_chat_template(
    conversation=[{
        "role": "user",
        "content": """\
You are a helpful assistant that breaks down complex questions into simpler sub-questions.
For multi-part or complex questions, generate 1-3 sub-questions that would help answer the main question.

Here are examples of how to decompose complex questions:
{demonstrations}

Follow the above examples when breaking down the user's question.
If the following question is already simple enough, just respond with "No follow-up needed."

Otherwise, break down the following question into simpler sub-questions. Format your response as:
Follow up: [sub-question]

Question: {input}"""
    }],
    add_generation_prompt=True,
    tokenize=False,
)

decompose_prompt_template = PromptTemplate.from_template(template=escape_f_string(decompose_prompt, "input", "demonstrations"))
decompose_chain = decompose_prompt_template | model

Étape 2 : créer une chaîne de réponse à une sous-requête

Le composant de réponse aux sous-requêtes traite chaque sous-question individuellement en récupérant les documents pertinents et en générant des réponses intermédiaires ciblées.

intermediate_prompt = tokenizer.apply_chat_template(
    conversation=[{
        "role": "user",
        "content": """\
You are a helpful assistant that answers specific questions based on the provided documents.

Focus only on the sub-question and provide a concise intermediate answer.
Please answer the following sub-question based on the provided documents.
Format your response as:
Intermediate answer: [your concise answer to the sub-question]

Sub-question: {input}
"""
    }],
    documents=[{
        "doc_id": "0",
        "text": "Placeholder{context}",
    }],
    add_generation_prompt=True,
    tokenize=False,
)

intermediate_prompt_template = PromptTemplate.from_template(template=escape_f_string(intermediate_prompt, "input", "context"))
intermediate_combine_docs_chain = create_stuff_documents_chain(
    llm=model,
    prompt=intermediate_prompt_template,
    document_prompt=document_prompt_template,
    document_separator=document_separator,
)
intermediate_chain = create_retrieval_chain(
    retriever=reordering_retriever,
    combine_docs_chain=intermediate_combine_docs_chain,
)

Étape 3 : créer une chaîne de génération de réponse finale

Le composant de génération de la réponse finale combine toutes les réponses intermédiaires pour produire une réponse complète à la question initiale.

final_prompt = tokenizer.apply_chat_template(
    conversation=[{
        "role": "user",
        "content": """\
You are a helpful assistant that provides comprehensive answers to questions.
Use the intermediate answers to sub-questions to formulate a complete final answer.
Please provide a final answer to the main question based on the intermediate answers to sub-questions.
Format your response as:
So the final answer is: [your comprehensive answer to the main question]

Main question: {input}

Sub-questions and intermediate answers:
{context}"""
    }],
    add_generation_prompt=True,
    tokenize=False,
)

final_prompt_template = PromptTemplate.from_template(template=escape_f_string(final_prompt, "input", "context"))
final_chain = final_prompt_template | model

Étape 4 : créer des exemples de démonstration pour IterDRAG

La création de démonstrations efficaces est cruciale pour les performances d’IterDRAG. Ces exemples montrent au modèle comment :

  1. Décomposer les questions complexes en sous-questions plus simples.

  2. Générer des réponses intermédiaires pertinentes.

  3. Combiner ces réponses pour former une réponse finale cohérente.
@dataclass
class IterDRAG_Demonstration_Base:
    query: str
    answer: str

@dataclass
class IterDRAG_Demonstration(IterDRAG_Demonstration_Base):
    intermediate: list[IterDRAG_Demonstration_Base]

    def __format__(self, format_spec: str) -> str:
        sub_questions="\n".join(
            f"Follow up: {sub.query}"
            for sub in self.intermediate
        )

        return f"Question: {self.query}\n{sub_questions}"

def create_iterdrag_demonstrations() -> list[IterDRAG_Demonstration]:
    """Create examples showing how to decompose and answer complex questions"""

    demonstrations = [
        IterDRAG_Demonstration(
            query="What impact did the pandemic have on the food bank's operations and distribution?",
            answer="The pandemic had a profound impact on food bank operations and distribution. Distribution volume increased by 60% to over 100 million pounds of food in 2020. Operationally, the food bank faced supply chain disruptions, volunteer shortages, and safety protocol challenges. In response, they implemented contactless distribution, expanded mobile pantries, created emergency food boxes for vulnerable populations, and developed virtual nutrition education. Despite these challenges, they successfully scaled operations to meet the unprecedented community need during the crisis.",
            intermediate=[
                IterDRAG_Demonstration_Base(
                    query="How did food distribution volume change during the pandemic?",
                    answer="Food distribution volume increased by 60% during the pandemic, rising from approximately 62 million pounds in 2019 to over 100 million pounds in 2020.",
                ),
                IterDRAG_Demonstration_Base(
                    query="What operational challenges did the food bank face during the pandemic?",
                    answer="The food bank faced challenges including supply chain disruptions, volunteer shortages due to social distancing requirements, and the need to implement new safety protocols for food handling and distribution.",
                ),
                IterDRAG_Demonstration_Base(
                    query="What new programs were implemented in response to the pandemic?",
                    answer="New programs included contactless distribution methods, expanded mobile pantry operations, emergency food boxes for vulnerable populations, and virtual nutrition education classes.",
                ),
            ],
        ),
        IterDRAG_Demonstration(
            query="How does the food bank's financial management compare to industry standards for non-profits?",
            answer="The food bank demonstrates excellent financial management compared to industry standards. With 94% of its budget allocated to program services and only 6% to administrative and fundraising costs, it exceeds the industry benchmark of 85-90% for program spending. This financial efficiency places the food bank among the top-performing non-profits in terms of maximizing donor impact and minimizing overhead expenses.",
            intermediate=[
                IterDRAG_Demonstration_Base(
                    query="What percentage of the food bank's budget goes to program services versus administrative costs?",
                    answer="94% of the food bank's budget goes directly to program services, with only 6% allocated to administrative and fundraising costs.",
                ),
                IterDRAG_Demonstration_Base(
                    query="What are the industry standards for program spending versus overhead for food banks?",
                    answer="Industry standards suggest that well-run food banks typically allocate 85-90% of their budget to program services, with 10-15% for administrative and fundraising expenses.",
                ),
            ],
        ),
    ]
    return demonstrations

Étape 5 : implémenter la fonction IterDRAG

Cette fonction coordonne l'ensemble du processus itératif :

  1. Décomposer la question principale en sous-interrogation.

  2. Pour chaque sous-interrogation, récupérer les documents pertinents et générer une réponse intermédiaire.

  3. Combiner toutes les réponses intermédiaires pour produire la réponse finale.
import re

def iterative_drag(main_question: str) -> dict[str, typing.Any]:
    """
    Implements IterDRAG: decomposing queries, retrieving documents for sub-queries,
    and generating a final answer based on intermediate answers.
    """
    print(f"\n=== Processing query with IterDRAG: '{main_question}' ===")

    # Step 1: Decompose the main question into sub-questions
    print("Step 1: Decomposing the query into sub-questions...")
    iterdrag_demonstrations = create_iterdrag_demonstrations()
    formatted_demonstrations = "\n\n".join(
        f"Example {i+1}:\n{demo}"
        for i, demo in enumerate(iterdrag_demonstrations)
    )
    decompose_result = decompose_chain.invoke({
        "input": main_question,
        "demonstrations": formatted_demonstrations,
    })
    decompose_answer = decompose_result

    # Extract sub-questions using regex
    sub_questions = re.findall(r"Follow up: (.*?)(?=Follow up:|\n|$)", decompose_answer, re.DOTALL)
    sub_questions = [sq.strip() for sq in sub_questions if sq.strip()]
    if not sub_questions:
        print("No decomposition needed or found. Using standard DRAG approach.")
        return drag_chain.invoke({"input": main_question})
    print(f"Decomposed into {len(sub_questions)} sub-questions")

    # Step 2: Answer each sub-question
    intermediate_pairs: list[dict[str, str]] = []
    for i, sub_question in enumerate(sub_questions):
        print(f"\nStep 2.{i+1}: Processing sub-question: '{sub_question}'")

        # Generate answer for this sub-question
        intermediate_result = intermediate_chain.invoke({"input": sub_question})
        intermediate_answer = intermediate_result["answer"]

        # Extract intermediate answer using regex
        intermediate_answer_match = re.search(r"Intermediate answer: (.*?)$", intermediate_answer, re.DOTALL)
        if intermediate_answer_match:
            intermediate_answer = intermediate_answer_match.group(1).strip()

        print(f"Generated intermediate answer: {intermediate_answer[:100]}...")

        # Store the sub-question and its answer
        intermediate_pairs.append({"input": sub_question, "answer": intermediate_answer})

    # Step 3: Generate the final answer based on sub-question answers
    print("\nStep 3: Generating final answer based on intermediate answers...")
    final_result = final_chain.invoke({
        "input": main_question,
        "context": "\n\n".join(
            f"Sub-question: {pair['input']}\nIntermediate answer: {pair['answer']}"
            for pair in intermediate_pairs
        ),
    })
    final_answer = final_result

    # Extract final answer
    final_answer_match = re.search(r"So the final answer is: (.*?)$", final_answer, re.DOTALL)
    if final_answer_match:
        final_answer = final_answer_match.group(1).strip()

    return {"input": main_question, "answer": final_answer, "intermediate": intermediate_pairs}

Comparaison des approches RAG

Maintenant que nous avons configuré les trois approches RAG, comparons leurs réponses à la même requête, cette fois beaucoup plus complexes pour voir les différences.

Ce comparatif nous aidera à comprendre les avantages de chaque approche et à déterminer quand il convient le mieux de l’utiliser.

# Run all approaches on the same complex query
comparison_query = "What was the full impact chain of the National Guard's assistance during the pandemic? Specifically, how did their involvement affect volunteer operations, what specific tasks did they perform, and how did this ultimately translate to community impact in terms of food distribution capabilities and reach?"

print("\n=== Standard RAG ===")
standard_result = rag_chain.invoke({"input": comparison_query})
print(standard_result["answer"])

print("\n=== DRAG ===")
drag_result = drag_chain.invoke({"input": comparison_query})
print(drag_result["answer"])

print("\n=== IterDRAG ===")
iterdrag_result = iterative_drag(comparison_query)
print(iterdrag_result["answer"])

Comparaison et analyse des résultats

Nous résumons ici les différences de performance entre les trois approches RAG mises en œuvre :

Approche

 

Points forts

 

Limites

 

Meilleurs cas d’utilisation

 

RAG standard

  • Une implémentation simple
  • Adapté aux questions simples
  • Réduction des exigences de calcul
  • Utilisation limitée du contexte
  • Performance stagnante avec plus de documents
  • Mauvais en raisonnement complexe 
  • Requêtes factuelles simples
  • Lorsque les capacités de calcul sont restreintes
  • Quand le contexte est faible 

DRAG

  • Meilleure utilisation du contexte
  • Amélioration des performances avec un plus grand nombre de documents
  • Adapté aux requêtes moyennement complexes
  • Toujours limité par la génération en une seule étape
  • Moins efficace pour les questions multi-hop
  • Requêtes de complexité modérée
  • Lorsque davantage de documents sont disponibles
  • Lorsque des exemples contextuels peuvent être fournis

IterDRAG

  • Idéal pour les requêtes complexes
  • Chaînes de raisonnement explicites
  • Utilisation optimale du contexte
  • Exigences de calcul les plus élevées
  • Mise en œuvre plus complexe
  • Questions multi-hop
  • Analyses complexes nécessitant un raisonnement composite
  • Quand des performances optimales sont nécessaires 
    

Comme nous l'avons observé dans notre implémentation, les techniques d'échelle d'inférence, telles que DRAG et IterDRAG, peuvent considérablement améliorer les performances de la RAG. Cette méthode est particulièrement vraie pour les requêtes complexes nécessitant une analyse approfondie de plusieurs documents.

Conclusion

Dans ce tutoriel, nous avons découvert comment la mise à l’échelle de l’inférence peut améliorer considérablement les performances. En allouant stratégiquement des calculs supplémentaires au temps d’inférence grâce à des techniques comme les DRAG et IterDRAG, nous pouvons obtenir des gains considérables dans la qualité des réponses pour les requêtes complexes.

Difficultés rencontrées avec les modèles traditionnels RAG et basés sur des transformateurs

Inférence coûteuse : les modèles basés sur des transformateurs, qui utilisent des mécanismes d'auto-attention, ont des coûts d'inférence qui augmentent de manière quadratique avec la longueur de l'entrée. Cette méthode rend le traitement des contextes longs coûteux en calcul, limitant l’application pratique de la RAG à des documents plus courts ou nécessitant une troncature agressive.

Utilisation du contexte limitée : les systèmes RAG standard récupèrent et traitent souvent un nombre fixe de documents, ce qui peut s'avérer insuffisant pour les requêtes complexes à plusieurs sauts. Les performances plafonnent à mesure que la longueur du contexte augmente, en particulier au-delà de 128 000 tokens, car le modèle a du mal à synthétiser les informations provenant de nombreux passages récupérés.

Allocation inefficace des ressources informatiques : sans une allocation minutieuse, l'ajout de documents ou de contexte supplémentaires augmente simplement le coût informatique sans gain proportionnel en termes de précision, ce qui conduit à une diminution des rendements, voire à une dégradation des performances en raison d'une surcharge d'informations.

Comment DRAG et IterDRAG répondent à ces défis

RAG basée sur la démonstration (DRAG) :

Le modèle DRAG exploite plusieurs exemples, questions et réponses récupérés comme démonstrations dans le prompt, ce qui lui permet d'apprendre dans son contexte comment localiser et appliquer les informations pertinentes.

Cette approche est particulièrement efficace pour les longueurs de contexte effectives plus courtes, car elle permet au modèle d’utiliser un contexte riche sans surcharger le mécanisme d’attention, ce qui améliore la qualité de la récupération et de la génération.

RAG basée sur la démonstration itérative (IterDRAG) :

L'IterDRAG décompose les requêtes complexes en sous-requêtes plus simples, en récupérant et en générant des réponses de manière itérative pour chaque sous-étape.

En combinant la récupération et la génération, IterDRAG construit des chaînes de raisonnement qui comblent le fossé pour les requêtes à sauts multiples, ce qui le rend particulièrement efficace pour les contextes exceptionnellement longs.

Ce processus permet au modèle d’allouer les calculs plus efficacement, en se concentrant sur les informations les plus pertinentes à chaque étape et en évitant le risque de surcharge d'attention dans un contexte trop large. En appliquant ces techniques de mise à l’échelle de l’inférence à vos applications RAG, vous pouvez obtenir des performances nettement meilleures sur les tâches à forte intensité de connaissances sans modifier vos modèles sous-jacents.

Étapes suivantes :

  • Expérimentez différents modèles de récupération et approches de prétraitement des documents.

  • Essayez différentes formulations de prompts pour la compréhension d'image.

  • Découvrez l’optimisation des paramètres de modèle pour trouver les paramètres idéaux pour votre cas d’utilisation spécifique.
Solutions connexes
IBM watsonx.ai

Entraînez, validez, réglez et déployez une IA générative, des modèles de fondation et des capacités de machine learning avec IBM watsonx.ai, un studio d’entreprise nouvelle génération pour les générateurs d’IA. Créez des applications d’IA en peu de temps et avec moins de données.

Découvrir watsonx.ai
Solutions d’intelligence artificielle

Mettez l’IA au service de votre entreprise en vous appuyant sur l’expertise de pointe d’IBM dans le domaine de l’IA et sur son portefeuille de solutions.

Découvrir les solutions d’IA
Conseils et services en matière d’IA

Réinventez les workflows et les opérations critiques en ajoutant l’IA pour optimiser les expériences, la prise de décision et la valeur métier en temps réel.

Découvrir les services d’IA
Passez à l’étape suivante

Bénéficiez d’un accès centralisé aux fonctionnalités couvrant le cycle de développement de l’IA. Produisez des solutions IA puissantes offrant des interfaces conviviales, des workflows et un accès à des API et SDK conformes aux normes du secteur.

  1. Découvrir watsonx.ai
  2. Réserver une démo en direct
Notes de bas de page

1. “A Survey of Frontiers in LLM Reasoning: Inference Scaling, Learning to Reason, and Agentic Systems,” Ke, Zixuan, Fangkai Jiao, Yifei Ming, Xuan-Phi Nguyen, Austin Xu, Do Xuan Long, Minzhi Li, et al.,  ArXiv.org, 2025.

2. “Reasoning in Granite 3.2 Using Inference Scaling,” Lastras, Luis. 2025,  IBM Research, IBM, 26 février 2025.

3. “Inference Scaling for Long-Context Retrieval Augmented Generation,” Zhenrui Yue, Honglei Zhuang, Aijun Bai, Kai Hui, Rolf Jagerman, Hansi Zeng, Zhen Qin, Dong Wang, Xuanhui Wang, Michael Bendersky, ArXiv.org, 2024.