Scalabilità dell'inferenza per migliorare la RAG multimodale

Scalabilità dell'inferenza per migliorare la RAG multimodale 

Il ridimensionamento dell'inferenza nell'intelligenza artificiale (AI) si riferisce a tecniche che migliorano le prestazioni del modello allocando risorse computazionali durante la fase di inferenza (quando i modelli generano output) piuttosto che fare affidamento su set di dati di addestramento più grandi o architetture di modelli. Poiché i modelli linguistici di grandi dimensioni (LLM) continuano a espandersi sia nei parametri del modello che nella scala del set di dati, l'ottimizzazione del tempo di inferenza e la gestione della scalabilità del calcolo dell'inferenza, in particolare sull'hardware GPU, sono diventate sfide centrali per implementare sistemi ad alte prestazioni di retrieval-augmented generation (RAG). 

Introduzione alla scalabilità dell'inferenza

I recenti progressi nelle strategie di inferenza che aumentano le risorse e utilizzano algoritmi complessi in fase di test stanno ridefinendo il modo in cui gli LLM affrontano compiti di ragionamento complessi e forniscono output di qualità superiore in diverse modalità di input. Il ridimensionamento dell'inferenza ottimizza la chain of thought (CoT) ampliando la profondità del ragionamento. Questa espansione consente ai modelli di produrre chain of thought più lunghe e dettagliate attraverso il prompt iterativo o la generazione in più fasi. La scalabilità dell'inferenza può essere utilizzata per migliorare la RAG multimodale, perché si concentra sull'interazione tra dimensioni del modello, budget dei computer e l'ottimizzazione pratica dei tempi di inferenza per applicazioni del mondo reale.

Inoltre, le leggi sulla scalabilità e i risultati dei benchmark enfatizzano i compromessi tra strategie di preformazione, messa a punto, tempo di inferenza e algoritmi avanzati per la selezione degli output. Sia i modelli più grandi che quelli più piccoli traggono beneficio dalla scalabilità dell'inferenza poiché consente anche ai sistemi a risorse limitate di avvicinarsi alle prestazioni degli LLM all'avanguardia. Questo tutorial dimostra l'impatto delle tecniche di ottimizzazione sulle prestazioni del modello, offrendo una guida attuabile per bilanciare precisione, latenza e costi nelle distribuzioni RAG multimodali.

Questo tutorial è progettato per sviluppatori, ricercatori e appassionati di intelligenza artificiale che desiderano migliorare la propria conoscenza della gestione dei documenti e delle tecniche avanzate di elaborazione del linguaggio naturale (NLP). Imparerai come sfruttare la potenza della scalabilità dell'inferenza per migliorare la pipeline RAG multimodale creata nella procedura precedente. Sebbene questo tutorial si concentri sulle strategie per la scalabilità nel RAG multimodale focalizzato specificamente sui modelli IBM® Granite Large Language, principi simili sono applicabili ai modelli più diffusi, inclusi quelli di OpenAI (ad esempio, GPT-4, GPT-4o, ChatGPT) e DeepMind.

