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.
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:
Durante este tutorial, también utilizará tres tecnologías de punta:
Al final de este tutorial, logrará lo siguiente:
Los modelos de lenguaje tradicionales tienen dificultades con los contextos largos por varias razones:
Las técnicas en este tutorial abordan estos desafíos mediante la asignación estratégica del cálculo de inferencias.
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.
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.
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."
! pip install "git+https://github.com/ibm-granite-community/utils.git" \
transformers \
pillow \
langchain_community \
langchain_huggingface \
langchain_milvus \
docling \
replicate
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)
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)
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:
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
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.
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})
Ahora que hemos convertido correctamente nuestros documentos y los hemos vectorizado, podemos configurar nuestro pipeline de RAG.
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.
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,
)
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'])
Si bien el enfoque de RAG estándar funciona razonablemente bien, tiene varias limitaciones clave cuando se trata de contenido largo o complejo:
Las técnicas de escalado de inferencia dirigen estas limitaciones asignando estratégicamente más cómputo en el momento de la inferencia.
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.
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
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)
)
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"))
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)
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,
)
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!
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:
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
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,
)
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
Crear demostraciones efectivas es crucial para el rendimiento de IterDRAG. Estos ejemplos muestran al modelo cómo:
@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
Esta función orquesta todo el proceso iterativo:
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}
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"])
Aquí resumimos las diferencias de rendimiento entre los tres enfoques RAG implementados:
Método
| Fortalezas
| Limitaciones
| Mejores casos de uso
|
|---|---|---|---|
RAG estándar |
|
|
|
DRAG |
|
|
|
IterDRAG |
|
|
|
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.
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.
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.
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.
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.
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.
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.
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.