Implementare strategie di chunking RAG con LangChain e watsonx.ai

In questo tutorial, scoprirai diverse strategie di chunking utilizzando LangChain e l'ultimo modello IBM® Granite ora disponibile su watsonx.ai. L'obiettivo generale è eseguire la retrieval-augmented generation (RAG).

Che cos'è il chunking?

Il chunking è il processo di suddivisione di grandi parti di testo in segmenti o blocchi di testo più piccoli. Per sottolineare l'importanza del chunking, è utile capire che cos'è la RAG. La RAG è una tecnica di elaborazione del linguaggio naturale (PNL) che combina il recupero delle informazioni e i modelli linguistici di grandi dimensioni (LLM) per ottenere informazioni pertinenti da set di dati supplementari per ottimizzare la qualità dell'output del LLM. Per gestire documenti di grandi dimensioni, possiamo utilizzare il chunking per suddividere il testo in frammenti più piccoli e significativi. Questi blocchi di testo possono quindi essere incorporati e memorizzati in un database vettoriali mediante l'uso di un modello di embedding. Infine, il sistema RAG può utilizzare la ricerca semantica per recuperare solo i blocchi più rilevanti. I blocchi più piccoli tendono ad avere prestazioni migliori di quelli più grandi, in quanto tendono ad essere più gestibili per modelli con finestre di contesto più piccole.

Alcuni componenti chiave del chunking includono:

  • Strategia di chunking: la scelta della giusta strategia di chunking per la sua applicazione RAG è importante in quanto determina i limiti per l'impostazione dei blocchi. Esploreremo alcuni di questi nella prossima sezione.
  • Dimensione dei blocchi: numero massimo di token da inserire in ogni blocco. Determinare la dimensione appropriata del blocco di solito comporta alcuni esperimenti.  
  • Sovrapposizione dei blocchi: il numero di token che si sovrappongono tra i blocchi per preservare il contesto. Si tratta di un parametro facoltativo.

Strategie di suddivisione in blocchi

Esistono diverse strategie di chunking tra cui scegliere. È importante selezionare la tecnica più efficace per il caso d'uso specifico della tua applicazione LLM. Alcuni processi di chunking comunemente utilizzati sono: 

  • Chunking a dimensione fissa: suddivisione del testo con una dimensione specifica dei blocchi e sovrapposizione facoltativa. Questo approccio è il più comune e semplice.
  • Chunking ricorsivo: iterazione dei separatori predefiniti fino a quando uno di essi non produce la dimensione del blocco preferita. I separatori predefiniti includono ["\n\n", "\n", " ", ""]. Questo metodo di chunking utilizza separatori gerarchici in modo che i paragrafi, seguiti da frasi e quindi parole, vengano tenuti insieme il più possibile.
  • Chunking semantico: suddivisione del testo in modo da raggruppare le frasi in base alla similarità semantica dei loro embedding. Gli embedding ad alta somiglianza semantica sono più vicini tra loro rispetto a quelli a bassa somiglianza semantica. Ciò si traduce in blocchi sensibili al contesto.
  • Chunking basato sul documento: suddivisione basata sulla struttura del documento. Questo splitter può utilizzare testo, immagini, tabelle e persino classi e funzioni di codice Python come modi per determinare la struttura. In tal modo, i documenti di grandi dimensioni possono essere suddivisi in blocchi ed elaborati dall'LLM.
  • Agentic chunking: utilizza l'agentic AI per consentire all'LLM di determinare la suddivisione appropriata dei documenti in base al significato semantico e alla struttura dei contenuti come tipi di paragrafo, titoli di sezione, istruzioni passo a passo e altro ancora. Questo chunker è sperimentale e tenta di simulare il ragionamento umano quando si elaborano documenti lunghi.  

Passaggi

Passaggio 1: configura il tuo ambiente

Sebbene sia possibile scegliere tra diversi strumenti, questo tutorial illustra come configurare un account IBM per utilizzare un Jupyter Notebook.

  1. Accedi a watsonx.ai usando il tuo account IBM® Cloud.

  2. Crea un progetto watsonx.ai.

    Puoi ottenere l'ID del progetto dall'interno del tuo progetto. Clicca sulla scheda Gestisci. Quindi, copia l'ID del progetto dalla sezione Dettagli della pagina Generali. Per questo tutorial ti serve questo ID.

  3. Crea un Jupyter Notebook.