Questo tutorial illustra i processi seguenti:

  • Preelaborazione dei documenti: Imparerai a gestire documenti provenienti da varie fonti, analizzarli e trasformarli in formati utilizzabili e memorizzarli in database vettoriale utilizzando Docling. Docling è un toolkit open source IBM utilizzato per analizzare in modo efficiente i formati di documenti, come PDF, DOCX, PPTX, XLSX, immagini, HTML, AsciiDoc e Markdown. Quindi esporta il contenuto del documento in formati leggibili dalle macchine come Markdown o JSON. Utilizzerai un modello di machine learning Granite per generare descrizioni delle immagini nei documenti. In questo tutorial, Docling scaricherà i documenti PDF e li elaborerà in modo da ottenere il testo e le immagini contenuti nei documenti. In questo tutorial, Docling scaricherà i documenti PDF e li elaborerà in modo da ottenere il testo e le immagini contenuti nei documenti.

  • Retrieval-augmented generation (RAG): capire come collegare LLM come Granite con basi di conoscenza esterne per migliorare le risposte alle query e generare preziosi insight. La RAG è una tecnica dei modelli linguistici di grandi dimensioni (LLM) utilizzata per connettere gli LLM con una knowledge base di informazioni esterna ai dati che l'LLM ha addestrato. Questa tecnica viene applicata agli LLM senza la necessità di messa a punto. La RAG tradizionale è limitata ai casi d'uso basati su testo, come il riepilogo del testo e i chatbot.

  • RAG multimodale: scopri come la RAG multimodale utilizza i modelli linguistici multimodali di grandi dimensioni (MLLM) per elaborare informazioni provenienti da più tipi di dati. Questi dati possono quindi essere inclusi come parte della knowledge base esterna utilizzata nei RAG. I dati multimodali possono includere testo, immagini, audio, video o altre forme. In questo tutorial, utilizzeremo l'ultimo modello di visione multimodale di IBM, Granite 3.2 vision.

  • Implementazione della RAG basata sulla dimostrazione (DRAG) e RAG basato sulla dimostrazione iterativa (IterDrag): applica le tecniche di scalabilità dell'inferenza del documento di ricerca per migliorare notevolmente le prestazioni RAG quando si lavora con un contesto lungo. Il metodo DRAG utilizza l'apprendimento contestuale per migliorare le prestazioni RAG. Includendo più esempi di RAG come dimostrazioni, DRAG aiuta i modelli a imparare a individuare le informazioni rilevanti in contesti lunghi. A differenza della RAG standard, che potrebbe stabilizzarsi con più documenti, DRAG mostra miglioramenti lineari con una maggiore lunghezza del contesto. IterDRAG è un'estensione di DRAG che risolve complesse query multihop scomponendole in query secondarie più semplici. Multihop è un processo in cui una query complessa viene suddivisa e risolta in semplici sottodomande. Ogni sottodomanda può richiedere informazioni recuperate e/o sintetizzate da fonti diverse. IterDrag collega le fasi di recupero e generazione, creando catene di ragionamento che colmano le lacune della composizione. Questo approccio è particolarmente efficace per gestire query complesse in contesti lunghi.

  • LangChain per l'orchestrazione del workflow: scopri come utilizzare LangChain per semplificare e orchestrare i workflow di elaborazione e recupero dei documenti, consentendo un'interazione perfetta tra i diversi componenti del sistema.

In questo tutorial, utilizzerai anche tre tecnologie all'avanguardia:

  1. Docling: un toolkit open source utilizzato per analizzare e convertire i documenti.

  2. Granite: una famiglia di LLM all'avanguardia che offre solide funzionalità di linguaggio naturale e un modello di linguaggio di visione che fornisce la generazione di immagini e testo.

  3. LangChain: un potente framework utilizzato per creare applicazioni basate su modelli linguistici, progettato per semplificare i workflow complessi e integrare strumenti esterni senza problemi.

Al termine di questo tutorial sarai in grado di:

  • Acquisire competenze nella pre-elaborazione dei documenti, nel chunking e nella comprensione delle immagini.

  • Integrare database vettoriali per migliorare le funzionalità di recupero.

  • Implementare DRAG e IterDrag per eseguire un recupero dei dati efficiente e accurato con scalabilità dell'inferenza.

  • Sperimentare in prima persona come il calcolo dell'inferenza può portare a miglioramenti quasi lineari delle prestazioni RAG.

Comprendere le sfide a lungo termine

I modelli linguistici tradizionali hanno difficoltà con contesti lunghi per diversi motivi:

  • I meccanismi di attenzione tradizionali come i trasformatori scalano in modo quadratico, il che può richiedere immense risorse di elaborazione. 

  • Difficoltà a localizzare informazioni rilevanti in sequenze molto lunghe. 

  • Sfide nel preservare la coerenza tra parti distanti dell'input. 

  • Aumento delle richieste computazionali per l'elaborazione di sequenze lunghe.

Le tecniche di questo tutorial risolvono queste sfide attraverso l'allocazione strategica del calcolo dell'inferenza.

Metodi di ridimensionamento dell'inferenza: DRAG e IterDRAG

DRAG vs IterDRAG
DRAG vs IterDRAG

Ulteriori informazioni su queste due tecniche avanzate di scaling dell'inferenza (DRAG e IterDrag) sono disponibili nel documento di ricerca "Inference Scaling for Long-Context Retrieval Augmented Generation"

Questi metodi dimostrano che la scalabilità del calcolo dell'inferenza può migliorare le prestazioni RAG in modo quasi lineare se allocate in modo ottimale, consentendo ai sistemi RAG di utilizzare meglio le funzionalità di lungo contesto degli LLM moderni. Per questa implementazione, utilizzeremo un modello IBM® Granite in grado di elaborare diverse modalità. Creerai un sistema AI per rispondere alle domande degli utenti in tempo reale provenienti da dati non strutturati, applicando i principi del documento.

