Escalado de inferencias para mejorar el RAG multimodal

Escalado de inferencias para mejorar el RAG multimodal 

El escalado de inferencias en inteligencia artificial (IA) se refiere a técnicas que mejoran el rendimiento del modelo asignando recursos computacionales durante la fase de inferencia (cuando los modelos generan outputs) en lugar de depender de conjuntos de datos de entrenamiento más grandes o arquitecturas de modelos. A medida que los modelos de lenguaje de gran tamaño (LLM) continúan expandiéndose tanto en parámetros de modelo como en escala de 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 implementar sistemas de generación aumentada por recuperación (RAG) de alto rendimiento. 

Introducción al escalado de inferencias

Los recientes avances en las estrategias de inferencia que aumentan los recursos 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 output de mayor calidad en diversas modalidades de entrada. El escalado de inferencias optimiza la cadena de pensamiento (CoT) ampliando la profundidad del razonamiento. Esta expansión permite a los modelos producir cadenas de pensamiento más largas y detalladas a través de prompts iterativos o generación de varios pasos. El escalado de inferencias 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 escalado y los resultados de referencia enfatizan las compensaciones entre el preentrenamiento, el ajuste, las estrategias de tiempo de inferencia y los algoritmos avanzados para la selección de output. 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 beneficio de los LLM de vanguardia. Este tutorial demuestra el impacto de las técnicas de optimización en el rendimiento del modelo, ofreciendo una guía que se puede ejecutar para equilibrar la precisión, la latencia y el coste en las implementaciones 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 del lenguaje natural (PLN). Aprenderá a aprovechar el poder del escalado de inferencias para mejorar el pipeline RAG multimodal creado en una receta anterior. Aunque este tutorial se centra en las estrategias de escalabilidad en RAG multimodal centradas específicamente en los grandes modelos de lenguaje 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 eficazmente 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 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): comprenda cómo conectar LLM como Granite con bases de conocimiento externas para mejorar las respuestas a las consultas y generar información valiosa. RAG es una técnica de modelo de lenguaje de gran tamaño (LLM) que se utiliza para conectar los LLM con una base de conocimiento de información fuera de los datos que el LLM ha entrenado. Esta técnica se aplica a los LLM sin necesidad de ajustes. El 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 multimodal de gran tamaño (MLLM) para procesar información de múltiples tipos de datos. Estos datos pueden incluirse como parte de la base de conocimientos externa utilizada en RAG. Los datos multimodales pueden incluir texto, imágenes, audio, vídeo u otras formas. En este tutorial, utilizamos el último modelo de visión multimodal de IBM, Granite 3.2 vision.

  • Implementación de RAG basado en demostración (DRAG) y RAG iterativo basado 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 RAG. Al incluir varios ejemplos de RAG como demostraciones, DRAG ayuda a los modelos a aprender a localizar información relevante en contextos largos. A diferencia del RAG estándar, que podría estancarse con más documentos, DRAG muestra mejoras lineales con una mayor longitud de contexto. IterDRAG es una extensión de DRAG que aborda consultas multisalto complejas descomponiéndolas en subconsultas más simples. El multisalto es un proceso en el que una consulta compleja se desglosa y se responde en subpreguntas sencillas. Cada subpregunta puede requerir información recuperada o sintetizada de diferentes fuentes. IterDRAG intercala los pasos de recuperación y generación, creando cadenas de razonamiento que salvan las brechas de composición. Este enfoque es especialmente eficaz para gestionar consultas complejas en contextos largos.

  • LangChain para integración de flujos de trabajo: descubra cómo utilizar LangChain para agilizar 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 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 sólidas capacidades 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 basadas en modelos de lenguaje, diseñado para simplificar flujos de trabajo complejos e integrar herramientas externas de manera fluida.

Al final de este tutorial, habrá logrado lo siguiente:

  • Obtenga competencia 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 de contexto largo

Los modelos lingüísticos tradicionales tienen dificultades con los contextos largos por varias razones:

  • Los mecanismos de atención tradicionales, como los transformadores, se escalan cuadráticamente, lo que puede implicar 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. 

  • Aumento de las 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 vs IterDRAG
DRAG vs IterDRAG

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 RAG casi linealmente cuando se asigna de manera óptima, lo que permite a los sistemas RAG hacer 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 a las consultas de los 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: Configuración del 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

Información de registro

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

NOTA: Puede omitir la ejecución de esta celda.

import logging

logging.basicConfig(level=logging.INFO)

Paso 3: Selección de los modelos de IA

Cargue los modelos Granite

Especifique el modelo de incrustaciones que se utilizará para generar vectores de embedding de texto. Aquí utilizaremos uno de los modelos de Granite Embeddings.

Para utilizar un modelo de embedding diferente, sustituya esta celda de código por una de esta receta del modelo de embeddings.

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. Utilizaremos el modelo de 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)

Especifique el modelo de lenguaje que se utilizará para la operación de generación de RAG. Aquí utilizamos 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 que no sea Replicate, sustituya esta celda de código por una de la receta 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)

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 }

Una vez procesados los documentos, procesamos aún más los elementos de texto de los documentos y los fragmentamos en tamaños adecuados para el modelo de embeddings 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 Markdown 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")

Por último, procesamos las imágenes de 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 de imagen crítica es fundamental, 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 "Describe la visualización 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 en 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")

A continuación, 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

Utilizando el embedding, cargamos los documentos de los fragmentos de texto y los subtítulos de imágenes generados 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 en función del modelo y el servicio de embedding.

Elija su base de datos vectorial