Questo passaggio aprirà un ambiente Notebook in cui potrai incollare il codice da questo tutorial. In alternativa, puoi scaricare questo notebook sul tuo sistema locale e caricarlo nel tuo progetto watsonx.ai come asset. Per visualizzare altri tutorial su Granite, visita l'IBM® Granite Community. Questo Jupyter Notebook insieme ai set di dati utilizzati sono disponibili su GitHub.

Passaggio 2: configura un'istanza di watsonx.ai Runtime e una chiave API

  1. Crea un'istanza di servizio watsonx.ai Runtime (seleziona l'area geografica appropriata e scegli il piano Lite, che è un'istanza gratuita).

  2. Genera una chiave API.

  3. Associa l'istanza del servizio watsonx.ai Runtime al progetto che hai creato in watsonx.ai.

Passaggio 3. Installare e importare le librerie pertinenti e configurare le tue credenziali

#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

Per impostare le nostre credenziali, abbiamo bisogno della WATSONX_APIKEY e della WATSONX_PROJECT_ID che hai generato nel passaggio 1. Imposteremo anche l'URL che funge da endpoint dell'API.

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"

Passaggio 4: inizializza il tuo LLM

Useremo Granite 3.1 come LLM per questo tutorial. Per inizializzare l'LLM, dobbiamo impostare i parametri del modello. Per maggiori informazioni sui parametri del modello, come i limiti minimi e massimi dei token, consulta la documentazione.

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

Passaggio 5: carica il tuo documento

Il contesto che stiamo utilizzando per la nostra pipeline RAG è l'annuncio ufficiale di IBM per il rilascio di Granite 3.1. Possiamo caricare il post del blog in un documento direttamente dalla pagina web utilizzando WebBaseLoader di LangChain.

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

Passaggio 6: esegui la suddivisione del testo

Forniamo un codice di esempio per l'implementazione di ciascuna delle strategie di chunking di cui abbiamo parlato in precedenza in questo tutorial, disponibile tramite LangChain.

Chunking a dimensioni fisse

Per implementare il chunking a dimensioni fisse possiamo usare CharacterTextSplitter di LangChain e impostare chunk_size e chunk_overlap. chunk_size è misurato dal numero di caratteri. Puoi sperimentare con valori diversi. Imposteremo anche il separatore in modo che sia il carattere di nuova riga in modo da poter distinguere tra i paragrafi. Per la tokenizzazione, possiamo usare il tokenizer di istruzioni granite-3.1-8b, che suddivide il testo in token che possono essere elaborati dall'LLM.

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

Possiamo stampare uno dei blocchi per una migliore comprensione della loro struttura.

fixed_size_chunks[1]

Output: (troncata)

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...’)

Possiamo anche usare il tokenizer per verificare il nostro processo e per controllare il numero di token presenti in ogni blocco. Questo passaggio è facoltativo e a scopo dimostrativo.

for idx, val in enumerate(fixed_size_chunks):
    token_count = len(tokenizer.encode(val.page_content))
    print(f”The chunk at index {idx} contains {token_count} tokens.”)

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.

Fantastico! Sembra che le dimensioni dei nostri chunk siano state implementate correttamente.

Chunking ricorsivo

Per il chunking ricorsivo, possiamo utilizzare RecursiveCharacterTextSplitter di LangChain. Come nell'esempio del chunking a dimensione fissa, possiamo sperimentare con diverse dimensioni di chunk e sovrapposizioni.

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

La barra di divisione ha eseguito correttamente il chunking del testo utilizzando i separatori predefiniti: ["\n\n", "\n", " ", ""].

Chunking semantico

Il chunking semantico richiede un modello di embedding o di codifica. Possiamo utilizzare il modello di embedding granite-embedding-30m-english come modello di embedding. Possiamo anche stampare uno dei blocchi per comprenderne meglio la struttura.

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]

Output: (troncata)

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...’)

Chunking basato sui documenti

Documenti di vari tipi di file sono compatibili con gli splitter di testo basati su documenti di LangChain. Ai fini di questo tutorial, utilizzeremo un file Markdown. Per esempi di suddivisione ricorsiva in JSON, suddivisione del codice e divisione HTML, fai riferimento alla documentazione di LangChain.

Un esempio di file Markdown che possiamo caricare è il file README per Granite 3.1 su GitHub di 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...’)]

