![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Los grandes modelos lingüísticos actuales tienen acceso a una cantidad cada vez mayor de información. Sin embargo, sigue habiendo una gran cantidad de datos privados que estos modelos no aprovechan. Por eso, una de las aplicaciones más populares de los LLM en entornos empresariales es la generación aumentada por recuperación (RAG, por sus siglas en inglés).
Aprenderá a utilizar LangChain, el marco de trabajo muy popular para crear sistemas RAG, para crear un sistema RAG simple. Al final del tutorial, tendremos un chatbot (con una interfaz Streamlit y todo) que utilizará RAG para acceder a algunos datos privados y dar respuestas a preguntas.
Para aclarar qué es RAG, consideremos un ejemplo sencillo.
Chandler, un estudiante universitario de primer año, está considerando faltar a algunas clases, pero quiere asegurarse de no violar la política de asistencia de la universidad. Como sucede con todo en estos días, le hace la pregunta a ChatGPT .
Por supuesto, ChatGPT no puede responderla. El chatbot no es tonto, simplemente no tiene acceso a los documentos universitarios de Chandler. Entonces, Chandler encuentra el documento de políticas él mismo y descubre que es una lectura larga y técnica que no quiere leer. En cambio, le da el documento completo a ChatGPT y vuelve a hacer la pregunta. Esta vez, obtiene su respuesta.
Este es un caso individual de generación aumentada por recuperación. La respuesta (generación) del modelo de lenguaje se aumenta (enriquece) con el contexto recuperado de una fuente que no forma parte de su entrenamiento original.
Una versión escalable de un sistema RAG podría responder cualquier pregunta de un estudiante buscando en los documentos universitarios, encontrando los relevantes y recuperando fragmentos de texto que probablemente contengan la respuesta.
En términos generales, en un sistema RAG, se recupera información de una fuente de datos privada y se la suministra a un modelo de lenguaje, lo que permite que el modelo proporcione una respuesta contextualmente relevante.
Un sistema de estas características, a pesar de parecer sencillo, tendría muchos componentes móviles. Antes de construir uno nosotros mismos, debemos repasar qué son y cómo funcionan juntos.
El primer componente es un documento o una colección de documentos. Según el tipo de sistema RAG que estemos construyendo, los documentos pueden ser archivos de texto, archivos PDF, páginas web (RAG sobre datos no estructurados) o bases de datos gráficas, SQL o NoSQL (RAG sobre datos estructurados). Se utilizan para incorporar varios tipos de datos al sistema.
LangChain implementa cientos de clases llamadas cargadores de documentos para leer datos de diversas fuentes de documentos, como PDF, Slack, Notion, Google Drive, etc.
Cada clase de cargador de documentos es única, pero todas comparten el mismo método .load()
. Por ejemplo, así es como se puede cargar un documento PDF y una página web en LangChain:
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader # pip install langchain-community pdf_loader = PyPDFLoader("framework_docs.pdf") web_loader = WebBaseLoader( "https://python.langchain.com/v0.2/docs/concepts/#document-loaders" ) pdf_docs = pdf_loader.load() web_docs = web_loader.load()
La clase PyPDFLoader maneja archivos PDF utilizando el paquete PyPDF2, mientras que WebBaseLoader extrae el contenido de la página web dada.
pdf_docs
contiene cuatro objetos de documento, uno para cada página:
>>> len(pdf_docs) 4
Mientras que web_docs
contiene solo uno:
>>> print(web_docs[0].page_content[125:300].strip()) You can view the v0.1 docs here.IntegrationsAPI referenceLatestLegacyMorePeopleContributingCookbooks3rd party tutorialsYouTubearXivv0.2v0.2v0.1🦜️🔗LangSmithLangSmith DocsLangCh
Estos objetos de documento se entregan posteriormente a modelos de inserción para comprender el significado semántico detrás de su texto.
Para obtener información específica sobre otros tipos de cargadores de documentos, LangChain ofrece una
Una vez que haya cargado sus documentos, es fundamental dividirlos en fragmentos de texto más pequeños y manejables. Estas son las principales razones:
LangChain ofrece muchos tipos de divisores de texto en su paquete langchain_text_splitters, y difieren según el tipo de documento.
A continuación se explica cómo utilizar RecursiveCharacterTextSplitter
para dividir texto sin formato en función de una lista de separadores y el tamaño del fragmento:
!pip install langchain_text_splitters from langchain_text_splitters import RecursiveCharacterTextSplitter # Example text text = """ RAG systems combine the power of large language models with external knowledge sources. This allows them to provide up-to-date and context-specific information. The process involves several steps including document loading, text splitting, and embedding. """ # Create a text splitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=50, chunk_overlap=10, length_function=len, separators=["\n\n", "\n", " ", ""], ) # Split the text chunks = text_splitter.split_text(text) # Print the chunks for i, chunk in enumerate(chunks): print(f"Chunk {i + 1}: {chunk}")
Producción:
Chunk 1: RAG systems combine the power of large language Chunk 2: language models with external knowledge sources. Chunk 3: This allows them to provide up-to-date and Chunk 4: and context-specific information. Chunk 5: The process involves several steps including Chunk 6: including document loading, text splitting, and Chunk 7: and embedding.
Este separador es versátil y funciona bien para muchos casos de uso. Crea cada fragmento con un recuento de caracteres lo más cercano posible a chunk_size
. Puede cambiar de forma recursiva entre los separadores en los que dividir para mantener el recuento de caracteres.
En el ejemplo anterior, nuestro divisor intenta dividir primero en nuevas líneas, luego en espacios simples y, finalmente, entre cualquier carácter para alcanzar el tamaño de fragmento deseado.
Hay muchos otros separadores dentro del paquete langchain_text_splitters
. A continuación se muestran algunos:
HTMLSectionSplitter
PythonCodeTexSplitter
RecursiveJsonSplitter
y así sucesivamente. Algunos de los divisores crean fragmentos semánticamente significativos mediante el uso de un modelo de transformador subyacente.
El divisor de texto correcto tiene un impacto significativo en el rendimiento de un sistema RAG.
Para obtener información específica sobre cómo utilizar los divisores de texto, consulte el documento correspondiente.
Una vez que los documentos se dividen en texto, es necesario codificarlos en su representación numérica, lo que es un requisito para todos los modelos de cálculo que trabajan con datos de texto.
En el contexto de RAG, esta codificación se denomina incrustación y se realiza mediante modelos de incrustación . Estos crean una representación vectorial de un fragmento de texto que captura su significado semántico. Al presentar el texto de esta manera, puede realizar operaciones matemáticas con él, como buscar en nuestra base de datos de documentos el texto más similar en significado o encontrar una respuesta a una consulta de un usuario.
LangChain es compatible con todos los principales proveedores de modelos de incrustación, como OpenAI, Cohere, HuggingFace, etc. Se implementan como clases Embedding
y proporcionan dos métodos: uno para incrustar documentos y otro para incrustar consultas (solicitudes).
Aquí hay un código de ejemplo que integra los fragmentos de texto que creamos en la sección anterior usando OpenAI:
from langchain_openai import OpenAIEmbeddings # Initialize the OpenAI embeddings embeddings = OpenAIEmbeddings() # Embed the chunks embedded_chunks = embeddings.embed_documents(chunks) # Print the first embedded chunk to see its structure print(f"Shape of the first embedded chunk: {len(embedded_chunks[0])}") print(f"First few values of the first embedded chunk: {embedded_chunks[0][:5]}")
Producción:
Shape of the first embedded chunk: 1536 First few values of the first embedded chunk: [-0.020282309502363205, -0.0015041005099192262, 0.004193042870610952, 0.00229285703971982, 0.007068077567964792]
El resultado anterior muestra que el modelo de inserción está creando un vector de 1536 dimensiones para todos los fragmentos de nuestros documentos.
Para integrar una única consulta, puede utilizar el método embed_query()
:
query = "What is RAG?" query_embedding = embeddings.embed_query(query) print(f"Shape of the query embedding: {len(query_embedding)}") print(f"First few values of the query embedding: {query_embedding[:5]}")
Producción:
Shape of the query embedding: 1536 First few values of the query embedding: [-0.012426204979419708, -0.016619959846138954, 0.007880032062530518, -0.0170428603887558, 0.011404196731746197]
En aplicaciones RAG a gran escala, donde puede haber gigabytes de documentos, terminará con miles de millones de fragmentos de texto y, por lo tanto, vectores. No sirven de nada si no puede almacenarlos de manera confiable.
Por eso, los almacenes de vectores o bases de datos están de moda en la actualidad. Además de almacenar sus incrustaciones, las bases de datos vectoriales se encargan de realizar la búsqueda de vectores por usted. Estas bases de datos están optimizadas para encontrar rápidamente los vectores más similares cuando se les proporciona un vector de consulta, lo que es esencial para recuperar información relevante en los sistemas RAG.
Aquí hay un fragmento de código que integra el contenido de una página web y almacena los vectores en una base de datos de vectores Chroma ( Chroma es una solución de base de datos de vectores de código abierto que se ejecuta completamente en su máquina):
!pip install chromadb langchain_chroma from langchain_community.document_loaders import WebBaseLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # Load the web page loader = WebBaseLoader("https://python.langchain.com/v0.2/docs/tutorials/rag/") docs = loader.load() # Split the documents into chunks text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) chunks = text_splitter.split_documents(docs)
Primero, cargamos la página con WebBaseLoader
y creamos nuestros fragmentos. Luego, podemos pasar directamente los fragmentos al método from_documents
de Chroma
junto con nuestro modelo de incrustación de elección:
from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma db = Chroma.from_documents(chunks, OpenAIEmbeddings())
Todos los objetos de base de datos vectoriales en LangChain exponen un método similarity_search
que acepta una cadena de consulta:
query = "What is indexing in the context of RAG?" docs = db.similarity_search(query) print(docs[1].page_content)
Producción:
If you are interested for RAG over structured data, check out our tutorial on doing question/answering over SQL data.ConceptsA typical RAG application has two main components:Indexing: a pipeline for ingesting data from a source and indexing it. This usually happens offline.Retrieval and generation: the actual RAG chain, which takes the user query at run time and retrieves the relevant data from the index, then passes that to the model.The most common full sequence from raw data to answer looks like:IndexingLoad: First we need to load our data. This is done with Document Loaders.Split: Text splitters break large Documents into smaller chunks. This is useful both for indexing data and for passing it in to a model, since large chunks are harder to search over and won't fit in a model's finite context window.Store: We need somewhere to store and index our splits, so that they can later be searched over. This is often done using a VectorStore and Embeddings model.Retrieval and
El resultado de similarity_search
es una lista de documentos que probablemente contienen la información que solicitamos en la consulta.
Para obtener información específica sobre cómo utilizar las tiendas de vectores, consulte la sección correspondiente.
Aunque todos los almacenes de vectores admiten la recuperación en forma de búsqueda por similitud, LangChain implementa una interfaz Retriever
dedicada que devuelve documentos dada una consulta no estructurada. Un recuperador solo necesita devolver o recuperar documentos, no almacenarlos.
A continuación se muestra cómo puede convertir cualquier almacén de vectores en un recuperador en LangChain:
# Convert the vector store to a retriever chroma_retriever = db.as_retriever() docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 4
Es posible limitar el número de documentos relevantes a los k principales utilizando search_kwargs
:
chroma_retriever = db.as_retriever(search_kwargs={"k": 1}) docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 1
Puede pasar otros parámetros relacionados con la búsqueda a search_kwargs. Obtenga más información sobre el uso de recuperadores en
Ahora que hemos cubierto los componentes clave de un sistema RAG, construiremos uno nosotros mismos. Te guiaré a través de una implementación paso a paso de un chatbot RAG diseñado específicamente para la documentación de código y tutoriales. Te resultará particularmente útil cuando necesites asistencia de codificación de IA para nuevos marcos o nuevas características de marcos existentes que aún no forman parte de la base de conocimientos de los LLM actuales.
Primero, complete su directorio de trabajo con la siguiente estructura de proyecto:
rag-chatbot/ ├── .gitignore ├── requirements.txt ├── README.md ├── app.py ├── src/ │ ├── __init__.py │ ├── document_processor.py │ └── rag_chain.py └── .streamlit/ └── config.toml
Aquí están los comandos:
$ touch .gitignore requirements.txt README.md app.py $ mkdir src .streamlit $ touch src/{.env,__init__.py,document_processor.py,rag_chain.py} $ touch .streamlit/{.env,config.toml}
En este paso, primero crea un nuevo entorno Conda y actívalo:
$ conda create -n rag_tutorial python=3.9 -y $ conda activate rag_tutorial
A continuación, abra el archivo requirements.txt
y pegue las siguientes dependencias:
langchain==0.2.14 langchain_community==0.2.12 langchain_core==0.2.35 langchain_openai==0.1.22 python-dotenv==1.0.1 streamlit==1.37.1 faiss-cpu pypdf
y los instalamos:
$ pip install -r requirements.txt
Además, crea un archivo .gitignore
para ocultar archivos de la indexación de git:
# .gitignore venv/ __pycache__/ .env *.pdf *.png *.jpg *.jpeg *.gif *.svg
A continuación, abra el archivo src/document_processor.py
y pegue los siguientes fragmentos de código.
Las importaciones necesarias:
import logging from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.text_splitter import Language from langchain_community.document_loaders import PyPDFLoader from langchain_community.document_loaders.parsers.pdf import ( extract_from_images_with_rapidocr, ) from langchain.schema import Document
Explicación de las importaciones:
RecursiveCharacterTextSplitter
: divide el texto en fragmentos más pequeños de forma recursiva.Language
: Enumeración para especificar lenguajes de programación en la división de texto.PyPDFLoader
: carga y extrae texto de archivos PDF.extract_from_images_with_rapidocr
: función de OCR para extraer texto de imágenes.Document
: Representa un documento con contenido y metadatos.logging
: proporciona funcionalidad de registro para depuración e información.
Luego, una función para procesar archivos PDF:
def process_pdf(source): loader = PyPDFLoader(source) documents = loader.load() # Filter out scanned pages unscanned_documents = [doc for doc in documents if doc.page_content.strip() != ""] scanned_pages = len(documents) - len(unscanned_documents) if scanned_pages > 0: logging.info(f"Omitted {scanned_pages} scanned page(s) from the PDF.") if not unscanned_documents: raise ValueError( "All pages in the PDF appear to be scanned. Please use a PDF with text content." ) return split_documents(unscanned_documents)
Así es como funciona:
PyPDFLoader
. La función se encarga de los casos en los que un PDF puede contener una combinación de texto y páginas escaneadas, lo que garantiza que solo se procesen las páginas basadas en texto. Esto es fundamental para las tareas de análisis de texto en las que las páginas escaneadas sin OCR serían inutilizables. Definiremos la función split_documents
más adelante.
A continuación, escribimos una función para recuperar información de imágenes (capturas de pantalla de fragmentos de código y/o páginas web):
def process_image(source): # Extract text from image using OCR with open(source, "rb") as image_file: image_bytes = image_file.read() extracted_text = extract_from_images_with_rapidocr([image_bytes]) documents = [Document(page_content=extracted_text, metadata={"source": source})] return split_documents(documents)
Esta función procesa un archivo de imagen extrayendo texto mediante OCR (reconocimiento óptico de caracteres). Lee el archivo de imagen, lo convierte en bytes y luego utiliza la biblioteca RapidOCR para extraer texto de la imagen. Luego, el texto extraído se envuelve en un objeto Document con metadatos que contienen la ruta del archivo de origen. Finalmente, la función divide el documento en fragmentos más pequeños mediante la función split_documents
, que definimos a continuación:
def split_documents(documents): # Split documents into smaller chunks for processing text_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=1000, chunk_overlap=200 ) return text_splitter.split_documents(documents)
La función utiliza la clase RecursiveCharacterTextSplitter con la sintaxis de Python para dividir el texto en fragmentos de 1000 caracteres y una superposición de 200 caracteres.
Nuestra función final combina las funciones del analizador de PDF e imágenes en una:
def process_document(source): # Determine file type and process accordingly if source.lower().endswith(".pdf"): return process_pdf(source) elif source.lower().endswith((".png", ".jpg", ".jpeg")): return process_image(source) else: raise ValueError(f"Unsupported file type: {source}")
Esta función final será utilizada por la interfaz de usuario de Streamlit en el futuro para crear, incrustar y almacenar fragmentos de los documentos proporcionados y pasarlos al componente RAG de nuestro sistema.
Ahora, abra el archivo src/rag_chain.py
y pegue los siguientes fragmentos de código.
Primero, importe los módulos necesarios:
import os from dotenv import load_dotenv from langchain.prompts import PromptTemplate from langchain_community.vectorstores import FAISS from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI, OpenAIEmbeddings # Load the API key from env variables load_dotenv() api_key = os.getenv("OPENAI_API_KEY")
A continuación se presenta una explicación de las importaciones:
• os
: interacciones del sistema operativo • dotenv
: cargar variables de entorno • componentes langchain
:
PromptTemplate
: creación de un mensaje personalizadoFAISS
: un almacén de vectores ligero para documentosStrOutputParser
: Convierte objetos de mensajes LLM en salidas de cadenaRunnablePassthrough
: crea cadenas componiblesChatOpenAI
, OpenAIEmbeddings
: interacciones del modelo OpenAI
A continuación, creamos nuestro mensaje para el sistema RAG:
RAG_PROMPT_TEMPLATE = """ You are a helpful coding assistant that can answer questions about the provided context. The context is usually a PDF document or an image (screenshot) of a code file. Augment your answers with code snippets from the context if necessary. If you don't know the answer, say you don't know. Context: {context} Question: {question} """ PROMPT = PromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
El mensaje del sistema RAG es uno de los factores críticos para su éxito. Nuestra versión es sencilla, pero funcionará la mayoría de las veces. En la práctica, dedicaría mucho tiempo a iterar y mejorar el mensaje.
Si te fijas, estamos usando una clase PromptTemplate
para construir el mensaje. Esta construcción nos permite incorporar dinámicamente el contexto recuperado de los documentos y la consulta del usuario en un mensaje final.
Hablando de documentos, necesitamos una función para formatearlos antes de que se pasen como contexto al indicador del sistema:
def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs)
Es una función simple que concatena el contenido de las páginas de los documentos recuperados.
Finalmente, creamos una función que desarrollará nuestra cadena RAG:
def create_rag_chain(chunks): embeddings = OpenAIEmbeddings(api_key=api_key) doc_search = FAISS.from_documents(chunks, embeddings) retriever = doc_search.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | PROMPT | llm | StrOutputParser() ) return rag_chain
La función acepta fragmentos de documentos, que serán proporcionados por la función process_document
dentro del script document_processor.py
.
La función comienza definiendo el modelo de incrustación y almacenando los documentos en un almacén de vectores FAISS. Luego, se convierte en una interfaz de recuperación con búsqueda por similitud que devuelve los cinco documentos principales que coinciden con la consulta del usuario.
Para el modelo de idioma, utilizaremos gpt-4o-mini
, pero puedes utilizar otros modelos como GPT-4o según tu presupuesto y necesidades.
Luego, juntaremos todos estos componentes usando el lenguaje de expresión LangChain (LCEL). El primer componente de la cadena es un diccionario con context
y question
como claves. Los valores de estas claves son proporcionados por el recuperador formateado por nuestra función de formato y RunnablePassthrough()
, respectivamente. La última clase actúa como un marcador de posición para la consulta del usuario.
Luego, el diccionario se pasa al indicador del sistema; el indicador se envía al LLM, que genera una clase de mensaje de salida. La clase de mensaje se entrega a un analizador de salida de cadena que devuelve una respuesta de texto sin formato.
En esta sección, crearemos la siguiente interfaz de usuario para nuestra aplicación:
Es una interfaz limpia y minimalista con dos campos de entrada: uno para el documento y el otro para hacer preguntas sobre el documento. En la barra lateral izquierda, se le pide al usuario que ingrese su clave API.
Para crear la interfaz, abra el script app.py
en el nivel más alto de su directorio de trabajo y pegue el siguiente código:
import streamlit as st import os from dotenv import load_dotenv from src.document_processor import process_document from src.rag_chain import create_rag_chain # Load environment variables load_dotenv() st.set_page_config(page_title="RAG Chatbot", page_icon="🤖") st.title("RAG Chatbot") # Initialize session state if "rag_chain" not in st.session_state: st.session_state.rag_chain = None # Sidebar for API key input with st.sidebar: api_key = st.text_input("Enter your OpenAI API Key", type="password") if api_key: os.environ["OPENAI_API_KEY"] = api_key # File uploader uploaded_file = st.file_uploader("Choose a file", type=["pdf", "png", "jpg", "jpeg"]) if uploaded_file is not None: if st.button("Process File"): if api_key: with st.spinner("Processing file..."): # Save the uploaded file temporarily with open(uploaded_file.name, "wb") as f: f.write(uploaded_file.getbuffer()) try: # Process the document chunks = process_document(uploaded_file.name) # Create RAG chain st.session_state.rag_chain = create_rag_chain(chunks) st.success("File processed successfully!") except ValueError as e: st.error(str(e)) finally: # Remove the temporary file os.remove(uploaded_file.name) else: st.error("Please provide your OpenAI API key.") # Query input query = st.text_input("Ask a question about the uploaded document") if st.button("Ask"): if st.session_state.rag_chain and query: with st.spinner("Generating answer..."): result = st.session_state.rag_chain.invoke(query) st.subheader("Answer:") st.write(result) elif not st.session_state.rag_chain: st.error("Please upload and process a file first.") else: st.error("Please enter a question.")
A pesar de tener sólo 65 líneas, implementa la siguiente funcionalidad:
Solo queda un paso: implementar nuestra aplicación Streamlit. Hay muchas opciones, pero la más sencilla es usar Streamlit Cloud, que es gratis y fácil de configurar.
Primero, abra el script .streamlit/config.toml
y pegue las siguientes configuraciones:
[theme] primaryColor = "#F63366" backgroundColor = "#FFFFFF" secondaryBackgroundColor = "#F0F2F6" textColor = "#262730" font = "sans serif"
Estos son algunos ajustes de tema que surgen de las preferencias personales. Luego, escribe el archivo README.md (puedes copiar su contenido desde este archivo alojado en GitHub ).
Por último, ve a GitHub.com y crea un nuevo repositorio. Copia el enlace y regresa a tu directorio de trabajo:
$ git init $ git add . $ git commit -m "Initial commit" $ git remote add origin https://github.com/YourUsername/YourRepo.git $ git push --set-upstream origin master
Los comandos anteriores inicializan Git, crean una confirmación inicial y envían todo al repositorio (no olvides reemplazar el enlace del repositorio con el tuyo).
Ahora, debes registrarte para obtener una cuenta gratuita en Streamlit Cloud . Conecta tu cuenta de GitHub y selecciona el repositorio que contiene tu aplicación.
A continuación, configure los ajustes de la aplicación:
app.py
OPENAI_API_KEY
) en la configuración de la aplicación
¡Por último, haga clic en "Implementar"!
La aplicación debería estar operativa en unos minutos. La aplicación que he creado para este tutorial se puede encontrar en este enlace . ¡Pruébala!
Este tutorial analiza la potente combinación de Retrieval-Augmented Generation (RAG) y Streamlit que forma un sistema interactivo de preguntas y respuestas basado en documentos. Guía al lector a través de todo el proceso, desde la configuración de un entorno y el procesamiento de documentos hasta la creación de una cadena RAG y la implementación de una aplicación web sencilla.
Los puntos importantes incluyen:
Este proyecto constituye la base para aplicaciones más avanzadas. Puede ampliarse de maneras significativas, como la incorporación de múltiples tipos de documentos, una mayor precisión en la recuperación y funciones como el resumen de documentos. Y, sin embargo, lo que realmente sirve es como demostración del potencial de estas tecnologías, tanto de forma individual como combinadas.