He estado leyendo los posts y artículos de Fareed y los encuentro increíblemente detallados y bien escritos. En este artículo publicado originalmente en su blog, puedes encontrar su repositorio para este post y enlaces a su otro contenido.
Construyendo todo el Ecosistema RAG y Optimizando Cada Componente
Enrutamiento, Indexación, Recuperación, Transformación y más.
Lee esta historia de forma gratuita: enlace
La mayoría de los equipos, al crear un sistema RAG listo para producción en sus datos, pasan por muchas rondas de experimentación y dependen de varios componentes diferentes, cada uno requiriendo su propia configuración, ajuste y manejo cuidadoso. Estos componentes incluyen…
-
Transformaciones de Consultas: Reescribir preguntas de usuarios para que sean más efectivas para la recuperación.
-
Enrutamiento Inteligente: Dirigir una consulta a la fuente de datos correcta o a una herramienta especializada.
-
Indexación: Crear una base de conocimiento multicapa.
-
Recuperación y Re-ranking: Filtrar ruido y priorizar el contexto más relevante.
-
Flujos Agénticos Autocorrectivos: Construir sistemas que puedan calificar y mejorar su propio trabajo.
-
Evaluación de Extremo a Extremo: Medir objetivamente el rendimiento de todo el pipeline.
y mucho más…
Aprenderemos y codificaremos cada parte del ecosistema RAG junto con visuales para una comprensión más fácil, comenzando desde lo básico hasta técnicas avanzadas.
Todo el código (Teoría + Notebook) está disponible en mi Repositorio de GitHub:
GitHub - FareedKhan-dev/rag-ecosystem: Entiende y codifica cada componente importante de RAG…
Mi tabla de contenidos está dividida en varias secciones. Echa un vistazo.
Entendiendo el Sistema RAG Básico
Transformaciones de Consultas Avanzadas
Enrutamiento y Construcción de Consultas
Estrategias de Indexación
Recuperación y Generación
Evaluación Manual de RAG
Evaluación con Frameworks
Resumiendo Todo
Entendiendo el Sistema RAG Básico
Antes de mirar los conceptos básicos de RAG, necesitamos establecer las variables de entorno para el rastreo y otras tareas, como el proveedor de API de LLMs que usaremos.
import os
# Establecer el endpoint de la API de LangChain y la clave de API
os.environ['LANGCHAIN_ENDPOINT'] = 'https://api.smith.langchain.com'
os.environ['LANGCHAIN_API_KEY'] = <your-api-key>
```Reemplaza con tu clave de API de LangChain
# Establece la clave de API de OpenAI
os.environ['OPENAI_API_KEY'] =`<your-api-key>` # Reemplaza con tu clave de API de OpenAI
Puedes obtener tu clave de API de LangSmith desde su documentación oficial para rastrear nuestro producto RAG a lo largo de este blog. Para el LLM, usaremos la API de OpenAI, pero como ya sabrás, LangChain también admite una variedad de proveedores de LLM.
El pipeline RAG principal es la base de cualquier sistema avanzado, y entender sus componentes es importante. Por lo tanto, antes de entrar en los detalles de componentes avanzados, primero necesitamos entender la lógica principal de cómo funciona un sistema RAG, pero puedes omitir esta sección si ya sabes cómo funciona un sistema RAG.
Este RAG más simple se puede dividir en tres componentes:
-
Indexación: Organizar y almacenar datos en un formato estructurado para permitir búsquedas eficientes.
-
Recuperación: Buscar y obtener datos relevantes basados en una consulta o entrada.
-
Generación: Crear una respuesta o salida final utilizando los datos recuperados.
Construyamos este pipeline simple desde cero para ver cómo funciona cada pieza.
Fase de Indexación
Antes de que nuestro sistema RAG pueda responder preguntas, necesita conocimiento del cual extraer. Para esto, usaremos un WebBaseLoader para extraer contenido directamente desde la excelente publicación de blog de Lilian Weng sobre agentes impulsados por LLM.
import bs4
from langchain_community.document_loaders import WebBaseLoader
# Inicializa un cargador de documentos web con instrucciones de análisis específicas
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",), # URL de la publicación de blog a cargar
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header") # Solo analiza las clases HTML especificadas
)
),
)
# Carga el contenido filtrado de la página web en documentos
docs = loader.load()
El argumento bs_kwargs nos ayuda a dirigirnos solo a las etiquetas HTML relevantes (post-content, post-title, etc.), limpiando nuestros datos desde el principio.
Ahora que tenemos el documento, enfrentamos nuestro primer desafío. Alimentar un documento masivo directamente en un LLM es ineficiente e a menudo imposible debido a los límites de la ventana de contexto.
Por eso chunking es un paso crítico. Necesitamos dividir el documento en piezas más pequeñas y semánticamente significativas.
El RecursiveCharacterTextSplitter es la herramienta recomendada para este trabajo porque inteligentemente intenta mantener párrafos y oraciones intactos.
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Crea un divisor de texto para dividir el texto en fragmentos de 1000 caracteres con superposición de 200 caracteres
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
# Divide los documentos cargados en fragmentos más pequeños
splits = text_splitter.split_documents(docs)
Con chunk_size=1000, estamos creando fragmentos de 1000 caracteres, y chunk_overlap=200 asegura que haya cierta continuidad entre ellos, lo que ayuda a preservar el contexto.
Nuestro texto ahora está dividido, pero sigue siendo solo texto. Para realizar búsquedas de similitud, necesitamos convertir estos fragmentos en representaciones numéricas llamadas embeddings. Luego almacenaremos estos embeddings en un vector store, que es una base de datos especializada diseñada para búsquedas eficientes de vectores.
El vector store Chroma y OpenAIEmbeddings hacen esto increíblemente simple. La siguiente línea maneja tanto la incrustación como la indexación en una sola pasada.
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Incrusta los fragmentos de texto y almacénalos en un vector store Chroma para búsqueda de similitud
vectorstore = Chroma.from_documents(
documents=splits,
embedding=OpenAIEmbeddings() # Usa el modelo de incrustación de OpenAI para convertir texto en vectores
)
Con nuestro conocimiento indexado, ahora estamos listos para comenzar a hacer preguntas.
Recuperación
El vector store es nuestra biblioteca, y el retriever es nuestro bibliotecario inteligente. Toma una consulta del usuario, la incrusta y luego obtiene los fragmentos más semánticamente similares del vector store.
Crear un retriever desde nuestro vectorstore es una sola línea.
# Crea un retriever desde el vector store
retriever = vectorstore.as_retriever()
Probémoslo. Haremos una pregunta y veremos qué encuentra nuestro retriever.
# Recupera documentos relevantes para una consulta
docs = retriever.get_relevant_documents("¿Qué es la Descomposición de Tareas?")
# Imprime el contenido del primer documento recuperado
print(docs[0].page_content)
#### SALIDA ####
La descomposición de tareas se puede hacer (1) por LLM con prompting simple ...
Tree of Thoughts (Yao et al. 2023) extiende CoT explorando múltiples ...
Como puedes ver, el retriever extrajo exitosamente el fragmento más relevante de la publicación de blog que discute directamente “Descomposición de tareas”. Este contexto es exactamente lo que el LLM necesita para formar una respuesta precisa.
Generación
Tenemos nuestro contexto, pero necesitamos un LLM para leerlo y formular una respuesta amigable para humanos. Este es el paso de “Generación” en RAG.
Primero, necesitamos una buena plantilla de prompt. Esto instruye al LLM sobre cómo comportarse. En lugar de escribir la nuestra, podemos extraer una preoptimizada desde LangChain Hub.
from langchain import hub
# Extrae un prompt RAG prefabricado desde LangChain Hub
prompt = hub.pull("rlm/rag-prompt")
# imprime el prompt
print(prompt)
#### SALIDA ####
human
Eres un asistente para tareas de respuesta a preguntas. Usa los siguientes fragmentos
de contexto recuperado para responder la pregunta. Si no conoces la respuesta,
solo di que no la sabes. Usa un máximo de tres oraciones y mantén la
respuesta concisa.
Pregunta: {question}
Contexto: {context}
Respuesta:
A continuación, inicializamos nuestro LLM. Usaremos gpt-3.5-turbo.
from langchain_openai import ChatOpenAI
# Inicializa el LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
Ahora para el paso final: encadenar todo junto. Usando el Lenguaje de Expresión de LangChain (LCEL), podemos canalizar la salida de un componente a la entrada del siguiente.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# Función auxiliar para formatear documentos recuperados
def format_docs(docs):
return "\\n\\n".join(doc.page_content for doc in docs)
# Define la cadena RAG completa
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
Desglosemos esta cadena:
-
{"context": retriever | format_docs, "question": RunnablePassthrough()}: Esta parte se ejecuta en paralelo. Envía la pregunta del usuario alretrieverpara obtener documentos, que luego se formatean en una sola cadena porformat_docs. Simultáneamente,RunnablePassthroughpasa la pregunta original sin cambios. -
| prompt: El contexto y la pregunta se alimentan en nuestra plantilla de prompt. -
| llm: El prompt formateado se envía al LLM. -
| StrOutputParser(): Esto limpia la salida del LLM en una cadena simple.
Ahora, invoquemos toda la cadena.
# Hacer una pregunta usando la cadena RAG
response = rag_chain.invoke("What is Task Decomposition?")
print(response)
#### OUTPUT ####
Task decomposition is a technique used to break down large tasks
into smaller, more manageable subgoals. This can be achieved by using a
Large Language Model (LLM) with simple prompts, task-specific instructions,
or human inputs. For example, ...
Y así lo tenemos, nuestro pipeline RAG recuperó exitosamente información relevante sobre “Task Decomposition” y la utilizó para generar una respuesta concisa y precisa. Esta cadena simple forma la base sobre la cual construiremos capacidades más avanzadas y poderosas.
Transformaciones Avanzadas de Consultas
Ahora que entendemos los fundamentos del pipeline RAG. Pero los sistemas de producción a menudo revelan las limitaciones de este enfoque básico. Uno de los puntos de fallo más comunes es la consulta del usuario en sí.
Una consulta podría ser demasiado específica, demasiado amplia, o usar vocabulario diferente al de nuestros documentos fuente, lo que lleva a resultados de recuperación deficientes.
La solución no es culpar al usuario, sino hacer que nuestro sistema sea más inteligente. Query Transformation es un conjunto de técnicas poderosas diseñadas para reescribir, expandir o desglosar la pregunta original para mejorar significativamente la precisión de la recuperación.
En lugar de depender de una única consulta, diseñaremos múltiples consultas mejor informadas para lanzar una red más amplia y precisa.
Para probar estas nuevas técnicas, utilizaremos la misma base de conocimientos indexada de la sección del pipeline RAG básico que acabamos de revisar. Esto nos asegura que podamos comparar directamente los resultados y ver las mejoras.
Como un recordatorio rápido, así es como configuramos nuestro recuperador:
# Cargar la publicación del blog
loader = WebBaseLoader(
web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header")
)
),
)
blog_docs = loader.load()
# Dividir los documentos en fragmentos
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
chunk_size=300,
chunk_overlap=50
)
splits = text_splitter.split_documents(blog_docs)
# Indexar los fragmentos en un almacén de vectores Chroma
vectorstore = Chroma.from_documents(documents=splits,
embedding=OpenAIEmbeddings())
# Crear nuestro recuperador
retriever = vectorstore.as_retriever()
Ahora, con nuestro recuperador listo, exploremos nuestra primera técnica de transformación de consultas.
Generación de Múltiples Consultas
Una única consulta del usuario representa solo una perspectiva. La búsqueda de similitud basada en distancia podría perder documentos relevantes que usan sinónimos o discuten conceptos relacionados.
El enfoque Multi-Query aborda esto utilizando un LLM para generar varias versiones diferentes de la pregunta del usuario, buscando efectivamente desde múltiples ángulos.
Comenzaremos creando un prompt que instruya al LLM para generar estas preguntas alternativas.
from langchain.prompts import ChatPromptTemplate
# Prompt para generar múltiples consultas
template = """You are an AI language model assistant. Your task is to generate five
different versions of the given user question to retrieve relevant documents from a vector
database. By generating multiple perspectives on the user question, your goal is to help
the user overcome some of the limitations of the distance-based similarity search.
Provide these alternative questions separated by newlines. Original question: {question}"""
prompt_perspectives = ChatPromptTemplate.from_template(template)
# Cadena para generar las consultas
generate_queries = (
prompt_perspectives
| ChatOpenAI(temperature=0)
| StrOutputParser()
| (lambda x: x.split("\n"))
)
Probemos esta cadena y veamos qué tipo de consultas genera para nuestra pregunta.
question = "What is task decomposition for LLM agents?"
generated_queries_list = generate_queries.invoke({"question": question})
# Imprimir las consultas generadas
for i, q in enumerate(generated_queries_list):
print(f"{i+1}. {q}")
#### OUTPUT ####
1. How can LLM agents break down complex tasks?
2. What is the process of task decomposition in the context of large language model agents?
3. What are the methods for decomposing tasks for LLM-powered agents?
4. Explain the concept of task decomposition as it applies to AI agents using LLMs.
5. In what ways do LLM agents handle task decomposition?
Esto es excelente. El LLM ha reformulado nuestra pregunta original usando palabras clave diferentes como “break down complex tasks”, “methods” y “process”. Ahora podemos recuperar documentos para todas estas consultas y combinar los resultados. Una forma simple de combinarlos es tomar el conjunto único de todos los documentos recuperados.
from langchain.load import dumps, loads
def get_unique_union(documents: list[list]):
""" Una función simple para obtener la unión única de documentos recuperados """
# Aplanar la lista de listas y convertir cada Documento a una cadena para la unicidad
flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
unique_docs = list(set(flattened_docs))
return [loads(doc) for doc in unique_docs]
# Construir la cadena de recuperación
retrieval_chain = generate_queries | retriever.map() | get_unique_union
# Invocar la cadena y verificar el número de documentos recuperados
docs = retrieval_chain.invoke({"question": question})
print(f"Total unique documents retrieved: {len(docs)}")
#### OUTPUT ####
Total unique documents retrieved: 6
Al buscar con cinco consultas diferentes, recuperamos un total de 6 documentos únicos, probablemente capturando un conjunto más completo de información que lo que una sola consulta habría recuperado. Ahora podemos alimentar este contexto en nuestra cadena RAG final.
from operator import itemgetter
# La cadena RAG final
template = """Answer the following question based on this context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
llm = ChatOpenAI(temperature=0)
final_rag_chain = (
{"context": retrieval_chain, "question": itemgetter("question")}
| prompt
| llm
| StrOutputParser()
)
final_rag_chain.invoke({"question": question})
#### OUTPUT ####
Task decomposition for LLM agents involves breaking down large,
complex tasks into smaller, more manageable sub-goals. This allows
the agent to work through a problem systematically. Methods for
decomposition include using the LLM itself with simple prompts ...
Esta respuesta es más robusta porque se basa en un conjunto más amplio de documentos relevantes.
RAG-Fusion
Multi-Query es un gran comienzo, pero simplemente tomar una unión de documentos los trata a todos por igual. ¿Qué pasa si un documento fue clasificado altamente por tres de nuestras consultas, mientras que otro fue un resultado de bajo rango de solo una?
El primero es claramente más importante. RAG-Fusion mejora Multi-Query no solo recuperando documentos, sino también …
re-clasificándolos usando una técnica llamada Reciprocal Rank Fusion (RRF).
RRF combina inteligentemente resultados de múltiples búsquedas. Aumenta la puntuación de documentos que aparecen consistentemente altos en diferentes listas de resultados, empujando el contenido más relevante hacia la parte superior.
El código es muy similar, pero reemplazaremos nuestra función get_unique_union con una implementación de RRF.
def reciprocal_rank_fusion(results: list[list], k=60):
"""Fusión de Rango Recíproco que combina inteligentemente múltiples listas clasificadas"""
fused_scores = {}
# Iterar a través de cada lista de documentos clasificados
for docs in results:
for rank, doc in enumerate(docs):
doc_str = dumps(doc)
if doc_str not in fused_scores:
fused_scores[doc_str] = 0
# El núcleo de RRF: los documentos clasificados más alto (valor de rango más bajo) obtienen una puntuación más grande
fused_scores[doc_str] += 1 / (rank + k)
# Ordenar documentos por sus nuevas puntuaciones fusionadas en orden descendente
reranked_results = [
(loads(doc), score)
for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]
return reranked_results
```
La función anterior reclasificará los documentos después de que se recuperen a través de búsqueda de similitud, pero aún no la hemos inicializado, así que hagámoslo ahora.
```python
# Usar un mensaje ligeramente diferente para RAG-Fusion
template = """Eres un asistente útil que genera múltiples consultas de búsqueda basadas en una única consulta de entrada. \n\nGenera múltiples consultas de búsqueda relacionadas con: {question} \n\nSalida (4 consultas):"""
prompt_rag_fusion = ChatPromptTemplate.from_template(template)
generate_queries = (
prompt_rag_fusion
| ChatOpenAI(temperature=0)
| StrOutputParser()
| (lambda x: x.split("\n"))
)
# Construir la nueva cadena de recuperación con RRF
retrieval_chain_rag_fusion = generate_queries | retriever.map() | reciprocal_rank_fusion
docs = retrieval_chain_rag_fusion.invoke({"question": question})
print(f"Total de documentos reclasificados recuperados: {len(docs)}")
#### SALIDA ####
Total de documentos reclasificados recuperados: 7
```
La cadena final sigue siendo la misma, pero ahora recibe un contexto clasificado de manera más inteligente. RAG-Fusion es una forma poderosa y de bajo esfuerzo para aumentar la calidad de tu recuperación.
### Descomposición
Algunas preguntas son demasiado complejas para ser respondidas en un solo paso. Por ejemplo, **"¿Cuáles son los componentes principales de un agente impulsado por LLM y cómo interactúan?"** Esto es realmente dos preguntas en una.
)](https://miro.medium.com/1*oYttQUN_G0J_TZtigWjsGQ.png)
)](https://miro.medium.com/1*bVJzw49ji0Qsd-wF0KR6gg.png)
La técnica de Descomposición utiliza un LLM para desglosar una consulta compleja en un conjunto de sub-preguntas más simples e independientes. Luego podemos responder cada una y sintetizar una respuesta final.
Comenzaremos con un mensaje diseñado para este propósito.
```python
# Mensaje de descomposición
template = """Eres un asistente útil que genera múltiples sub-preguntas relacionadas con una pregunta de entrada. \n\nEl objetivo es desglosar la entrada en un conjunto de sub-problemas / sub-preguntas que puedan ser respondidas de forma aislada. \n\nGenera múltiples consultas de búsqueda relacionadas con: {question} \n\nSalida (3 consultas):"""
prompt_decomposition = ChatPromptTemplate.from_template(template)
# Cadena para generar sub-preguntas
generate_queries_decomposition = (
prompt_decomposition
| ChatOpenAI(temperature=0)
| StrOutputParser()
| (lambda x: x.split("\n"))
)
# Generar e imprimir las sub-preguntas
question = "¿Cuáles son los componentes principales de un sistema de agente autónomo impulsado por LLM?"
sub_questions = generate_queries_decomposition.invoke({"question": question})
print(sub_questions)
#### SALIDA ####
[
'1. ¿Cuáles son los componentes principales ... agente?',
'2. ¿Cómo se implementa la memoria en agentes impulsados por LLM?',
'3. ¿Qué papel juega la planificación y descomposición de tareas ... LLMs?'
]
```
El LLM descompuso exitosamente nuestra pregunta compleja. Ahora podemos responder cada una de estas individualmente y combinar los resultados. Un método efectivo es responder cada sub-pregunta y usar los pares de Q&A resultantes como contexto para sintetizar una respuesta final y completa.
```python
# Mensaje RAG
prompt_rag = hub.pull("rlm/rag-prompt")
# Una lista para mantener las respuestas a nuestras sub-preguntas
rag_results = []
for sub_question in sub_questions:
# Recuperar documentos para cada sub-pregunta
retrieved_docs = retriever.get_relevant_documents(sub_question)
# Usar nuestra cadena RAG estándar para responder la sub-pregunta
answer = (prompt_rag | llm | StrOutputParser()).invoke({"context": retrieved_docs, "question": sub_question})
rag_results.append(answer)
def format_qa_pairs(questions, answers):
"""Formatear pares de P y R"""
formatted_string = ""
for i, (question, answer) in enumerate(zip(questions, answers), start=1):
formatted_string += f"Pregunta {i}: {question}\nRespuesta {i}: {answer}\n\n"
return formatted_string.strip()
# Formatear los pares de P&R en una única cadena de contexto
context = format_qa_pairs(sub_questions, rag_results)
# Mensaje de síntesis final
template = """Aquí hay un conjunto de pares de P+R:
{context}
Usa estos para sintetizar una respuesta a la pregunta original: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
final_rag_chain = (
prompt
| llm
| StrOutputParser()
)
final_rag_chain.invoke({"context": context, "question": question})
#### SALIDA ####
Un sistema de agente autónomo impulsado por LLM consta principalmente de tres
componentes principales: planificación, memoria y uso de herramientas. ...
```
Al desglosar el problema, construimos una respuesta mucho más detallada y estructurada de lo que habríamos obtenido de otra manera.
### Indicación de Paso Atrás
A veces, la consulta de un usuario es demasiado específica, mientras que nuestros documentos contienen la información más general y subyacente necesaria para responderla.
)](https://miro.medium.com/1*6lrhGv1fdcmLKMVu5tU3uQ.png)
> Por ejemplo, un usuario podría preguntar, "¿Podrían los miembros de The Police realizar arrestos legales?"
Una búsqueda directa de esto podría fallar. La técnica de Paso Atrás utiliza un LLM para dar un "paso atrás" y formular una pregunta más general, como "¿Cuáles son los poderes y deberes de la banda The Police?" Luego recuperamos contexto tanto para las preguntas específicas como generales, proporcionando un contexto más rico para la respuesta final.
Podemos enseñar al LLM este patrón usando ejemplos de pocos disparos.
```python
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
# Ejemplos de pocos disparos para enseñar al modelo cómo generar preguntas de paso atrás (más genéricas)
examples = [
{
"input": "¿Podrían los miembros de The Police realizar arrestos legales?",
"output": "¿Qué pueden hacer los miembros de The Police?",
},
{
"input": "¿En qué país nació Jan Sindel?",
"output": "¿Cuál es el historial personal de Jan Sindel?",
},
]
# Definir cómo se formatea cada ejemplo en el mensaje
example_prompt = ChatPromptTemplate.from_messages([
("human", "{input}"), # Entrada del usuario
("ai", "{output}") # Respuesta del modelo
])
# Envolver los ejemplos de pocos disparos en una plantilla de mensaje reutilizable
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
# El mensaje completo incluye instrucción del sistema, ejemplos de pocos disparos y la pregunta del usuario
prompt = ChatPromptTemplate.from_messages([
("system",
"Eres un experto en conocimiento mundial. Tu tarea es dar un paso atrás y parafrasear una pregunta "
"a una pregunta de paso atrás más genérica, que es más fácil de responder. Aquí hay algunos ejemplos:"),
few_shot_prompt,
("user", "{question}"),
])
```
Ahora, simplemente podemos definir la cadena para el enfoque de paso atrás, así que hagámoslo.
```makefile
# Define a chain to generate step-back questions using the prompt and an OpenAI model
generate_queries_step_back = prompt | ChatOpenAI(temperature=0) | StrOutputParser()
# Run the chain on a specific question
question = "What is task decomposition for LLM agents?"
step_back_question = generate_queries_step_back.invoke({"question": question})
# Output the original and generated step-back question
print(f"Original Question: {question}")
print(f"Step-Back Question: {step_back_question}")
#### OUTPUT ####
Original Question: What is task decomposition for LLM agents?
Step-Back Question: What are the different approaches to task decomposition
in software engineering?
```
Esta es una pregunta de retroceso importante. Amplía el alcance a la ingeniería de software general, lo que probablemente extraerá documentos fundamentales que luego se pueden combinar con el contexto específico sobre agentes LLM. Ahora podemos construir una cadena que use ambos.
```python
from langchain_core.runnables import RunnableLambda
# Prompt for the final response
response_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.
# Normal Context
{normal_context}
# Step-Back Context
{step_back_context}
# Original Question: {question}
# Answer:"""
response_prompt = ChatPromptTemplate.from_template(response_prompt_template)
# The full chain
chain = (
{
# Retrieve context using the normal question
"normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
# Retrieve context using the step-back question
"step_back_context": generate_queries_step_back | retriever,
# Pass on the original question
"question": lambda x: x["question"],
}
| response_prompt
| ChatOpenAI(temperature=0)
| StrOutputParser()
)
chain.invoke({"question": question})
```
Este es el resultado que obtenemos cuando ejecutamos esta cadena de indicaciones de retroceso con nuestra consulta.
```python
####### OUTPUT OF STEP BACK ###########
Task decomposition is a fundamental concept in software engineering
where a complex problem is broken down into smaller, more manageable parts.
In the context of LLM agents, this principle is applied to enable them to
handle large tasks. By decomposing a task into sub-goa ...
```
### HyDE
Esta técnica final es una de las más ingeniosas. El problema central de la recuperación es que la consulta de un usuario podría usar palabras diferentes a las del documento (el problema de "desajuste de vocabulario").
)](https://miro.medium.com/1*YQVJMOpDBU6l54atHoFpJg.png)
**HyDE (Hypothetical Document Embeddings)** propone una solución radical: Primero, haz que un LLM genere una respuesta _hipotética_ a la pregunta. Este documento falso, aunque no sea factualmente correcto, será semánticamente rico y usará el tipo de lenguaje que esperamos encontrar en una respuesta real.
Luego incrustamos este documento hipotético y usamos su incrustación para realizar la recuperación. El resultado es que encontramos documentos reales que son semánticamente muy similares a una respuesta ideal.
Comencemos creando un indicador para generar este documento hipotético.
```python
# HyDE prompt
template = """Please write a scientific paper passage to answer the question
Question: {question}
Passage:"""
prompt_hyde = ChatPromptTemplate.from_template(template)
# Chain to generate the hypothetical document
generate_docs_for_retrieval = (
prompt_hyde
| ChatOpenAI(temperature=0)
| StrOutputParser()
)
# Generate and print the hypothetical document
hypothetical_document = generate_docs_for_retrieval.invoke({"question": question})
print(hypothetical_document)
#### OUTPUT ####
Task decomposition in large language model (LLM) agents refers to the
process of breaking down a complex, high-level task ...
```
Este pasaje es una respuesta perfecta, de estilo de libro de texto. Ahora, usamos su incrustación para encontrar documentos reales.
```bash
# Retrieve documents using the HyDE approach
retrieval_chain = generate_docs_for_retrieval | retriever
retrieved_docs = retrieval_chain.invoke({"question": question})
# Use our standard RAG chain to generate the final answer from the retrieved context
final_rag_chain.invoke({"context": retrieved_docs, "question": question})
#### OUTPUT ####
Task decomposition for LLM agents involves breaking down a larger task
into smaller, more manageable subgoals. This can be done using techni ...
```
Al usar un documento hipotético como **cebo**, HyDE nos ayudó a enfocarnos en los fragmentos más relevantes en nuestra base de conocimientos, demostrando otra herramienta poderosa en nuestro kit de herramientas RAG.
## Enrutamiento y Construcción de Consultas
Nuestro sistema RAG es cada vez más inteligente, pero en un escenario del mundo real, el conocimiento no se almacena en una única biblioteca uniforme.
> A menudo tenemos múltiples fuentes de datos: documentación para diferentes lenguajes de programación, wikis internos, sitios web públicos o bases de datos con metadatos estructurados.
)](https://miro.medium.com/1*cost0_AWB8NKp0WxZlH7fA.png)
Enviar cada consulta a cada fuente es extremadamente ineficiente y puede llevar a resultados ruidosos e irrelevantes.
Aquí es donde nuestro sistema RAG necesita evolucionar de un simple bibliotecario a un **operador de centralita inteligente**. Necesita la capacidad de primero _analizar_ una consulta entrante y luego _enrutarla_ al destino correcto o _construir_ una consulta más precisa y estructurada para la recuperación. Esta sección profundiza en las técnicas que hacen esto posible.
### Enrutamiento Lógico
El enrutamiento es un problema de clasificación. Dada una pregunta del usuario, necesitamos clasificarla en una de varias categorías predefinidas. Aunque los modelos de ML tradicionales pueden hacer esto, podemos aprovechar el potente motor de razonamiento que ya tenemos: el LLM mismo.
)](https://miro.medium.com/1*PK9xKW0o-72xmmLaAAozeA.png)
Al proporcionar al LLM un esquema claro (un conjunto de categorías posibles), podemos pedirle que tome la decisión de clasificación por nosotros.
Comenzaremos definiendo el "contrato" para la salida de nuestro LLM usando un modelo Pydantic. Este esquema le dice explícitamente al LLM los posibles destinos para una consulta.
```python
from typing import Literal
from langchain_core.pydantic_v1 import BaseModel, Field
# Define the data model for our router's output
class RouteQuery(BaseModel):
"""A data model to route a user query to the most relevant datasource."""
# The 'datasource' field must be one of the three specified literal strings.
# This enforces a strict set of choices for the LLM.
datasource: Literal["python_docs", "js_docs", "golang_docs"] = Field(
..., # The '...' indicates that this field is required.
description="Given a user question, choose which datasource would be most relevant for answering their question.",
)
```
Con nuestro esquema definido, ahora podemos construir la cadena del enrutador. Usaremos un indicador para dar al LLM sus instrucciones y luego usaremos el método `.with_structured_output()` para asegurar que su respuesta coincida perfectamente con nuestro modelo `RouteQuery`.
```python
# Initialize our LLM
llm = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)
# Create a new LLM instance that is "structured" to output our Pydantic model
structured_llm = llm.with_structured_output(RouteQuery)
# The system prompt provides the core instruction for the LLM's task.
system = """You are an expert at routing a user question to the appropriate data source.
Based on the programming language the question is referring to, route it to the relevant data source."""
# The full prompt template combines the system message and the user's question.
prompt = ChatPromptTemplate.from_messages(
[
("system", system),
("human", "{question}"),
]
)
# Define the complete router chain
router = prompt | structured_llm
```
Ahora, probemos nuestro enrutador. Pasaremos una pregunta que claramente es sobre Python e inspeccionaremos la salida.
```python
question = """¿Por qué el siguiente código no funciona:
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(["human", "speak in {language}"])
prompt.invoke("french")
"""
# Invocar el router y verificar el resultado
result = router.invoke({"question": question})
print(result)
#### OUTPUT ####
datasource='python_docs'
```
El resultado es una instancia de nuestro modelo `RouteQuery`, y el LLM ha identificado correctamente `python_docs` como la fuente de datos apropiada. Este resultado estructurado es ahora algo que podemos usar de manera confiable en nuestro código para implementar lógica de ramificación.
```python
def choose_route(result):
"""Una función para determinar la lógica descendente basada en la salida del router."""
if "python_docs" in result.datasource.lower():
# En una aplicación real, esto sería una cadena RAG completa para documentos de Python
return "chain for python_docs"
elif "js_docs" in result.datasource.lower():
# Esta sería la cadena para documentos de JavaScript
return "chain for js_docs"
else:
# Y esta para documentos de Go
return "chain for golang_docs"
# La cadena completa ahora incluye la lógica de enrutamiento y ramificación
full_chain = router | RunnableLambda(choose_route)
# Ejecutemos la cadena completa
final_destination = full_chain.invoke({"question": question})
print(final_destination)
#### OUTPUT ####
chain for python_docs
```
Nuestra centralita enrutó correctamente la consulta relacionada con Python. Este enfoque es increíblemente poderoso para construir sistemas RAG de múltiples fuentes.
### Enrutamiento Semántico
El enrutamiento lógico funciona perfectamente cuando tienes categorías claramente definidas. Pero ¿qué pasa si quieres enrutar basándote en el _estilo_ o _dominio_ de una pregunta? Por ejemplo, podrías querer responder preguntas de física con un tono serio y académico, y preguntas de matemáticas con un enfoque paso a paso y pedagógico. Aquí es donde entra el **Enrutamiento Semántico**.
)](https://miro.medium.com/1*mzz-ncmrzdwQU37GFgPeTw.png)
> En lugar de clasificar la consulta, definimos múltiples indicaciones de expertos.
Luego incrustamos la consulta del usuario y cada una de nuestras plantillas de indicaciones, y usamos similitud de coseno para encontrar la indicación que está más alineada semánticamente con la consulta.
Primero, definamos nuestras dos personas expertas.
```python
from langchain_core.prompts import PromptTemplate
# Una indicación para un profesor de física
physics_template = """Eres un profesor de física muy inteligente. \
Eres excelente respondiendo preguntas sobre física de manera concisa y fácil de entender. \
Cuando no sabes la respuesta a una pregunta, admites que no la sabes.
Aquí hay una pregunta:
{query}"""
# Una indicación para un experto en matemáticas
math_template = """Eres un muy buen matemático. Eres excelente respondiendo preguntas de matemáticas. \
Eres tan bueno porque puedes desglosar problemas difíciles en sus partes componentes, \
responder las partes componentes y luego juntarlas para responder la pregunta más amplia.
Aquí hay una pregunta:
{query}"""
```
Ahora, crearemos la función de enrutamiento que realiza la incrustación y la comparación de similitud.
```python
from langchain.utils.math import cosine_similarity
# Inicializar el modelo de incrustación
embeddings = OpenAIEmbeddings()
# Almacenar nuestras plantillas y sus incrustaciones para comparación
prompt_templates = [physics_template, math_template]
prompt_embeddings = embeddings.embed_documents(prompt_templates)
def prompt_router(input):
"""Una función para enrutar la consulta de entrada a la plantilla de indicación más similar."""
# 1. Incrustar la consulta del usuario entrante
query_embedding = embeddings.embed_query(input["query"])
# 2. Calcular la similitud de coseno entre la consulta y todas las plantillas de indicaciones
similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]
# 3. Encontrar el índice de la indicación más similar
most_similar_index = similarity.argmax()
# 4. Seleccionar la plantilla de indicación más similar
chosen_prompt = prompt_templates[most_similar_index]
print(f"DEBUG: Usando plantilla {'MATH' if most_similar_index == 1 else 'PHYSICS'}.")
# 5. Devolver el objeto de indicación elegido
return PromptTemplate.from_template(chosen_prompt)
```
Con la lógica de enrutamiento en su lugar, podemos construir la cadena completa que selecciona dinámicamente al experto adecuado para el trabajo.
```python
# La cadena final que combina el router con el LLM
chain = (
{"query": RunnablePassthrough()}
| RunnableLambda(prompt_router) # Seleccionar dinámicamente la indicación
| ChatOpenAI()
| StrOutputParser()
)
# Hacer una pregunta de física
print(chain.invoke("What's a black hole"))
#### OUTPUT ####
DEBUG: Usando plantilla PHYSICS.
Un agujero negro es una región del espacio-tiempo donde la gravedad es tan fuerte
que nada—ni partículas ni radiación electromagnética como
la luz—puede escapar de él. El límite de no escape se llama el
horizonte de eventos. Aunque tiene un gran efecto en el destino
y las circunstancias de un objeto que lo cruza, no tiene características
localmente detectables. De muchas maneras, un agujero negro actúa como un cuerpo negro ideal,
ya que no refleja luz.
```
Perfecto. El router identificó correctamente la pregunta como relacionada con la física y utilizó la indicación del profesor de física, lo que resultó en una respuesta concisa y precisa. Esta técnica es excelente para crear agentes especializados que adapten su persona a las necesidades del usuario.
### Estructuración de Consultas
Hasta ahora, nos hemos enfocado en recuperar de texto no estructurado. Pero la mayoría de los datos del mundo real son _semi-estructurados_; contienen metadatos valiosos como fechas, autores, conteos de visualizaciones o categorías. Una búsqueda vectorial simple no puede aprovechar esta información.
> **Estructuración de Consultas** es la técnica de convertir una pregunta en lenguaje natural en una consulta estructurada que puede usar estos filtros de metadatos para una recuperación muy precisa.
Para ilustrar, veamos los metadatos disponibles de una transcripción de video de YouTube.
```python
from langchain_community.document_loaders import YoutubeLoader
# Cargar una transcripción de YouTube para inspeccionar sus metadatos
docs = YoutubeLoader.from_youtube_url(
"https://www.youtube.com/watch?v=pbAd8O1Lvm4", add_video_info=True
).load()
# Imprimir los metadatos del primer documento
print(docs[0].metadata)
#### OUTPUT ####
{
'source': 'pbAd8O1Lvm4',
'title': 'Self-reflective RAG with LangGraph: Self-RAG and CRAG',
'description': 'Unknown',
'view_count': 11922,
'thumbnail_url': 'https://i.ytimg.com/vi/pbAd8O1Lvm4/hq720.jpg',
'publish_date': '2024-02-07 00:00:00',
'length': 1058,
'author': 'LangChain'
}
```
Este documento tiene metadatos ricos: `view_count`, `publish_date`, `length`. Queremos que nuestros usuarios puedan filtrar en estos campos usando lenguaje natural. Para hacer esto, definiremos otro esquema Pydantic, esta vez para una consulta de búsqueda de video estructurada.
``````python
import datetime
from typing import Optional
class TutorialSearch(BaseModel):
"""Un modelo de datos para buscar en una base de datos de videos tutoriales."""
# La consulta principal para una búsqueda de similitud sobre la transcripción del video.
content_search: str = Field(..., description="Consulta de búsqueda de similitud aplicada a transcripciones de videos.")
# Una consulta más sucinta para buscar solo en el título del video.
title_search: str = Field(..., description="Versión alternativa de la consulta de búsqueda de contenido para aplicar a títulos de videos.")
# Filtros de metadatos opcionales
min_view_count: Optional[int] = Field(None, description="Filtro de recuento mínimo de visualizaciones, inclusivo.")
max_view_count: Optional[int] = Field(None, description="Filtro de recuento máximo de visualizaciones, exclusivo.")
earliest_publish_date: Optional[datetime.date] = Field(None, description="Filtro de fecha de publicación más antigua, inclusivo.")
latest_publish_date: Optional[datetime.date] = Field(None, description="Filtro de fecha de publicación más reciente, exclusivo.")
min_length_sec: Optional[int] = Field(None, description="Duración mínima del video en segundos, inclusivo.")
max_length_sec: Optional[int] = Field(None, description="Duración máxima del video en segundos, exclusivo.")
def pretty_print(self) -> None:
"""Una función auxiliar para imprimir los campos completados del modelo."""
for field in self.__fields__:
if getattr(self, field) is not None:
print(f"{field}: {getattr(self, field)}")
```
Este esquema es nuestro objetivo. Ahora crearemos una cadena que tome una pregunta del usuario y complete este modelo.
```python
# Indicación del sistema para el analizador de consultas
system = """Eres un experto en convertir preguntas de usuarios en consultas de base de datos. \
Tienes acceso a una base de datos de videos tutoriales sobre una biblioteca de software para construir aplicaciones impulsadas por LLM. \
Dada una pregunta, devuelve una consulta de base de datos optimizada para recuperar los resultados más relevantes.
Si hay acrónimos o palabras que no reconoces, no intentes reformularlos."""
prompt = ChatPromptTemplate.from_messages([("system", system), ("human", "{question}")])
structured_llm = llm.with_structured_output(TutorialSearch)
# La cadena final del analizador de consultas
query_analyzer = prompt | structured_llm
```
Probemos esto con algunas preguntas diferentes para ver su poder.
```python
# Prueba 1: Una consulta simple
query_analyzer.invoke({"question": "rag from scratch"}).pretty_print()
#### SALIDA ####
content_search: rag from scratch
title_search: rag from scratch
```
Como se esperaba, completa los campos de búsqueda de contenido y título. Ahora para una consulta más compleja.
```python
# Prueba 2: Una consulta con un filtro de fecha
query_analyzer.invoke(
{"question": "videos on chat langchain published in 2023"}
).pretty_print()
#### SALIDA ####
content_search: chat langchain
title_search: chat langchain 2023
earliest_publish_date: 2023-01-01
latest_publish_date: 2024-01-01
```
Esto es brillante. El LLM interpretó correctamente "in 2023" y creó un filtro de rango de fechas. Intentemos uno más con una restricción de tiempo.
```python
# Prueba 3: Una consulta con un filtro de duración
query_analyzer.invoke(
{
"question": "how to use multi-modal models in an agent, only videos under 5 minutes"
}
).pretty_print()
#### SALIDA ####
content_search: multi-modal models agent
title_search: multi-modal models agent
max_length_sec: 300
```
Convirtió perfectamente "under 5 minutes" a `max_length_sec: 300`. Esta consulta estructurada ahora puede pasarse a un almacén de vectores que admita filtrado de metadatos, permitiendo una recuperación increíblemente precisa y eficiente que va mucho más allá de la búsqueda semántica simple.
## Estrategias Avanzadas de Indexación
Hasta ahora, nuestro enfoque para la indexación ha sido directo: dividir documentos en fragmentos e incrustarlos. Esto funciona, pero tiene una limitación fundamental.
Los fragmentos pequeños y enfocados son excelentes para la precisión de recuperación (contienen menos ruido), pero a menudo carecen del contexto más amplio necesario para que el LLM genere una respuesta completa.
)](https://miro.medium.com/1*PrdpYBmw3-ln5AaZLjyUaw.png)
Por el contrario, los fragmentos grandes proporcionan un gran contexto pero funcionan mal en la recuperación porque su significado central se diluye.
> Este es el dilema clásico del "tamaño de fragmento". ¿Cómo podemos obtener lo mejor de ambos mundos?
La respuesta está en estrategias de indexación más avanzadas que separan la representación del documento utilizada para _recuperación_ de la utilizada para _generación_. Profundicemos.
### Indexación Multi-Representación
La idea central de la Indexación Multi-Representación es simple pero poderosa: en lugar de incrustar los fragmentos de documento completos, creamos una representación más pequeña y enfocada de cada fragmento (como un resumen) e incrust_amos_ esa en su lugar.
)](https://miro.medium.com/1*1TbTDTSvgVbpKxSW7feMng.png)
Durante la recuperación, buscamos en estos resúmenes concisos. Una vez que encontramos el mejor resumen, usamos su ID para buscar y recuperar el fragmento de documento original completo.
De esta manera, obtenemos la precisión de buscar en resúmenes pequeños y densos y el contexto rico de los documentos principales más grandes para la generación.
Primero, necesitamos cargar algunos documentos para trabajar. Obtendremos dos publicaciones del blog de Lilian Weng.
```python
from langchain_community.document_loaders import WebBaseLoader
# Cargar dos publicaciones de blog diferentes para crear una base de conocimiento más diversa
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()
loader = WebBaseLoader("https://lilianweng.github.io/posts/2024-02-05-human-data-quality/")
docs.extend(loader.load())
print(f"Loaded {len(docs)} documents.")
#### SALIDA ####
Loaded 2 documents.
```
A continuación, crearemos una cadena para generar un resumen para cada uno de estos documentos.
```python
import uuid
# La cadena para generar resúmenes
summary_chain = (
# Extraer page_content del objeto documento
{"doc": lambda x: x.page_content}
# Canalizarlo a una plantilla de indicación
| ChatPromptTemplate.from_template("Summarize the following document:\n\n{doc}")
# Usar un LLM para generar el resumen
| ChatOpenAI(model="gpt-3.5-turbo", max_retries=0)
# Analizar la salida en una cadena
| StrOutputParser()
)
# Usar .batch() para ejecutar la summarización en paralelo para mayor eficiencia
summaries = summary_chain.batch(docs, {"max_concurrency": 5})
# Inspeccionemos el primer resumen
print(summaries[0])
#### SALIDA ####
The document discusses building autonomous agents powered by Large
Language Models (LLMs). It outlines the key components of such a system, ...
```
Ahora viene la parte crucial. Necesitamos un `MultiVectorRetriever` que requiere dos componentes principales:
1. Un `vectorstore` para almacenar las incrustaciones de nuestros resúmenes.
2. Un `docstore` (un almacén simple de clave-valor) para mantener los documentos originales completos.
```python
from langchain.storage import InMemoryByteStore
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain_core.documents import Document
# El vectorstore para indexar las incrustaciones de resumen
vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings())
# La capa de almacenamiento para los documentos principales
store = InMemoryByteStore()
id_key = "doc_id" # Esta clave vinculará resúmenes a sus documentos principales
# El recuperador que orquesta todo el proceso
retriever = MultiVectorRetriever(
vectorstore=vectorstore,
byte_store=store,
id_key=id_key,
)
# Generar IDs únicos para cada uno de nuestros documentos originales
doc_ids = [str(uuid.uuid4()) for _ in docs]
# Crear nuevos objetos Document para los resúmenes, agregando 'doc_id' a sus metadatos
summary_docs = [
Document(page_content=s, metadata={id_key: doc_ids[i]})
for i, s in enumerate(summaries)
]
# Agregar los resúmenes al vectorstore
retriever.vectorstore.add_documents(summary_docs)
# Agregar los documentos originales al docstore, vinculándolos por los mismos IDs
retriever.docstore.mset(list(zip(doc_ids, docs)))
```
Nuestro índice avanzado ahora está construido. Probemos el proceso de recuperación. Haremos una pregunta sobre "Memory in agents" y veamos qué sucede.
``````python
query = "Memory in agents"
# First, let's see what the vectorstore finds by searching the summaries
sub_docs = vectorstore.similarity_search(query, k=1)
print("--- Result from searching summaries ---")
print(sub_docs[0].page_content)
print("\n--- Metadata showing the link to the parent document ---")
print(sub_docs[0].metadata)
#### OUTPUT ####
--- Result from searching summaries ---
The document discusses the concept of building autonomous agents powered by Large Language Models (LLMs) as their core controllers. It covers components such as planning, memory, and tool use, along with case studies and proof-of-concept examples like AutoGPT and GPT-Engineer. Challenges like finite context length, planning difficulties, and reliability of natural language interfaces are also highlighted. The document provides references to related research papers and offers a comprehensive overview of LLM-powered autonomous agents.
--- Metadata showing the link to the parent document ---
{'doc_id': 'cf31524b-fe6a-4b28-a980-f5687c9460ea'}
```
Como puedes ver, la búsqueda encontró el resumen que menciona "memory". Ahora, el `MultiVectorRetriever` utilizará el `doc_id` de los metadatos de este resumen para obtener automáticamente el documento padre completo del `docstore`.
```python
# Let the full retriever do its job
retrieved_docs = retriever.get_relevant_documents(query, n_results=1)
# Print the beginning of the retrieved full document
print("\n--- The full document retrieved by the MultiVectorRetriever ---")
print(retrieved_docs[0].page_content[0:500])
#### OUTPUT ####
"\n\n\n\n\n\nLLM Powered Autonomous Agents | Lil'Log\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n ...."
```
¡Esto es exactamente lo que queríamos! Buscamos en resúmenes concisos pero obtuvimos el documento completo y rico en contexto, resolviendo el dilema del tamaño de fragmentos.
### Indexación Jerárquica (RAPTOR) Árbol de Conocimiento
**La Teoría:** RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval) lleva la idea de múltiples representaciones un paso más allá. En lugar de solo una capa de resúmenes, RAPTOR construye un árbol multinivel de resúmenes. Comienza agrupando pequeños fragmentos de documentos. Luego resume cada grupo.
)](https://miro.medium.com/1*95v0K13O2rvsAYJ96ldhew.png)
Luego, toma estos resúmenes, los agrupa _entre sí_, y resume los nuevos grupos. Este proceso se repite, creando una jerarquía de conocimiento desde detalles granulares hasta conceptos de alto nivel. Cuando consultas, puedes buscar en diferentes niveles de este árbol, permitiendo una recuperación que puede ser tan específica o general como sea necesario.
Esta es una técnica más avanzada, y aunque no implementaremos el algoritmo completo aquí, puedes encontrar un análisis profundo y código completo en el [RAPTOR Cookbook](https://github.com/langchain-ai/langchain/blob/a2529cd805b18230f187a8c49b2d2453540afe62/cookbook/RAPTOR.ipynb). Representa la vanguardia de la indexación estructurada.
### Precisión a Nivel de Token (ColBERT)
**La Teoría:** Los modelos de embedding estándar crean un único vector para un fragmento completo de texto (esto se llama un enfoque de "bolsa de palabras"). Esto puede perder mucho matiz.
)](https://miro.medium.com/1*VL6Ny9Z8S9kRqgYyFhhdsA.png)
> **ColBERT (Contextualized Late Interaction over BERT)** ofrece un enfoque más granular. Genera un embedding separado y consciente del contexto para _cada token individual_ en el documento.
Cuando haces una consulta, ColBERT también incrusta cada token en tu consulta. Luego, en lugar de comparar un vector de documento con un vector de consulta, encuentra la similitud máxima entre cada token de consulta y _cualquier_ token de documento.
Esta "interacción tardía" permite una comprensión mucho más granular de la relevancia, destacándose en búsquedas de estilo de palabras clave.
Podemos usar fácilmente ColBERT a través de la biblioteca `RAGatouille`.
```python
# Install the required library
!pip install -U ragatouille
from ragatouille import RAGPretrainedModel
# Load a pre-trained ColBERT model
RAG = RAGPretrainedModel.from_pretrained("colbert-ir/colbertv2.0")
```
Ahora, indexemos una página de Wikipedia usando el enfoque único a nivel de token de ColBERT.
```python
import requests
def get_wikipedia_page(title: str):
"""A helper function to retrieve content from Wikipedia."""
# Wikipedia API endpoint and parameters
URL = "https://en.wikipedia.org/w/api.php"
params = { "action": "query", "format": "json", "titles": title, "prop": "extracts", "explaintext": True }
headers = {"User-Agent": "MyRAGApp/1.0"}
response = requests.get(URL, params=params, headers=headers)
data = response.json()
page = next(iter(data["query"]["pages"].values()))
return page.get("extract")
full_document = get_wikipedia_page("Hayao_Miyazaki")
# Index the document with RAGatouille. It handles the chunking and token-level embedding internally.
RAG.index(
collection=[full_document],
index_name="Miyazaki-ColBERT",
max_document_length=180,
split_documents=True,
)
```
El proceso de indexación es más complejo, ya que está creando embeddings para cada token, pero `RAGatouille` lo maneja sin problemas. Ahora, busquemos en nuestro nuevo índice.
```python
# Search the ColBERT index
results = RAG.search(query="What animation studio did Miyazaki found?", k=3)
print(results)
#### OUTPUT ####
[{'content': 'In April 1984, ...', 'score': 25.9036, 'rank': 1, ...},
{'content': 'Hayao Miyazaki ...', 'score': 25.5716, 'rank': 2, ...},
{'content': 'Glen Keane said ...', 'score': 24.8411, 'rank': 3, ...}]
```
El resultado principal menciona directamente la fundación de Studio Ghibli. También podemos envolverlo fácilmente como un recuperador estándar de LangChain.
```python
# Convertir el modelo de RAGatouille en un recuperador compatible con LangChain
colbert_retriever = RAG.as_langchain_retriever(k=3)
# Usarlo como cualquier otro recuperador
retrieved_docs = colbert_retriever.invoke("¿Qué estudio de animación fundó Miyazaki?")
print(retrieved_docs[0].page_content)
#### OUTPUT ####
En abril de 1984, Miyazaki abrió su propia oficina en el distrito de Suginami, nombrándola Nibariki.
=== Studio Ghibli ===
==== Primeras películas (1985–1996) ====
En junio de 1985, Miyazaki, Takahata, Tokuma y Suzuki fundaron la empresa de producción de animación Studio Ghibli, con financiamiento de Tokuma Shoten. La primera película de Studio Ghibli, Laputa: Castle in the Sky (1986), empleó el mismo equipo de producción de Nausicaä. Los diseños de Miyazaki para el escenario de la película fueron inspirados por la arquitectura griega y "plantillas urbanísticas europeas".
```
ColBERT proporciona una alternativa poderosa y de grano fino a la búsqueda vectorial tradicional, demostrando que la forma en que construimos nuestra biblioteca es tan importante como la forma en que la buscamos.
## Recuperación y Generación Avanzadas
Hemos creado un sistema RAG sofisticado con enrutamiento inteligente e indexación avanzada. Ahora, hemos llegado a la última milla: recuperación y generación. Aquí es donde nos aseguramos de que el contexto que proporcionamos al LLM sea de la más alta calidad posible y que la respuesta final del LLM sea relevante, precisa y fundamentada en ese contexto.
)](https://miro.medium.com/1*RJzBqSbw8V0LPpzYN7VFjA.png)
Incluso con la mejor indexación, nuestra recuperación inicial aún puede contener ruido: documentos menos relevantes que se cuelan. Y los LLM, por poderosos que sean, a veces pueden malinterpretar el contexto o alucinar.
Esta sección introduce las técnicas avanzadas que actúan como la capa final de control de calidad para nuestro pipeline.
### Re-ranking Dedicado
Los métodos de recuperación estándar nos dan una lista clasificada de documentos, pero esta clasificación inicial no siempre es perfecta. **Re-ranking** es un paso crucial de segundo paso donde tomamos el conjunto inicial de documentos recuperados y usamos un modelo más sofisticado (y a menudo más costoso) para reordenarlos según su relevancia para la consulta.
)](https://miro.medium.com/1*rnQCpniADswmhbTFiCN1Gg.png)
> Esto asegura que los documentos más relevantes se coloquen en la parte superior del contexto que proporcionamos al LLM.
Ya hemos visto un método de re-ranking poderoso: Reciprocal Rank Fusion (RRF) en nuestra sección RAG-Fusion. Es una forma excelente y sin modelo para combinar resultados. Pero para un enfoque aún más poderoso, podemos usar un modelo de re-ranking dedicado, como el proporcionado por Cohere.
Primero, configuremos un recuperador estándar. Usaremos la misma publicación de blog de nuestros ejemplos anteriores.
```python
# Cargar, dividir e indexar el documento
loader = WebBaseLoader(web_paths=("https://lilianweng.github.io/posts/2023-06-23-agent/",))
blog_docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(chunk_size=300, chunk_overlap=50)
splits = text_splitter.split_documents(blog_docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())
# Recuperador de primer paso: obtener los 10 documentos potencialmente relevantes principales
retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
```
Ahora, introducimos el `ContextualCompressionRetriever`. Este recuperador especial envuelve nuestro recuperador base y añade un paso de "compresión". Aquí, nuestro compresor será el modelo `CohereRerank`.
Tomará los 10 documentos de nuestro recuperador base y los reordenará, devolviendo solo los más relevantes.
```python
# Necesitarás instalar cohere: pip install cohere
# Y establecer tu variable de entorno COHERE_API_KEY
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CohereRerank
# Inicializar el modelo Cohere Rerank
compressor = CohereRerank()
# Crear el recuperador de compresión
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=retriever
)
# Probémoslo con nuestra consulta
question = "What is task decomposition for LLM agents?"
compressed_docs = compression_retriever.get_relevant_documents(question)
# Imprimir los documentos re-clasificados
print("--- Re-ranked and Compressed Documents ---")
for doc in compressed_docs:
print(f"Relevance Score: {doc.metadata['relevance_score']:.4f}")
print(f"Content: {doc.page_content[:150]}...\\n")
#### OUTPUT ####
--- Re-ranked and Compressed Documents ---
Relevance Score: 0.9982
Content: Task decomposition can be done (1) by LLM with simple prompting like "Steps for XYZ.", "What are the subgoals for achieving XYZ?", (2) by using task...
Relevance Score: 0.9851
Content: Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into mult...
Relevance Score: 0.9765
Content: LLM-powered autonomous agents have been an exciting concept. They can be used for task decomposition by prompting, using task-specific instructions, or ...
```
El resultado es notable. El modelo `CohereRerank` no solo ha reordenado los documentos, sino que también ha asignado una `relevance_score` a cada uno. Ahora podemos estar mucho más seguros de que el contexto que pasamos al LLM es de la más alta calidad, lo que lleva directamente a respuestas mejores y más precisas.
### Autocorrección usando Agentes de IA
¿Y si nuestro sistema RAG pudiera verificar su propio trabajo antes de dar una respuesta? Esa es la idea detrás de arquitecturas RAG autocorrectivas como **CRAG (Corrective RAG)** y **Self-RAG**.
)](https://miro.medium.com/1*LpQrsvNj09aJPMhhh4fc-A.png)
Estos no son solo cadenas simples, son gráficos dinámicos (a menudo construidos con LangGraph) que pueden razonar sobre la calidad de la información recuperada y decidir un curso de acción.
- **CRAG:** Si los documentos recuperados son irrelevantes o ambiguos para una consulta dada, un sistema CRAG no simplemente los pasará al LLM. En su lugar, activa una búsqueda web nueva y más robusta para encontrar información mejor, corrige los documentos recuperados y luego procede con la generación.
- **Self-RAG:** Este enfoque va un paso más allá. En cada paso, utiliza un LLM para generar "tokens de reflexión" que critican el proceso. Califica los documentos recuperados por relevancia. Si no son relevantes, recupera de nuevo. Una vez que tiene buenos documentos, genera una respuesta y luego califica esa respuesta por consistencia factual, asegurando que esté fundamentada en los documentos fuente.
Estas técnicas representan el estado del arte en la construcción de RAG confiable y de nivel de producción. Implementarlas desde cero implica construir una máquina de estados o un gráfico. Aunque la implementación completa es extensa, puedes encontrar excelentes y detallados tutoriales aquí:
- [Notebook CRAG](github.com/langchain-ai/langgraph/blob/8ccead9560f6cd76537f632d7a310ba41e38f28b/examples/rag/langgraph_crag.ipynb)
- [Notebook Self-RAG](github.com/langchain-ai/langgraph/blob/8ccead9560f6cd76537f632d7a310ba41e38f28b/examples/rag/langgraph_self_rag_mistral_nomic.ipynb)
Estos marcos de agentes son la clave para pasar de simples bots de preguntas y respuestas a crear verdaderos motores de razonamiento robusto.
### Impacto del Contexto Largo
Un tema recurrente en RAG ha sido las ventanas de contexto limitadas de los LLM. Pero con el auge de modelos con ventanas de contexto masivas (128k, 200k, o incluso 1 millón de tokens), surge una pregunta:
)](https://miro.medium.com/1*g3NCw9EzZcylHpOJMlGr8A.png)
> **¿Todavía necesitamos RAG?** ¿Podemos simplemente meter todos nuestros documentos en el prompt?
La respuesta es matizada. Aunque los modelos de contexto largo son increíblemente poderosos, no son una solución milagrosa.
La investigación ha demostrado que su rendimiento puede degradarse cuando la información crucial está enterrada en el medio de un contexto muy largo (el problema de "buscar una aguja en un pajar").
- **Ventaja de RAG:** RAG destaca en _encontrar_ la aguja primero y presentar solo eso al LLM. Es una herramienta de precisión.
- **Ventaja del Contexto Largo:** Los modelos de contexto largo son fantásticos para tareas que requieren sintetizar información de _muchas partes diferentes_ de un documento simultáneamente, algo que RAG podría perder.
El futuro probablemente sea un enfoque híbrido: usar RAG para realizar una recuperación inicial y precisa de los documentos más relevantes y luego alimentar este contexto de alta calidad y pre-filtrado en un modelo de contexto largo para la síntesis final.
Para un análisis profundo de este tema, esta presentación es un excelente recurso:
- **Diapositivas sobre Contexto Largo:** [El Impacto del Contexto Largo en RAG](https://docs.google.com/presentation/d/1mJUiPBdtf58NfuSEQ7pVSEQ2Oqmek7F1i4gBwR6JDss/edit#slide=id.g26c0cb8dc66_0_0)
## Evaluación Manual de RAG
Hemos construido un pipeline RAG cada vez más sofisticado, añadiendo técnicas avanzadas para recuperación, indexación y generación. Pero una pregunta crucial permanece: **¿cómo probamos que realmente funciona?**
En un entorno de producción, "parece que funciona" no es suficiente. Necesitamos métricas objetivas y repetibles para medir el rendimiento, identificar debilidades y guiar mejoras.
Aquí es donde entra la evaluación. Es la ciencia de hacer que nuestro sistema RAG sea responsable. En esta parte, exploraremos cómo medir cuantitativamente la calidad de nuestro sistema construyendo nuestros propios evaluadores desde los primeros principios.
### Las Métricas Principales: ¿Qué Deberíamos Medir?
Antes de sumergirnos en el código, definamos cómo se ve una "buena" respuesta RAG. Podemos desglosarla en algunos principios principales:
1. **Fidelidad:** ¿Se adhiere la respuesta estrictamente al contexto proporcionado? Una respuesta fiel no inventa información ni utiliza el conocimiento pre-entrenado del LLM para responder. Esta es la métrica más importante para prevenir alucinaciones.
2. **Corrección:** ¿Es la respuesta factualmente correcta cuando se compara con una respuesta de "verdad fundamental" o de referencia?
3. **Relevancia Contextual:** ¿Era el contexto que recuperamos realmente relevante para la pregunta del usuario? Esto evalúa el rendimiento de nuestro recuperador, no del generador.
Exploremos cómo medir estos, comenzando con el método más transparente: construir los evaluadores nosotros mismos.
### Construir Evaluadores desde Cero con LangChain
La mejor manera de entender la evaluación es construirla. Usando componentes básicos de LangChain, podemos crear cadenas personalizadas que instruyan a un LLM para actuar como un "juez" imparcial, calificando la salida de nuestro sistema RAG basándose en criterios que definimos en un prompt. Esto nos da el máximo control y transparencia.
Comencemos con **Corrección**. Nuestro objetivo es crear una cadena que compare la generated_answer con una respuesta ground_truth y devuelva una puntuación de 0 a 1.
``````python
from langchain.prompts import PromptTemplate
# Usaremos un LLM poderoso como gpt-4o para actuar como nuestro "juez" para una evaluación confiable.
llm = ChatOpenAI(temperature=0, model_name="gpt-4o", max_tokens=4000)
# Define el esquema de salida para nuestra puntuación de evaluación para garantizar una salida consistente y estructurada.
class ResultScore(BaseModel):
score: float = Field(..., description="La puntuación del resultado, que va de 0 a 1 donde 1 es la mejor puntuación posible.")
# Esta plantilla de prompt instruye claramente al LLM sobre cómo puntuar la corrección de la respuesta.
correctness_prompt = PromptTemplate(
input_variables=["question", "ground_truth", "generated_answer"],
template="""
Question: {question}
Ground Truth: {ground_truth}
Generated Answer: {generated_answer}
Evalúa la corrección de la respuesta generada en comparación con la verdad fundamental.
Puntúa de 0 a 1, donde 1 es perfectamente correcto y 0 es completamente incorrecto.
Score:
"""
)
# Construimos la cadena de evaluación canalizando el prompt al LLM con salida estructurada.
correctness_chain = correctness_prompt | llm.with_structured_output(ResultScore)
```
Ahora, envolvamos esto en una función simple y pruébalo. ¿Qué pasa si la verdad fundamental es "París y Madrid" pero nuestro sistema RAG solo respondió parcialmente con "París"?
```python
def evaluate_correctness(question, ground_truth, generated_answer):
"""Una función auxiliar para ejecutar nuestra cadena de evaluación de corrección personalizada."""
result = correctness_chain.invoke({
"question": question,
"ground_truth": ground_truth,
"generated_answer": generated_answer
})
return result.score
# Prueba la cadena de corrección con una respuesta parcialmente correcta.
question = "¿Cuál es la capital de Francia y España?"
ground_truth = "París y Madrid"
generated_answer = "París"
score = evaluate_correctness(question, ground_truth, generated_answer)
print(f"Correctness Score: {score}")
### OUTPUT ###
Correctness Score: 0.5
```
Este es un resultado perfecto. Nuestro LLM juez razonó correctamente que la respuesta generada era solo la mitad correcta y asignó una puntuación apropiada de 0.5.
A continuación, construyamos un evaluador para **Fidelidad**. Esto es posiblemente más importante que la corrección para RAG, ya que es nuestra defensa principal contra alucinaciones.
Aquí, el LLM juez debe ignorar si la respuesta es factualmente correcta y _solo_ preocuparse si la respuesta puede derivarse del `context` dado.
```python
# La plantilla de prompt para fidelidad incluye varios ejemplos (few-shot prompting)
# para que las instrucciones al LLM juez sean cristalinas.
faithfulness_prompt = PromptTemplate(
input_variables=["question","context", "generated_answer"],
template="""
Question: {question}
Context: {context}
Generated Answer: {generated_answer}
Evalúa si la respuesta generada a la pregunta puede deducirse del contexto.
Puntuación de 0 o 1, donde 1 es perfectamente fiel *Y PUEDE DERIVARSE DEL CONTEXTO* y 0 en caso contrario.
No te importa si la respuesta es correcta; todo lo que te importa es si la respuesta puede deducirse del contexto.
[... algunos ejemplos del notebook para guiar al LLM ...]
Ejemplo:
Question: ¿Cuánto es 2+2?
Context: 4.
Generated Answer: 4.
En este caso, el contexto dice '4', pero no proporciona información para deducir la respuesta a '¿Cuánto es 2+2?', por lo que la puntuación debe ser 0.
"""
)
# Construye la cadena de fidelidad usando el mismo LLM estructurado.
faithfulness_chain = faithfulness_prompt | llm.with_structured_output(ResultScore)
```
Hemos proporcionado varios ejemplos en el prompt para guiar el razonamiento del LLM, especialmente para casos límite complicados. Pruébalo con el ejemplo "2+2", que es una prueba clásica de fidelidad.
```python
def evaluate_faithfulness(question, context, generated_answer):
"""Una función auxiliar para ejecutar nuestra cadena de evaluación de fidelidad personalizada."""
result = faithfulness_chain.invoke({
"question": question,
"context": context,
"generated_answer": generated_answer
})
return result.score
# Prueba la cadena de fidelidad. ¿La respuesta es correcta, pero es fiel?
question = "¿cuánto es 3+3?"
context = "6"
generated_answer = "6"
score = evaluate_faithfulness(question, context, generated_answer)
print(f"Faithfulness Score: {score}")
#### OUTPUT ####
Faithfulness Score: 0.0
```
Esto demuestra el poder y la precisión de una métrica de fidelidad bien definida. Aunque la respuesta **6** es factualmente correcta, no podría deducirse lógicamente del contexto proporcionado "6".
El contexto no dijo **3+3 es igual a 6**. Nuestro sistema marcó correctamente esto como una respuesta infiel, que probablemente es una alucinación donde el LLM utilizó su propio conocimiento preentrenado en lugar del contexto proporcionado.
Construir estos evaluadores desde cero proporciona una comprensión profunda de lo que estamos midiendo. Sin embargo, puede llevar mucho tiempo. En la siguiente parte, veremos cómo lograr los mismos resultados de manera más eficiente utilizando marcos de evaluación especializados.
## Evaluación con Marcos
En la parte anterior, construimos nuestras propias cadenas de evaluación desde cero. Esta es una forma fantástica de entender los principios fundamentales de las métricas de RAG.
> Sin embargo, para pruebas más rápidas y robustas, los marcos de evaluación dedicados son el camino a seguir.
)](https://miro.medium.com/1*uBn-2vN1Bz--NXfaeR2hyw.png)
Estas bibliotecas proporcionan métricas preintegradas y ajustadas que manejan la complejidad de la evaluación por nosotros, permitiéndonos enfocarnos en analizar los resultados.
Exploraremos tres marcos populares: `deepeval`, `grouse` y el gigante específico de RAG, `RAGAS`.
### Evaluación Rápida con `deepeval`
`deepeval` es un marco poderoso y de código abierto diseñado para hacer que la evaluación de LLM sea simple e intuitiva. Proporciona un conjunto de métricas bien definidas que pueden aplicarse fácilmente a los resultados de tu pipeline de RAG.
El flujo de trabajo implica crear objetos `LLMTestCase` y medirlos contra métricas preintegradas como `Correctness`, `Faithfulness` y `ContextualRelevancy`.
```python
# Necesitarás instalar deepeval: pip install deepeval
from deepeval import evaluate
from deepeval.metrics import GEval, FaithfulnessMetric, ContextualRelevancyMetric
from deepeval.test_case import LLMTestCase
# Crear casos de prueba
test_case_correctness = LLMTestCase(
input="¿Cuál es la capital de España?",
expected_output="Madrid es la capital de España.",
actual_output="MadriD."
)
test_case_faithfulness = LLMTestCase(
input="¿cuánto es 3+3?",
actual_output="6",
retrieval_context=["6"]
)
# La función evaluate() ejecuta todos los casos de prueba contra todas las métricas especificadas
evaluation_results = evaluate(
test_cases=[test_case_correctness, test_case_faithfulness],
metrics=[GEval(name="Correctness", model="gpt-4o"), FaithfulnessMetric()]
)
print(evaluation_results)
#### OUTPUT ####
✨ Evaluation Results ✨
-------------------------
Overall Score: 0.50
-------------------------
Metrics Summary:
- Correctness: 1.00
- Faithfulness: 0.00
-------------------------
```
La vista agregada de `deepeval` nos da inmediatamente una imagen de alto nivel del desempeño de nuestro sistema, facilitando la identificación de áreas que necesitan mejora.
### Otra Alternativa Poderosa con `grouse`
`grouse` es otra excelente opción de código abierto, que ofrece un conjunto similar de métricas pero con un enfoque único en permitir una personalización profunda de los prompts del "juez". Esto es útil para ajustar los criterios de evaluación para un dominio específico.
``````python
# Necesitarás instalar grouse: pip install grouse-eval
from grouse import EvaluationSample, GroundedQAEvaluator
evaluator = GroundedQAEvaluator()
unfaithful_sample = EvaluationSample(
input="¿Dónde se encuentra la Torre Eiffel?",
actual_output="La Torre Eiffel se encuentra en la Rue Rabelais en París.",
references=[
"La Torre Eiffel es una torre de celosía de hierro forjado en el Campo de Marte en París, Francia",
"Gustave Eiffel murió en su apartamento en la Rue Rabelais en París."
]
)
result = evaluator.evaluate(eval_samples=[unfaithful_sample]).evaluations[0]
print(f"Puntuación de fidelidad de Grouse (0 o 1): {result.faithfulness.faithfulness}")
#### SALIDA ####
Puntuación de fidelidad de Grouse (0 o 1): 0
```
Como `deepeval`, `grouse` detecta efectivamente errores sutiles, proporcionando otra herramienta robusta para nuestro kit de herramientas de evaluación.
### Evaluación con `RAGAS`
Aunque `deepeval` y `grouse` son excelentes evaluadores de propósito general, **`RAGAS` (Retrieval-Augmented Generation Assessment)** es un marco construido específicamente para evaluar canalizaciones RAG. Proporciona un conjunto completo de métricas que miden cada componente de tu sistema, desde el recuperador hasta el generador.
Para usar `RAGAS`, primero necesitamos preparar nuestros datos de evaluación en un formato específico. Requiere cuatro piezas clave de información para cada caso de prueba:
- `question`: La consulta de entrada del usuario.
- `answer`: La respuesta final generada por nuestro sistema RAG.
- `contexts`: La lista de documentos recuperados por nuestro recuperador.
- `ground_truth`: La respuesta correcta de referencia.
Preparemos un conjunto de datos de ejemplo.
```python
# 1. Preparar los datos de evaluación
questions = [
"¿Cuál es el nombre del perro de tres cabezas que custodia la Piedra Filosofal?",
"¿Quién le dio a Harry Potter su primera escoba?",
"¿Qué casa consideró inicialmente el Sombrero Seleccionador para Harry?",
]
# Estas serían las respuestas generadas por nuestro sistema RAG
generated_answers = [
"El perro de tres cabezas se llama Fluffy.",
"La Profesora McGonagall le dio a Harry su primera escoba, una Nimbus 2000.",
"El Sombrero Seleccionador consideró fuertemente poner a Harry en Slytherin.",
]
# La verdad fundamental, o respuestas "perfectas"
ground_truth_answers = [
"Fluffy",
"Profesora McGonagall",
"Slytherin",
]
# El contexto recuperado por nuestro sistema RAG para cada pregunta
retrieved_documents = [
["Un perro masivo de tres cabezas custodiaba una trampilla. Hagrid mencionó que su nombre era Fluffy."],
["A los de primer año no se les permite escobas, pero la Profesora McGonagall, jefa de Gryffindor, hizo una excepción para Harry."],
["El Sombrero Seleccionador susurró en el oído de Harry, 'Podrías ser grande, ya sabes, todo está aquí en tu cabeza, y Slytherin te ayudará en el camino hacia la grandeza...'"],
]
```
A continuación, estructuramos estos datos utilizando la biblioteca `datasets` de Hugging Face, con la que `RAGAS` se integra sin problemas.
```python
# Necesitarás instalar ragas y datasets: pip install ragas datasets
from datasets import Dataset
# 2. Estructurar los datos en un objeto Dataset de Hugging Face
data_samples = {
'question': questions,
'answer': generated_answers,
'contexts': retrieved_documents,
'ground_truth': ground_truth_answers
}
dataset = Dataset.from_dict(data_samples)
```
Ahora podemos definir nuestras métricas y ejecutar la evaluación. `RAGAS` ofrece varias métricas poderosas específicas para RAG de forma inmediata.
```python
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_recall,
answer_correctness,
)
# 3. Definir las métricas que queremos usar para la evaluación
metrics = [
faithfulness, # ¿Qué tan consistente es la respuesta con el contexto? (Previene alucinaciones)
answer_relevancy, # ¿Qué tan relevante es la respuesta para la pregunta?
context_recall, # ¿Recuperamos todo el contexto necesario para responder la pregunta?
answer_correctness, # ¿Qué tan precisa es la respuesta en comparación con la verdad fundamental?
]
# 4. Ejecutar la evaluación
result = evaluate(
dataset=dataset,
metrics=metrics
)
# 5. Mostrar los resultados en un formato de tabla limpio
results_df = result.to_pandas()
print(results_df)
```
<script src="https://gist.github.com/khalid557557/7a4cf3b5e00280389b8a7394bf52001b.js"></script>
Podemos ver que nuestro sistema es altamente fiel y recupera contexto relevante bien (`faithfulness` y `context_recall` son perfectos). Las respuestas también son altamente relevantes y correctas, con solo desviaciones menores.
`RAGAS` hace increíblemente fácil ejecutar este tipo de evaluación integral de extremo a extremo, dándonos los datos que necesitamos para desplegar y mejorar con confianza nuestras aplicaciones RAG.
## Resumiendo todo
Entonces, resumamos lo que hemos hecho hasta ahora en nuestro camino para construir un sistema RAG listo para producción.
1. En la **Parte 1**, construimos un sistema RAG fundamental desde cero, cubriendo los tres componentes principales: **Indexación** de nuestros datos, **Recuperación** de contexto relevante y **Generación** de una respuesta final.
2. En la **Parte 2**, pasamos a **Transformaciones de consultas avanzadas**, utilizando técnicas como RAG-Fusion, Descomposición e HyDE para reescribir y expandir preguntas de usuarios para una recuperación mucho más precisa.
3. En la **Parte 3**, convertimos nuestro sistema en una centralita inteligente, agregando **Enrutamiento** para dirigir consultas a la fuente de datos correcta y **Estructuración de consultas** para aprovechar poderosos filtros de metadatos.
4. En la **Parte 4**, nos enfocamos en **Indexación avanzada**, explorando estrategias como Indexación de múltiples representaciones y ColBERT a nivel de token para crear una biblioteca de conocimiento más inteligente y eficiente.
5. En la **Parte 5**, pulimos la salida final con técnicas de **Recuperación avanzada** como la reordenación para priorizar el mejor contexto e introdujimos conceptos autocorrectivos y agentivos como CRAG y Self-RAG.
6. Finalmente, en las **Partes 6 y 7**, abordamos el paso crucial de **Evaluación**. Aprendimos cómo medir el rendimiento de nuestro sistema con métricas clave como fidelidad y corrección, tanto construyendo evaluadores desde cero como usando marcos poderosos como deepeval, grouse y RAGAS.
> Si disfrutas este blog, siéntete libre de **[seguirme en Medium](https://medium.com/@fareedkhandev)** solo escribo aquí.