Ora possiamo utilizzare MarkdownHeaderTextSplitter di LangChain per dividere il file in base al tipo di intestazione, che abbiamo impostato nell'elenco headers_to_split_on. Stamperemo anche uno dei blocchi come esempio.

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

Come puoi vedere nell'output, il chunking ha suddiviso correttamente il testo per tipo di intestazione.

Passaggio 6: crea un database vettoriale

Ora che abbiamo sperimentato diverse strategie di chunking, passiamo all'implementazione della nostra RAG. Per questo tutorial, scegliamo i blocchi prodotti dagli agenti e li convertiamo in embedding. Un database vettoriale open source che possiamo utilizzare è Chroma DB. Possiamo accedere facilmente alle funzionalità Chroma attraverso il pacchetto langchain_chroma.

Inizializziamo il nostro database vettoriale Chroma, forniamogli il nostro embedding e aggiungiamo i nostri documenti prodotti tramite chunking semantico.

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’]

Passaggio 7: struttura il modello di prompt

A seguire, possiamo passare alla creazione di un modello di prompt per il nostro LLM. Questo modello di prompt ci consente di porre più domande senza alterare la struttura iniziale del prompt. Possiamo anche fornire il nostro database vettoriale come retriever. Questo passaggio finalizza la struttura RAG.

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|>Usa i seguenti elementi di contesto per rispondere alla domanda alla fine. Se non conosci la risposta, è sufficiente rispondere che non la conosci, senza cercare di inventare una risposta.
{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)

Fase 8: prompt della catena RAG

Utilizzando il nostro workflow RAG completato, richiamiamo una query utente. Innanzitutto, possiamo effettuare un prompt strategico del modello senza alcun contesto aggiuntivo dal database vettoriale che abbiamo creato per verificare se il modello utilizza le sue conoscenze integrate o se utilizza realmente il contesto RAG. Il blog di annuncio di Granite 3.1 fa riferimento a Docling, lo strumento IBM per analizzare vari tipi di documenti e convertirli in Markdown o JSON. Chiediamo all'LLM informazioni su Docling.

output = llm.invoke(“Che cos'è 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.’

Chiaramente, il modello non è stato addestrato su Docling e, senza strumenti o informazioni esterni, non può fornirci queste informazioni. Ora proviamo a fornire la stessa query alla catena RAG che abbiamo costruito.

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

Fantastico! Il modello Granite ha utilizzato correttamente il contesto RAG per fornirci informazioni corrette su Docling preservando la coerenza semantica. Abbiamo dimostrato che questo stesso risultato non era possibile senza l'uso della RAG.

Riepilogo

In questo tutorial, hai creato una pipeline RAG e hai sperimentato diverse strategie di chunking per migliorare la precisione di recupero del sistema. Utilizzando il modello Granite 3.1, abbiamo prodotto con successo le risposte appropriate del modello a una domanda dell'utente relativa ai documenti forniti come contesto. Il testo che abbiamo usato per questa implementazione RAG è stato caricato da un blog su ibm.com che annunciava il rilascio di Granite 3.1. Il modello ci ha fornito informazioni accessibili solo attraverso il contesto fornito, poiché non faceva parte della sua base di conoscenze iniziale.

Se sei alla ricerca di ulteriori letture, controlla i risultati di un progetto che confronta le prestazioni di LLM utilizzando il chunking strutturato HTML rispetto al chunking di watsonx.

Soluzioni correlate
IBM watsonx.ai

Addestra, convalida, adatta e implementa le funzionalità di AI generativa, foundation model e machine learning con IBM watsonx.ai, uno studio aziendale di nuova generazione per builder AI. Crea applicazioni AI in tempi ridotti e con una minima quantità di dati.

Scopri watsonx.ai
Soluzioni di intelligenza artificiale

Metti l'AI al servizio della tua azienda grazie all'esperienza leader di settore e alla gamma di soluzioni di IBM nel campo dell'AI.

Esplora le soluzioni AI
Servizi AI

Reinventa i flussi di lavoro e le operazioni critiche aggiungendo l'AI per massimizzare le esperienze, il processo decisionale in tempo reale e il valore di business.

Esplora i servizi AI
Fai il passo successivo

Ottieni l'accesso completo a funzionalità che coprono l'intero ciclo di vita dello sviluppo dell'AI. Crea soluzioni AI all'avanguardia con interfacce intuitive, workflow e accesso alle API e agli SDK standard di settore.

Esplora watsonx.ai Prenota una demo live