Especifique la base de datos que se utilizará para almacenar y recuperar vectores de embedding. A los efectos de este tutorial, utilizaremos Milvus via  Langchain. Como base de datos vectorial, Milvus almacenará, indexará y gestionará embeddings numéricos generados por redes neuronales y varios algoritmos de ML (machine learning).

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, añadimos 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 RAG.

Valide la calidad de la 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 a 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 completo de RAG. Queremos ver si los documentos devueltos son relevantes para nuestra consulta.

No dude en 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 RAG.

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

Cree el pipeline RAG para Granite

En primer lugar, creamos las instrucciones para que Granite realice la consulta RAG. Utilizamos la plantilla de chat 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 alimentará el modelo como contexto del documento para responder a nuestra pregunta.

A continuación, construimos el pipeline RAG utilizando las plantillas de instrucciones de Granite que hemos creado.

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

Aunque el enfoque 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 la recuperación: sin orientación sobre cómo utilizar la información recuperada, los modelos suelen centrarse en las partes equivocadas 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 la RAG estándar.

  4. Rendimiento: añadir más documentos a la RAG estándar suele dar lugar a Resultados decrecientes a partir de un determinado umbral.

Las técnicas de escalado de inferencia dirigen estas limitaciones asignando estratégicamente más computación 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.

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

Paso 1: Cree demostraciones de muestra en contexto

Normalmente procederían de un conjunto de datos seleccionados 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

A continuación, 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 instrucción DRAG

Después, creamos la instrucción 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 primero el documento más similar. Definimos un recuperador de reordenación para invertir el orden de los resultados. El pedido ahora muestra el documento más similar en último lugar, 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 pipeline DRAG

Creamos la canalización para la consulta DRAG utilizando la plantilla de instrucción DRAG y el recuperador de reordenación.

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'])

Genial, parece que hemos mejorado la respuesta dándole algunos ejemplos. ¡Probemos a continuación una técnica RAG aún más completa!

Implementación de IterDRAG (RAG iterativo basado en demostración)

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

  • Desglosa las preguntas complejas en piezas manejables.

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

  • Crea cadenas de razonamiento explícitas.

  • Permite abordar preguntas que serían difíciles 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 específicas 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 gestiona cada subpregunta individual recuperando documentos relevantes y generando respuestas intermedias específicas.

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 de generación de respuestas final

El componente de generación de respuestas final combina todas las respuestas intermedias para producir una respuesta completa 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

La creación de demostraciones eficaces es crucial para el rendimiento de IterDRAG. Estos ejemplos muestran al modelo cómo:

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

  2. Generar respuestas intermedias relevantes.

  3. Combinar 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. Descompone la pregunta principal en subpreguntas.

  2. Para cada subpregunta, recupera los documentos relevantes y genera una respuesta intermedia.

  3. Combina 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 RAG

Ahora que tenemos configurados los tres enfoques 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 utilizar 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

A continuación resumimos las diferencias de rendimiento entre los tres enfoques RAG implementados:

Método

 

Puntos fuertes

 

Limitaciones

 

Mejores casos de uso

 

RAG estándar

  • Implementación sencilla
  • Bueno para consultas sencillas
  • Menores requisitos informáticos
  • Uso limitado del contexto
  • El rendimiento se estanca con más documentos
  • Pobre 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
  • Bueno para consultas moderadamente complejas
  • Todavía limitado por la generación en un solo paso
  • Menos eficaz para preguntas multisalto
  • Consultas de complejidad moderada
  • Cuando haya más documentos disponibles
  • Cuándo se pueden proporcionar ejemplos en contexto

IterDRAG

  • Lo mejor para consultas complejas
  • Cadenas de razonamiento explícito
  • Uso más eficaz del contexto
  • Los más altos requisitos computacionales
  • Implementación más compleja
  • Preguntas multisalto
  • 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 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 inferencias puede mejorar drásticamente el rendimiento de RAG. Mediante la asignación estratégica de potencia de computación adicional en el momento de la inferencia, mediante técnicas como DRAG e IterDRAG, podemos lograr mejoras sustanciales en la calidad de la respuesta a consultas complejas.

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

Inferencia costosa: los modelos basados en transformadores, que utilizan mecanismos de autoatención, tienen costes 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 computación ineficiente: sin una asignación cuidadosa, agregar más documentos o contexto recuperados simplemente aumenta el coste computacional sin ganancias proporcionales en precisión, lo que conduce a rendimientos decrecientes o incluso a una degradación del rendimiento debido a la sobrecarga de información.

Cómo abordan estos desafíos DRAG e IterDRAG

RAG basado en demostraciones (DRAG):

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

Este enfoque es especialmente eficaz para longitudes de contexto efectivas más cortas, ya que permite que el modelo utilice un contexto enriquecido sin sobrecargar el mecanismo de atención, lo que mejora tanto la recuperación como la calidad de la generación.

RAG iterativo basado en demostraciones (IterDRAG):

IterDRAG descompone las consultas complejas en subconsultas más sencillas, recuperando y generando respuestas de forma iterativa para cada subpaso.

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

Este proceso permite al modelo asignar 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 RAG, puede lograr un rendimiento significativamente mejor en tareas intensivas en conocimiento sin cambiar sus modelos subyacentes.

Próximos 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 e implemente IA generativa, modelos fundacionales y capacidades de machine learning con IBM watsonx.ai, un estudio empresarial de nueva generación para desarrolladores de IA. Cree 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 líder en IA del sector de IBM y junto a su cartera de soluciones.

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

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

Explore los servicios de IA
Dé el siguiente paso

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

Explore watsonx.ai Solicite una demostración en directo
Notas a 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.