Prerequisiti

  • Familiarità con la programmazione Python.

  • Comprensione di base di LLM, concetti di NLP e computer vision.

Passaggi

Assicurati di eseguire Python 3.10, 3.11 o 3.12 in un ambiente virtuale appena creato. Nota: puoi accedere a questo tutorial anche su GitHub.

Passaggio 1: configurazione dell'ambiente

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."

Passaggio 2: installare le dipendenze

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

Registrazione

Per visualizzare alcune informazioni di registrazione, possiamo configurare il livello di registro INFO.

NOTA: se vuoi, puoi saltare l'esecuzione di questa cella.

import logging

logging.basicConfig(level=logging.INFO)

Passaggio 3: Selezionere i modelli AI

Caricare i modelli Granite

Specifica il modello di embedding da utilizzare per generare vettori di embedding del testo. Qui utilizzeremo uno dei modelli Granite Embeddings.

Per utilizzare un modello di embedding diverso, sostituisca questa cella di codice con una tratta da questa procedura Embeddings Model.

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)

Specifica il file MLLM da utilizzare per la comprensione dell'immagine. Useremo il modello Granite Vision.

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)

Specifica il modello linguistico da utilizzare per l'operazione di generazione RAG. Qui utilizzeremo il client Replicate LangChain per connetterci a un modello Granite dell'organizzazione ibm-granite su Replicate.

Per configurare Replicate, vedi Guida introduttiva a Replicate.

Per connettersi a un modello su un provider diverso da Replicate, sostituire questa cella di codice con una della procedura del componente 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)

Passaggio 4: Preparare i documenti per il database vettoriale con 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 }

Una volta elaborati i documenti, elaboriamo ulteriormente gli elementi di testo nei documenti e li suddividiamo in dimensioni appropriate per il modello di embedding che stiamo utilizzando. Dai blocchi di testo viene creato un elenco di documenti LangChain.

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")

A seguire, elaboriamo tutte le tabelle presenti nei documenti. Convertiamo i dati della tabella in formato markdown in modo che il modello linguistico possa elaborarli. Un elenco di documenti LangChain viene creato dai rendering markdown della tabella.

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")

Infine, elaboriamo tutte le immagini nei documenti. Qui usiamo il modello linguistico di visione per comprendere il contenuto delle immagini. In questo esempio, siamo interessati a qualsiasi informazione testuale nell'immagine.

La scelta di un prompt appropriato è critica in quanto indica su quali aspetti dell'immagine si concentrerà il modello. Ad esempio:

  • Una richiesta come "Fornisci una descrizione dettagliata di ciò che è raffigurato nell'immagine" (usata di seguito) fornirà informazioni generali su tutti gli elementi visivi.

  • Un prompt come "Quale testo appare in questa immagine?" si concentrerà specificamente sull'estrazione di contenuti testuali.

  • Un prompt come "Descrivi la visualizzazione dei dati in questa immagine" è migliore per diagrammi e grafici.

  • Ti consigliamo di sperimentare diversi prompt in base ai tipi di immagini presenti nei tuoi documenti e alle informazioni che devi estrarne.

NOTA: l'elaborazione delle immagini potrebbe richiedere un tempo di elaborazione molto lungo, a seconda del numero di immagini e del servizio eseguito dal modello linguistico di visione.

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")

Possiamo quindi visualizzare i documenti LangChain creati dai documenti di input.

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

Compilare il database vettoriale

Utilizzando l'embedding, carichiamo i documenti dai blocchi di testo e dalle didascalie delle immagini generate in un database vettoriale. La creazione di questo database vettoriale ci consente di condurre facilmente una ricerca di somiglianza semantica tra i nostri documenti.

NOTA: La popolazione del database vettoriale potrebbe richiedere un tempo di elaborazione molto lungo, a seconda del modello di embedding e del servizio.

Scegliere il tuo database vettoriale

Specifica il database da utilizzare per memorizzare e recuperare i vettori di embedding. Per lo scopo di questo tutorial utilizzeremo Milvus tramite Langchain. Come database vettoriale, Milvus memorizzerà, indicizzerà e gestirà gli embedding numerici generati da reti neurali e vari algoritmi di ML.

