Implemente estrategias de fragmentación de RAG con LangChain y watsonx.ai

En este tutorial, experimentará con varias estrategias de fragmentación utilizando LangChain y el modelo más reciente de IBM® Granite ahora disponible en watsonx.ai. El objetivo general es realizar la generación aumentada por recuperación (RAG, por sus siglas en inglés).

¿Qué es la fragmentación?

La fragmentación se refiere al proceso de dividir grandes fragmentos de texto en segmentos o fragmentos de texto más pequeños. Para enfatizar la importancia de la fragmentación, es útil comprender la RAG. La RAG es una técnica de procesamiento de lenguaje natural (PLN) que combina la recuperación de información y los modelos de lenguaje grandes (LLM) para recuperar información relevante de conjuntos de datos complementarios para optimizar la calidad de la salida del LLM. Para gestionar documentos grandes, podemos usar la fragmentación para dividir el texto en fragmentos más pequeños de fragmentos significativos. Estos fragmentos de texto se pueden incorporar y almacenar en una base de datos vectorial mediante el uso de un modelo de incorporación. Finalmente, el sistema RAG puede usar la búsqueda semántica para recuperar solo los fragmentos más relevantes. Los fragmentos más pequeños tienden a superar a los fragmentos más grandes, ya que tienden a ser piezas más manejables para modelos de tamaño de ventana de contexto más pequeño.

Algunos componentes clave de la fragmentación incluyen:

  • Estrategia de fragmentación: elegir la estrategia adecuada para su aplicación de RAG es importante, ya que determina los límites para establecer fragmentos. Exploraremos algunos de estos en la siguiente sección.
  • Tamaño del fragmento: número máximo de tokens que deben estar en cada fragmento. Determinar el tamaño de fragmento adecuado suele implicar cierta experimentación.  
  • Superposición de fragmentos: el número de tokens que se superponen entre fragmentos para preservar el contexto. Este es un parámetro opcional.

Estrategias de fragmentación

Hay varias estrategias de fragmentación diferentes para elegir. Es importante seleccionar la técnica de fragmentación más eficaz para el caso de uso específico de su aplicación de LLM. Algunos procesos de fragmentación comúnmente utilizados incluyen: 

  • Fragmentación de tamaño fijo: división de texto con un tamaño de fragmento específico y superposición de fragmentos opcional. Este enfoque es el más común y directo.
  • Fragmentación recursiva: iteración de separadores predeterminados hasta que uno de ellos produzca el tamaño de fragmento preferido. Los separadores predeterminados incluyen ["\n\n", "\n", " ", ""]. Este método de fragmentación utiliza separadores jerárquicos para que los párrafos, seguidos de oraciones y luego palabras, se mantengan juntos tanto como sea posible.
  • Fragmentación semántica: dividir el texto de una manera que agrupe oraciones en función de la similitud semántica de sus incorporaciones. Las incorporaciones de alta similitud semántica están más juntas que las de baja similitud semántica. Esto da como resultado fragmentos sensibles al contexto.
  • Fragmentación basada en documentos: división basada en la estructura del documento. Este divisor puede utilizar texto Markdown, imágenes, tablas e incluso clases y funciones de código Python como formas de determinar la estructura. Al hacerlo, el LLM puede fragmentar y procesar documentos grandes.
  • División agéntica: aprovecha la IA agéntica al permitir que el LLM determine la división adecuada de documentos en función del significado semántico, así como de la estructura del contenido, como tipos de párrafo, encabezados de sección, instrucciones paso a paso y más. Este fragmentador es experimental e intenta simular el razonamiento humano al procesar documentos largos.  

Pasos

Paso 1: Configurar el entorno

Si bien puede elegir entre varias herramientas, este tutorial lo guiará a través de cómo configurar una cuenta de IBM para usar un Jupyter Notebook.

  1. Inicie sesión en watsonx.ai con su cuenta de IBM Cloud.

  2. Cree un proyecto watsonx.ai.

    Puede obtener su ID de proyecto desde dentro de su proyecto. Haga clic en la pestaña Manage. Luego, copie el ID del proyecto de la sección Details de la página General. Necesita este ID para este tutorial.

  3. Cree un Jupyter Notebook.

