Die Inferenzskalierung in der künstlichen Intelligenz (KI) bezieht sich auf Techniken, die die Leistung von Modellen verbessern, indem sie Rechenressourcen während der Inferenzphase (wenn Modelle Ausgaben erzeugen) zuweisen, anstatt sich auf größere Trainingsdatensätze oder Modellarchitekturen zu verlassen. Da große Sprachmodelle (Large Language Models, LLMs) sowohl in Bezug auf die Modellparameter als auch den Umfang des Datensatzes immer größer werden, sind die Optimierung der Inferenzzeit und die Verwaltung der Skalierung der Inferenzberechnung - insbesondere auf GPU-Hardware - zu einer zentralen Herausforderung für den Einsatz hochleistungsfähiger multimodaler Retrieval-Augmented Generation (RAG)-Systeme geworden.
Jüngste Fortschritte bei Inferenzstrategien, die die Rechenressourcen erhöhen und komplexe Algorithmen zur Testzeit einsetzen, definieren neu, wie LLMs komplexe Schlussfolgerungsaufgaben bewältigen und qualitativ hochwertigere Ergebnisse über verschiedene Eingabemodalitäten hinweg liefern. Die Inferenzskalierung optimiert die Gedankenkette (Chain of Thought, CoT) durch die Erweiterung der Argumentationstiefe. Diese Erweiterung ermöglicht es den Modellen, durch iteratives Prompting oder mehrstufige Generierung längere, detailliertere Gedankenketten zu erzeugen. Die Inferenzskalierung kann genutzt werden, um die multimodale RAG zu verbessern, wobei der Schwerpunkt auf dem Zusammenspiel von Modellgrößen, Computerbudgets und der praktischen Optimierung der Inferenzzeit für reale Anwendungen liegt.
Darüber hinaus unterstreichen Skalierungsgesetze und Benchmark-Ergebnisse die Kompromisse zwischen Vortraining, Feinabstimmung, Inferenzzeit-Strategien und fortschrittlichen Algorithmen für die Ausgabe-Auswahl. Sowohl größere als auch kleinere Modelle profitieren von der Skalierung der Inferenz, da sie es auch ressourcenbeschränkten Systemen ermöglicht, sich der Leistung modernster LLMs anzunähern. Dieses Tutorial demonstriert die Auswirkungen von Optimierungstechniken auf die Modellleistung und bietet umsetzbare Anleitungen für den Ausgleich von Genauigkeit, Latenz und Kosten bei multimodalen RAG-Bereitstellungen.
Dieses Tutorial richtet sich an Entwickler, Forscher und Enthusiasten der künstlichen Intelligenz, die ihr Wissen über Dokumentenmanagement und fortgeschrittene Techniken der Verarbeitung natürlicher Sprache (NLP) erweitern möchten. Sie werden lernen, wie Sie die Leistung der Inferenzskalierung nutzen können, um die multimodale RAG-Pipeline zu verbessern, die in einem früheren Rezept erstellt wurde. Dieses Tutorial konzentriert sich zwar auf Strategien für die Skalierbarkeit in multimodalen RAG, die speziell auf IBM® Granite® große Sprachmodelle ausgerichtet sind, aber ähnliche Prinzipien sind auf die meisten gängigen Modelle anwendbar, einschließlich der Modelle von OpenAI (z. B. GPT-4, GPT-4o, ChatGPT) und DeepMind.
Dieses Tutorial führt Sie durch die folgenden Prozesse:
In diesem Tutorial werden Sie auch drei hochmoderne Technologien verwenden:
Am Ende dieses Tutorials werden Sie Folgendes erreicht haben:
Traditionelle Sprachmodelle haben aus mehreren Gründen Probleme mit langen Kontexten:
Die Techniken in diesem Tutorial gehen diese Herausforderungen durch die strategische Zuweisung von Inferenzberechnungen an.
Weitere Informationen zu diesen beiden fortschrittlichen Inferenzskalierungstechniken (DRAG und IterDRAG) finden Sie im Forschungsbericht „Inference Scaling for Long-Context Retrieval Augmented Generation“ (Inferenzskalierung für Long-Context Retrieval Augmented Generation)
Diese Methoden zeigen, dass eine Skalierung der Inferenzberechnung die RAG-Leistung bei optimaler Zuordnung nahezu linear verbessern kann, sodass RAG-Systeme die Funktionen moderner LLMs besser nutzen können. Für diese Implementierung werden wir ein IBM Granite-Modell verwenden, das verschiedene Modalitäten verarbeiten kann. Sie werden ein KI-System erstellen, um Benutzeranfragen aus unstrukturierten Daten in Echtzeit zu beantworten und dabei die Prinzipien aus dem Papier anzuwenden.
Stellen Sie sicher, dass Sie Python 3.10, 3.11 oder 3.12 in einer neu erstellten virtuellen Umgebung ausführen. Hinweis: Sie können dieses Tutorial auch auf GitHub aufrufen.
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
Um einige Protokollierungsinformationen zu sehen, können wir die Protokollstufe INFO konfigurieren.
HINWEIS: Es ist in Ordnung, die Ausführung dieser Zelle zu überspringen.
import logging
logging.basicConfig(level=logging.INFO)
Geben Sie das Einbettungsmodell an, das für die Erzeugung von Texteinbettungsvektoren verwendet werden soll. Hier werden wir eines der Granite Embeddings Modelle verwenden.
Wenn Sie ein anderes Einbettungsmodell verwenden möchten, ersetzen Sie diese Codezelle durch eine Zelle aus diesem Rezept für Einbettungsmodelle.
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)
Geben Sie das MLLM an, das für das Bildverständnis verwendet werden soll. Wir verwenden das Visionsmodell 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)
Geben Sie das Sprachmodell an, das für die RAG-Generierungsoperation verwendet werden soll. Hier verwenden wir den Replicate LangChain-Client, um eine Verbindung zu einem Granite-Modell von der ibm-granite-Organisation auf Replicate herzustellen.
Informationen zur Einrichtung von Replicate finden Sie unter Erste Schritte mit Replicate.
Um eine Verbindung zu einem Modell auf einem anderen Provider als Replicate herzustellen, ersetzen Sie diese Codezelle durch eine aus dem LLM-Komponentenrezept.
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 }
Nach der Verarbeitung der Dokumente verarbeiten wir die Textelemente in den Dokumenten weiter und teilen sie in geeignete Größen für das von uns verwendete Einbettungsmodell ein. Aus den Textbausteinen wird eine Liste von LangChain-Dokumenten erstellt.
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")
Als nächstes bearbeiten wir alle Tabellen in den Dokumenten. Wir konvertieren die Tabellendaten in das Markdown-Format, damit das Sprachmodell sie verarbeiten kann. Eine Liste von LangChain-Dokumenten wird aus den Markdown-Renderings der Tabelle erstellt.
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")
Abschließend verarbeiten wir alle Bilder in den Dokumenten. Hier verwenden wir das Vision-Sprachmodell, um den Inhalt der Bilder zu verstehen. In diesem Beispiel interessieren uns alle Textinformationen im Bild.
Die Wahl eines geeigneten Prompts ist kritisch, da er festlegt, auf welche Aspekte des Bildes sich das Modell konzentrieren soll. Einige Beispiele:
HINWEIS: Die Bildverarbeitung kann je nach Anzahl der Bilder und des Dienstes, der das Vision-Sprachmodell ausführt, eine erhebliche Verarbeitungszeit in Anspruch nehmen.
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")
Dann können wir die LangChain-Dokumente anzeigen, die aus den Eingabedokumenten erstellt wurden.
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
Mithilfe des Einbetten-Modells laden wir die Dokumente aus den Textblöcken und generierten Bildunterschriften in eine Vektordatenbank. Die Erstellung dieser Vektordatenbank ermöglicht es uns, auf einfache Weise eine semantische Ähnlichkeitssuche in unseren Dokumenten durchzuführen.
HINWEIS: Die Bevölkerung der Vektordatenbank kann je nach Einbetten und Service eine erhebliche Verarbeitungszeit erfordern.
Geben Sie die Datenbank an, die zum Speichern und Abrufen von Einbetten-Vektoren verwendet werden soll. Für dieses Tutorial verwenden wir Milvus mittels Langchain. Als Vektordatenbank speichert, indiziert und verwaltet Milvus numerische Einbettungen, die von neuronalen Netzwerken und verschiedenen ML-Algorithmen erzeugt werden.
Wenn Sie eine Verbindung zu einer anderen Vektordatenbank als Milvus herstellen möchten, ersetzen Sie diese Zelle durch eine Zelle aus diesem Rezept für den Vektorspeicher.
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"},
)
Jetzt fügen wir alle LangChain-Dokumente für die Text-, Tabellen- und Bildbeschreibungen zur Vektordatenbank hinzu.
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})
Nachdem wir unsere Dokumente nun erfolgreich konvertiert und vektorisiert haben, können wir unsere RAG-Pipeline einrichten.
Hier testen wir die Vektordatenbank, indem wir im Vektorraum nach Abschnitten mit relevanten Informationen für unsere Abfrage suchen. Es werden die Dokumente angezeigt, die mit der abgerufenen Bildbeschreibung verknüpft sind.
Dieser Validierungsschritt ist wichtig, um sicherzustellen, dass unser Abfragesystem korrekt funktioniert, bevor wir unsere vollständige RAG-Pipeline aufbauen. Wir möchten sehen, ob die zurückgegebenen Dokumente für unsere Abfrage relevant sind.
Probieren Sie gerne verschiedene Abfragen aus.
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
Die zurückgegebenen Dokumente sollten auf die Anfrage reagieren. Lassen Sie uns fortfahren und unsere RAG-Pipeline konstruieren.
Die zurückgegebenen Dokumente sollten auf die Anfrage reagieren. Lassen Sie uns fortfahren und unsere RAG-Pipeline konstruieren.
Zuerst erstellen wir die Prompts für Granite, um die RAG-Abfrage durchzuführen. Wir verwenden die Chatvorlage Granite und geben die Platzhalterwerte an, die durch die LangChain RAG-Pipeline ersetzt werden.
{context} enthält die abgerufenen Chunks, wie in der vorherigen Suche gezeigt, und gibt diese an das Modell als Dokumentenkontext zur Beantwortung unserer Frage weiter.
Anschließend konstruieren wir die RAG-Pipeline mithilfe der von uns erstellten Granite-Prompt-Vorlagen.
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,
)
Die Pipeline verwendet die Abfrage, um Dokumente aus der Vektordatenbank zu finden und sie als Kontext für die Abfrage zu verwenden.
outputs = rag_chain.invoke({"input": query})
print(outputs['answer'])
Obwohl der RAG-Standardansatz einigermaßen gut funktioniert, weist er bei der Arbeit mit langen oder komplexen Inhalten mehrere wesentliche Einschränkungen auf:
Techniken zur Skalierung von Schlussfolgerungen beheben diese Einschränkungen, indem sie strategisch mehr Rechenleistung zur Schlussfolgerungszeit zuweisen.
Jetzt werden wir die DRAG-Technik aus dem Forschungspapier „Inference Scaling for Long-Context Retrieval Augmented Generation“ implementieren, um unser RAG-System zu verbessern.
DRAG verwendet kontextbezogene Beispiele, um dem Modell zu demonstrieren, wie Informationen aus Dokumenten extrahiert und verwendet werden können, was die Leistung bei Szenarien mit langem Kontext verbessert.
Diese stammen in der Regel aus einem kuratierten Datensatz mit qualitativ hochwertigen QA-Paaren. Zu diesem Zweck erstellen wir einige synthetische Beispiele, die der erwarteten Domain entsprechen.
Hier definieren wir eine Datenklasse, die eine einzelne Demonstration darstellt, und erstellen dann einige Demonstrationen.
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
Anschließend formatieren wir alle Demonstrationen gemeinsam für den Prompt.
# Format all demonstrations together
demonstrations = create_enhanced_drag_demonstrations(vector_db)
formatted_demonstrations = "\n\n".join(
f"Example {i+1}:\n{demo}"
for i, demo in enumerate(demonstrations)
)
Anschließend erstellen wir den DRAG-Prompt für das Modell, der die formatierten Demonstrationsbeispiele enthält.
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"))
Normalerweise gibt der Retriever die Dokumente in der Reihenfolge der Ähnlichkeit zurück, wobei das ähnlichste Dokument zuerst an der Reihe ist. Wir definieren einen Neuordnungsabruf (Reordering Retriever), um die Reihenfolge der Ergebnisse umzukehren. In der Bestellung wird nun das ähnlichste Dokument zuletzt angezeigt, also näher am Ende des Prompts.
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)
Wir erstellen die Pipeline für die DRAG-Abfrage unter Verwendung der DRAG-Promptvorlage und des Neuordnungsabrufs.
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'])
Großartig, es sieht so aus, als hätten wir die Antwort durch einige Beispiele verbessert. Probieren wir als Nächstes eine noch gründlichere RAG-Technik aus!
IterDRAG erweitert DRAG, indem es komplexe Abfragen in einfachere Unterabfragen zerlegt und eine verschachtelte Abfrage durchführt. Dieser Ansatz ist besonders effektiv für komplexe Multihop-Fragen, die die Integration von Informationen aus mehreren Quellen oder eine Argumentation in mehreren Schritten erfordern.
Die wichtigsten Hauptvorteile des iterativen Ansatzes:
Der Schritt der Zerlegung ist entscheidend, da er eine komplexe Abfrage in einfachere, gezieltere Unterabfragen zerlegt, die einzeln beantwortet werden können.
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
Die Komponente zur Beantwortung von Unterfragen bearbeitet jede einzelne Unterfrage, indem sie relevante Dokumente abruft und gezielte Zwischenantworten erzeugt.
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,
)
Die abschließende Komponente zur Generierung von Antworten kombiniert alle Zwischenantworten, um eine umfassende Antwort auf die ursprüngliche Frage zu erhalten.
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
Die Erstellung effektiver Demonstrationen ist entscheidend für die Leistung von InterDRAG. In diesen Beispielen wird gezeigt, wie das Modell Folgendes ausführt:
@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
Diese Funktion orchestriert den gesamten iterativen Prozess:
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}
Nun, da wir alle drei RAG-Ansätze eingerichtet haben, lassen Sie uns ihre Antworten auf dieselbe Abfrage vergleichen, die dieses Mal viel komplexer ist, um die Unterschiede zu sehen.
Der Vergleich wird uns helfen, die Nutzen der einzelnen Ansätze zu verstehen und herauszufinden, wann jeder Ansatz am besten geeignet sein könnte.
# 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"])
Hier fassen wir die Leistungsunterschiede zwischen den drei implementierten RAG-Ansätzen zusammen:
Lösung
| Stärken
| Begrenzungen
| Die besten Anwendungsfälle
|
|---|---|---|---|
Standard RAG |
|
|
|
DRAG |
|
|
|
IterDRAG |
|
|
|
Wie wir bei unseren Implementierungsinferenz-Skalierungstechniken wie DRAG und IterDRAG gesehen haben, können die RAG-Leistung erheblich verbessern. Diese Methode eignet sich insbesondere für komplexe Abfragen, die eine tiefgreifende Analyse mehrerer Dokumente erfordern.
In diesem Tutorial haben wir erkundet, wie die RAG-Leistung durch Inferenzskalierung erheblich verbessert werden kann. Durch die strategische Zuweisung zusätzlicher Berechnungen zur Inferenzzeit durch Techniken wie DRAG und IterDRAG können wir die Antwortqualität bei komplexen Abfragen erheblich steigern.
Kostenintensive Inferenz: Transformer-basierte Modelle, die Selbstbeobachtungsmechanismen verwenden, haben Inferenzkosten, die quadratisch mit der Eingabelänge skalieren. Diese Methode macht den Umgang mit langen Kontexten rechenintensiv, wodurch die praktische Anwendung von RAG auf kürzere Dokumente beschränkt wird oder eine aggressive Kürzung erforderlich ist.
Eingeschränkte Kontextnutzung: Standard-RAG-Systeme rufen oft eine feste Anzahl von Dokumenten ab und verarbeiten sie, was für komplexe MultiHop-Abfragen möglicherweise nicht ausreicht. Die Leistung sinkt mit zunehmender Länge des Kontexts, insbesondere jenseits von 128.000 Token, weil das Modell Schwierigkeiten hat, Informationen über viele abgefragte Passagen hinweg zu synthetisieren.
Ineffiziente Zuteilung von Berechnungen: Ohne eine sorgfältige Zuteilung erhöht das Hinzufügen weiterer abgerufener Dokumente oder Kontexte einfach nur die Rechenkosten, ohne dass sich die Genauigkeit proportional erhöht. Dies führt zu abnehmenden Erträgen oder sogar zu Leistungseinbußen aufgrund von Informationsüberlastung.
Demonstrationsbasierte RAG (DRAG):
DRAG nutzt mehrere abrufbare Beispiele, Questions and Answers als Demonstrationen innerhalb der Eingabeaufforderung, so dass das Modell im Kontext lernen kann, wie man relevante Informationen findet und anwendet.
Dieser Ansatz ist besonders effektiv bei kürzeren effektiven Kontextlängen, da er es dem Modell ermöglicht, reichhaltigen Kontext zu nutzen, ohne den Aufmerksamkeitsmechanismus zu überfordern, was sowohl die Abfrage- als auch die Generierungsqualität verbessert.
Iterative demonstrationsbasierte RAG (IterDRAG):
IterDRAG zerlegt komplexe Abfragen in einfachere Unterabfragen, wobei die Antworten für jeden Teilschritt iterativ abgerufen und generiert werden.
Durch die Verschachtelung von Abruf und Generierung baut IterDRAG Schlussfolgerungsketten auf, die die Lücke für Multihop-Abfragen überbrücken und damit besonders effektiv für außergewöhnlich lange Kontexte sind.
Dieser Prozess ermöglicht es dem Modell, die Berechnungen effizienter zu verteilen, sich bei jedem Schritt auf die relevantesten Informationen zu konzentrieren und das Risiko einer Überlastung der Aufmerksamkeit bei langen Kontexten zu vermeiden. Wenn Sie diese Techniken zur Skalierung von Schlussfolgerungen auf Ihre RAG-Anwendungen anwenden, können Sie eine deutlich bessere Leistung bei wissensintensiven Aufgaben erzielen, ohne Ihre zugrunde liegenden Modelle zu ändern.
Trainieren, validieren, optimieren und implementieren Sie generative KI, Foundation Models und maschinelle Lernfunktionen mit IBM watsonx.ai, einem Studio der nächsten Generation für AI Builder in Unternehmen. Erstellen Sie KI-Anwendungen mit einem Bruchteil der Zeit und Daten.
Setzen Sie KI in Ihrem Unternehmen ein – mit branchenführendem Fachwissen im Bereich KI und dem Lösungsportfolio von IBM an Ihrer Seite.
Erfinden Sie kritische Workflows und Abläufe neu, indem Sie KI einsetzen, um Erfahrungen, Entscheidungsfindung in Echtzeit und den geschäftlichen Nutzen zu maximieren.
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 Forschung, IBM, 26. Februar 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.