Per connetterti a un database vettoriale diverso da Milvus, sostituisci questa cella di codice con una di questa procedura di database vettoriale.

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"},
)

Ora, aggiungiamo tutti i documenti LangChain per il testo, le tabelle e le descrizioni delle immagini al database vettoriale.

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})

Passaggio 5: RAG con Granite

Ora che abbiamo convertito e vettorializzato con successo i nostri documenti, possiamo impostare la nostra pipeline RAG.

Convalida della qualità del recupero

Qui testiamo il database vettoriale cercando blocchi con informazioni pertinenti alla nostra query nello spazio vettoriale. Mostriamo i documenti associati alla descrizione dell'immagine recuperata.

Questa fase di convalida è importante per garantire che il nostro sistema di recupero funzioni correttamente prima di costruire la pipeline RAG completa. Vogliamo vedere se i documenti restituiti sono rilevanti per la nostra query.

Puoi provare query diverse.

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

I documenti restituiti devono rispondere alla query. Andiamo avanti e costruiamo la nostra pipeline RAG.

I documenti restituiti devono rispondere alla query. Andiamo avanti e costruiamo la nostra pipeline RAG.

Creare la pipeline RAG per Granite

Per prima cosa, creiamo i prompt affinché Granite esegua la query RAG. Utilizziamo il modello di chat Granite e forniamo i valori segnaposto che la pipeline LangChain RAG sostituirà.

{context} conterrà i blocchi recuperati, come mostrato nella ricerca precedente, e li invierà al modello come contesto del documento per rispondere alla nostra domanda.

Quindi, costruiamo la pipeline RAG utilizzando i modelli di prompt di Granite che abbiamo creato.

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,
)

Generare una risposta aumentata per il recupero a una domanda

La pipeline utilizza la query per individuare i documenti dal database vettoriale e utilizzarli come contesto per la query.

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

Limitazioni RAG standard e perché è necessario il ridimensionamento delle inferenze

Sebbene l'approccio RAG standard funzioni ragionevolmente bene, presenta diverse limitazioni chiave quando si tratta di contenuti lunghi o complessi:

  1. Gestione del contesto: quando ci si trova davanti a molti documenti, la RAG standard fatica a utilizzare efficacemente tutto il contesto disponibile.

  2. Qualità del recupero: senza indicazioni su come utilizzare le informazioni recuperate, i modelli spesso si concentrano sulle parti sbagliate dei documenti.

  3. Ragionamento compositivo: il processo di comprensione di query complesse che richiedono un ragionamento in più fasi è impegnativo per la RAG standard.

  4. Prestazioni: l'aggiunta di più documenti alla RAG standard spesso comporta un calo delle prestazioni dopo una certa soglia.

Le tecniche di scalabilità dell'inferenza risolvono queste limitazioni allocando strategicamente più calcolo al momento dell'inferenza.

RAG migliorato con DRAG (RAG basato su dimostrazioni)

Ora implementeremo la tecnica DRAG tratta dal documento di ricerca "Inference Scaling for Long-Context Retrieval Augmented Generation" per migliorare il nostro sistema RAG.

DRAG utilizza esempi contestuali per dimostrare al modello come estrarre e utilizzare informazioni dai documenti, migliorando le prestazioni per scenari a lungo termine.

Passaggio 1: Creare esempi di dimostrazioni contestuali

In genere, questi dati provengono da un set di dati curato di coppie QA di alta qualità. A questo scopo, creeremo alcuni esempi sintetici che corrispondono al dominio previsto.

Qui definiamo una classe di dati per rappresentare una dimostrazione individuale e quindi creiamo alcune dimostrazioni.

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

Passaggio 2: Formattare le dimostrazioni per includerle nel prompt

Quindi formattiamo tutte le dimostrazioni insieme per il 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)
)

Passaggio 3: Creare il modello di prompt DRAG

A seguire, creiamo il prompt DRAG per il modello che include gli esempi dimostrativi formattati.

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"))

Passaggio 4: Creare un retriever personalizzato che riordina i documenti

Normalmente, il retriever restituirà i documenti in ordine di somiglianza, dove il documento più simile è il primo. Definiamo un retriever di riordino per invertire l'ordine dei risultati. L'ordine ora mostra il documento più simile per ultimo, quindi più vicino alla fine del 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)

