De la teoría a la práctica: creando un agente conversacional basado en RAG
Fecha del documento: 18-02-2025

Introducción
En anteriores contenidos, hemos explorado a fondo el apasionante mundo de los Modelos Grandes de Lenguaje (LLM) y, en particular, las técnicas de Generación Aumentada por Recuperación (RAG) que están revolucionando la forma en que interactuamos con los agentes conversacionales. Este ejercicio marca un hito en nuestra serie, ya que no solo explicaremos los conceptos, sino que también te guiaremos paso a paso en la construcción de tu propio agente conversacional potenciado con RAG. Para ello, utilizaremos un notebook de Google Colab.
Accede al repositorio del laboratorio de datos en Github.
Ejecuta el código de pre-procesamiento de datos sobre Google Colab.
A través de este notebook, construiremos un chat que utiliza RAG para mejorar sus respuestas, partiendo desde cero. El notebook guiará al usuario a través de todo el proceso:
- Instalación de dependencias.
- Configuración del entorno.
- Integración de una fuente de información en forma de post.
- Incorporación de dicha fuente a la base de conocimiento del chat utilizando técnicas RAG.
- Finalmente, podremos observar cómo la respuesta del modelo cambia antes y después de proporcionar el post y realizar una pregunta específica sobre su contenido.
Herramientas utilizadas
Antes de comenzar, es necesario introducir y explicar qué herramientas hemos utilizado y por qué hemos escogido estas. Para la construcción de esta aplicación RAG hemos utilizado 3 piezas de tecnología o herramientas: Google Colab, OpenAI y LangChain. Tanto Google Colab como OpenAI son viejos conocidos y los hemos utilizado varias veces en contenidos previos. Por eso, en esta sección, ponemos especial atención en explicar qué es LangChain puesto que es una nueva herramienta que no hemos utilizado en anteriores posts.
- Google Colab. Como es habitual en nuestros ejercicios, cuando son necesarios recursos de computación, así como un entorno de programación amigable, empleamos Google Colab, en la medida de lo posible. Google Colab nos garantiza que cualquier usuario que quiera reproducir el ejercicio lo pueda hacer sin complicaciones derivadas de la configuración de los entornos particulares de cada programador. Cabe destacar que adecuar este ejercicio inspirado en recursos previos disponibles en LangChain al entorno de Google Colab ha sido un reto.
- OpenAI. Como proveedor del modelo grande del lenguaje (LLM) Chat GPT, OpenAI ofrece una variedad de modelos de lenguaje potentes, como GPT-4, GPT-4o, GPT-4o mini, etc. que se utilizan para procesar y generar texto en lenguaje natural. En este caso, el modelo de lenguaje de OpenAI se utiliza en la zona de generación de la respuesta, donde se combinan la pregunta del usuario y los documentos recuperados para producir una respuesta precisa.
- LangChain. Es un framework (conjunto de bibliotecas) de código abierto diseñado para facilitar el desarrollo de aplicaciones basadas en modelos de lenguaje de gran escala (LLM). Este framework es especialmente útil para integrar y gestionar flujos complejos que combinan múltiples componentes, como modelos de lenguaje, bases de datos vectoriales, y herramientas de recuperación de información, entre otros.
LangChain es ampliamente utilizado en el desarrollo de aplicaciones como:
- Sistemas de preguntas y respuestas (QA systems).
- Asistentes virtuales con conocimiento específico.
- Sistemas de generación de texto personalizados.
- Herramientas de análisis de datos basadas en lenguaje natural.
Características principales de LangChain
- Modularidad y flexibilidad. LangChain está diseñado con una arquitectura modular que permite a los desarrolladores conectar diferentes herramientas y servicios. Esto incluye modelos de lenguaje (como OpenAI, Hugging Face, o LLM locales) y bases de datos vectoriales (como Pinecone, ChromaDB o Weaviate). La La lista de modelos de chat con los que se puede interactuar a través de Langchain es muy amplia.
- Soporte para técnicas RAG (Recuperación Aumentada por Generación). Langhain facilita la implementación de técnicas RAG al permitir la integración directa de modelos de recuperación de información y generación de texto. Esto mejora la precisión de las respuestas al permitir que los LLM trabajen con conocimiento actualizado y específico.
- Optimización del manejo de prompts. Langhain ayuda a diseñar y gestionar prompts complejos de manera eficiente. Permite construir dinámicamente un contexto relevante que se trabaja con el modelo, optimizando el uso de tokens y asegurando que las respuestas sean precisas y útiles.
- Los tokens representan las unidades básicas que un modelo de IA utiliza para procesar texto. Un token puede ser una palabra completa, una parte de una palabra o un signo de puntuación. En la frase "¡Hola mundo!" existen, por ejemplo, cuatro tokens distintos: "¡", "Hola", "mundo", "!". El procesamiento de texto requiere más recursos computacionales a medida que aumenta el número de tokens. Las versiones gratuitas de modelos de IA, incluida la que usamos en este ejercicio, establecen límites en la cantidad de tokens procesables.
- Integración con múltiples fuentes de datos. El framework puede conectarse a diversas fuentes de datos, como bases de datos, API o documentos cargados por los usuarios. Esto lo hace ideal para construir aplicaciones que necesitan acceso a grandes volúmenes de información estructurada o no estructurada.
- Interoperabilidad con múltiples LLM. LangChain es agnóstico (se puede adaptar a varios proveedores de modelos de lenguaje) respecto al proveedor del modelo de lenguaje, lo que significa que puedes utilizar OpenAI, Cohere, Anthropic o incluso modelos de lenguaje alojados localmente.
Para terminar con esta sección, cabe destacar el carácter open source de Langhain, algo que facilita la colaboración y la innovación en el desarrollo de aplicaciones basadas en modelos de lenguaje. Además, LangChain nos aporta una increíble flexibilidad porque permite a los desarrolladores integrar fácilmente diferentes LLM, vectorizadores y hasta interfaces web finales en sus aplicaciones.
Exploración del ejercicio paso a paso
Introducción al Repositorio
El repositorio de Github que utilizaremos contiene todos los recursos necesarios para construir nuestra aplicación RAG. En su interior, encontrarás:
- README: este archivo proporciona una descripción general del proyecto, instrucciones de uso y recursos adicionales.
- Jupyter Notebook: el ejemplo lo hemos desarrollado usando un formato de Jupyter Notebook que ya hemos empleado en el pasado para codificar ejercicios prácticos combinando un documento de texto con fragmentos de código ejecutable en Google Colab. Aquí se encuentra la implementación detallada de la aplicación, incluyendo la carga y procesamiento de datos, la integración con modelos de lenguaje como GPT-44, la configuración de sistemas de recuperación de información y la generación de respuestas basadas en los datos recuperados.
Notebook: preparando el entorno
Antes de comenzar, es recomendable contar con los siguientes requisitos.
- Conocimientos básicos de Python y Procesamiento de Lenguaje Natural (PLN): si bien el notebook es autoexplicativo, una comprensión básica de estos conceptos facilitará el aprendizaje.
- Acceso a Google Colab: el notebook se ejecuta en este entorno, que nos proporciona la infraestructura necesaria.
- Cuentas activas en OpenAI y LangChain con claves de API válidas. Estos servicios son gratuitos y esenciales para la ejecución del notebook. Una vez que te registres en estos servicios, necesitarás generar una API Key para interactuar con los servicios. Deberás tener a mano esta clave para poder pegarla en el momento de ejecutar el fragmento de código correspondiente. Si necesitas ayuda para obtener estas claves, cualquier asistente conversacional como chatGPT o Google Gemini te pueden ayudar paso a paso a conseguir las claves. Si necesitas guía visual en youtube encontraras miles de tutoriales
- OpenAI API: https://openai.com/api/
- Lanchain API: https://www.langchain.com/
Explorando el Notebook: bloque por bloque
El notebook se divide en varios bloques, cada uno dedicado a una etapa específica del desarrollo de nuestra aplicación RAG. A continuación, describiremos cada bloque en detalle, incluyendo el código utilizado y su explicación.
Nota para el usuario. A continuación, vamos a ir reproduciendo bloques del código presentes en el notebook de Colab. Por claridad hemos dividido el código en unidades autocontenidas y hemos formateado el código para resaltar la sintaxis del lenguaje de programación Python. Además, las salidas que el Notebook proporciona, las hemos formateado y resaltado en formato JSON para que sean más legibles. Ha de tenerse en cuenta que este Notebook invoca API de modelos del lenguaje y por lo tanto, la respuesta del modelo cambia con cada ejecución. Esto hace que las salidas (las respuestas) que presentamos en este post puedan no ser exactamente iguales a las que el usuario reciba cuándo ejecute el Notebook en Colab
Bloque 1: instalación y configuración inicial
import os |
Es muy importante que ejecutes estas dos líneas al principio del ejercicio y luego ya no lo vuelvas a ejecutar más hasta que cierres y salgas de Google Colab.
%%capture |
!pip install langchain --quiet |
import getpass os.environ["LANGCHAIN_TRACING_V2"] = "true" |
Cuando ejecutes este fragmento, aparecerá un pequeño cuadro de diálogo debajo del fragmento. Ahí debes de pegar tu API Key de Langchain.
!pip install -qU langchain-openai |
import getpass |
Cuando ejecutes este fragmento, aparecerá un pequeño cuadro de diálogo debajo del fragmento. Ahí debes de pegar tu API Key de OpenAI.
En este primer bloque, hemos instalado las bibliotecas necesarias para nuestro proyecto. Algunas de las más relevantes son:
- openai: Para interactuar con la API de OpenAI y acceder a modelos como GPT-4.
- langchain: Un framework que simplifica el desarrollo de aplicaciones con LLM.
- langchain-text-splitters: Para dividir textos largos en fragmentos más pequeños que puedan ser procesados por los modelos de lenguaje.
- langchain-community: Una colección de herramientas y componentes adicionales para LangChain.
- langchain-openai: Para integrar LangChain con la API de OpenAI.
- langgraph: Para visualizar el flujo de trabajo de nuestra aplicación RAG.
- Además de instalar las bibliotecas, también configuramos las claves de API para OpenAI y LangChain, utilizando la función getpass.getpass() para introducirlas de forma segura.
Bloque 2: inicializamos la interacción con el LLM
A continuación, iniciamos la primera interacción programática (le pasamos nuestro primer prompt) con el modelo del lenguaje. Para comprobar que todo funciona le pedimos traducir una sencilla frase.
import getpass import os ] |
Si todo ha ido bien obtendremos una salida como esta:
{ |
Este bloque es una introducción básica a la utilización de un LLM para una tarea sencilla: la traducción. Se configura la clave de API de OpenAI y se instancia un modelo de lenguaje gpt-4o-mini utilizando ChatOpenAI.
Se definen dos mensajes:
- SystemMessage: Instrucción al modelo para traducir del inglés al italiano.
- HumanMessage: El texto que se desea traducir ("hi!").
Finalmente, se invoca al modelo con llm.invoke(messages) para obtener la traducción.
Bloque 3: creando Embeddings
Para entender el concepto del Embeddings aplicado al contexto del procesamiento del lenguaje natural recomendamos leer este post.
import getpass pip install -qU langchain-core from langchain_core.vectorstores import InMemoryVectorStore |
Cuando ejecutes este fragmento, aparecerá un pequeño cuadro de diálogo debajo del fragmento. Ahí debes de pegar tu API Key de OpenAI.
Este bloque se centra en la creación de embeddings (representaciones vectoriales de texto) que capturan su significado semántico. Utilizamos la clase OpenAIEmbeddings para acceder al modelo text-embedding-3-large de OpenAI, que genera embeddings de alta calidad.
Los embeddings se almacenarán en un InMemoryVectorStore, una estructura de datos en memoria que permite realizar búsquedas eficientes basadas en similitud semántica.
Bloque 4: implementando RAG
#RAG import bs4 from langchain_community.document_loaders import WebBaseLoader # Manten únicamente el título del post, los encabezados y el contenido del HTML bs4_strainer = bs4.SoupStrainer(class_=("post-title", "post-header", "post-content")) loader = WebBaseLoader( web_paths=("https://datos.gob.es/es/blog/slm-llm-rag-y-fine-tuning-pilares-de-la-ia-...",) ) docs = loader.load() assert len(docs) == 1 print(f"Total characters: {len(docs.page_content)}") from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, add_start_index=True, ) all_splits = text_splitter.split_documents(docs) print(f"Split blog post into {len(all_splits)} sub-documents.") document_ids = vector_store.add_documents(documents=all_splits) print(document_ids[:3]) |
Este bloque es el corazón de la implementación RAG. Comienza cargando el contenido de un post, utilizando WebBaseLoader y la URL del post sobre SLM, LLM, RAG y Fine-tuning.
Para preparar nuestro sistema de Recuperación Aumentada por Generación (RAG), comenzamos procesando el texto del post mediante técnicas de segmentación. Este paso inicial resulta fundamental, ya que dividimos el contenido en fragmentos más pequeños pero completos en significado. Utilizamos las herramientas de LangChain para realizar esta segmentación, asignando a cada fragmento un identificador único (id). Esta preparación previa nos permite posteriormente realizar búsquedas eficientes y precisas cuando el sistema necesite recuperar información relevante para responder a las consultas.
Se utiliza bs4.SoupStrainer para extraer solo las secciones relevantes del HTML. El texto del post se divide en fragmentos más pequeños con RecursiveCharacterTextSplitter, asegurando un solapamiento entre fragmentos para mantener el contexto. Estos fragmentos se añaden al vector_store creado en el bloque anterior, generando embeddings para cada uno.
Vemos que el resultado de uno de los fragmentos nos informa que ha dividido el documento en 21 sub-documentos.
Split blog post into 21 sub-documents. |
Los documentos tienen un identificador propio. Por ejemplo, los 3 primeros se identifican como:
["409f1bcb-1710-49b0-80f8-e45b7ca51a96", "e242f16c-71fd-4e7b-8b28-ece6b1e37a1c", "9478b11c-61ab-4dac-9903-f8485c4770c6"] |
Bloque 5: definiendo el Prompt y visualizando el flujo de trabajo
from langchain import hub prompt = hub.pull("rlm/rag-prompt") example_messages = prompt.invoke( {"context": "(context goes here)", "question": "(question goes here)"} ).to_messages() assert len(example_messages) == 1 print(example_messages.content) from langchain_core.documents import Document from typing_extensions import List, TypedDict class State(TypedDict): question: str context: List[Document] answer: str def retrieve(state: State): retrieved_docs = vector_store.similarity_search(state["question"]) return {"context": retrieved_docs} def generate(state: State): docs_content = "\n\n".join(doc.page_content for doc in state["context"]) messages = prompt.invoke({"question": state["question"], "context": docs_content}) response = llm.invoke(messages) return {"answer": response.content} from langgraph.graph import START, StateGraph graph_builder = StateGraph(State).add_sequence([retrieve, generate]) graph_builder.add_edge(START, "retrieve") graph = graph_builder.compile() from IPython.display import Image, display display(Image(graph.get_graph().draw_mermaid_png())) result = graph.invoke({"question": "What is Task Decomposition?"}) print(f"Context: {result["context"]}\n\n") print(f"Answer: {result["answer"]}") for step in graph.stream( {"question": "¿Cual es el futuro de la IA Generativa?"}, stream_mode="updates" ): print(f"{step}\n\n----------------\n") |
Este bloque define el prompt que se utilizará para interactuar con el LLM. Se utiliza un prompt predefinido de LangChain Hub (rlm/rag-prompt) que está diseñado para tareas RAG.
Se definen dos funciones:
- retrieve: busca en el vector_store los fragmentos más similares a la pregunta del usuario.
- generate: genera una respuesta utilizando el LLM, teniendo en cuenta el contexto proporcionado por los fragmentos recuperados.
Se utiliza langgraph para visualizar el flujo de trabajo RAG.
Figura 1: flujo de trabajo RAG. Elaboración propia.
Finalmente, se prueba el sistema con dos preguntas: una en inglés ("What is Task Decomposition?") y otra en español ("¿Cual es el futuro de la IA Generativa?").
La primera pregunta, "What is Task Decomposition?, está en inglés y es una pregunta genérica, sin relación con nuestro post de contenido. Por esto, pese a que el sistema, busca en su base de conocimiento previamente creada con la vectorización del documento (post) no encuentra relación entre la pregunta y este contexto.
Este texto puede variar con cada ejecución
Answer: No se menciona explícitamente el concepto de "Task Decomposition" en el contexto proporcionado. Por lo tanto, no tengo información sobre qué es Task Decomposition. |
Answer: Task Decomposition es un proceso que descompone una tarea compleja en subtareas más pequeñas y manejables. Esto permite abordar cada subtarea de manera independiente, facilitando su resolución y mejorando la eficiencia general. Aunque el contexto proporcionado no define explícitamente Task Decomposition, este concepto es común en la IA y optimización de tareas. |
Esta respuesta es la que proporciona el modelo del lenguaje sin ninguna base de conocimiento específica. Ahora bien, cuando preguntamos por algo que tiene que ver con el post que hemos cargado como base de conocimiento, la técnica RAG entra en funcionamiento y ejecuta los mecanismos secuenciales de retrieve y generate.
{ |
Cómo se ve en la respuesta, el sistema recupera 4 documentos (en el diagrama anterior, esto corresponde a la etapa de “Retrieve”) con sus correspondientes “id” (identificadores) cómo por ejemplo, el primer documento "id": "53962c40-c08b-4547-a74a-26f63cced7e8" que se corresponde con un fragmento del post original "title": "SLM, LLM, RAG y Fine-tuning: Pilares de la IA Generativa Moderna | datos.gob.es"
Con esos 4 fragmentos el sistema considera que tiene suficiente información relevante para proporcionar (en el diagrama anterior, la etapa “generate”) una respuesta satisfactoria a la pregunta.
{ |
Bloque 6: personalizando el prompt
from langchain_core.prompts import PromptTemplate template = """Use the following pieces of context to answer the question at the end. If you don"t know the answer, just say that you don"t know, don"t try to make up an answer. Use three sentences maximum and keep the answer as concise as possible. Always say "thanks for asking!" at the end of the answer. {context} Question: {question} Helpful Answer:""" custom_rag_prompt = PromptTemplate.from_template(template) |
Este bloque personaliza el prompt para que las respuestas sean más concisas y añadan una frase de cortesía al final. Se utiliza PromptTemplate para crear un nuevo prompt con las instrucciones deseadas.
Bloque 7: añadiendo metadatos y refinando la búsqueda
total_documents = len(all_splits) third = total_documents // 3 for i, document in enumerate(all_splits): if i < third: document.metadata["section"] = "beginning" elif i < 2 * third: document.metadata["section"] = "middle" else: document.metadata["section"] = "end" all_splits.metadata from langchain_core.vectorstores import InMemoryVectorStore vector_store = InMemoryVectorStore(embeddings) _ = vector_store.add_documents(all_splits) from typing import Literal from typing_extensions import Annotated class Search(TypedDict): """Search query.""" query: Annotated[str, ..., "Search query to run."] section: Annotated( Literal["beginning", "middle", "end"], ..., "Section to query.", ] class State(TypedDict): question: str query: Search context: List[Document] answer: str def analyze_query(state: State): structured_llm = llm.with_structured_output(Search) query = structured_llm.invoke(state["question"]) return {"query": query} def retrieve(state: State): query = state["query"] retrieved_docs = vector_store.similarity_search( query["query"], filter=lambda doc: doc.metadata.get("section") == query["section"], ) return {"context": retrieved_docs} def generate(state: State): docs_content = "\n\n".join(doc.page_content for doc in state["context"]) messages = prompt.invoke({"question": state["question"], "context": docs_content}) response = llm.invoke(messages) return {"answer": response.content} graph_builder = StateGraph(State).add_sequence([analyze_query, retrieve, generate]) graph_builder.add_edge(START, "analyze_query") graph = graph_builder.compile() display(Image(graph.get_graph().draw_mermaid_png())) for step in graph.stream( {"question": "¿Cual es el furturo de la IA Generativa en palabras del autor?"}, stream_mode="updates", ): print(f"{step}\n\n----------------\n") |
En este bloque, se añaden metadatos a los fragmentos del post, dividiéndolos en tres secciones: "beginning", "middle" y "end". Esto permite realizar búsquedas más refinadas, limitando la búsqueda a una sección específica del post.
Se introduce una nueva función analyze_query que utiliza el LLM para determinar la sección del post más relevante para la pregunta del usuario. El flujo de trabajo RAG se actualiza para incluir esta nueva etapa.
Finalmente, se prueba el sistema con una pregunta en español ("¿Cuál es el futuro de la IA Generativa en palabras del autor?"), observando cómo el sistema utiliza la información de la sección "end" del post para generar una respuesta más precisa.
Veamos el resultado:
Figura 2: flujo de trabajo RAG. Elaboración propia.
{ ---------------- { |
{ |
Conclusiones
A través de este recorrido por el notebook de Google Colab, hemos experimentado de primera mano la construcción de un agente conversacional con RAG. Hemos aprendido a:
- Instalar las bibliotecas necesarias.
- Configurar el entorno de desarrollo.
- Cargar y procesar datos.
- Crear embeddings y almacenarlos en un vector_store.
- Implementar las etapas de recuperación y generación de RAG.
- Personalizar el prompt para obtener respuestas más específicas.
- Añadir metadatos para refinar la búsqueda.
Este ejercicio práctico te proporciona las herramientas y conocimientos necesarios para comenzar a explorar el potencial de RAG y desarrollar tus propias aplicaciones.
¡Anímate a experimentar con diferentes fuentes de información, modelos de lenguaje y prompts para crear agentes conversacionales cada vez más sofisticados!
Contenido elaborado por Alejandro Alija,experto en Transformación Digital e Innovación. Los contenidos y los puntos de vista reflejados en esta publicación son responsabilidad exclusiva de su autor.