Escalado de inferencias para mejorar el RAG multimodal

Escalado de inferencias para mejorar el RAG multimodal 

El escalado de inferencia en inteligencia artificial (IA) se refiere a técnicas que mejoran el rendimiento del modelo mediante la asignación de recursos computacionales durante la fase de inferencia (cuando los modelos generan resultados) en lugar de depender de conjuntos de datos de entrenamiento más grandes o arquitecturas de modelos. A medida que los modelos de lenguaje grandes (LLM) siguen expandiéndose tanto en los parámetros del modelo como en la escala del conjunto de datos, optimizar el tiempo de inferencia y gestionar el escalado de cómputo de inferencia, particularmente en hardware de GPU, se han convertido en desafíos centrales para desplegar sistemas de generación aumentada por recuperación (RAG, por sus siglas en inglés)) de alto rendimiento. 

Introducción al escalado de inferencia

Los avances recientes en las estrategias de inferencia que aumentan los recursos computacionales y emplean algoritmos complejos en el momento de la prueba están redefiniendo la forma en que los LLM abordan tareas de razonamiento complejas y ofrecen resultados de mayor calidad en diversas modalidades de entrada. El escalado de inferencias optimiza la cadena de pensamiento (CoT) al ampliar la profundidad del razonamiento. Esta expansión permite que los modelos produzcan cadenas de pensamiento más largas y detalladas a través de instrucciones iterativas o generación de varios pasos. El escalado de inferencia se puede aprovechar para mejorar el RAG multimodal, centrándose en la interacción entre los tamaños de los modelos, los presupuestos informáticos y la optimización práctica del tiempo de inferencia para aplicaciones del mundo real.

Además, las leyes de escala y los resultados de punto de referencia enfatizan las compensaciones entre el entrenamiento previo, el ajuste, las estrategias de tiempo de inferencia y los algoritmos avanzados para la selección de resultados. Tanto los modelos más grandes como los más pequeños se benefician del escalado de inferencia, ya que también permite que los sistemas con recursos limitados se acerquen al rendimiento de los LLM de vanguardia. Este tutorial demuestra el impacto de las técnicas de optimización en el rendimiento del modelo, ofreciendo orientación aplicable en la práctica para equilibrar la precisión, la latencia y el costo en despliegues de RAG multimodales.

Este tutorial está diseñado para desarrolladores, investigadores y entusiastas de la inteligencia artificial que buscan mejorar sus conocimientos sobre gestión de documentos y técnicas avanzadas de procesamiento de lenguaje natural (PLN). Aprenderá a aprovechar el poder del escalado de inferencia para mejorar el pipeline de RAG multimodal creado en una receta anterior. Si bien este tutorial se centra en estrategias de escalabilidad en RAG multimodal centrado específicamente en modelos de lenguaje grandes de IBM® Granite, principios similares son aplicables a los modelos más populares, incluidos los de OpenAI (por ejemplo, GPT-4, GPT-4o, ChatGPT) y DeepMind.

