Mettre en œuvre des stratégies de découpage RAG avec LangChain et watsonx.ai

Dans ce tutoriel, vous allez expérimenter plusieurs stratégies de découpage à l’aide de LangChain et du dernier modèle IBM Granite désormais disponible sur watsonx.ai. L'objectif général est de réaliser une génération augmentée de récupération (RAG).

Qu’est-ce que le découpage ?

Le découpage (ou fragmentation) fait référence au processus consistant à décomposer de gros éléments de texte en segments de texte ou en blocs plus petits. Pour souligner l’importance du découpage, il est utile de comprendre la RAG. La RAG est une technique de traitement automatique du langage naturel (NLP) qui combine la récupération d’informations et de grands modèles de langage (LLM) pour récupérer des informations pertinentes à partir de jeux de données supplémentaires afin d’optimiser la qualité de la sortie du LLM. Pour gérer les documents volumineux, nous pouvons utiliser le découpage pour diviser le texte en plus petits extraits de blocs significatifs. Ces morceaux de texte peuvent ensuite être incorporés et stockés dans une base de données vectorielle à l’aide d’un modèle d’embedding. Enfin, le système RAG peut ensuite utiliser la recherche sémantique pour récupérer uniquement les morceaux les plus pertinents. Les petits blocs ont tendance à être plus performants que les gros morceaux, car ils ont tendance à être plus gérables pour les modèles avec une plus petite taille de fenêtre contextuelle.

Voici quelques-uns des éléments clés du découpage :

  • Stratégie de découpage : il est important de choisir la bonne stratégie de découpage pour votre application, car elle détermine les limites de définition des fragments. Nous en découvrirons certaines dans la section suivante.
  • Taille de bloc : nombre maximal de tokens pouvant se trouver dans chaque bloc. Déterminer la taille de bloc appropriée implique généralement quelques expérimentations.  
  • Chevauchement des blocs : le nombre de tokens qui se chevauchent entre les blocs pour préserver le contexte. Il s’agit d’un paramètre facultatif.

Stratégies de segmentation

Il existe plusieurs stratégies de découpage différentes. Il est important de sélectionner la technique de découpage la plus efficace pour le cas d’utilisation spécifique de votre application LLM. Voici quelques-uns des processus de découpage couramment utilisés : 

  • Découpage à taille fixe : fractionnement du texte avec une taille de bloc spécifique et chevauchement facultatif des blocs. Cette approche est la plus courante et la plus simple.
  • Découpage récursif : itération des séparateurs par défaut jusqu’à ce que l’un d’entre eux produise la taille de bloc préférée. Les séparateurs par défaut incluent ["\n\n", "\n", " ", ""]. Cette méthode de découpage utilise des séparateurs hiérarchiques afin que les paragraphes, suivis de phrases, puis de mots, restent ensemble autant que possible.
  • Découpage sémantique : découpage du texte de manière à regrouper les phrases en fonction de la similarité sémantique de leurs embeddings. Les embeddings de similarité sémantique élevée sont plus proches que ceux de similarité sémantique faible. Cela donne des fragments qui tiennent compte du contexte.
  • Découpage basé sur les documents : découpage basé sur la structure des documents. Ce séparateur peut utiliser du texte, des images, des tableaux et même des classes de code et des fonctions Python comme moyens de déterminer la structure. Ce faisant, des documents volumineux peuvent être fragmentés et traités par le LLM.
  • Découpage agentique : exploite l’IA agentique en permettant au LLM de déterminer le découpage approprié des documents en fonction de la signification sémantique ainsi que de la structure de contenu comme les types de paragraphes, les en-têtes de section, les instructions étape par étape, etc. Ce segmentateur est expérimental et tente de simuler le raisonnement humain lors du traitement de longs documents.  

Étapes

Étape 1. Configurez votre environnement

Bien que vous puissiez faire votre choix parmi plusieurs outils, ce tutoriel vous guide pas à pas pour configurer un compte IBM à l’aide d’un Jupyter Notebook.

  1. Connectez-vous à watsonx.ai depuis votre compte IBM Cloud.

  2. Créez un projet watsonx.ai.

    Vous pouvez obtenir l’ID de votre projet à partir de ce dernier. Cliquez sur l’onglet Manage (Gérer). Ensuite, copiez l’ID du projet à partir de la section Details (Détails) de la page General (Général). Vous aurez besoin de cet ID pour ce tutoriel.

  3. Créez un Jupyter Notebook.

