Roger Pou Lopez
Data Scientist
Un RAG, acrónimo de «Retrieval Augmented Generation», representa una estrategia innovadora dentro del procesamiento de lenguaje natural. Se integra con los Grandes Modelos de Lenguaje (LLM, por sus siglas en inglés), tales como los que usa ChatGPT internamente (GPT-3.5-turbo o GPT-4), con el objetivo de mejorar la calidad de la respuesta y reducir ciertos comportamientos no deseados, como las alucinaciones.
Estos sistemas combinan los conceptos de vectorización y búsqueda semántica, junto con los LLMs para retroalimentar su conocimiento con información externa que no se incluyó durante su fase de entrenamiento y que, por lo tanto, desconocen.
Existen ciertos puntos a favor de utilizar RAGs:
- Permiten reducir el nivel de alucinaciones que presentan los modelos. A menudo, los LLM responden con información incorrecta (o inventada), aunque semánticamente su respuesta tenga sentido. A esto se le denomina alucinación. Uno de los objetivos principales del RAG es intentar reducir al máximo este tipo de situaciones, especialmente cuando se pregunta por cosas concretas. Esto es de alta utilidad si se quiere utilizar un LLM de forma productiva.
- Utilizando un RAG, ya no es necesario reentrenar el LLM. Este proceso puede llegar a ser costoso económicamente, dado que necesitaría GPUs para su entrenamiento, además de la complejidad que puede conllevar ese entrenamiento.
- Son sistemas económicos, rápidos (utilizan información indexada) y además, no dependen del modelo que se está utilizando (en cualquier momento podemos cambiarlo por de GPT-3.5 a Llama-2-70B).
En contra:
- Se va a necesitar ayuda de código, matemáticas y no va a ser tan sencillo como lanzar un simple prompt modificado.
- En la evaluación de los RAGs (veremos más adelante en el artículo) vamos a necesitar modelos potentes como GPT-4.
Ejemplo de caso de uso
Existen varios ejemplos donde los RAGs están siendo utilizados. El ejemplo más típico es su uso con chatbots para consultar información muy específica del negocio.
- En call-centers, los agentes están empezando a utilizar un chatbot con información sobre tarifas para poder responder de forma rápida y eficaz a las llamadas que reciben.
- En chatbots, como asistentes de venta donde están ganando popularidad. Aquí, los RAGs ayudan a responder a comparativas entre productos o cuando se consulta de manera específica sobre un servicio, haciendo recomendaciones de productos similares.
Componentes de un RAG
Vamos a hablar en detalle sobre los distintos componentes que conforman un RAG para poder tener una idea aproximada, y luego vamos a hablar de cómo interaccionan entre sí estos elementos.
Base de conocimiento
Este elemento es un concepto un poco abierto pero también lógico: se refiere al conocimiento objetivo del cual sabemos que el LLM no es consciente y que tiene un alto riesgo de alucinación. Este conocimiento, en formato de texto, puede estar en muchos formatos: PDF, Excel, Word, etc… Los RAGs avanzados son capaces también de detectar conocimientos en imágenes y tablas.
En general, todo contenido va a ser en formato de texto y va a necesitar ser indexado. Como los textos humanos son muchas veces desestructurados, se recurre a la subdivisión de los textos con estrategias llamadas chunking.
Modelo de Embeddings
Un embedding es la representación vectorial generada por una red neuronal entrenada sobre un cuerpo de datos (texto, imágenes, sonido, etc.) que es capaz de resumir la información de un objeto de ese mismo tipo hacia un vector dentro de un espacio vectorial concreto.
Por ejemplo, en el caso de un texto que se refiere a “Me gustan los patitos de goma azules” y otro que dice “Adoro los patitos de goma amarillos”, al ser convertidos en vectores, estos estarán más próximos en distancia entre sí que un texto que se refiere a “Los automóviles del futuro son los coches eléctricos”.
Este componente es el que, posteriormente, nos permitirá indexar de forma correcta los distintos chunks de información de texto.
Base de datos vectorial
Es el lugar donde vamos a guardar y indexar la información vectorial de los chunks mediante los embeddings. Se trata de un componente muy importante y complejo donde, afortunadamente, ya existen varias soluciones open source muy válidas para poder desplegarlo de forma «fácil», como Milvus o Chroma.
LLM
Es lógico, puesto que el RAG es una solución que nos permite ayudar a responder de forma más veraz a estos LLMs. No tenemos por qué restringirnos a modelos muy grandes y eficientes (pero no económicos como GPT-4), sino que pueden ser modelos más pequeños y más «sencillos» en cuanto a la calidad de respuestas y número de parámetros.
A continuación podemos ver una imagen representativa del proceso de carga de información en la base de datos vectoriales.
Funcionamiento a Alto Nivel
Ahora que tenemos un poco más claras las piezas del rompecabezas, surgen algunas dudas:
- ¿Cómo interactúan estos componentes entre sí?
- ¿Por qué hace falta una base de datos vectorial?
Vamos a intentar esclarecer un poco el asunto.
La idea intuitiva del funcionamiento de un RAG es la siguiente:
- El usuario hace una pregunta. Transformamos la pregunta a un vector con el mismo sistema de embedding que hemos utilizado para guardar los chunks. Esto nos va a permitir comparar nuestra pregunta con toda la información que tenemos indexada en nuestra base de datos vectorial.
- Calculamos las distancias entre la pregunta y todos los vectores que tenemos en la base de datos. Seleccionamos, con una estrategia, algunos de los chunks y añadimos todas esas piezas de información dentro del prompt como contexto. La estrategia más sencilla es basarse en seleccionar un número (K) de vectores más próximos a la pregunta.
- Se lo pasamos al LLM para que genere la respuesta en base a los contextos. Es decir, el prompt contiene instrucciones + pregunta + contexto devuelto por el sistema de Retrieval. Por este motivo, la parte de «Augmentation» en las siglas del RAG, dado que estamos haciendo prompt augmentation.
- El LLM nos ha generado una respuesta en base a la pregunta que hacemos y el contexto que le hemos pasado. Esta será la respuesta que el usuario va a visualizar.
Es por eso que necesitamos un embedding y la base de datos vectorial. Ahí está un poco el truco. Si eres capaz de encontrar información muy parecida a tu pregunta en tu base de datos vectorial, entonces puedes detectar contenido que puede ser de utilidad para tu pregunta. Pero para todo ello, necesitamos un elemento que nos permita poder comparar textos de forma objetiva y esa información no podemos tenerla guardada de forma desestructurada si necesitamos hacer preguntas de forma frecuente.
También, que al final todo esto termina en el prompt, que nos permite que sea un flujo independiente del modelo de LLM que vayamos a usar.
Evaluación de los RAG
De igual manera que los modelos de estadística o ciencias de datos más clásicos, tenemos una necesidad de cuantificar cómo está funcionando un modelo antes de utilizarlo de manera productiva.
La estrategia más básica (por ejemplo, para medir la efectividad de una regresión lineal) consiste en dividir el conjunto de datos en distintas partes como train y test (80 y 20% respectivamente), entrenando el modelo en train y evaluando en test con métricas como el root-mean-square error, dado que el conjunto de test son datos que no ha visto el modelo. Sin embargo, un RAG no consta de entrenamiento sino de un sistema compuesto de distintos elementos donde una de sus partes es usar un modelo de generación de texto.
Más allá de esto, aquí ya no tenemos datos cuantitativos (es decir, números) y la naturaleza del dato consiste en texto generado que puede variar en función de la pregunta que le hagamos, el contexto detectado por el sistema de Retrieval y incluso el comportamiento no determinista que tienen los modelos de redes neuronales.
Una estrategia básica que podemos pensar es en ir analizando a mano qué tan bueno está funcionando nuestro sistema, en base a hacer preguntas y ver cómo están funcionando las respuestas y los contextos devueltos. Pero este enfoque se vuelve impracticable cuando queremos evaluar todas las posibilidades de preguntas en documentos muy grandes y de forma recurrente.
¿Entonces, cómo podemos hacer esta evaluación?
El truco: Aprovechando los propios LLMs. Con ellos podemos construir un conjunto de datos sintético con el que se haya simulado la misma acción de hacer preguntas a nuestro sistema, tal como si un humano lo hubiera hecho. Incluso le podemos añadir un nivel de fineza mayor: utilizar un modelo más inteligente que el anterior y que funcione como un crítico, que nos indique si lo que está sucediendo tiene sentido o no.
Ejemplo de conjunto de datos de evaluación
Aquí lo que tenemos son muestras de Pregunta-Respuesta de cómo hubiera funcionado nuestro sistema de RAG simulando las preguntas que le podría hacer un humano en comparativa al modelo que estamos evaluando. Para hacer esto, necesitamos dos modelos: el LLM que utilizaríamos en nuestro RAG, por ejemplo, GPT-3.5-turbo (Answer) y otro modelo con mejor funcionamiento para generar una “verdad” (Ground Truth), como GPT-4.
Es decir, en otras palabras, el ChatGPT 3.5 sería el sistema generador de preguntas y el ChatGPT 4 sería como la parte crítica.
Una vez generado nuestro conjunto de datos de evaluación, lo que nos queda es cuantificar numéricamente con algún tipo de métrica.
Métricas de Evaluación
La evaluación de las respuestas es algo nuevo pero ya existen proyectos de código abierto que logran cuantificar de forma efectiva la calidad de los RAGs. Estos sistemas de evaluación permiten medir la parte de «Retrieval» y «Generation» por separado.
Faitfulness Score
Mide la veracidad de nuestras respuestas dado un contexto. Es decir, con qué porcentaje lo que se pregunta es verdad en función del contexto conseguido a través de nuestro sistema. Esta métrica sirve para intentar controlar las alucinaciones que pueden tener los LLMs. Un valor muy bajo en esta métrica implicaría que el modelo se está inventando cosas, aunque se le dé un contexto. Por lo tanto, es una métrica que debe estar lo más cercano a uno.
Answer Relevancy Score
Cuantifica la relevancia de la respuesta en base a la pregunta que se le hace a nuestro sistema. Si la respuesta no es relevante a lo que le preguntamos, no nos está respondiendo adecuadamente. Por lo tanto cuanto más alta sea esta métrica, mejor.
Context Precision Score
Evalua si todos los elementos de nuestros ground-truth ítems dentro de los contextos, son rankeados de forma prioritaria o no.
Context Recall Score
Cuantifica si el contexto devuelto se alinea con la respuesta anotada. En otras palabras, cómo de relevante es el contexto respecto a la pregunta que hacemos. Una valor bajo indicaría que el contexto devuelto es poco relevante y no nos ayuda a responder la pregunta.
El cómo todas estas métricas se están evaluando es un poco más complejo pero podemos encontrar ejemplos bien explicados en la documentación de RAGAS.
Ejemplo práctico utilizando LangChain, OpenAI y ChromaDB
Vamos a utilizar el framework de LangChain que nos permite construir un RAG de forma muy fácil.
El dataset que vamos a utilizar es un ensayo de Paul Graham, un dataset típico y pequeño en cuanto a tamaño.
La base de datos vectorial que vamos a utilizar va a ser Chroma, open-source y con plena integración con LangChain. El uso de esta va a ser completamente transparente, utilizando los parámetros por defecto.
NOTA: Cada llamada a un modelo asociado, tiene un coste monetario y conviene revisar el pricing de OpenAI. Nosotros vamos a trabajar con un dataset pequeño de 10 preguntas pero si se escalase, el coste podría incrementarse.
import os from dotenv import load_dotenv load_dotenv() # Configurar OpenAI API Key from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma from langchain.prompts import ChatPromptTemplate embeddings = OpenAIEmbeddings( model="text-embedding-ada-002" ) text_splitter = RecursiveCharacterTextSplitter( chunk_size = 700, chunk_overlap = 50 ) loader = TextLoader('paul_graham/paul_graham_essay.txt') text = loader.load() documents = text_splitter.split_documents(text) print(f'Número de chunks generados gracias al documento: {len(documents)}') vector_store = Chroma.from_documents(documents, embeddings) retriever = vector_store.as_retriever()
Número de chunks generados gracias al documento: 158
Dado que el texto del libro está en inglés, debemos de hacer nuestro template de prompt esté en inglés.
from langchain.prompts import ChatPromptTemplate template = """Answer the question based only on the following context. If you cannot answer the question with the context, please respond with 'I don't know': Context: {context} Question: {question} """ prompt = ChatPromptTemplate.from_template(template)
Ahora vamos a definir nuestro RAG mediante LCEL. El modelo a utilizar que responderá a las preguntas de nuestro RAG va a ser GPT-3.5-turbo. Importante es que el parámetro de la temperatura esté a 0 para que el modelo no sea creativo.
from operator import itemgetter from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough primary_qa_llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0) retrieval_augmented_qa_chain = ( {"context": itemgetter("question") | retriever, "question": itemgetter("question")} | RunnablePassthrough.assign(context=itemgetter("context")) | {"response": prompt | primary_qa_llm, "context": itemgetter("context")} )
.. y ahora es posible hacerle ya preguntas a nuestro sistema RAG.
question = "What was doing the author before collegue? " result = retrieval_augmented_qa_chain.invoke({"question" : question}) print(f' Answer the question based: {result["response"].content}')
Answer the question based: The author was working on writing and programming before college.
También podemos investigar cuales han sido los contextos devueltos por nuestro retriever. Como hemos mencionado, la estrategia de Retrieval es la por defecto y nos devolverá los top 4 contextos para responder a una pregunta.
display(retriever.get_relevant_documents(question))
display(retriever.get_relevant_documents(question)) [Document(page_content="What I Worked On\n\nFebruary 2021\n\nBefore college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I imagined made them deep.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}), Document(page_content="Over the next several years I wrote lots of essays about all kinds of different topics. O'Reilly reprinted a collection of them as a book, called Hackers & Painters after one of the essays in it. I also worked on spam filters, and did some more painting. I used to have dinners for a group of friends every thursday night, which taught me how to cook for groups. And I bought another building in Cambridge, a former candy factory (and later, twas said, porn studio), to use as an office.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}), Document(page_content="In the print era, the channel for publishing essays had been vanishingly small. Except for a few officially anointed thinkers who went to the right parties in New York, the only people allowed to publish essays were specialists writing about their specialties. There were so many essays that had never been written, because there had been no way to publish them. Now they could be, and I was going to write them. [12]\n\nI've worked on several different things, but to the extent there was a turning point where I figured out what to work on, it was when I started publishing essays online. From then on I knew that whatever else I did, I'd always write essays too.", metadata={'source': 'paul_graham/paul_graham_essay.txt'}), Document(page_content="Wow, I thought, there's an audience. If I write something and put it on the web, anyone can read it. That may seem obvious now, but it was surprising then. In the print era there was a narrow channel to readers, guarded by fierce monsters known as editors. The only way to get an audience for anything you wrote was to get it published as a book, or in a newspaper or magazine. Now anyone could publish anything.", metadata={'source': 'paul_graham/paul_graham_essay.txt'})]
Evaluando nuestro RAG
Ahora que ya tenemos nuestro RAG montado gracias a LangChain, nos falta evaluarlo.
Parece que tanto LangChain como LlamaIndex empiezan a tener maneras de evaluar de forma fácil los RAGs sin moverse del framework. Sin embargo, por ahora, la mejor opción es utilizar RAGAS, una librería que ya habíamos mencionado y está específicamente diseñada con ese propósito. Internamente, va a utilizar GPT-4 como modelo crítico, tal y como hemos mencionado anteriormente.
from ragas.testset.generator import TestsetGenerator from ragas.testset.evolutions import simple, reasoning, multi_context text = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size = 1000, chunk_overlap = 200 ) documents = text_splitter.split_documents(text) generator = TestsetGenerator.with_openai() testset = generator.generate_with_langchain_docs( documents, test_size=10, distributions={simple: 0.5, reasoning: 0.25, multi_context: 0.25} ) test_df = testset.to_pandas() display(test_df)
question | contexts | ground_truth | evolution_type | episode_done | |
---|---|---|---|---|---|
0 | What is the batch model and how does it relate… | [The most distinctive thing about YC is the ba… | The batch model is a method used by YC (Y Comb… | simple | True |
1 | How did the use of Scheme in the new version o… | [In the summer of 2006, Robert and I started w… | The use of Scheme in the new version of Arc co… | simple | True |
2 | How did learning Lisp expand the author’s conc… | [There weren’t any classes in AI at Cornell th… | Learning Lisp expanded the author’s concept of… | simple | True |
3 | How did Moore’s Law contribute to the downfall… | [[4] You can of course paint people like still… | Moore’s Law contributed to the downfall of com… | simple | True |
4 | Why did the creators of Viaweb choose to make … | [There were a lot of startups making ecommerce… | The creators of Viaweb chose to make their eco… | simple | True |
5 | During the author’s first year of grad school … | [I applied to 3 grad schools: MIT and Yale, wh… | reasoning | True | |
6 | What suggestion from a grad student led to the… | [McCarthy didn’t realize this Lisp could even … | reasoning | True | |
7 | What makes paintings more realistic than photos? | [life interesting is that it’s been through a … | By subtly emphasizing visual cues, paintings c… | multi_context | True |
8 | «What led Jessica to compile a book of intervi… | [Jessica was in charge of marketing at a Bosto… | Jessica’s realization of the differences betwe… | multi_context | True |
9 | Why did the founders of Viaweb set their price… | [There were a lot of startups making ecommerce… | The founders of Viaweb set their prices low fo… | simple | True |
test_questions = test_df["question"].values.tolist() test_groundtruths = test_df["ground_truth"].values.tolist() answers = [] contexts = [] for question in test_questions: response = retrieval_augmented_qa_chain.invoke({"question" : question}) answers.append(response["response"].content) contexts.append([context.page_content for context in response["context"]]) from datasets import Dataset # HuggingFace response_dataset = Dataset.from_dict({ "question" : test_questions, "answer" : answers, "contexts" : contexts, "ground_truth" : test_groundtruths }) from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_recall, context_precision, ) metrics = [ faithfulness, answer_relevancy, context_recall, context_precision, ] results = evaluate(response_dataset, metrics) results_df = results.to_pandas().dropna()
question | answer | contexts | ground_truth | faithfulness | answer_relevancy | context_recall | context_precision | |
---|---|---|---|---|---|---|---|---|
0 | What is the batch model and how does it relate… | The batch model is a system where YC funds a g… | [The most distinctive thing about YC is the ba… | The batch model is a method used by YC (Y Comb… | 0.750000 | 0.913156 | 1.0 | 1.000000 |
1 | How did the use of Scheme in the new version o… | The use of Scheme in the new version of Arc co… | [In the summer of 2006, Robert and I started w… | The use of Scheme in the new version of Arc co… | 1.000000 | 0.910643 | 1.0 | 1.000000 |
2 | How did learning Lisp expand the author’s conc… | Learning Lisp expanded the author’s concept of… | [So I looked around to see what I could salvag… | Learning Lisp expanded the author’s concept of… | 1.000000 | 0.924637 | 1.0 | 1.000000 |
3 | How did Moore’s Law contribute to the downfall… | Moore’s Law contributed to the downfall of com… | [[5] Interleaf was one of many companies that … | Moore’s Law contributed to the downfall of com… | 1.000000 | 0.940682 | 1.0 | 1.000000 |
4 | Why did the creators of Viaweb choose to make … | The creators of Viaweb chose to make their eco… | [There were a lot of startups making ecommerce… | The creators of Viaweb chose to make their eco… | 0.666667 | 0.960447 | 1.0 | 0.833333 |
5 | What suggestion from a grad student led to the… | The suggestion from grad student Steve Russell… | [McCarthy didn’t realize this Lisp could even … | The suggestion from a grad student, Steve Russ… | 1.000000 | 0.931730 | 1.0 | 0.916667 |
6 | What makes paintings more realistic than photos? | By subtly emphasizing visual cues such as the … | [copy pixel by pixel from what you’re seeing. … | By subtly emphasizing visual cues, paintings c… | 1.000000 | 0.963414 | 1.0 | 1.000000 |
7 | «What led Jessica to compile a book of intervi… | Jessica was surprised by how different reality… | [Jessica was in charge of marketing at a Bosto… | Jessica’s realization of the differences betwe… | 1.000000 | 0.954422 | 1.0 | 1.000000 |
8 | Why did the founders of Viaweb set their price… | The founders of Viaweb set their prices low fo… | [There were a lot of startups making ecommerce… | The founders of Viaweb set their prices low fo… | 1.000000 | 1.000000 | 1.0 | 1.000000 |
Visualizamos las distribuciones estadísticas que salen:
results_df.plot.hist(subplots=True,bins=20)
Podemos observar que el sistema no es perfecto aunque hemos generado solamente 10 preguntas (haría falta generar muchas más) y también se puede observar que en una de ellas, la pipeline del RAG ha fallado en crear el ground truth.
Aun así podríamos sacar algunas conclusiones:
- algunas veces no es capaz de dar respuestas muy veraces (faithfulness)
- la relevancia de la respuesta es variable pero consistentmente buena (answer_relevancy)
- el context recall es perfecto pero el context precision ya no tanto
Ahora aquí nos podemos plantear probar con distintos elementos:
- cambiar el embedding utilizado por uno que podemos encontrar en HuggingFace MTEB Leaderboard.
- mejorar el sistema de retrieval con estrategias diferentes a la por defecto
- evaluar con otros LLMs
Con estas posibilidades, es viable analizar cada una de esas estrategias anteriores y escoger la que mejor se ajuste a nuestros datos o criterios monetarios.
Conclusiones
En este artículos hemos visto en qué consiste un RAG y cómo podemos evaluar un workflow completo. Todo esta materia está en auge ahora mismo dado que es una de las alternativas más eficaces y económicas para evitar el fine-tuning de los LLMs.
Es posible que se encuentren nuevas métricas, nuevos frameworks, que hagan la evaluación de estos más sencilla y eficaz; pero en los próximos artículos no solo vamos a poder ver su evolución, sino también cómo llevar a production una arquitectura basada en RAGs.