Este tutorial le guiará a través de los siguientes procesos:

  • Preprocesamiento de documentos: aprenderá a manejar documentos de diversas fuentes, analizarlos y transformarlos en formatos utilizables y almacenarlos en bases de datos vectoriales mediante Docling. Docling es un kit de herramientas de código abierto de IBM que se utiliza para analizar de manera eficiente formatos de documentos, como PDF, DOCX, PPTX, XLSX, imágenes, HTML, AsciiDoc y Markdown. A continuación, exporta el contenido del documento a formatos legibles por máquina, como Markdown o JSON. Utilizará un modelo de machine learning (ML) de Granite para generar descripciones de imágenes en los documentos. En este tutorial, Docling descargará los documentos PDF y los procesará para que podamos obtener el texto y las imágenes que contienen. En este tutorial, Docling descargará los documentos PDF y los procesará para que podamos obtener el texto y las imágenes que contienen.

  • Generación aumentada por recuperación (RAG, por sus siglas en inglés)): comprenda cómo conectar LLM como Granite con bases de conocimiento externas para mejorar las respuestas a las consultas y generar insights valiosos. La RAG es una técnica de modelo de lenguaje grande (LLM) que se utiliza para conectar los LLM con una base de conocimientos de información fuera de los datos que se han entrenado en el LLM. Esta técnica se aplica a los LLM sin necesidad de ajustes. La RAG tradicional se limita a casos de uso basados en texto, como el resumen de texto y los chatbots.

  • RAG multimodal: descubra cómo el RAG multimodal utiliza modelos de lenguaje grandes multimodales (MLLM) para procesar información de múltiples tipos de datos. Estos datos se pueden incluir como parte de la base de conocimientos externa utilizada en la RAG. Los datos multimodales pueden incluir texto, imágenes, audio, video u otras formas. En este tutorial, utilizamos el último modelo de visión multimodal de IBM, Granite 3.2 vision.

  • Implementación de RAG basada en demostración (DRAG) y RAG iterativo basada en demostración (IterDRAG): aplique las técnicas de escalado de inferencia del documento de investigación para mejorar significativamente el rendimiento de RAG cuando se trabaja con un contexto largo. El método DRAG aprovecha el aprendizaje en contexto para mejorar el rendimiento de la RAG. Al incluir múltiples ejemplos de la RAG como demostraciones, la DRAG ayuda a los modelos a aprender a localizar información relevante en contextos largos. A diferencia de la RAG estándar que podría estancarse con más documentos, la DRAG muestra mejoras lineales con una mayor longitud de contexto. La IterDRAG es una extensión de la DRAG que aborda consultas complejas multihop descomponiéndolas en subconsultas más simples. Multihop es un proceso en el que una consulta compleja se desglosa y responde en subpreguntas simples. Cada subpregunta puede requerir información recuperada o sintetizada de diferentes fuentes. La IterDRAG intercala pasos de recuperación y generación, creando cadenas de razonamiento que cierran las brechas de composición. Este enfoque es particularmente eficaz para manejar consultas complejas en contextos largos.

  • LangChain para la integración de flujos de trabajo: descubra cómo usar LangChain para optimizar y orquestar los flujos de trabajo de procesamiento y recuperación de documentos, lo que permite una interacción perfecta entre los diferentes componentes del sistema.

Durante este tutorial, también utilizará tres tecnologías de punta:

  1. Docling: un kit de herramientas de código abierto que se utiliza para analizar y convertir documentos.

  2. Granite: una familia de LLM de última generación que proporciona capacidades sólidas de lenguaje natural y un modelo de lenguaje de visión que proporciona imagen a la generación de texto.

  3. LangChain: un potente marco utilizado para crear aplicaciones impulsadas por modelos de lenguaje, diseñado para simplificar flujos de trabajo complejos e integrar herramientas externas perfectamente.

Al final de este tutorial, logrará lo siguiente:

  • Obtenga aptitud en el preprocesamiento de documentos, la fragmentación y la comprensión de imágenes.

  • Integre bases de datos vectoriales para mejorar las capacidades de recuperación.

  • Implemente DRAG e IterDRAG para realizar una recuperación de datos eficiente y precisa con escalado de inferencia.

  • Experimente de primera mano cómo escalar el cálculo de inferencia puede conducir a mejoras de rendimiento casi lineales en el rendimiento de RAG.

Comprender los desafíos del contexto a largo plazo

Los modelos de lenguaje tradicionales tienen dificultades con los contextos largos por varias razones:

  • Los mecanismos de atención tradicionales, como los transformadores, escalan cuadráticamente, lo que puede incurrir en inmensos recursos computacionales. 

  • Dificultad para localizar información relevante en secuencias muy largas. 

  • Desafíos para preservar la coherencia en partes distantes de la entrada. 

  • Mayores demandas computacionales para procesar secuencias largas.

Las técnicas en este tutorial abordan estos desafíos mediante la asignación estratégica del cálculo de inferencias.

Métodos de escalado de inferencia: DRAG e IterDRAG

DRAG frente a IterDRAG
DRAG frente a IterDRAG

Se puede encontrar más información sobre estas dos técnicas avanzadas de escalado de inferencia (DRAG e IterDRAG) en el documento de investigación "Inference Scaling for Long-Context Retrieval Augmented Generation"