Cette étape ouvre un environnement de notebook dans lequel vous pouvez copier le code de ce tutoriel. Vous pouvez également télécharger ce notebook sur votre système local et le charger dans votre projet watsonx.ai en tant qu’actif. Pour voir d'autres tutoriels Granite, consultez la Communauté IBM  Granite. Ce Jupyter Notebook, ainsi que les jeux de données utilisés, se trouvent sur GitHub.

Étape 2 : configurer une instance d’exécution watsonx.ai et une clé d’API

  1. Créez une instance de service d’exécution watsonx.ai (sélectionnez votre région et choisissez le plan Lite, qui est une instance gratuite).

  2. Générez une clé d’API.

  3. Associez l'instance de service Runtime watsonx.ai au projet que vous avez créé dans watsonx.ai.

Étape 3. Installer et importer les bibliothèques pertinentes et configurer vos identifiants

#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

Pour définir nos identifiants, nous avons besoin des identifiants WATSONX_APIKEY et WATSONX_PROJECT_ID que vous avez générés à l’étape 1. Nous définirons également l’URL servant de point de terminaison de l’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"

Étape 4. Initialisez votre LLM

Nous utiliserons Granite 3.1 comme LLM pour ce tutoriel. Pour initialiser le LLM, nous devons définir les paramètres du modèle. Pour en savoir plus sur ces paramètres de modèle, tels que les limites minimales et maximales de token, reportez-vous à la documentation.

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

Étape 5. Chargez votre document

Le contexte que nous utilisons pour notre pipeline RAG est le communiqué officiel d’IBM pour la sortie de Granite 3.1. Nous pouvons charger le blog dans un document directement à partir de la page Web à l’aide du WebBaseLoader de LangChain.

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

Étape 6. Effectuez la segmentation de texte

Prenons un exemple de code pour implémenter chacune des stratégies de découpage abordées plus haut dans ce tutoriel, disponible via LangChain.

Découpage à taille fixe

Pour implémenter un découpage en blocs à taille fixe, nous pouvons utiliser le CharacterTextSplitter de LangChain, définir une chunk_size et un chunk_overlap. La chunk_size est mesurée par le nombre de caractères. N'hésitez pas à expérimenter d'autres valeurs. Nous définirons également le séparateur comme caractère de nouvelle ligne afin de pouvoir différencier les paragraphes. Pour la tokenisation, nous pouvons utiliser le tokenizer granite-3.1-8b-instruct. Le tokeniseur divise le texte en tokens pouvant être traités par le 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])

Nous pouvons imprimer un des blocs pour une meilleure compréhension de leur structure.

fixed_size_chunks[1]

Sortie : (tronquée)

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

Nous pouvons également utiliser le tokenizer pour vérifier notre processus et contrôler le nombre de tokens présents dans chaque bloc. Cette étape est facultative et destinée à des fins de démonstration.

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.

Super ! Il semble que nos tailles de fragments aient été correctement mises en œuvre.

Découpage récursif

Pour le découpage récursif, on peut utiliser le RecursiveCharacterTextSplitter de LangChain. Comme pour l’exemple du découpage à taille fixe, nous pouvons expérimenter avec différentes tailles de blocs et de chevauchements.

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

Le séparateur a réussi à segmenter le texte en utilisant les séparateurs par défaut : [“\n\n”, “\n”, “ “, “”].

Découpage sémantique

Le découpage sémantique nécessite un modèle d'embedding ou d'encodeur. Nous pouvons utiliser le modèle granite-embedding-30m-english comme modèle d'embedding. Nous pouvons également imprimer un des blocs pour une meilleure compréhension de leur structure.

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]

Sortie : (tronquée)

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

Découpage basé sur les documents

Les documents de différents types de fichiers sont compatibles avec les séparateurs de texte basés sur les documents de LangChain. Pour les besoins de ce tutoriel, nous utiliserons un fichier Markdown. Pour des exemples de partitionnement récursif en JSON, de code et de HTML, consultez la documentation de LangChain.

Le fichier README pour Granite 3.1 sur le GitHub d’IBM est un exemple de fichier Markdown.

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

Maintenant, nous pouvons utiliser le MarkdownHeaderTextSplitter de LangChain pour découper le fichier par type d’en-tête, que nous avons défini dans la liste « headdown_to_split_on ». Nous imprimerons également l’un des blocs à titre d’exemple.

