Profile Software Services

Cómo crear un chatbot con RAG e IA generativa paso a paso

Los modelos de Inteligencia Artificial están en boca de todo el mundo desde la irrupción de ChatGPT. Sin embargo; en lo que se refiere a entornos corporativos, estas herramientas no suelen tener el contexto interno (APIs, documentación, manuales, procesos, etc.) para responder bien a las cuestiones de los usuarios.

La arquitectura RAG (Retrieval-Augmented Generation) resuelve este problema aportando bases externas de conocimiento. En lugar de aplicar fine-tuning a un LLM desde cero, RAG hace algo más directo: recupera los fragmentos relevantes de tu documentación y se añaden como contexto antes de generar la respuesta.

Algunos de los beneficios que ofrece la arquitectura RAG:

  1. Eficiente en coste de implementación, ya que no requiere entrenar modelos desde cero.
  2. Acceso a la información específica de un dominio.
  3. Reducción del riesgo de alucinaciones.
  4. Mejora en la confianza de los usuarios al recibir respuestas fundamentadas.
arquitectura RAG

A lo largo de este post vamos a implementar un ejemplo de cómo crear un chatbot con RAG e IA generativa paso a paso, desde cero. Para ello, vamos a emplear las siguientes herramientas dentro del ecosistema Python

Comenzaremos con la base común de la que parte cualquier proyecto que involucra el aprendizaje automático: los datos. 