Estos métodos muestran que escalar el cálculo de inferencia puede mejorar el rendimiento de la RAG casi linealmente cuando se asigna de manera óptima, lo que permite que los sistemas RAG hagan un mejor uso de las capacidades de contexto largo de los LLM modernos. Para esta implementación, utilizaremos un modelo IBM Granite capaz de procesar diferentes modalidades. Creará un sistema de IA para responder consultas de usuarios en tiempo real a partir de datos no estructurados, aplicando los principios del documento.

Requisitos previos

  • Familiaridad con la programación Python.

  • Conocimientos básicos de LLM, conceptos de PLN y visión artificial.

Pasos

Asegúrese de ejecutar Python 3.10, 3.11 o 3.12 en un entorno virtual recién creado. Tenga en cuenta que también puede acceder a este tutorial en GitHub.

Paso 1: Configurar el entorno

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

Paso 2: Instalar dependencias

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

Registro

Para ver información de registro, podemos configurar el nivel de registro INFO.

NOTA: Está bien omitir la ejecución de esta celda.

import logging

logging.basicConfig(level=logging.INFO)

Paso 3: Seleccionar los modelos de IA

Cargue los modelos Granite

Especifique el modelo de incorporación que se utilizará para generar vectores de incorporación de texto. Aquí usaremos uno de los modelos de Granite Embeddings.

Para usar un modelo de incorporación diferente, reemplace esta celda de código con una de esta receta de modelo de incorporación.

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)

Especifique el MLLM que se utilizará para la comprensión de imágenes. Usaremos el modelo de visión 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)

Especifique el modelo de lenguaje que se utilizará para la operación de generación de RAG. Aquí usamos el cliente Replicate LangChain para conectarnos a un modelo Granite de la organización ibm-granite en Replicate.

Para configurar Replicate, consulte Primeros pasos con Replicate.

Para conectarse a un modelo en un proveedor distinto de Replicate, sustituya esta celda de código por una de la receta de componentes de 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)

Paso 4: Preparar los documentos para la base de datos vectorial 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 }

Con los documentos procesados, procesamos aún más los elementos de texto en los documentos y los fragmentamos en tamaños apropiados para el modelo de incorporación que estamos utilizando. Se crea una lista de documentos LangChain a partir de los fragmentos de texto.

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 continuación, procesamos las tablas de los documentos. Convertimos los datos de la tabla al formato Markdown para que el modelo de lenguaje pueda procesarlos. Se crea una lista de documentos LangChain a partir de las representaciones de rebajas de la tabla.

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

Finalmente, procesamos cualquier imagen en los documentos. Aquí utilizamos el modelo de lenguaje de visión para comprender el contenido de las imágenes. En este ejemplo, nos interesa cualquier información textual de la imagen.

Elegir una instrucción crítica es crítico, ya que indica en qué aspectos de la imagen se centrará el modelo. Por ejemplo:

  • Una instrucción como "Dé una descripción detallada de lo que se muestra en la imagen" (utilizado a continuación) proporcionará información general sobre todos los elementos visuales.

  • Una instrucción como "¿Qué texto aparece en esta imagen?" se centraría específicamente en extraer contenido textual.

  • Una instrucción como "Describa la visualización gráfica de datos en esta imagen" sería mejor para gráficos y tablas.

  • Debe experimentar con diferentes instrucciones basadas en los tipos de imágenes de sus documentos y la información que necesita extraer de ellos.

NOTA: El procesamiento de imágenes puede requerir un tiempo de procesamiento significativo en función del número de imágenes y del servicio que ejecuta el modelo de lenguaje de visión.

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

Luego podemos mostrar los documentos LangChain creados a partir de los documentos de entrada.

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

Rellene la base de datos vectorial

Usando el modelo de incorporación, cargamos los documentos de los fragmentos de texto y la generación de subtítulos de imágenes en una base de datos vectorial. La creación de esta base de datos vectorial nos permite realizar fácilmente una búsqueda de similitud semántica en nuestros documentos.

NOTA: El llenado de la base de datos vectorial puede requerir un tiempo de procesamiento significativo según su modelo y servicio de incorporación.

Elija su base de datos vectorial

Especifique la base de datos que se utilizará para almacenar y recuperar vectores de incorporación. Para el propósito de este tutorial, usaremos Milvus a través de Langchain. Como base de datos vectorial, Milvus almacenará, indexará y gestionará incorporaciones numéricas generadas por redes neuronales y varios algoritmos de ML.