#découpage basé sur les documents
de 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```’)

Comme vous pouvez le voir dans la sortie, le découpage a réussi à diviser le texte par type d’en-tête.

Étape 7. Créez une base de données vectorielle

Maintenant que nous avons expérimenté différentes stratégies de découpage, passons à notre implémentation RAG. Pour ce tutoriel, nous allons choisir les blocs produits par la division sémantique et les convertir en embeddings. Pour cela, nous pouvons utiliser la base de données vectorielle open source Chroma DB. Nous pouvons facilement accéder aux fonctionnalités de Chroma via le package langchain_chroma. 

Initialisons notre base de données vectorielle Chroma, fournissons-lui notre modèle d’embeddings et ajoutons nos documents produits par découpage semantique.

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

Étape 8. Structurez le modèle de prompt

Ensuite, nous pouvons passer à la création d'un modèle de prompt pour notre LLM. Ce modèle de prompt nous permet de poser plusieurs questions sans modifier la structure initiale du prompt. Nous pouvons également fournir notre base de données vectorielle comme récupérateur. Cette étape finalise la structure RAG.

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

prompt_template = """<|start_of_role|>user<|end_of_role|>Utilisez les éléments de contexte suivants pour répondre à la question à la fin. Si vous ne connaissez pas la réponse, veuillez simplement indiquer que vous ne savez pas, sans tenter d'inventer une réponse.
{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)

Étape 8. Promptez la chaîne RAG

À l'aide de notre workflow RAG terminé, nous allons invoquer une requête utilisateur. Tout d’abord, nous pouvons envoyer un prompt stratégique au modèle sans aucun contexte supplémentaire à partir de la base de données vectorielle que nous avons construit pour tester si le modèle utilise ses connaissances intégrées ou véritablement en utilisant le contexte RAG. Le blog annonçant Granite 3.1 fait référence à Docling, l'outil d'IBM permettant d'analyser divers types de documents et de les convertir en Markdown ou JSON. Interrogons le LLM sur Docling.

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

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

Il est évident que le modèle n'a pas été formé à partir d'informations sur Docling et, sans outils ou informations externes, il ne peut pas nous fournir ces informations. Essayons maintenant de soumettre la même requête à la chaîne RAG que nous avons construite.

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

Super ! Le modèle Granite a correctement utilisé le contexte RAG pour nous fournir des informations exactes sur Docling tout en préservant la cohérence sémantique. Nous avons prouvé que ce même résultat n’était pas possible sans l’utilisation de la RAG.

Récapitulatif

Dans ce tutoriel, vous avez créé un pipeline RAG et expérimenté plusieurs stratégies de découpage pour améliorer la précision de récupération du système. En utilisant le modèle Granite 3.1, nous avons réussi à produire des réponses de modèle appropriées à une requête d’utilisateur concernant les documents fournis en tant que contexte. Le texte que nous avons utilisé pour cette implémentation RAG provient d’un blog sur ibm.com annonçant la publication de Granite 3.1. Le modèle nous a fourni des informations accessibles uniquement via le contexte fourni, car celui-ci ne faisait pas partie de la base de connaissances initiale du modèle.

Pour ceux qui souhaitent en savoir plus, consultez les résultats d’un projet comparant les performances d’un LLM à l’aide du découpage structuré HTML par rapport au découpage watsonx.

Solutions connexes
IBM watsonx.ai

Entraînez, validez, réglez et déployez une IA générative, des modèles de fondation et des capacités de machine learning avec IBM watsonx.ai, un studio d’entreprise nouvelle génération pour les générateurs d’IA. Créez des applications d’IA en peu de temps et avec moins de données.

Découvrir watsonx.ai
Solutions d’intelligence artificielle

Mettez l’IA au service de votre entreprise en vous appuyant sur l’expertise de pointe d’IBM dans le domaine de l’IA et sur son portefeuille de solutions.

Découvrir les solutions d’IA
Services d’IA

Réinventez les workflows et les opérations critiques en ajoutant l’IA pour optimiser les expériences, la prise de décision et la valeur métier en temps réel.

Découvrir les services d’IA
Passez à l’étape suivante

Bénéficiez d’un accès centralisé aux fonctionnalités couvrant le cycle de développement de l’IA. Produisez des solutions IA puissantes offrant des interfaces conviviales, des workflows et un accès à des API et SDK conformes aux normes du secteur.

Découvrir watsonx.ai Réserver une démo en direct