Este paso abrirá un entorno de Notebook donde podrá copiar el código de este tutorial. También puede descargar este notebook en su sistema local y cargarlo en su proyecto watsonx.ai como activo. Para ver más tutoriales de Granite, consulte la comunidad de IBM Granite. Este Jupyter Notebook y los conjuntos de datos utilizados se pueden encontrar en GitHub.

Paso 2. Configure una instancia de watsonx.ai Runtime y una clave de API

  1. Cree una instancia de servicio watsonx.ai Runtime (seleccione la región adecuada y elija el plan Lite, que es una instancia gratuita).

  2. Genere una clave de API.

  3. Asocie la instancia del servicio watsonx.ai Runtime al proyecto que creó en watsonx.ai.

Paso 3. Instalación e importación de bibliotecas relevantes y configuración de sus credenciales

#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

Para establecer nuestras credenciales, necesitamos el WATSONX_APIKEY y WATSONX_PROJECT_ID que generó en el paso 1. También estableceremos la URL que sirve como endpoint de la 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"

Paso 4. Inicialice su LLM

Usaremos Granite 3.1 como nuestro LLM para este tutorial. Para inicializar el LLM, necesitamos establecer los parámetros del modelo. Para aprender más sobre estos parámetros del modelo, como los límites mínimo y máximo de tokens, consulte la documentación.

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

Paso 5. Cargue su documento

El contexto que estamos utilizando para nuestro pipeline de RAG es el anuncio oficial de IBM para el lanzamiento de Granite 3.1. Podemos cargar el blog a un documento directamente desde la página web utilizando WebBaseLoader de LangChain.

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

Paso 6. Realice la división de texto

Proporcionemos código de muestra para implementar cada una de las estrategias de fragmentación que cubrimos anteriormente en este tutorial disponible a través de LangChain.

Fragmentación de tamaño fijo

Para implementar la fragmentación de tamaño fijo, podemos usar CharacterTextSplitter de LangChain y establecer un chunk_size, así como un chunk_overlap. El tamaño del fragmento se mide por el número de caracteres. Siéntase libre de experimentar con diferentes valores. También estableceremos que el separador sea el carácter de nueva línea para que podamos diferenciar entre párrafos. Para la tokenización, podemos usar el tokenizador granite-3.1-8b-instruct . El tokenizador desglosa el texto en tokens que el LLM puede procesar.

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

Podemos imprimir uno de los fragmentos para comprender mejor su estructura.

fixed_size_chunks[1]

Salida: (truncada)

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

También podemos usar el tokenizador para verificar nuestro proceso y la cantidad de tokens presentes en cada fragmento. Este paso es opcional y tiene fines demostrativos.

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

Resultado:

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.

¡Excelente! Parece que nuestros tamaños de fragmentos se implementaron adecuadamente.

Fragmentación recursiva

Para la fragmentación recursiva, podemos usar RecursiveCharacterTextSplitter de LangChain. Al igual que en el ejemplo de fragmentación de tamaño fijo, podemos experimentar con diferentes tamaños de fragmentación y superposición.

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]

Resultado:

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

El divisor fragmentó correctamente el texto utilizando los separadores predeterminados: [“\n\n”, “\n”, “ “, “”].

Fragmentación semántica

La fragmentación semántica requiere un modelo de incorporación o codificador. Podemos usar el modelo granite-embedding-30m-english como nuestro modelo de incorporación. También podemos imprimir uno de los fragmentos para comprender mejor su estructura.

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]

Salida: (truncada)

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

Fragmentación basada en documentos

Los documentos de varios tipos de archivos son compatibles con los divisores de texto basados en documentos de LangChain. Para los fines de este tutorial, utilizaremos un archivo Markdown. Para ver ejemplos de división recursiva de JSON, división de código y división de HTML, consulte la documentación de LangChain.

Un ejemplo de un archivo Markdown que podemos cargar es el archivo README para Granite 3.1 en GitHub de 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

Resultado:

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

Ahora, podemos usar MarkdownHeaderTextSplitter de LangChain para dividir el archivo por tipo de encabezado, que establecemos en la lista headers_to_split_on. También imprimiremos uno de los fragmentos como ejemplo.

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

Resultado:

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