Passaggio 5: Creare la pipeline DRAG

Creiamo la pipeline per la query DRAG utilizzando il modello di prompt DRAG e il reordering retriever.

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,
)

Passaggio 6: Generare una risposta potenziata con DRAG a una domanda

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

Ottimo, abbiamo ottenuto alcuni miglioramenti nella risposta fornendo alcuni esempi. Ora proviamo una tecnica RAG ancora più approfondita!

Implementazione di IterDRAG (RAG iterativo basato su dimostrazioni)

IterDrag estende DRAG scomponendo le query complesse in sottoquery più semplici ed eseguendo il recupero interlacciato. Questo approccio è particolarmente efficace per domande complesse con più passaggi che richiedono l'integrazione di informazioni provenienti da più fonti o il ragionamento in più fasi.
 
Vantaggi fondamentali dell'approccio iterativo:

  • Scompone le domande complesse in parti gestibili.

  • Recupera informazioni più pertinenti per ogni sottodomanda.

  • Crea catene di ragionamento esplicite.

  • Consente di affrontare in un unico passaggio questioni che sarebbero complesse.

Passaggio 1: Creare una catena di scomposizione delle query

La fase di scomposizione è critica perché richiede una query complessa e la suddivide in query secondarie più semplici e mirate a cui è possibile rispondere individualmente.

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

Passaggio 2: Creare una catena di risposte per query secondarie

Il componente di risposta alle query secondarie gestisce ogni singola sottodomanda recuperando i documenti pertinenti e generando risposte intermedie mirate.

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,
)

Passaggio 3: Creare una catena di generazione di risposte finali

La componente finale per la generazione delle risposte combina tutte le risposte intermedie per produrre una risposta completa alla domanda originale.

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

Passaggio 4: Creare dimostrazioni di esempio per IterDRAG

Creare dimostrazioni efficaci è fondamentale per le prestazioni di IterDrag. Questi esempi mostrano al modello come:

  1. Suddividere le domande complesse in sottodomande più semplici.

  2. Generare risposte intermedie pertinenti.

  3. Combinare queste risposte in una risposta finale coerente.
@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

Passaggio 5: Implementare la funzione IterDRAG

Questa funzione orchestra l'intero processo iterativo:

  1. Scomporre la domanda principale in sottodomande.

  2. Per ogni sottodomanda, recuperare i documenti pertinenti e genera una risposta intermedia.

  3. Combinare tutte le risposte intermedie per ottenere la risposta 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}

Confronto tra gli approcci RAG

Ora che abbiamo impostato tutti e tre gli approcci RAG, confrontiamo le loro risposte alla stessa domanda, questa volta molto più complessa, per vedere le differenze.

Il confronto ci aiuterà a comprendere i vantaggi di ciascun approccio e quando potrebbe essere più appropriato utilizzarlo.

# 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"])

Confronto e analisi dei risultati

Qui riassumiamo le differenze tra le prestazioni dei tre approcci RAG implementati:

Approach

 

Punti di forza

 

Limitazioni

 

Migliori casi d'uso

 

RAG standard

  • Implementazione semplice
  • Ideale per query semplici
  • Requisiti computazionali inferiori
  • Utilizzo limitato del contesto
  • Prestazioni piatte con più documenti
  • Scarso nel ragionamento complesso 
  • Semplici query fattuali
  • Quando il calcolo è limitato
  • Quando il contesto è piccolo 

DRAG

  • Migliore utilizzo del contesto
  • Prestazioni migliorate con più documenti
  • Ideale per query moderatamente complesse
  • Ancora limitato dalla generazione in un unico passaggio
  • Meno efficace per le domande multi-hop
  • Query di complessità moderata
  • Quando sono disponibili più documenti
  • Quando è possibile fornire esempi contestuali

IterDRAG

  • Ideale per query complesse
  • Catene di ragionamento esplicito
  • Uso più efficace del contesto
  • Requisiti di calcolo più elevati
  • Implementazione più complessa
  • Domande multi-hop
  • Analisi complesse che richiedono un ragionamento composito
  • Quando sono necessarie le massime prestazioni 
    

Come abbiamo visto nella nostra implementazione, le tecniche di ridimensionamento dell'inferenza, come DRAG e IterDRAG, possono migliorare significativamente le prestazioni RAG. Questo metodo è particolarmente vero per le query complesse che richiedono un'analisi approfondita di più documenti.

