Implementierung von RAG-Chunking-Strategien mit LangChain und watsonx.ai

In diesem Tutorial experimentieren Sie mit mehreren Chunking-Strategien, die LangChain und das neueste IBM Granite-Modell verwenden, das jetzt auf watsonx.ai verfügbar ist. Das übergeordnete Ziel ist es, eine Retrieval-Augmented Generation (RAG) durchzuführen.

Was ist Chunking?

Chunking bezieht sich auf den Prozess des Aufteilens großer Textstücke in kleinere Textsegmente oder Blöcke. Um die Bedeutung des Chunking zu unterstreichen, ist es hilfreich, RAG zu verstehen. RAG ist eine Technik in der Verarbeitung natürlicher Sprache (NLP), die Informationsabfrage und große Sprachmodelle (LLMs) kombiniert, um relevante Informationen aus ergänzenden Datensätzen abzurufen, um die Qualität der Ausgabe des LLMs zu optimieren. Um große Dokumente zu verwalten, können wir Chunking verwenden, um den Text in kleinere Snippets mit sinnvollen Blöcken aufzuteilen. Diese Textabschnitte können dann mithilfe eines Einbettungsmodells in eine Vektordatenbank eingebettet und gespeichert werden. Schließlich kann das RAG-System dann die semantische Suche verwenden, um nur die relevantesten Chunks abzurufen. Kleinere Chunks übertreffen tendenziell größere Chunks, da sie für Modelle mit kleinerer Kontextfenstergröße tendenziell besser zu handhaben sind.

Zu den wichtigsten Komponenten des Chunkings gehören:

  • Chunking-Strategie: Die Wahl der richtigen Chunking-Strategie für Ihre RAG-Anwendung ist wichtig, da sie die Boundaries für das Festlegen von Chunks festlegt. Wir werden einige davon im nächsten Abschnitt erkunden.
  • Chunk-Größe: Maximale Anzahl von Token, die sich in jedem Chunk befinden sollen. Die Bestimmung der geeigneten Chunk-Größe ist in der Regel mit einigem Experimentieren verbunden.  
  • Chunk-Überlappung: Die Anzahl der Token, die sich zwischen den Chunks überlappen, um den Kontext zu erhalten. Dies ist ein optionaler Parameter.

Chunking-Strategien