Como puede ver en el resultado, la fragmentación dividió correctamente el texto por tipo de encabezado.

Paso 7. Cree un almacén de vectores

Ahora que hemos experimentado con varias estrategias, sigamos adelante con nuestra implementación de RAG. Para este tutorial, elegiremos los fragmentos producidos por la división semántica y los convertiremos en incorporaciones vectoriales. Un almacén de vectores de código abierto que podemos usar es Chroma DB. Podemos acceder fácilmente a la funcionalidad de Chroma a través del paquete langchain_chroma.

Inicialicemos nuestra base de datos vectorial Chroma, proporcionemos nuestro modelo de incorporación y agreguemos nuestros documentos producidos por fragmentación semántica.

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)

Resultado:

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

Paso 8. Estructura la plantilla de instrucciones

A continuación, podemos crear una plantilla de instrucciones para nuestro LLM. Esta plantilla de instrucciones nos permite hacer varias preguntas sin alterar la estructura inicial de las instrucciones. También podemos proporcionar nuestro almacén de vectores como el recuperador. Este paso finaliza la estructura de 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|>Use the following pieces of context to answer the question at the end. Si no sabe la respuesta, simplemente diga que no sabe; no trate de inventar una respuesta.
{context}
Pregunta: {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)

Paso 9. De una instrucción la cadena de RAG

Usando nuestro flujo de trabajo de RAG completo, invoquemos una consulta de usuario. En primer lugar, podemos dar una instrucción estratégica al modelo sin ningún contexto adicional del almacén de vectores que construimos para probar si el modelo está utilizando su conocimiento incorporado o realmente el contexto de RAG. El blog de anuncios de Granite 3.1 hace referencia a Docling, la herramienta de IBM para analizar varios tipos de documentos y convertirlos en Markdown o JSON. Preguntemos al LLM sobre Docling.

salida = llm.invoke(“¿Qué es Docling?”)
salida

Resultado:

‘?\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.’

Claramente, el modelo no fue entrenado con información sobre Docling y sin herramientas o información externas, no puede proporcionarnos esta información. Ahora, intentemos proporcionar la misma consulta a la cadena de RAG que creamos.

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

Resultado:

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

¡Excelente! El modelo Granite utilizó correctamente el contexto de RAG para darnos información correcta sobre Docling mientras preservaba la coherencia semántica. Probamos que este mismo resultado no era posible sin el uso de RAG.

Resumen

En este tutorial, creó un pipeline de RAG y experimentó con varias estrategias de fragmentación para mejorar la precisión de recuperación del sistema. Con el modelo Granite 3.1, producimos correctamente respuestas de modelo adecuadas a una consulta de usuario relacionada con los documentos proporcionados como contexto. El texto que utilizamos para esta implementación de RAG se cargó desde un blog en ibm.com que anunciaba el lanzamiento de Granite 3.1. El modelo nos proporcionó información a la que solo se puede acceder a través del contexto proporcionado, ya que no formaba parte de la base de conocimientos inicial del modelo.

Para aquellos que buscan lecturas adicionales, consulte los resultados de un proyecto que compara el rendimiento de LLM mediante fragmentación estructurada HTML en comparación con la fragmentación de watsonx.

Soluciones relacionadas
IBM watsonx.ai

Entrene, valide, ajuste y despliegue IA generativa, modelos fundacionales y capacidades de machine learning con IBM watsonx.ai, un estudio empresarial de próxima generación para creadores de IA. Diseñe aplicaciones de IA en menos tiempo y con menos datos.

Descubra watsonx.ai
Soluciones de inteligencia artificial

Ponga la IA a trabajar en su negocio con la experiencia en IA líder en la industria y la cartera de soluciones de IBM a su lado.

Explore las soluciones de IA
Servicios de IA

Reinvente los flujos de trabajo y las operaciones críticas añadiendo IA para maximizar las experiencias, la toma de decisiones en tiempo real y el valor empresarial.

Conozca los servicios de IA
Dé el siguiente paso

Obtenga acceso único a capacidades que abarcan el ciclo de vida del desarrollo de IA. Produzca potentes soluciones de IA con interfaces fáciles de usar, flujos de trabajo y acceso a API y SDK estándar de la industria.

Explore watsonx.ai Reserve una demostración en vivo