Conclusione

In questo tutorial, abbiamo esplorato come il ridimensionamento dell'inferenza può migliorare notevolmente le prestazioni RAG. Allocando strategicamente calcoli aggiuntivi al momento dell'inferenza attraverso tecniche come DRAG e IterDRAG, possiamo ottenere miglioramenti sostanziali nella qualità delle risposte per query complesse.

Sfide con i modelli tradizionali basati su RAG e trasformatori

Inferenza costosa: i modelli basati sui trasformatori, che utilizzano meccanismi di auto-attenzione, hanno costi di inferenza che scalano quadraticamente con la lunghezza dell'input. Questo metodo rende la gestione di contesti lunghi dispendiosa dal punto di vista computazionale, limitando l'applicazione pratica di RAG a documenti più brevi o richiedendo un troncamento aggressivo.

Utilizzo del contesto limitato: i sistemi RAG standard spesso recuperano ed elaborano un numero fisso di documenti che possono essere insufficienti per query complesse e multihop. Le prestazioni si stabilizzano con l'aumentare della lunghezza del contesto, soprattutto oltre i 128.000 token, perché il modello fatica a sintetizzare le informazioni in molti passaggi recuperati.

Allocazione computazionale inefficiente: senza un'allocazione accurata, l'aggiunta di altri documenti o contesti recuperati aumenta semplicemente i costi computazionali senza aumenti proporzionali in termini di precisione, con conseguente diminuzione dei rendimenti o addirittura un peggioramento delle prestazioni a causa del sovraccarico di informazioni.

In che modo DRAG e IterDRAG trattano queste sfide

RAG basata su dimostrazioni (DRAG):

La DRAG utilizza diversi esempi recuperati, domande e risposte come dimostrazioni all'interno del prompt, consentendo al modello di imparare in contesto come individuare e applicare le informazioni pertinenti.

Questo approccio è particolarmente efficace per contesti più brevi in quanto consente al modello di utilizzare un contesto ricco senza sovraccaricare il meccanismo di attenzione, migliorando sia il recupero che la qualità della generazione.

RAG iterativa basata sulle dimostrazioni (IterDRAG):

IterDRAG scompone query complesse in query secondarie più semplici, recuperando e generando risposte iterative per ogni passaggio secondario.

Inserendo recupero e generazione, IterDrag crea catene di ragionamento che colmano il divario per le query multihop, rendendolo particolarmente efficace per contesti eccezionalmente lunghi.

Questo processo consente al modello di allocare il calcolo in modo più efficiente, concentrandosi sulle informazioni più rilevanti in ogni fase ed evitando il rischio di un sovraccarico di attenzione a lungo termine. Applicando queste tecniche di scalabilità dell'inferenza alle sue applicazioni RAG, può ottenere prestazioni molto migliori in attività ad alta intensità di conoscenze senza modificare i modelli sottostanti.

Fasi successive:

  • Sperimentare con diversi modelli di recupero e approcci di pre-elaborazione dei documenti.

  • Provare diverse formulazioni di prompt per la comprensione delle immagini.

  • Esplorare l'ottimizzazione dei parametri del modello per trovare le impostazioni ideali per il tuo caso d'uso specifico.
Soluzioni correlate
IBM watsonx.ai

Addestra, convalida, adatta e implementa le funzionalità di AI generativa, foundation model e machine learning con IBM watsonx.ai, uno studio aziendale di nuova generazione per builder AI. Crea applicazioni AI in tempi ridotti e con una minima quantità di dati.

Scopri watsonx.ai
Soluzioni di intelligenza artificiale

Metti l'AI al servizio della tua azienda grazie all'esperienza leader di settore e alla gamma di soluzioni di IBM nel campo dell'AI.

Esplora le soluzioni AI
Consulenza e servizi sull'AI

Reinventa i flussi di lavoro e le operazioni critiche aggiungendo l'AI per massimizzare le esperienze, il processo decisionale in tempo reale e il valore di business.

Esplora i servizi AI
Fai il passo successivo

Ottieni l'accesso completo a funzionalità che coprono l'intero ciclo di vita dello sviluppo dell'AI. Crea soluzioni AI all'avanguardia con interfacce intuitive, workflow e accesso alle API e agli SDK standard di settore.

Esplora watsonx.ai Prenota una demo live
Note a piè di pagina

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 febbraio 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.