Para conectarse a una base de datos vectorial distinta de Milvus, reemplace esta celda de código con una de esta receta de almacén de vectores. .

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

Ahora, agregamos todos los documentos LangChain para el texto, las tablas y las descripciones de imágenes a la base de datos vectorial.

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

Paso 5: RAG con Granite

Ahora que hemos convertido correctamente nuestros documentos y los hemos vectorizado, podemos configurar nuestro pipeline de RAG.

Valide la calidad de recuperación

Aquí probamos la base de datos vectorial buscando fragmentos con información relevante para nuestra consulta en el espacio vectorial. Mostramos los documentos asociados con la descripción de la imagen recuperada.

Este paso de validación es importante para ayudar a garantizar que nuestro sistema de recuperación funcione correctamente antes de construir nuestro pipeline de RAG completo. Queremos ver si los documentos devueltos son relevantes para nuestra consulta.

Siéntase libre de probar diferentes consultas.

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

Los documentos devueltos deben responder a la consulta. Avancemos y construyamos nuestro pipeline de RAG.

Los documentos devueltos deben responder a la consulta. Avancemos y construyamos nuestro pipeline de RAG.

Cree el pipeline de RAG para Granite

Primero, creamos las instrucciones para que Granite realice la consulta de RAG. Usamos la plantilla de chat de Granite y proporcionamos los valores de marcador de posición que reemplazará el pipeline LangChain RAG.

{context} contendrá los fragmentos recuperados, como se muestra en la búsqueda anterior, y los alimentará al modelo como contexto del documento para responder a nuestra pregunta.

Luego, construimos el pipeline de RAG utilizando las plantillas de instrucciones de Granite que creamos.

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

Genere una respuesta aumentada por recuperación a una pregunta

El pipeline utiliza la consulta para localizar documentos de la base de datos vectorial y utilizarlos como contexto para la consulta.

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

Limitaciones estándar de RAG y por qué necesitamos escalado de inferencia

Si bien el enfoque de RAG estándar funciona razonablemente bien, tiene varias limitaciones clave cuando se trata de contenido largo o complejo:

  1. Gestión del contexto: cuando se trata de muchos documentos, el RAG estándar tiene dificultades para utilizar eficazmente todo el contexto disponible.

  2. Calidad de recuperación: sin orientación sobre cómo utilizar la información recuperada, los modelos a menudo se centran en las partes incorrectas de los documentos.

  3. Razonamiento composicional: el proceso de comprensión de consultas complejas que requieren un razonamiento de varios pasos es un desafío para el RAG estándar.

  4. Topes de rendimiento: agregar más documentos al RAG estándar a menudo da como resultado rendimientos decrecientes después de cierto umbral.

Las técnicas de escalado de inferencia dirigen estas limitaciones asignando estratégicamente más cómputo en el momento de la inferencia.

RAG mejorado con DRAG (RAG basado en demostración)

Ahora implementaremos la técnica DRAG del documento de investigación "Inference Scaling for Long-Context Retrieval Augmented Generation" para mejorar nuestro sistema RAG.

La DRAG utiliza ejemplos en contexto para demostrar al modelo cómo extraer y usar información de documentos, mejorando el rendimiento para escenarios de contexto largo.

Paso 1: Cree demostraciones de muestra en contexto

Por lo general, estos provendrían de un conjunto de datos curados de pares de control de calidad de alta calidad. Para ello, crearemos algunos ejemplos sintéticos que coincidan con el dominio esperado.

Aquí, definimos una clase de datos para representar una demostración individual y luego creamos algunas demostraciones.

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

Paso 2: Formatee las demostraciones para incluirlas en la instrucción

Luego formateamos todas las demostraciones juntas para la instrucción.

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

Paso 3: Cree la plantilla de instrucciones de DRAG

Luego creamos la instrucción de DRAG para el modelo que incluye los ejemplos de demostración formateados.

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

Paso 4: Cree un recuperador personalizado que reordene los documentos

Normalmente, el recuperador devolverá los documentos en orden de similitud, siendo el documento más similar el primero. Definimos un recuperador de reordenamiento para invertir el orden de los resultados. La orden ahora muestra el documento más similar al final, por lo tanto, más cerca del final de la instrucción.

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)