Preparando los datos 

    Una vez creado un proyecto en blanco, vamos a obtener una base de documentación de la plataforma de pagos Stripe, disponible en el repositorio stripe_docs_markdown. Colocamos los ficheros dentro de un directorio docs para poder ampliar el corpus en el futuro

    docs/ 
      stripe/ 
        billing/ 
        connect/ 
        ... 

    Indexar la documentación (embeddings + FAISS) 

      Antes de hablar de los modelos generativos, necesitamos preparar el conocimiento. Aquí entran los embeddings

      En el contexto de RAG, los documentos (sean de tipo texto, imágenes, audio…) se convierten en vectores utilizando modelos de embeddings, posteriormente se almacenan en una base de datos vectorial y se recuperan según su similitud con la consulta del usuario.  

      Para este proyecto, vamos a utilizar FAISS como índice de datos vectorial, así como LangChain para tratar la colección de documentos.  

      Dependencias y carga de datos 

        En general, para todo el proyecto, necesitaremos las siguientes dependencias

        # requirements.txt 
        # Gestión de lenguaje natural y procesamiento de texto 
        langchain==1.2.3 
        langchain-community==0.4.1 
        langchain-text-splitters==1.1.0 
        unstructured==0.18.27 
        unstructured[md] 
        sentence-transformers==5.2.0 
        # FAISS para búsqueda eficiente de vectores y LangChain HuggingFace para integración de modelos 
        faiss-cpu==1.13.2 
        langchain-huggingface==1.2.0 
        # Streamlit para la creación de aplicaciones web interactivas (chatbot) 
        streamlit==1.52.2 
        # Ajuste de dependencias para compatibilidad 
        torch==2.8.0 

        En este caso, los documentos están en formato Markdown, pero como mencionamos anteriormente podrían ser HTML, PDF o cualquier otro formato, LangChain tiene soporte para la gran mayoría. 

        El siguiente código leerá y procesará en fragmentos de 1000 caracteres cada documento (con cierto solapamiento para mantener contexto). Estos valores se pueden ajustar en función del tipo de documentación, pero con fragmentos demasiado grandes se pueden generar alucinaciones en las respuestas y con fragmentos muy pequeños se pierde el contexto local. 

        Finalmente, este script creará la base de datos vectorial con los embeddings de HuggingFace

        # data_loader.py 
        
        import os 
        import shutil 
        from langchain_community.document_loaders import DirectoryLoader 
        from langchain_text_splitters import RecursiveCharacterTextSplitter 
        from langchain_core.documents import Document 
        from langchain_community.vectorstores import FAISS 
        from langchain_huggingface import HuggingFaceEmbeddings 
        
        # Rutas de configuración 
        DOCS_PATH = "./docs/" 
        FAISS_DB_PATH = "faiss_db" 
        
        # Configuración para el chunking de texto 
        TEXT_CHUNK_SIZE = 1000 
        TEXT_CHUNK_OVERLAP = 200 
        
        def main(): 
            # Punto de entrada principal 
            documents = load_documents() 
            chunks = split_text(documents) 
            save_to_faiss(chunks) 
        
        
        def load_documents(): 
            # Carga los documentos desde el directorio especificado 
            directory_loader = DirectoryLoader(DOCS_PATH, glob="**/*.md") 
            docs = directory_loader.load() 
            print(f"Cargados {len(docs)} documentos desde {DOCS_PATH}.") 
            return docs 
        
        def split_text(documents, chunk_size=TEXT_CHUNK_SIZE, chunk_overlap=TEXT_CHUNK_OVERLAP): 
            # Divide los documentos en chunks manejables 
            text_splitter = RecursiveCharacterTextSplitter( 
                chunk_size=chunk_size, 
                chunk_overlap=chunk_overlap, 
                length_function=len, 
                add_start_index=True, 
            ) 
            chunks = text_splitter.split_documents(documents) 
        
            # Ejemplos de depuración 
            print(f"Documento dividido en {len(chunks)} chunks.") 
            print(f"Metadata del primer chunk: {chunks[0].metadata}") 
            print(f"Contenido del primer chunk: {chunks[0].page_content}") 
        
            return chunks 
        
        def save_to_faiss(chunks: list[Document]): 
            # Elimina la base de datos existente si existe 
            if os.path.exists(FAISS_DB_PATH): 
                shutil.rmtree(FAISS_DB_PATH) 
        
            embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") 
        
            print(f"Creando índice FAISS con {len(chunks)} chunks...") 
        
            # FAISS crea el índice directamente desde los documentos 
            db = FAISS.from_documents(chunks, embeddings) 
        
            # Guarda el índice localmente 
            db.save_local(FAISS_DB_PATH) 
        
            print(f"Almacenados {len(chunks)} chunks en {FAISS_DB_PATH}.") 
        
         
        if __name__ == "__main__": 
            main() 

        La salida producida por la ejecución de este script sería la siguiente: 

        Cargados 777 documentos desde ./docs/. 
        Documento dividido en 10502 chunks. 
        Metadata del primer chunk: {'source': 'docs\\stripe\\introduction\\api_reference.md', 'start_index': 0} 
        Contenido del primer chunk: API Reference [...] 

        Configurar API Key de HuggingFace

        Para este paso necesitamos dar de alta una cuenta de HuggingFace, es totalmente gratuito. 

        Una vez creada, debemos ir a la sección de tokens para dar de alta uno nuevo, al que asignaremos permisos de lectura. Es muy importante que almacenemos en un lugar seguro la clave generada, ya que no será recuperable después. 

        Crear chatbot con Streamlit

        Este paso ya es conocido, pues utiliza una base parecida a la publicada en el artículo «Crea un chatbot con la API de OpenAI en 40 líneas de código«. En esta ocasión no vamos a emplear la API de OpenAI, sino un modelo Gemma disponible a través de HuggingFace, al que vamos a proporcionarle contexto con RAG. 

        La idea es que tras mandar nuestra consulta, ocurra el RAG en tres pasos

        1. Recuperación del contexto con FAISS (similarity_search_with_score). 
        2. Construcción un prompt estricto con el contexto agregado. 
        3. Llamada al modelo para generar una respuesta. 

            Al ejecutar el chatbot con el comando streamlit run chatbot.py, se abrirá un navegador donde ya podremos interactuar con el modelo utilizando la base de datos vectorial para ampliar su contexto de conocimiento. En este caso, el rol que va a tener el modelo es para soportar la documentación de Stripe y nada más. 

            # chatbot.py 
            
            import os 
            import streamlit as st 
            from langchain_community.vectorstores import FAISS 
            from langchain_huggingface import HuggingFaceEmbeddings, HuggingFaceEndpoint, ChatHuggingFace 
            from langchain_core.messages import HumanMessage, SystemMessage
            
            FAISS_DB_PATH = "faiss_db" 
            
            os.environ["HUGGINGFACEHUB_API_TOKEN"] = ""  # Introduce tu token aquí 
            
            @st.cache_resource 
            def load_vectorstore(): 
                # Carga el vectorstore FAISS desde el disco 
                embeddings = HuggingFaceEmbeddings( 
                    model_name="sentence-transformers/all-MiniLM-L6-v2", 
                    model_kwargs={"device": "cpu"}, 
                    encode_kwargs={"normalize_embeddings": True}, 
                ) 
            
                return FAISS.load_local( 
                    FAISS_DB_PATH, 
                    embeddings, 
                    allow_dangerous_deserialization=True 
                ) 
            
            @st.cache_resource 
            def load_llm(): 
                # Configura el modelo de lenguaje desde HuggingFace Endpoint 
                llm = HuggingFaceEndpoint( 
                    repo_id="google/gemma-3-27b-it", 
                    task="text-generation", 
                    provider="featherless-ai", 
                    max_new_tokens=512,   
                    temperature=0.2, 
                    repetition_penalty=1.3, 
                    timeout=120, 
                ) 
            
                return ChatHuggingFace(llm=llm) 
            
            def get_relevant_context(db, query, k=8): 
                # Recupera los documentos más relevantes para la consulta 
                docs_with_scores = db.similarity_search_with_score(query, k=k) 
            
                context_parts = [] 
                sources = [] 
            
                for doc, score in docs_with_scores: 
                    source = doc.metadata.get('source', 'unknown') 
                    sources.append(f"{source} (score: {score:.3f})") 
                    context_parts.append(f"[Source: {source}]\n{doc.page_content}") 
            
                # Construye el contexto completo 
                context = "\n\n---\n\n".join(context_parts) 
            
                return context 
            
            # Inicialización 
            db = load_vectorstore() 
            llm = load_llm() 
            
            st.title("Chatbot con arquitectura RAG de documentación de la API de Stripe") 
            
            if "messages" not in st.session_state: 
                st.session_state.messages = [] 
            
            # Mostrar historial 
            for message in st.session_state.messages: 
            
                with st.chat_message(message["role"]): 
                    st.markdown(message["content"]) 
            
            # Input del usuario 
            if prompt := st.chat_input("Write a message..."): 
                st.session_state.messages.append({"role": "user", "content": prompt}) 
            
            
                with st.chat_message("user"): 
                    st.markdown(prompt) 
            
                with st.spinner("Thinking..."): 
                    # Recupera el contexto relevante 
                    context = get_relevant_context(db, prompt) 
            
                    # Construye los mensajes para el modelo conversacional 
                    system_prompt = f"""You are a Stripe API documentation assistant. Answer questions using ONLY the documentation provided below. 
             
            
            RULES: 
            1. Use ONLY information from the DOCUMENTATION section 
            2. If asked about required fields, look for parameters marked as "required" or without "optional" 
            3. Be specific and cite the exact parameter names from the documentation 
            4. If information is not in the documentation, say: "This information is not in my current documentation." 
            5. Give ONE direct answer. Do NOT generate follow-up questions or continue the conversation. 
            6. STOP after answering. Do not write anything after your answer. 
            
            DOCUMENTATION: 
            {context} 
            
            END OF DOCUMENTATION. 
            
            Answer the question directly and then STOP.""" 
            
                    messages = [ 
                        SystemMessage(content=system_prompt), 
                        HumanMessage(content=prompt) 
                    ] 
            
                    result = llm.invoke(messages) 
                    response = result.content 
            
                st.session_state.messages.append({"role": "assistant", "content": response}) 
            
                with st.chat_message("assistant"): 
                    st.markdown(response) 

            No todos los modelos de Hugging Face soportan todas las tareas (por ejemplo <code>text-generation</code> vs <code>chat-completion).
            En proyectos reales conviene: 

            Tras haberle hecho una consulta y recibido su respuesta, podemos cotejar en la propia documentación de Stripe que los valores son correctos para la pregunta realizada. 

            Conclusiones 

            De esta forma, en pocos minutos hemos construido un prototipo de arquitectura RAG con LangChain + FAISS + Streamlit que permite potenciar nuestro Chatbot. Tomando como referencia este proyecto, utilizando otros modelos junto a una base de datos vectorial con mayor cantidad y calidad de información sobre un dominio concreto, tenemos a nuestro alcance una poderosa herramienta para facilitar el acceso a la información de forma contextual de dominios específicos. 

            ¿Tienes experiencia o más información relevante sobre la IA? ¡Déjala en nuestras redes sociales! Síguenos en nuestro canal de YouTube para mantenerte al día sobre lo último en el sector digital y tech.

            Salir de la versión móvil