Es stehen verschiedene Hacking-Strategien zur Auswahl. Es ist wichtig, die effektivste Chunking-Technik für den spezifischen Anwendungsfall Ihrer LLM-Anwendung auszuwählen. Einige häufig verwendete Chunking-Prozesse sind:r: 

  • Feste Chunking-Größe: Text wird in Chunks einer bestimmten Größe aufgeteilt, wobei eine optionale Überlappung der Chunks möglich ist. Dieser Ansatz ist der gebräuchlichste und unkomplizierteste.
  • Rekursives Chunking: Iterieren von Standard-Separatoren, bis eines von ihnen die bevorzugte Blockgröße erzeugt. Zu den Standardtrennzeichen gehören ["\n\n", "\n", " ", "]. Bei dieser Chunking-Methode werden hierarchische Trennzeichen verwendet, sodass Absätze, gefolgt von Sätzen und Wörtern, so weit wie möglich zusammengehalten werden.
  • Semantisches Chunking: Das Zerlegen von Text auf eine Weise, die Sätze auf der Grundlage der semantischen Ähnlichkeit ihrer Einbettungen gruppiert. Einbettungen mit hoher semantischer Ähnlichkeit liegen näher beieinander als solche mit geringer semantischer Ähnlichkeit. Das Ergebnis sind kontextabhängige Chunks.
  • Dokumentbasiertes Chunking: Aufteilung basierend auf der Dokumentstruktur. Dieser Splitter kann Markdown-Text, Bilder, Tabellen und sogar Python-Code-Klassen und -Funktionen zur Bestimmung der Struktur verwenden. Auf diese Weise können große Dokumente in Chunks aufgeteilt und vom LLM verarbeitet werden.
  • Agentisches Chunking: Nutzt agentische KI, indem es dem LLM ermöglicht, anhand der semantischen Bedeutung sowie der Inhaltsstruktur wie Absatztypen, Abschnittsüberschriften, Schritt-für-Schritt-Anleitungen und mehr eine angemessene Aufteilung des Dokuments zu bestimmen. Dieser Chunking-Algorithmus befindet sich noch im Versuchsstadium und versucht, das menschliche Denken bei der Verarbeitung langer Dokumente zu simulieren.  

Schritte

Schritt 1. Einrichten Ihrer Umgebung

Sie können zwar aus mehreren Tools wählen, aber dieses Tutorial führt Sie durch die Einrichtung eines IBM Kontos für die Verwendung eines Jupyter Notebook.

  1. Melden Sie sich bei watsonx.ai mit Ihrem IBM® Cloud-Konto an.

  2. Erstellen Sie ein watsonx.ai-Projekt.

    Sie können Ihre Projekt-ID in Ihrem Projekt abrufen. Klicken Sie auf die Registerkarte Verwalten. Kopieren Sie dann die Projekt-ID aus dem Abschnitt Details der Seite Allgemein. Sie benötigen diese ID für dieses Tutorial.

  3. Erstellen Sie ein Jupyter Notebook.

Dieser Schritt öffnet eine Notebook-Umgebung, in die Sie den Code aus diesem Tutorial kopieren können. Alternativ können Sie dieses Notebook auf Ihr lokales System herunterladen und als Asset in Ihr watsonx.ai-Projekt hochladen. Weitere Granite-Tutorials finden Sie in der IBM Granite-Community. Dieses Jupyter Notebook sowie die verwendeten Datensätze sind auf GitHub zu finden.

Schritt 2. Richten Sie eine watsonx.ai-Laufzeit-Instanz und einen API-Schlüssel ein

  1. Erstellen Sie eine watsonx.ai Runtime -Service-Instanz (wählen Sie Ihre entsprechende Region aus und wählen Sie den Lite-Plan, der eine kostenlose Instanz ist).

  2. Generieren Sie einen API-Schlüssel.

  3. Verknüpfen Sie die Instanz des watsonx.ai-Runtime-Service mit dem Projekt, das Sie in watsonx.ai erstellt haben.

Schritt 3. Installieren und importieren Sie relevante Bibliotheken und richten Sie Ihre Zugangsdaten ein.

#installations
!pip install -q langchain langchain-ibm langchain_experimental langchain-text-splitters langchain_chroma transformers bs4 langchain_huggingface sentence-transformers
# imports
import getpass
from langchain_ibm import WatsonxLLM
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from transformers import AutoTokenizer

Um unsere Anmeldedaten festzulegen, benötigen wir den WATSONX_APIKEY und die WATSONX_PROJECT_ID, die Sie in Schritt 1 erstellt haben. Wir werden auch die URL festlegen, die als API-Endgerät dient.

WATSONX_APIKEY = getpass.getpass("Please enter your watsonx.ai Runtime API key (hit enter): ")
WATSONX_PROJECT_ID = getpass.getpass("Please enter your project ID (hit enter): ")
URL = "https://us-south.ml.cloud.ibm.com"

Schritt 4. Initialisieren Sie Ihr LLM

Wir verwenden Granite 3.1 als LLM für dieses Tutorial. Um das LLM zu initialisieren, müssen wir die Modellparameter festlegen. Weitere Informationen zu diesen Modellparametern, z. B. den minimalen und maximalen Token-Grenzwerten, finden Sie in der Dokumentation.

llm = WatsonxLLM(
        model_id= "ibm/granite-3-8b-instruct",
        url=URL,
        apikey=WATSONX_APIKEY,
        project_id=WATSONX_PROJECT_ID,
        params={
            GenParams.DECODING_METHOD: "greedy",
            GenParams.TEMPERATURE: 0,
            GenParams.MIN_NEW_TOKENS: 5,
            GenParams.MAX_NEW_TOKENS: 2000,
            GenParams.REPETITION_PENALTY:1.2
        }
)

Schritt 5. Laden Sie Ihr Dokument

Der Kontext, den wir für unsere RAG-Pipeline verwenden, ist die offizielle IBM-Ankündigung zur Veröffentlichung von Granite 3.1. Mit dem WebBaseLoader von LangChain können wir den Blog direkt von der Webseite in ein Dokument laden.

url = "https://www.ibm.com/de-de/new/announcements/ibm-granite-3-1-powerful-performance-long-context-and-more"
doc = WebBaseLoader(url).load()

Schritt 6. Durchführen einer Textaufteilung

Stellen Sie Beispielcode für die Implementierung jeder der Chunking-Strategien bereit, die wir bereits in diesem Tutorial behandelt haben, das über LangChain verfügbar ist.

Chunking in fester Größe

Zur Implementierung von Chunking mit fester Größe können wir den CharacterTextSplitter von LangChain verwenden und sowohl eine chunk_size als auch ein chunk_overlap festlegen. Die chunk_size wird anhand der Anzahl der Zeichen gemessen. Experimentieren Sie gerne mit verschiedenen Werten. Wir werden auch das Trennzeichen auf das Zeilenumbruchzeichen setzen, damit wir zwischen den Absätzen unterscheiden können. Für die Tokenisierung können wir den granite-3.1-8b-instruct Tokenizer verwenden. Der Tokenizer zerlegt Text in Token, die vom LLM verarbeitet werden können.

from langchain_text_splitters import CharacterTextSplitter
tokenizer = AutoTokenizer.from_pretrained(“ibm-granite/granite-3.1-8b-instruct”)
text_splitter = CharacterTextSplitter.from_huggingface_tokenizer(
                    tokenizer,
                    separator=”\n”, #default: “\n\n”
                    chunk_size=1200, chunk_overlap=200)
fixed_size_chunks = text_splitter.create_documents([doc[0].page_content])

Wir können einen der Chunks ausdrucken, um ihre Struktur besser zu verstehen.

fixed_size_chunks[1]

Ausgabe: (abgeschnitten)

Document(metadata={}, page_content=’As always, IBM’s historical commitment to open source is reflected in the permissive and standard open source licensing for every offering discussed in this article.\n\r\n Granite 3.1 8B Instruct: raising the bar for lightweight enterprise models\r\n \nIBM’s efforts in the ongoing optimization the Granite series are most evident in the growth of its flagship 8B dense model. IBM Granite 3.1 8B Instruct now bests most open models in its weight class in average scores on the academic benchmarks evaluations included in the Hugging Face OpenLLM Leaderboard...’)

Wir können den Tokenizer auch zur Überprüfung unseres Prozesses und zur Kontrolle der Anzahl der in jedem Chunk vorhandenen Tokens verwenden. Dieser Schritt ist optional und dient lediglich zu Demonstrationszwecken.

for idx, val in enumerate(fixed_size_chunks):
    token_count = len(tokenizer.encode(val.page_content))
    print(f”Der Chunk bei Index {idx} beinhaltet {token_count} Token.”)

Output:

The chunk at index 0 contains 1106 tokens.
The chunk at index 1 contains 1102 tokens.
The chunk at index 2 contains 1183 tokens.
The chunk at index 3 contains 1010 tokens.

Großartig! Es sieht so aus, als ob unsere Chunk-Größen angemessen implementiert wurden.

Rekursives Chunking

Für rekursives Chunking können wir den RecursiveCharacterTextSplitter von LangChain verwenden. Wie bei dem Chunking-Beispiel mit fester Größe können wir mit verschiedenen Chunk- und Überlappungsgrößen experimentieren.

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
recursive_chunks = text_splitter.create_documents([doc[0].page_content])
recursive_chunks[:5]

Output:

[Document(metadata={}, page_content=’IBM Granite 3.1: powerful performance, longer context and more’),
Document(metadata={}, page_content=’IBM Granite 3.1: powerful performance, longer context, new embedding models and more’),
Document(metadata={}, page_content=’Artificial Intelligence’),
Document(metadata={}, page_content=’Compute and servers’),
Document(metadata={}, page_content=’IT automation’)]

Der Splitter hat den Text erfolgreich unter Verwendung der Standardtrennzeichen in Abschnitte unterteilt: [“\n\n”, “\n”, “ “, “”].

Semantisches Chunking

Semantisches Chunking erfordert ein Einbetten- oder Encodermodell. Als Einbetten-Modell können wir das Modell granite-embedding-30m-english verwenden. Wir können auch einen der Chunks drucken, um ein besseres Verständnis ihrer Struktur zu erhalten.

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_experimental.text_splitter import SemanticChunker

embeddings_model = HuggingFaceEmbeddings(model_name=”ibm-granite/granite-embedding-30m-english”)
text_splitter = SemanticChunker(embeddings_model)
semantic_chunks = text_splitter.create_documents([doc[0].page_content])
semantic_chunks[1]

Ausgabe: (abgeschnitten)

Document(metadata={}, page_content=’Our latest dense models (Granite 3.1 8B, Granite 3.1 2B), MoE models (Granite 3.1 3B-A800M, Granite 3.1 1B-A400M) and guardrail models (Granite Guardian 3.1 8B, Granite Guardian 3.1 2B) all feature a 128K token context length.We’re releasing a family of all-new embedding models. The new retrieval-optimized Granite Embedding models are offered in four sizes, ranging from 30M–278M parameters. Like their generative counterparts, they offer multilingual support across 12 different languages: English, German, Spanish, French, Japanese, Portuguese, Arabic, Czech, Italian, Korean, Dutch and Chinese. Granite Guardian 3.1 8B and 2B feature a new function calling hallucination detection capability, allowing increased control over and observability for agents making tool calls...’)

Dokumentbasiertes Chunking

Dokumente verschiedener Dateitypen sind mit den dokumentenbasierten Textsplittern von LangChain kompatibel. Für die Zwecke dieses Tutorials verwenden wir eine Markdown-Datei. Beispiele für rekursives JSON-Splitting, Code-Splitting und HTML-Splitting finden Sie in der LangChain-Dokumentation.

Ein Beispiel für eine Markdown-Datei, die wir laden können, ist die README-Datei für Granite 3.1 auf GitHub von IBM.

url = “https://raw.githubusercontent.com/ibm-granite/granite-3.1-language-models/refs/heads/main/README.md”
markdown_doc = WebBaseLoader(url).load()
markdown_doc

Output:

[Document(metadata={‘source’: ‘https://raw.githubusercontent.com/ibm-granite/granite-3.1-language-models/refs/heads/main/README.md’}, page_content=’\n\n\n\n :books: Paper (comming soon)\xa0 | :hugs: HuggingFace Collection\xa0 | \n :speech_balloon: Discussions Page\xa0 | 📘 IBM Granite Docs\n\n\n---\n## Introduction to Granite 3.1 Language Models\nGranite 3.1 language models are lightweight, state-of-the-art, open foundation models that natively support multilinguality, coding, reasoning, and tool usage, including the potential to be run on constrained compute resources. All the models are publicly released under an Apache 2.0 license for both research and commercial use. The models\’ data curation and training procedure were designed for enterprise usage and customization, with a process that evaluates datasets for governance, risk and compliance (GRC) criteria, in addition to IBM\’s standard data clearance process and document quality checks...’)]

Nun können wir den MarkdownHeaderTextSplitter von LangChain verwenden, um die Datei nach Header-Typ zu teilen, den wir in der Liste headers_to_split_on festgelegt haben. Wir werden einen der Chunks auch als Beispiel ausdrucken.

#document based chunking
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
    (“#”, “Header 1”),
    (“##”, “Header 2”),
    (“###”, “Header 3”),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)
document_based_chunks = markdown_splitter.split_text(markdown_doc[0].page_content)
document_based_chunks[3]

Output:

Document(metadata={‘Header 2’: ‘How to Use our Models?’, ‘Header 3’: ‘Inference’}, page_content=’This is a simple example of how to use Granite-3.1-1B-A400M-Instruct model. \n```python\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\ndevice = “auto”\nmodel_path = “ibm-granite/granite-3.1-1b-a400m-instruct”\ntokenizer = AutoTokenizer.from_pretrained(model_path)\n# drop device_map if running on CPU\nmodel = AutoModelForCausalLM.from_pretrained(model_path, device_map=device)\nmodel.eval()\n# change input text as desired\nchat = [\n{ “role”: “user”, “content”: “Please list one IBM Research laboratory located in the United States. You should only output its name and location.” },\n]\nchat = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)\n# tokenize the text\ninput_tokens = tokenizer(chat, return_tensors=”pt”).to(device)\n# generate output tokens\noutput = model.generate(**input_tokens,\nmax_new_tokens=100)\n# decode output tokens into text\noutput = tokenizer.batch_decode(output)\n# print output\nprint(output)\n```’)

Wie Sie in der Ausgabe sehen können, hat das Chunking den Text erfolgreich nach Kopfzeilentyp aufgeteilt.

Schritt 7. Erstellen Sie einen Vektorspeicher

Nachdem wir nun verschiedene Chunking-Strategien erprobt haben, fahren wir mit der Implementierung von RAG fort. Für dieses Tutorial wählen wir die durch die semantische Aufteilung erzeugten Chunks aus und konvertieren sie in Vektoreinbettungen. Ein Open-Source-Vektorspeicher, den wir verwenden können, ist Chroma DB. Über das Paket langchain_chrona können wir ganz einfach auf die Chroma-Funktionalität zugreifen.

Zunächst initialisieren wir unsere Chroma-Vektordatenbank, stellen wir ihr unser Einbettungsmodell zur Verfügung und fügen wir unsere durch semantisches Chunking erstellten Dokumente hinzu.

vector_db = Chroma(
    collection_name=”example_collection”,
    embedding_function=embeddings_model,
    persist_directory=”./chroma_langchain_db”, # Where to save data locally
)
vector_db.add_documents(semantic_chunks)

Output:

[‘84fcc1f6-45bb-4031-b12e-031139450cf8’,
‘433da718-0fce-4ae8-a04a-e62f9aa0590d’,
‘4bd97cd3-526a-4f70-abe3-b95b8b47661e’,
‘342c7609-b1df-45f3-ae25-9d9833829105’,
‘46a452f6-2f02-4120-a408-9382c240a26e’]

Schritt 8. Strukturieren Sie die Prompt-Vorlage

Als Nächstes können wir eine Prompt-Vorlage für unser LLM erstellen. Diese Prompt-Vorlage ermöglicht es uns, mehrere Fragen zu stellen, ohne die ursprüngliche Promptstruktur zu ändern. Wir können auch unseren Vektorspeicher als Retriever bereitstellen. Mit diesem Schritt wird die RAG-Struktur finalisiert.

from langchain.chains import create_retrieval_chain
from langchain.prompts import PromptTemplate
from langchain.chains.combine_documents import create_stuff_documents_chain

prompt_template = """<|start_of_role|>user<|end_of_role|>Use the following pieces of context to answer the question at the end. Wenn Sie die Antwort nicht wissen, sagen Sie einfach, dass Sie sie nicht wissen, und versuchen Sie nicht, eine Antwort zu erfinden.
{context}
Question: {input}<|end_of_text|>
<|start_of_role|>assistant<|end_of_role|>"""

qa_chain_prompt = PromptTemplate.from_template(prompt_template)
combine_docs_chain = create_stuff_documents_chain(llm, qa_chain_prompt)
rag_chain = create_retrieval_chain(vector_db.as_retriever(), combine_docs_chain)

Schritt 9: Starten Sie den Prozess der RAG-Kette.

Rufen wir mit unserem abgeschlossenen RAG-Workflow eine Benutzerabfrage auf. Zunächst können wir das Modell ohne zusätzlichen Kontext aus dem von uns erstellten Vektorspeicher strategisch ansprechen, um zu testen, ob es sein eingebautes Wissen nutzt oder den RAG-Kontext. Der Granite 3.1 Ankündigungsblog verweist auf Docling,, das IBM Tool zum Parsen verschiedener Dokumenttypen und deren Umwandlung in Markdown oder JSON. Fragen wir das LLM nach Docling.

output = llm.invoke(“What is Docling?”)
output

Output:

‘?\n\n”Docling” does not appear to be a standard term in English. It might be a typo or a slang term specific to certain contexts. If you meant “documenting,” it refers to the process of creating and maintaining records, reports, or other written materials that provide information about an activity, event, or situation. Please check your spelling or context for clarification.’

Es ist offensichtlich, dass das Modell nicht mit Informationen über Docling trainiert wurde und ohne externe Tools oder Informationen kann es uns diese Informationen nicht liefern. Versuchen wir nun, die gleiche Abfrage für die RAG-Kette bereitzustellen, die wir erstellt haben.

rag_output = rag_chain.invoke({“input”: “What is Docling?”})
rag_output[‘answer’]

Output:

‘Docling is a powerful tool developed by IBM Deep Search for parsing documents in various formats such as PDF, DOCX, images, PPTX, XLSX, HTML, and AsciiDoc, and converting them into model-friendly formats like Markdown or JSON. This enables easier access to the information within these documents for models like Granite for tasks such as RAG and other workflows. Docling is designed to integrate seamlessly with agentic frameworks like LlamaIndex, LangChain, and Bee, providing developers with the flexibility to incorporate its assistance into their preferred ecosystem. It surpasses basic optical character recognition (OCR) and text extraction methods by employing advanced contextual and element-based preprocessing techniques. Currently, Docling is open-sourced under the permissive MIT License, and the team continues to develop additional features, including equation and code extraction, as well as metadata extraction.’

Großartig! Das Granite-Modell hat den RAG-Kontext korrekt verwendet, um uns die korrekten Informationen über Docling zu liefern und gleichzeitig die semantische Kohärenz zu bewahren. Wir haben bewiesen, dass das gleiche Ergebnis ohne den Einsatz von RAG nicht möglich war.

Zusammenfassung

In diesem Tutorial haben Sie eine RAG-Pipeline erstellt und mit mehreren Chunking-Strategien experimentiert, um die Abrufgenauigkeit des Systems zu verbessern. Mit dem Granite 3.1-Modell haben wir erfolgreich geeignete Modellantworten auf eine Benutzeranfrage zu den als Kontext bereitgestellten Dokumenten erstellt. Der Text, den wir für diese RAG-Implementierung verwendet haben, stammt aus einem Blog auf ibm.com, in dem die Veröffentlichung von Granite 3.1 angekündigt wurde. Das Modell lieferte uns Informationen, die nur über den bereitgestellten Kontext zugänglich waren, da sie nicht Teil der ursprünglichen Wissensbasis des Modells waren.

Für diejenigen, die weiterführende Informationen suchen, bieten wir die Ergebnisse eines Projekts an, in dem die LLM-Leistung mit HTML Structured Chunking mit watsonx Chunking verglichen wird.

Weiterführende Lösungen
IBM watsonx.ai

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.

Entdecken sie watsonx.ai
Lösungen im Bereich künstlicher Intelligenz

Setzen Sie KI in Ihrem Unternehmen ein – mit branchenführendem Fachwissen im Bereich KI und dem Lösungsportfolio von IBM an Ihrer Seite.

Erkunden Sie KI-Lösungen
KI-Services

Erfinden Sie kritische Workflows und Abläufe neu, indem Sie KI einsetzen, um Erfahrungen, Entscheidungsfindung in Echtzeit und den geschäftlichen Nutzen zu maximieren.

KI-Services entdecken
Machen Sie den nächsten Schritt

Profitieren Sie von einem einheitlichen Zugriff auf Funktionen, die den gesamten Lebenszyklus der KI-Entwicklung abdecken. Erstellen Sie leistungsstarke KI-Lösungen mit benutzerfreundlichen Oberflächen, Workflows und Zugriff auf branchenübliche APIs und SDKs.

watsonx.ai erkunden Buchen Sie eine Live-Demo