Paso 5: Cree un pipeline de DRAG

Creamos el pipeline para la consulta de DRAG mediante la plantilla de instrucción de DRAG y el recuperador de reordenamiento.

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

Paso 6: Genere una respuesta mejorada con DRAG a una pregunta

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

Excelente; parece que obtuvimos algunas mejoras en la respuesta al darle algunos ejemplos. ¡Probemos una técnica de RAG aún más completa a continuación!

Implementación de IterDRAG (RAG iterativa basada en demostración)

La IterDRAG amplía la DRAG descomponiendo consultas complejas en subconsultas más sencillas y realizando una recuperación intercalada. Este enfoque es particularmente efectivo para preguntas complejas multihop que requieren integrar información de múltiples fuentes o razonamiento en varios pasos.
 
Beneficios clave del enfoque iterativo:

  • Desglosa las preguntas complejas en partes manejables.

  • Recupera información más relevante para cada subpregunta.

  • Crea cadenas de razonamiento explícitas.

  • Permite abordar preguntas que serían desafiantes en un solo paso.

Paso 1: Cree una cadena de descomposición de consultas

El paso de descomposición es crítico porque toma una consulta compleja y la divide en subconsultas más simples y enfocadas que pueden responderse 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

Paso 2: Cree una cadena de respuesta a subconsultas

El componente de respuesta a subconsultas maneja cada subpregunta individual recuperando documentos relevantes y generando respuestas intermedias enfocadas.

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

Paso 3: Cree una cadena final de generación de respuestas

El componente de generación de respuesta final combina todas las respuestas intermedias para producir una respuesta integral a la pregunta original.

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

Paso 4: Cree demostraciones de ejemplo para IterDRAG

Crear demostraciones efectivas es crucial para el rendimiento de IterDRAG. Estos ejemplos muestran al modelo cómo:

  1. Desglose las preguntas complejas en subpreguntas más sencillas.

  2. Genere respuestas intermedias relevantes.

  3. Combine estas respuestas en una respuesta final coherente.
@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

Paso 5: Implemente la función IterDRAG

Esta función orquesta todo el proceso iterativo:

  1. Descomponga la pregunta principal en subpreguntas.

  2. Para cada subpregunta, recupere documentos relevantes y genere una respuesta intermedia.

  3. Combine todas las respuestas intermedias para producir la respuesta final.
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}

Comparación de enfoques de GAR

Ahora que tenemos configurados los tres enfoques de RAG, comparemos sus respuestas a la misma consulta, esta vez mucho más compleja para ver las diferencias.

La comparación nos ayudará a comprender los beneficios de cada enfoque y cuándo podría ser más apropiado usar cada uno.

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

Comparación y análisis de resultados

Aquí resumimos las diferencias de rendimiento entre los tres enfoques RAG implementados:

Método

 

Fortalezas

 

Limitaciones

 

Mejores casos de uso

 

RAG estándar

  • Implementación sencilla
  • Útil para consultas sencillas
  • Menores requisitos computacionales
  • Uso limitado del contexto
  • El rendimiento se estanca con más documentos
  • Deficiente en razonamiento complejo 
  • Consultas fácticas simples
  • Cuando el cálculo es limitado
  • Cuando el contexto es pequeño 

DRAG

  • Mejor utilización del contexto
  • Rendimiento mejorado con más documentos
  • Adecuado para consultas de complejidad moderada
  • Todavía limitado por la generación de un solo paso
  • Menos eficaz para preguntas multihop
  • Consultas de complejidad moderada
  • Cuando haya más documentos disponibles
  • Cuando se pueden proporcionar ejemplos en contexto

IterDRAG

  • Lo mejor para consultas complejas
  • Cadenas de razonamiento explícito
  • Uso más eficaz del contexto
  • Los requisitos computacionales más altos
  • Implementación más compleja
  • Preguntas multihop
  • Análisis complejos que requieren razonamiento compuesto
  • Cuando se necesita el máximo rendimiento 
    

Como hemos visto en nuestra implementación, las técnicas de escalado de inferencia, como DRAG e IterDRAG, pueden mejorar significativamente el rendimiento de la RAG. Este método es especialmente cierto para consultas complejas que requieren un análisis profundo de varios documentos.

Conclusión

En este tutorial, hemos explorado cómo el escalado de inferencia puede mejorar drásticamente el rendimiento de RAG. Al asignar estratégicamente computación adicional en el momento de la inferencia a través de técnicas como la DRAG e IterDRAG, podemos lograr ganancias sustanciales en la calidad de respuesta para consultas complejas.

Desafíos con los modelos tradicionales de RAG y basados en transformadores

Inferencia costosa: los modelos basados en transformadores, que utilizan mecanismos de autoatención, tienen costos de inferencia que se escalan cuadráticamente con la longitud de entrada. Este método hace que el manejo de contextos largos sea computacionalmente costoso, limitando la aplicación práctica de RAG a documentos más cortos o requiriendo un truncamiento agresivo.

Utilización limitada del contexto: los sistemas RAG estándar a menudo recuperan y procesan un número fijo de documentos que pueden ser insuficientes para consultas complejas de varios saltos. El rendimiento se estanca a medida que aumenta la longitud del contexto, especialmente más allá de 128 000 tokens, porque el modelo tiene dificultades para sintetizar la información en muchos pasajes recuperados.

Asignación de cálculo ineficiente: sin una asignación cuidadosa, agregar más documentos o contexto recuperados simplemente aumenta el costo computacional sin ganancias proporcionales en precisión, lo que lleva a rendimientos decrecientes o incluso a un rendimiento degradado debido a la sobrecarga de información.

Cómo DRAG e IterDRAG abordan estos desafíos

RAG basada en demostración (DRAG):

La DRAG aprovecha múltiples ejemplos recuperados, preguntas y respuestas como demostraciones dentro de la instrucción, lo que permite que el modelo aprenda en contexto cómo localizar y aplicar información relevante.

Este enfoque es particularmente efectivo para longitudes de contexto efectivas más cortas, ya que permite que el modelo utilice un contexto enriquecido sin abrumar el mecanismo de atención, mejorando tanto la calidad de la recuperación como la de la generación.

RAG iterativa basada en demostraciones (IterDRAG):

La IterDRAG descompone consultas complejas en subconsultas más simples, recuperando y generando respuestas iterativamente para cada subpaso.

Al intercalar la recuperación y la generación, la IterDRAG crea cadenas de razonamiento que cierran la brecha para las consultas multihop, lo que lo hace especialmente eficaz para contextos excepcionalmente largos.

Este proceso permite que el modelo asigne el cálculo de manera más eficiente, centrándose en la información más relevante en cada paso y evitando el riesgo de sobrecarga de atención de contexto largo. Al aplicar estas técnicas de escalado de inferencia a sus aplicaciones de RAG, puede lograr un rendimiento significativamente mejor en tareas intensivas en conocimiento sin cambiar sus modelos subyacentes.

Siguientes pasos:

  • Experimente con diferentes modelos de recuperación y enfoques de preprocesamiento de documentos.

  • Pruebe diferentes formulaciones de instrucciones para la comprensión de imágenes.

  • Explore la optimización de parámetros del modelo para encontrar la configuración ideal para su caso de uso específico.
Soluciones relacionadas
IBM watsonx.ai

Entrene, valide, ajuste y despliegue IA generativa, modelos fundacionales y capacidades de machine learning con IBM watsonx.ai, un estudio empresarial de próxima generación para creadores de IA. Diseñe aplicaciones de IA en menos tiempo y con menos datos.

Descubra watsonx.ai
Soluciones de inteligencia artificial

Ponga la IA a trabajar en su negocio con la experiencia en IA líder en la industria y la cartera de soluciones de IBM a su lado.

Explore las soluciones de IA
Consultoría y servicios de IA

Reinvente los flujos de trabajo y las operaciones críticas añadiendo IA para maximizar las experiencias, la toma de decisiones en tiempo real y el valor empresarial.

Conozca los servicios de IA
Dé el siguiente paso

Obtenga acceso único a capacidades que abarcan el ciclo de vida del desarrollo de IA. Produzca potentes soluciones de IA con interfaces fáciles de usar, flujos de trabajo y acceso a API y SDK estándar de la industria.

Explore watsonx.ai Reserve una demostración en vivo
Notas de pie de página

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 de febrero de 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.