Los sistemas de recuperación de generación aumentada, mejor conocidos como sistemas RAG, se han convertido en el estándar de facto para crear asistentes de inteligencia artificial personalizados que respondan preguntas sobre datos empresariales personalizados sin las molestias de un costoso ajuste fino. Modelos de lenguaje grande (LLM)Uno de los principales desafíos de los sistemas RAG ingenuos es obtener la información de contexto recuperada correcta para responder a las consultas de los usuarios. La fragmentación divide los documentos en fragmentos de contexto más pequeños que a menudo pueden terminar perdiendo la información de contexto general de todo el documento. En esta guía, analizaremos y crearemos un sistema RAG contextual inspirado en El famoso método de recuperación contextual de Anthropic y combinarlo con búsqueda híbrida y reclasificación mediante un ejemplo práctico paso a paso. ¡Comencemos!
Tabla de contenidos.
Arquitectura del sistema RAG Naive
Una arquitectura de sistema estándar de generación aumentada de recuperación ingenua (RAG) generalmente consta de dos pasos principales:
- Procesamiento e indexación de datos
- Recuperación y generación de respuesta
En el paso 1, Procesamiento e indexación de datos, nos centramos en convertir nuestros datos empresariales personalizados en un formato más fácil de usar cargando normalmente el contenido de texto de estos documentos, dividiendo los elementos de texto grandes en fragmentos más pequeños (que suelen ser independientes y aislados), convirtiéndolos en incrustaciones mediante un modelo de incrustación y luego almacenando estos fragmentos e incrustaciones en una base de datos vectorial como se muestra en la siguiente figura.
En el paso 2, el flujo de trabajo comienza cuando el usuario hace una pregunta, los fragmentos de documentos de texto relevantes que son similares a la pregunta de entrada se recuperan de la base de datos vectorial y luego la pregunta y los fragmentos de documentos de contexto se envían a un LLM para generar una respuesta similar a la humana como se muestra en la siguiente figura.
Este flujo de trabajo de dos pasos se usa comúnmente en la industria para construir un sistema RAG estándar e ingenuo, sin embargo tiene su propio conjunto de limitaciones, algunas de las cuales analizamos a continuación en detalle.
Limitaciones del sistema RAG Naive
Los sistemas RAG ingenuos tienen varias limitaciones, algunas de las cuales se mencionan a continuación:
- Los documentos grandes se dividen en fragmentos aislados e independientes
- Pierde información contextual y el tema general del documento en fragmentos independientes más pequeños.
- El rendimiento y la calidad de la recuperación pueden verse afectados debido a los problemas mencionados anteriormente.
- La búsqueda estándar basada en similitud semántica a menudo no es suficiente
En este artículo nos centraremos especialmente en resolver las limitaciones de los sistemas RAG ingenuos en términos de añadir información contextual a fragmentos de documentos y mejorar la búsqueda semántica estándar con búsqueda híbrida y reclasificación.
Flujo de trabajo RAG híbrido estándar
Una forma de mejorar el rendimiento de los sistemas RAG estándar es utilizar un enfoque RAG híbrido. Básicamente, se trata de un sistema RAG impulsado por una búsqueda híbrida, que utiliza una combinación de búsqueda semántica y de palabras clave, como se muestra en la siguiente figura.
La idea, como se muestra en la figura anterior, es tomar los documentos, dividirlos en fragmentos utilizando cualquier mecanismo de división en fragmentos estándar, como la división recursiva de texto de caracteres, y luego crear incrustaciones a partir de estos fragmentos y almacenarlos en una base de datos vectorial para enfocarnos en la búsqueda semántica. También extraemos las palabras de estos fragmentos, contamos sus frecuencias y las normalizamos para obtener vectores TF-IDF y almacenarlos en un índice TF-IDF. También podríamos usar BM25 para representar estos vectores de fragmentos centrándonos más en la búsqueda de palabras clave. BM25 funciona basándose en el modelo de espacio vectorial TF-IDF (frecuencia de término-frecuencia inversa de documento). TF-IDF suele ser un valor que mide la importancia de una palabra para un documento en un corpus de documentos. BM25 refina esto utilizando la siguiente representación matemática.
Así, BM25 considera la longitud del documento y aplica una función de saturación a la frecuencia de los términos, lo que ayuda a evitar que las palabras comunes dominen los resultados.
Una vez creada la base de datos vectorial y el índice vectorial BM25, el sistema RAG híbrido funciona de la siguiente manera:
- La consulta del usuario ingresa y va al modelo de incrustación de la base de datos vectorial para obtener una incrustación de consulta y la base de datos vectorial utiliza la similitud semántica de incrustación para encontrar fragmentos de documentos similares de los K principales.
- La consulta del usuario también ingresa al índice vectorial BM25, se crea una representación vectorial de consulta y se recuperan los fragmentos de documentos similares principales mediante la similitud BM25.
- Combinamos y deduplicamos los resultados de las dos recuperaciones anteriores utilizando Fusión de rangos recíprocos (RRF)
- Estos fragmentos de documentos se envían como contexto junto con la consulta del usuario en un mensaje de instrucciones al LLM para generar una respuesta.
Si bien el RAG híbrido es mejor que el RAG ingenuo, aún tiene algunos problemas, como se destaca también en la investigación de Anthropic sobre el RAG contextual. El problema principal es que los documentos se dividen en fragmentos independientes y aislados. Funciona en muchos casos, pero a menudo, debido a que estos fragmentos carecen de contexto suficiente, la calidad de la recuperación y las respuestas pueden no ser lo suficientemente buenas. Esto se destaca claramente en el ejemplo dado por Anthropic en su Segun una investigacion.
También mencionan que este problema podría resolverse mediante recuperación contextual y han realizado varios experimentos al respecto.
Comprender la recuperación contextual
El objetivo principal de la recuperación contextual es mejorar la calidad de la información contextual en cada fragmento de documento. Esto se hace mediante anteponiendo Información de contexto explicativa específica de cada fragmento en cada fragmento con respecto al documento general. Solo entonces enviamos estos fragmentos para crear incrustaciones y vectores TF-IDF. El siguiente es un ejemplo de Anthropic que muestra cómo un fragmento puede transformarse en un fragmento contextual.
También ha habido otros enfoques para mejorar el contexto en el pasado, que incluyen: Agregar resúmenes de documentos genéricos a fragmentos , Incorporación hipotética de documentosy indexación basada en resúmenes. Basándose en experimentos, Anthropic descubrió que no funcionan tan bien como la recuperación contextual. Sin embargo, ¡siéntase libre de explorar, experimentar e incluso combinar enfoques!
Implementación de la recuperación contextual
Una forma ideal de incorporar contexto a cada fragmento es que los humanos lean cada documento, lo entiendan y luego agreguen información de contexto relevante a cada fragmento. Sin embargo, eso puede llevar mucho tiempo, especialmente si tiene muchos documentos y miles o incluso millones de fragmentos de documentos. Por lo tanto, podemos aprovechar el poder de los LLM de contexto largo como GPT-4o, Gemini 1.5 o Claude 3.5 y hacer esto automáticamente con algunas indicaciones inteligentes. El siguiente es un ejemplo de la indicación utilizada por Anthropic para indicarle a Claude 3.5 que ayude a obtener información de contexto para cada fragmento con respecto a su documento general.
Todo el documento se colocaría en el TODO EL DOCUMENTO variable de marcador de posición y cada fragmento se colocaría en el CONTENIDO DEL TROZO variable de marcador de posición. El texto contextual resultante, normalmente de 50 a 100 tokens (puede controlar la longitud mediante el mensaje de solicitud), se antepone al fragmento antes de crear la base de datos de vectores y los índices BM25.
Recuerde que, según su caso de uso, dominio y requisitos, puede modificar el mensaje anterior según sea necesario. Por ejemplo, en esta guía agregaremos contexto a fragmentos que pertenecen a artículos de investigación, por lo que utilicé el siguiente mensaje personalizado para generar el contexto para cada fragmento que luego se antepondría al fragmento.
Puede mencionar claramente lo que debe o no debe estar en la información de contexto de cada fragmento y también restricciones específicas como número de líneas, palabras, etc.
Arquitectura de preprocesamiento de recuperación contextual
La siguiente figura muestra el flujo arquitectónico de preprocesamiento para implementar la recuperación contextual. Recuerde que tiene la libertad de elegir sus propios cargadores y divisores de documentos según sus deseos, en función de sus experimentos y casos de uso.
En nuestro caso de uso, crearemos un sistema RAG con una combinación de documentos de diferentes fuentes y formatos. Tenemos artículos de Wikipedia breves de uno o dos párrafos disponibles como documentos JSON y tenemos algunos artículos de investigación de IA populares, disponibles como archivos PDF.
Flujo de trabajo con canalización de preprocesamiento
El siguiente flujo de trabajo se sigue en la cadena de preprocesamiento.
- Utilizamos un cargador de documentos JSON para extraer el contenido de texto de los artículos de Wikipedia en formato JSON. Como no son muy grandes, los conservamos tal como están y no los fragmentamos más.
- Utilizamos un cargador de documentos PDF como PyMuPDF para extraer el contenido de texto de cada archivo PDF.
- Luego, utilizamos una técnica de fragmentación de documentos, como la división recursiva de texto de caracteres, para fragmentar el texto del documento PDF en fragmentos más pequeños.
- A continuación, pasamos cada fragmento junto con todo el documento a una plantilla de solicitud de instrucciones (representada como la Solicitud de generador de contexto en la figura anterior).
- Luego, este mensaje se envía a un LLM de contexto largo como GPT-4o para generar información contextual para cada fragmento.
- Luego, la información de contexto de cada fragmento se antepone al contenido del fragmento.
- Recopilamos todos los fragmentos procesados que luego están listos para ser incrustados e indexados.
Recuerde que crear un contexto para cada fragmento es costoso porque la solicitud tendrá toda la información del documento que se enviará cada vez junto con el fragmento y se le cobrará en función de la cantidad de tokens, especialmente si está utilizando LLM comerciales. Hay algunas formas de abordar esto:
- Aproveche la función de almacenamiento en caché rápido de los LLM más populares como Claude y GPT-4o Lo que le permite ahorrar costes.
- No envíe el documento completo, sino quizás la página específica donde está el fragmento o algunas páginas cercanas al fragmento.
- Envió un resumen del documento en lugar del documento completo
Experimente siempre con lo que funcione mejor para su situación; recuerde que no existe un único método que sea el mejor para el preprocesamiento contextual. Ahora conectemos esta secuencia de comandos a la secuencia de comandos RAG general y hablemos sobre la arquitectura general de RAG contextual.
RAG contextual con arquitectura híbrida de búsqueda y reclasificación
La siguiente figura muestra el flujo de arquitectura de extremo a extremo para nuestro sistema RAG contextual que también implementa búsqueda híbrida y reclasificación para mejorar la calidad de los fragmentos de documentos recuperados antes de la generación de la respuesta.
Flujo de trabajo de preprocesamiento contextual
El lado izquierdo de la figura anterior muestra el flujo de trabajo de preprocesamiento contextual que acabamos de analizar en la sección anterior. Aquí asumimos que este preprocesamiento del paso anterior ya se llevó a cabo y ahora tenemos los fragmentos de documentos procesados (con información contextual agregada) listos para ser indexados.
primer Paso
El primer paso aquí implica tomar estos fragmentos de documentos y pasarlos a través de un modelo de incrustación relevante como el de OpenAI. Modelo de incrustación de texto 3-Small y crear incrustaciones de fragmentos. Estos luego se indexan en una base de datos vectorial como la Base de datos de vectores cromáticos que es una base de datos vectorial liviana y de código abierto que permite una recuperación semántica súper rápida (generalmente mediante el uso de similitud de coseno incrustado) para recuperar fragmentos de documentos similares a las consultas del usuario.
Segundo paso
El siguiente paso es tomar los mismos fragmentos de documentos y crear vectores de frecuencia de palabras clave dispersos (TF-IDF) e indexarlos en un índice BM25 que utilizará la similitud BM25 como describimos anteriormente para recuperar fragmentos de documentos similares para las consultas del usuario.
Ahora, en función de una consulta de usuario que ingresa al sistema, como se muestra en la figura anterior a la derecha, primero recuperamos fragmentos de documentos similares de la base de datos Vector y el índice BM25. Luego, utilizamos un recuperador de conjuntos para habilitar la búsqueda híbrida, donde tomamos los documentos recuperados de la búsqueda semántica y de palabras clave de la base de datos Vector y el índice BM25 y tomamos fragmentos de documentos únicos (deduplicación) y luego usamos Fusión de rangos recíprocos (RRF) para reclasificar aún más los documentos e intentar clasificar en un lugar más alto fragmentos de documentos más relevantes.
Tercer paso
A continuación, pasamos los fragmentos de consulta y documento a un reranker para centrarnos en la clasificación basada en la relevancia en lugar de solo en la clasificación basada en la similitud. El reranker que utilizamos en nuestra implementación es el popular Rerankeador BGE de BAAI que se encuentra alojado en Hugging Face y es de código abierto. Tenga en cuenta que necesita una GPU para ejecutar esto más rápido (o también puede usar rerankers basados en API que suelen ser comerciales y tienen un costo). En este paso, los fragmentos del documento de contexto se reclasifican en función de su relevancia para la consulta de entrada.
Último paso
Por último, enviamos la consulta del usuario y los fragmentos del documento de contexto reclasificados a una plantilla de solicitud de instrucciones que indica al LLM que utilice la información de contexto solo para responder la consulta del usuario. Luego, esta se envía al LLM (en nuestro caso, usamos GPT-4o) para la generación de respuestas.
Finalmente, obtenemos la respuesta contextual relevante a la consulta del usuario desde el LLM y eso completa el flujo general. ¡Implementemos este flujo de trabajo de principio a fin en la siguiente sección!
Implementación práctica de nuestro sistema RAG contextual
Ahora implementaremos el flujo de trabajo de extremo a extremo para nuestro sistema RAG contextual basado en la arquitectura que discutimos en detalle en la sección anterior, paso a paso con explicaciones detalladas, código y resultados.
Instalar dependencias
Comenzamos instalando las dependencias necesarias que serán las librerías que usaremos para construir nuestro sistema. Esto incluye langchain, pymupdf, jq, así como dependencias necesarias como openai, chroma y bm25.
!pip install langchain==0.3.4
!pip install langchain-openai==0.2.3
!pip install langchain-community==0.3.3
!pip install jq==1.8.0
!pip install pymupdf==1.24.12
!pip install httpx==0.27.2
# install vectordb and bm25 utils
!pip install langchain-chroma==0.1.4
!pip install rank_bm25==0.2.2
Ingrese la clave API abierta de AI
Ingresamos nuestra clave Open AI usando la función getpass() para no exponer accidentalmente nuestra clave en el código.
from getpass import getpass
OPENAI_KEY = getpass('Enter Open AI API Key: ')
Configurar variables de entorno
A continuación, configuramos algunas variables de entorno del sistema que se utilizarán más adelante al autenticar nuestro LLM.
import os
os.environ['OPENAI_API_KEY'] = OPENAI_KEY
Obtener el conjunto de datos
Descargamos nuestro conjunto de datos que consta de algunos artículos de Wikipedia en formato JSON y algunos documentos de investigación en formato PDF desde nuestro Google Drive de la siguiente manera
!gdown 1aZxZejfteVuofISodUrY2CDoyuPLYDGZ
Salida:
Descargando ...
Desde: https://drive.google.com/uc?id=1aZxZejfteVuofISodUrY2CDoyuPLYDGZ
Para: /content/rag_docs.zip
100% 5.92M/5.92M [00:00<00:00, 134MB/s]
Luego descomprimimos y extraemos los documentos del archivo comprimido.
!unzip rag_docs.zip
Salida:
Archivo: rag_docs.zip
creando: rag_docs/
inflando: rag_docs/attention_paper.pdf
inflando: rag_docs/cnn_paper.pdf
inflando: rag_docs/resnet_paper.pdf
inflando: rag_docs/vision_transformer.pdf
inflar: rag_docs/wikidata_rag_demo.jsonl
Ahora preprocesaremos los documentos según sus tipos.
Cargar y procesar documentos JSON de Wikipedia
Ahora cargaremos los documentos de Wikipedia desde el archivo JSON y los procesaremos.
from langchain.document_loaders import JSONLoader
loader = JSONLoader(file_path='./rag_docs/wikidata_rag_demo.jsonl',
jq_schema='.',
text_content=False,
json_lines=True)
wiki_docs = loader.load()
wiki_docs[3]
Salida:
Documento(metadatos={'fuente': '/content/rag_docs/wikidata_rag_demo.jsonl',
'seq_num': 4}, page_content='{"id": "71548", "title": "Chi-cuadrado
distribución", "párrafos": ["En teoría de probabilidad y estadística, la
Distribución de chi-cuadrado (también distribución de chi-cuadrado o fórmula_1u00a0)
es una de las distribuciones de probabilidad teóricas más utilizadas. Chi-
La distribución cuadrada con fórmula de 2 grados de libertad se escribe como
formula_3. ... Otra es que las diferentes variables aleatorias (o
"Las observaciones) deben ser independientes entre sí."]}')
Ahora los convertimos en documentos LangChain ya que resulta más fácil procesarlos e indexarlos más adelante e incluso agregamos campos de metadatos adicionales si es necesario.
import json
from langchain.docstore.document import Document
wiki_docs_processed = []
for doc in wiki_docs:
doc = json.loads(doc.page_content)
metadata = {
"title": doc['title'],
"id": doc['id'],
"source": "Wikipedia",
"page": 1
}
data = ' '.join(doc['paragraphs'])
wiki_docs_processed.append(Document(page_content=data, metadata=metadata))
wiki_docs_processed[3]
Salida
Documento(metadata={'title': 'Distribución de chi-cuadrado', 'id': '71548',
'fuente': 'Wikipedia', 'página': 1}, page_content='En teoría de la probabilidad y
estadística, la distribución chi-cuadrado (también chi-cuadrado o formula_1xa0
La distribución de probabilidad (distribución de probabilidad) es una de las teorías de probabilidad más utilizadas.
distribuciones. La distribución de chi-cuadrado con fórmula_2 grados de libertad es
escrito como fórmula_3. ... Otra es que las diferentes variables aleatorias
(o las observaciones) deben ser independientes entre sí.')
Cargar y procesar documentos de investigación en formato PDF con información contextual
Ahora cargaremos los archivos PDF de los trabajos de investigación, los procesaremos y también agregaremos información contextual a cada fragmento para permitir la recuperación contextual como lo discutimos anteriormente. Comenzamos creando una cadena LangChain para generar información contextual para los fragmentos de la siguiente manera.
# create chunk context generation chain
from langchain.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from langchain_openai import ChatOpenAI
chatgpt = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
def generate_chunk_context(document, chunk):
chunk_process_prompt = """You are an AI assistant specializing in research
paper analysis. Your task is to provide brief,
relevant context for a chunk of text based on the
following research paper.
Here is the research paper:
<paper>
{paper}
</paper>
Here is the chunk we want to situate within the whole
document:
<chunk>
{chunk}
</chunk>
Provide a concise context (3-4 sentences max) for this
chunk, considering the following guidelines:
- Give a short succinct context to situate this chunk
within the overall document for the purposes of
improving search retrieval of the chunk.
- Answer only with the succinct context and nothing
else.
- Context should be mentioned like 'Focuses on ....'
do not mention 'this chunk or section focuses on...'
Context:
"""
prompt_template = ChatPromptTemplate.from_template(chunk_process_prompt)
agentic_chunk_chain = (prompt_template
|
chatgpt
|
StrOutputParser())
context = agentic_chunk_chain.invoke({'paper': document, 'chunk': chunk})
return context
Usamos esto para generar información de contexto para fragmentos de nuestros artículos de investigación utilizando LangChain.
Aquí hay una breve explicación:
- Modelo ChatGPT:Inicializa ChatOpenAI con temperatura 0 para obtener resultados consistentes y utiliza el LLM GPT-4o-mini.
- generar_contexto_fragmento Función:
- Entradas: documento (artículo completo) y fragmento (sección específica).
- Construye un mensaje para indicarle a la IA que resuma el contexto del fragmento en relación con el documento.
- Prompt: Guía al LLM para crear un contexto corto (3-4 oraciones) enfocado en mejorar la recuperación de la búsqueda y evitar frases repetitivas.
- Configuración de la cadena:Combina el mensaje de aviso, el modelo chatgpt y StrOutputParser() para el procesamiento estructurado.
- Ejecución:Genera y devuelve un contexto sucinto para el fragmento.
A continuación, definimos una función de preprocesamiento para cargar cada documento PDF, dividirlo en fragmentos mediante división recursiva de texto de caracteres, generar contexto para cada fragmento mediante la canalización anterior y agregar el contexto al comienzo (anteponer) de cada fragmento.
from langchain.document_loaders import PyMuPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import uuid
def create_contextual_chunks(file_path, chunk_size=3500, chunk_overlap=0):
print('Loading pages:', file_path)
loader = PyMuPDFLoader(file_path)
doc_pages = loader.load()
print('Chunking pages:', file_path)
splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,
chunk_overlap=chunk_overlap)
doc_chunks = splitter.split_documents(doc_pages)
print('Generating contextual chunks:', file_path)
original_doc = 'n'.join([doc.page_content for doc in doc_chunks])
contextual_chunks = []
for chunk in doc_chunks:
chunk_content = chunk.page_content
chunk_metadata = chunk.metadata
chunk_metadata_upd = {
'id': str(uuid.uuid4()),
'page': chunk_metadata['page'],
'source': chunk_metadata['source'],
'title': chunk_metadata['source'].split('/')[-1]
}
context = generate_chunk_context(original_doc, chunk_content)
contextual_chunks.append(Document(page_content=context+'n'+chunk_content,
metadata=chunk_metadata_upd))
print('Finished processing:', file_path)
print()
return contextual_chunks
La función anterior procesa los documentos de investigación en formato PDF en fragmentos contextualizados para un mejor análisis y recuperación. A continuación, se ofrece una breve explicación:
- Importaciones:
- Utiliza PyMuPDFLoader para cargar PDF y RecursiveCharacterTextSplitter para dividir el texto.
- uuid genera identificadores únicos para cada fragmento.
- crear_fragmentos_contextuales Función:
- Ingresos:Ruta de archivo, tamaño de fragmento y tamaño de superposición.
- Proceso:
- Carga las páginas del documento utilizando PyMuPDFLoader.
- Divide el documento en fragmentos más pequeños utilizando RecursiveCharacterTextSplitter.
- Para cada fragmento:
- Los metadatos se actualizan con una identificación única, número de página, fuente y título.
- Genera información contextual para el fragmento utilizando generate_chunk_context que definimos anteriormente.
- Antepone el contexto al fragmento original y luego lo agrega a una lista como un objeto de documento.
- Salida:Devuelve una lista de fragmentos procesados con metadatos contextuales y contenido.
Esta función carga los documentos de investigación en formato PDF, los divide en fragmentos y agrega un contexto significativo a cada fragmento. Ahora ejecutamos esta función en nuestros archivos PDF de la siguiente manera.
from glob import glob
pdf_files = glob('./rag_docs/*.pdf')
paper_docs = []
for fp in pdf_files:
paper_docs.extend(create_contextual_chunks(file_path=fp,
chunk_size=3500))
Salida:
Cargando páginas: ./rag_docs/attention_paper.pdf
Fragmentación de páginas: ./rag_docs/attention_paper.pdf
Generando fragmentos contextuales: ./rag_docs/attention_paper.pdf
Procesamiento finalizado: ./rag_docs/attention_paper.pdfCargando páginas: ./rag_docs/resnet_paper.pdf
Fragmentación de páginas: ./rag_docs/resnet_paper.pdf
Generando fragmentos contextuales: ./rag_docs/resnet_paper.pdf
Procesamiento finalizado: ./rag_docs/resnet_paper.pdf
...
paper_docs[0]
Salida:
Document(metadata={'id': 'd5c90113-2421-42c0-bf09-813faaf75ac7', 'page': 0,
'fuente': './rag_docs/resnet_paper.pdf', 'título': 'resnet_paper.pdf'},
page_content='Se centra en la introducción de un marco de aprendizaje residual
Diseñado para facilitar el entrenamiento de redes neuronales significativamente más profundas,
Abordar desafíos como la desaparición de gradientes y la degradación de
precisión. Destaca el éxito empírico de las redes residuales,
En particular, su desempeño en el conjunto de datos ImageNet y sus
papel fundamental en la victoria en múltiples competiciones en 2015.nResidual profundo
Aprendizaje para el reconocimiento de imágenesKaiming HenXiangyu ZhangnShaoqing
RennJian SunnMicrosoft Researchn{kahe, v-xiangz, v-shren,
jiansun}@microsoft.comnResumennLas redes neuronales más profundas son más difíciles
para entrenar. Presentamos un marco de aprendizaje residual para facilitar la capacitación de
redes que son sustancialmente más profundas que las utilizadas anteriormente...')
En el fragmento anterior, puede ver que tenemos información contextual generada por LLM seguida del contenido del fragmento real. Por último, combinamos todos los fragmentos de documentos de nuestros documentos JSON y PDF en una sola lista.
total_docs = wiki_docs_processed + paper_docs
len(total_docs)
Salida:
1880
Crear un índice de base de datos vectorial y configurar la recuperación semántica
Ahora crearemos incrustaciones para nuestros fragmentos de documentos y los indexaremos en nuestra base de datos vectorial utilizando el siguiente código:
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
openai_embed_model = OpenAIEmbeddings(model='text-embedding-3-small')
# create vector DB of docs and embeddings - takes < 30s on Colab
chroma_db = Chroma.from_documents(documents=total_docs,
collection_name='my_context_db',
embedding=openai_embed_model,
collection_metadata={"hnsw:space": "cosine"},
persist_directory="./my_context_db")
Luego, configuramos una estrategia de recuperación semántica que utiliza la similitud de incrustación de coseno y recupera los 5 fragmentos de documentos principales similares a las consultas del usuario.
similarity_retriever = chroma_db.as_retriever(search_type="similarity",
search_kwargs={"k": 5})
Crear índice BM25 y configurar la recuperación de palabras clave
Ahora crearemos vectores TF-IDF para nuestros fragmentos de documentos y los indexaremos en nuestro índice BM25 y configuraremos un recuperador para usar BM25 para devolver los 5 fragmentos de documentos principales de manera similar a las consultas del usuario usando el siguiente código.
from langchain.retrievers import BM25Retriever
bm25_retriever = BM25Retriever.from_documents(documents=total_docs,
k=5)
Habilitar la búsqueda híbrida con recuperación de conjuntos
Ahora habilitaremos la ejecución de una búsqueda híbrida durante la recuperación mediante un recuperador de conjunto que combina los resultados de la recuperación semántica y de palabras clave y utiliza la fusión de rangos recíprocos (RRF), como hemos comentado anteriormente. También podemos dar pesos específicos a cada recuperador y, en este caso, damos la misma ponderación a cada uno.
from langchain.retrievers import EnsembleRetriever
# reciprocal rank fusion
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, similarity_retriever],
weights=[0.5, 0.5]
)
Mejorando el retriever con Reranker
Ahora, conectaremos nuestro modelo de reclasificación que analizamos anteriormente para reclasificar los fragmentos de documentos de contexto del recuperador de conjuntos en función de su relevancia para la consulta de entrada. Aquí utilizamos un modelo de reclasificación de codificador cruzado de código abierto.
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain.retrievers import ContextualCompressionRetriever
# download an open-source reranker model - BAAI/bge-reranker-v2-m3
reranker = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
reranker_compressor = CrossEncoderReranker(model=reranker, top_n=5)
# Retriever 2 - Uses a Reranker model to rerank retrieval results from the previous retriever
final_retriever = ContextualCompressionRetriever(
base_compressor=reranker_compressor,
base_retriever=ensemble_retriever
)
Poniendo a prueba nuestro pipeline de recuperación
Ahora probaremos nuestro proceso de recuperación aprovechando la búsqueda híbrida y la reclasificación para ver cómo funciona en algunas consultas de usuarios de muestra.
from IPython.display import display, Markdown
def display_docs(docs):
for doc in docs:
print('Metadata:', doc.metadata)
print('Content Brief:')
display(Markdown(doc.page_content[:1000]))
print()
query = "what is machine learning?"
top_docs = final_retriever.invoke(query)
display_docs(top_docs)
Salida:
Metadatos: {'id': '564928', 'page': 1, 'source': 'Wikipedia', 'title':
'Aprendizaje automático'Resumen de contenido:
El aprendizaje automático brinda a las computadoras la capacidad de aprender sin ser...
Programación explícita (Arthur Samuel, 1959). Es un subcampo de la informática.
Ciencia. La idea surgió del trabajo en inteligencia artificial. Máquina
El aprendizaje explora el estudio y la construcción de algoritmos...Metadatos: {'id': '663523', 'page': 1, 'source': 'Wikipedia', 'title': 'Profundo
aprendiendo'}Resumen de contenido:
Aprendizaje profundo (también llamado aprendizaje estructurado profundo o aprendizaje jerárquico)
Es un tipo de aprendizaje automático que se utiliza principalmente con ciertos tipos de
redes neuronales...
...
query = "what is the difference between transformers and vision transformers?"
top_docs = final_retriever.invoke(query)
display_docs(top_docs)
Salida:
Metadatos: {'id': '07117bc3-34c7-4883-aa9b-6f9888fc4441', 'page': 0, 'source':
'./rag_docs/transformador_de_vision.pdf', 'título': 'transformador_de_vision.pdf'}Resumen de contenido:
Se centra en la introducción del modelo Vision Transformer (ViT), que
aplica una arquitectura Transformer pura a las tareas de clasificación de imágenes mediante
tratando parches de imagen como tokens...Metadatos: {'id': 'b896c93d-6330-421c-a236-af9437e9c725', 'page': 1, 'source':
'./rag_docs/transformador_de_vision.pdf', 'título': 'transformador_de_vision.pdf'}Resumen de contenido:
Se centra en el rendimiento del Transformador de Visión (ViT) en comparación con
redes neuronales convolucionales (CNN), destacando las ventajas de las redes neuronales de gran tamaño.
entrenamiento a escala en conjuntos de datos como ImageNet-21k y JFT-300M. Se analiza cómo
ViT logra resultados de vanguardia en los puntos de referencia de reconocimiento de imágenes a pesar de
Carece de ciertos sesgos inductivos inherentes a las CNN. Además,
referencias relacionadas con trabajos sobre mecanismos de autoatención......
En general, parece que funciona bastante bien y que se obtienen los fragmentos de contexto adecuados con información contextual agregada. Construyamos ahora nuestra secuencia de comandos RAG.
Construyendo nuestra línea de productos RAG contextual
Ahora juntaremos todos los componentes y crearemos nuestro pipeline RAG contextual de extremo a extremo. Comenzaremos construyendo una plantilla de instrucciones RAG estándar.
from langchain_core.prompts import ChatPromptTemplate
rag_prompt = """You are an assistant who is an expert in question-answering tasks.
Answer the following question using only the following pieces of
retrieved context.
If the answer is not in the context, do not make up answers, just
say that you don't know.
Keep the answer detailed and well formatted based on the
information from the context.
Question:
{question}
Context:
{context}
Answer:
"""
rag_prompt_template = ChatPromptTemplate.from_template(rag_prompt)
La plantilla de solicitud toma fragmentos de documentos de contexto recuperados y le indica al LLM que los use para responder las consultas de los usuarios. Por último, creamos nuestra secuencia de comandos RAG utilizando la sintaxis declarativa LCEL de LangChain, que muestra claramente el flujo de información en la secuencia de comandos paso a paso.
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI
chatgpt = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
def format_docs(docs):
return "nn".join(doc.page_content for doc in docs)
qa_rag_chain = (
{
"context": (final_retriever
|
format_docs),
"question": RunnablePassthrough()
}
|
rag_prompt_template
|
chatgpt
)
La cadena es nuestro flujo de trabajo de generación aumentada de recuperación (RAG) que procesa fragmentos de documentos recuperados para responder a las consultas de los usuarios mediante LangChain. Estos son los componentes clave:
- Manejo de entrada:
- "contexto":
- Comienza con nuestro final_retriever (recupera documentos relevantes mediante búsqueda híbrida + reclasificación).
- Pasa los documentos recuperados a la función format_docs, que formatea el contenido del documento en una cadena estructurada.
- "pregunta":
- Utiliza RunnablePassthrough() para pasar directamente la consulta del usuario sin ninguna modificación.
- "contexto":
- Plantilla de solicitud:
- Combina el contexto formateado y la pregunta del usuario en rag_prompt_template.
- Esto le indica al modelo que responda basándose únicamente en el contexto proporcionado.
- Ejecución del modelo:
- Pasa el mensaje completado al modelo chatgpt (gpt-4o-mini) para la generación de respuesta con temperatura 0 para respuestas deterministas.
Esta cadena garantiza que el LLM responda las preguntas utilizando únicamente la información recuperada relevante, brindando respuestas basadas en el contexto sin alucinaciones. ¡Lo único que queda ahora es probar nuestro sistema RAG!
Poniendo a prueba nuestro sistema RAG contextual
Ahora probemos nuestro sistema RAG contextual en algunas consultas de muestra como se muestra en los ejemplos a continuación.
from IPython.display import display, Markdown
query = "What is machine learning?"
result = qa_rag_chain.invoke(query)
display(Markdown(result.content))
Salida
El aprendizaje automático es un subcampo de la informática que proporciona a las computadoras...
la capacidad de aprender sin ser programado explícitamente. El concepto fue
Introducido por Arthur Samuel en 1959 y tiene sus raíces en la ingeniería artificial.
inteligencia. El aprendizaje automático se centra en el estudio y la construcción de
algoritmos que pueden aprender de los datos y hacer predicciones o decisiones basadas en ellos
sobre esos datos. Estos algoritmos siguen instrucciones programadas pero también pueden
adaptar y mejorar su desempeño mediante la construcción de modelos a partir de entradas de muestra.El aprendizaje automático es particularmente útil en escenarios donde el diseño y
La programación de algoritmos explícitos no es práctica. Algunas aplicaciones comunes de
El aprendizaje automático incluye:1. Filtrado de spam
2. Detección de intrusos en la red o personas malintencionadas
3. Reconocimiento óptico de caracteres (OCR)
4. Motores de búsqueda
5. Visión por computadoraDentro del ámbito del aprendizaje automático, existe un subconjunto conocido como aprendizaje profundo.
aprendizaje, que utiliza principalmente ciertos tipos de redes neuronales.
El aprendizaje implica sesiones de aprendizaje que pueden ser no supervisadas, semipresenciales.
supervisado, o supervisado, y a menudo incluye múltiples capas de
procesamiento, lo que permite que el modelo aprenda cada vez más cosas abstractas
representaciones de los datos.En general, el aprendizaje automático representa un avance significativo en la capacidad
de las computadoras para procesar información y tomar decisiones informadas basadas en
Esa información.
query = "How is a resnet better than a CNN?"
result = qa_rag_chain.invoke(query)
display(Markdown(result.content))
Salida
Se considera que una ResNet (red residual) es mejor que una CNN tradicional
(Red neuronal convolucional) por varias razones, particularmente en el
contexto de entrenamiento de arquitecturas más profundas y consecución de un mejor rendimiento en
Varias tareas. Estas son las principales ventajas de las ResNets sobre las CNN estándar:1. Mitigación del problema de degradación: las CNN tradicionales a menudo enfrentan el
Problema de degradación, donde el aumento de la profundidad de la red conduce a
Error de entrenamiento más alto. ResNets soluciona este problema introduciendo un atajo
Conexiones que permiten que los gradientes fluyan más fácilmente durante la retropropagación.
Esto hace que sea más fácil optimizar redes más profundas, ya que el aprendizaje residual
El marco permite que el modelo aprenda asignaciones residuales en lugar de
asignaciones originales sin referencia.2. Mayor precisión con mayor profundidad: las ResNets pueden ser significativamente más profundas.
que las CNN tradicionales sin sufrir degradación del rendimiento.
Por ejemplo, se han desarrollado arquitecturas ResNet con 50, 101 o incluso 152 capas.
Se ha demostrado que se logra una mayor precisión en comparación con redes menos profundas.
Los resultados empíricos demuestran que las ResNets más profundas pueden producir resultados sustancialmente mejores.
mejores resultados en conjuntos de datos como ImageNet y CIFAR-10.3. Rendimiento de generalización: las ResNets muestran una buena generalización.
rendimiento en diversas tareas de reconocimiento. El contexto menciona que
Reemplazar VGG-16 por ResNet-101 en el marco Faster R-CNN condujo a una
aumento notable en las métricas de detección en conjuntos de datos desafiantes como COCO,
lo que indica que las ResNets pueden generalizarse mejor a datos no vistos.4. Eficiencia arquitectónica: A pesar de ser más profundas, las ResNets mantienen niveles más bajos
complejidad computacional en comparación con arquitecturas tradicionales como VGG-16.
Por ejemplo, una ResNet de 152 capas tiene una complejidad menor (11.3 mil millones de FLOP).
que VGG-16 (15.3 mil millones de FLOP), lo que permite un entrenamiento más eficiente y
inferencia.5. Éxito empírico en competiciones: Las ResNets han logrado los primeros puestos en
Varias competiciones, como ILSVRC y COCO 2015, que demuestran su
eficacia en aplicaciones del mundo real. El contexto destaca que los modelos
Basado en redes residuales profundas ganó primeros lugares en varias pistas,
demostrando su desempeño superior.En resumen, las ResNets mejoran las CNN tradicionales al abordar eficazmente
El problema de la degradación, que permite entrenar arquitecturas más profundas
con éxito, logrando una mayor precisión y demostrando una gran solidez.
Capacidades de generalización en diferentes tareas.
query = "How does a resnet work?"
result = qa_rag_chain.invoke(query)
display(Markdown(result.content))
Salida
Una ResNet, o red residual, funciona según el principio de aprendizaje residual.
para abordar los desafíos asociados con el entrenamiento de redes neuronales profundas.
Aquí hay una explicación detallada de cómo funciona:Conceptos clave de ResNet
1. Mapeo Residual:
En lugar de aprender directamente el mapeo subyacente deseado (H(x)), ResNets
Concéntrese en aprender una función residual (F(x) = H(x) - x). Esto significa que
La red aprende la diferencia entre la salida deseada y la entrada,
que a menudo es más fácil de optimizar.2. Conexiones de acceso directo:
Las ResNets utilizan conexiones de acceso directo que omiten una o más capas.
Las conexiones realizan un mapeo de identidad, lo que permite agregar la entrada (x)
directamente a la salida de las capas apiladas. Esto se puede calcular matemáticamente.
representado como: [ H(x) = F(x) + x ]La adición de la entrada (x) ayuda a mitigar el gradiente que desaparece.
problema, facilitando así el aprendizaje de la red.3. Beneficios de la optimización:
La formulación de ( F(x) + x ) permite que la red empuje el residuo (
F(x) ) hacia cero si la aplicación de identidad es óptima. Esto es generalmente
más fácil que ajustar directamente un mapeo complejo, especialmente porque la profundidad de
La red aumenta.Arquitectura
1. Las ResNets se pueden construir con varias profundidades, como 18, 34, 50, 101,
y hasta 152 capas. La arquitectura incluye:Capas convolucionales: estas capas extraen características de las imágenes de entrada.
Normalización por lotes: se aplica después de cada convolución para estabilizar y
acelerar el entrenamiento.Capas de agrupación: se utilizan para reducir el tamaño de los mapas de características.
Capas totalmente conectadas: Al final de la red para tareas de clasificación.
Performance
1. Las ResNets han mostrado mejoras significativas en la precisión a medida que aumenta la profundidad.
aumenta, a diferencia de las redes simples tradicionales, que sufren mayores
errores de entrenamiento con mayor profundidad. Por ejemplo, una ResNet de 34 capas
Supera a una ResNet de 18 capas, lo que demuestra que se pueden crear redes más profundas.
entrenado eficazmente sin degradación del rendimiento.Resultados empíricos
1. Se han realizado experimentos exhaustivos en conjuntos de datos como ImageNet y CIFAR-10.
validaron la eficacia de las ResNets. Han logrado avances de última generación
Resultados, incluida la victoria en la competencia ILSVRC 2015 con 152 capas
ResNet, que tenía menor complejidad que modelos anteriores como VGG-16/19.En resumen, las ResNets aprovechan el aprendizaje residual y las conexiones de acceso directo para
Facilitar el entrenamiento de redes muy profundas, superando la optimización
dificultades que suelen surgir con una mayor profundidad. Esta arquitectura
Ha demostrado ser muy eficaz en diversas tareas de reconocimiento de imágenes.
query = "What is the difference between AI, ML and DL?"
result = qa_rag_chain.invoke(query)
display(Markdown(result.content))
Aquí está el resultado
La diferencia entre IA, ML y DL se puede resumir de la siguiente manera:Inteligencia Artificial (AI)
1. Definición: La IA se refiere a la capacidad de un programa informático o una máquina para
Piensa y aprende, imitando la cognición humana. Abarca una amplia gama de
tecnologías y aplicaciones destinadas a hacer que las máquinas sean "inteligentes".2. Origen: El término "Inteligencia Artificial" fue acuñado por John McCarthy en
1955.3. Funcionalidad: Los sistemas de IA pueden interpretar datos externos, aprender de ellos y
adaptarse para lograr objetivos específicos. A medida que avanza la tecnología, las tareas una vez
Se considera que requieren inteligencia, como el reconocimiento óptico de caracteres.
Ya no se clasifica como IA.Aprendizaje automático (ML)
1. Definición: ML es un subcampo de la IA que se centra en el desarrollo de
algoritmos que permiten a las computadoras aprender y hacer predicciones basadas en
datos sin ser programados explícitamente.2. Funcionalidad: Los algoritmos ML construyen modelos a partir de entradas de muestra y pueden hacer
decisiones o predicciones basadas en datos. Es particularmente útil en
Escenarios en los que la programación tradicional no es práctica, como el spam.
filtrado y visión por computadora.Aprendizaje profundo (DL)
1. Definición: DL es un subconjunto especializado del aprendizaje automático que principalmente
utiliza redes neuronales con múltiples capas (redes neuronales multicapa) para
procesar datos.2. Funcionalidad: En el aprendizaje profundo, la información procesada se convierte en
cada vez más abstracto con cada capa añadida, lo que lo hace particularmente
Eficaz para tareas complejas como el reconocimiento de voz e imágenes. Los modelos DL son
inspirados en el sistema nervioso biológico pero difieren significativamente de los
Propiedades estructurales y funcionales del cerebro humano.En resumen, la IA es un campo general que incluye tanto el aprendizaje automático como el aprendizaje automático, siendo el aprendizaje automático
siendo un enfoque específico dentro de la IA que permite el aprendizaje a partir de datos y aprendizaje automático.
siendo una especialización adicional de ML que utiliza redes neuronales profundas para
Tareas de procesamiento de datos más complejas.
query = "What is the difference between transformers and vision transformers?"
result = qa_rag_chain.invoke(query)
display(Markdown(result.content))
Salida
La principal diferencia entre los Transformers tradicionales y los Vision
Los transformadores (ViT) se basan en su aplicación y métodos de procesamiento de entrada.1. Representación de entrada:
Transformadores: En el procesamiento del lenguaje natural (PLN), los transformadores operan en
secuencias de tokens (palabras) que normalmente se representan como incrustaciones.
La entrada es una secuencia 1D de estas incrustaciones de tokens.Transformadores de visión (ViT): ViT adapta la arquitectura del transformador para la imagen
tareas de clasificación al tratar los parches de imagen como tokens. Una imagen es
dividido en parches de tamaño fijo, que luego se aplanan y se colocan linealmente.
incrustado en una secuencia. Esta secuencia de incrustaciones de parches se introduce en la
Transformador, similar a cómo se procesan las incrustaciones de palabras en PNL.2. Arquitectura:
Transformadores: La arquitectura estándar de Transformer consta de capas de
Redes neuronales de autoatención y retroalimentación multidireccionales, diseñadas para
capturar relaciones y dependencias en datos secuenciales.Transformadores de visión (ViT): Si bien ViT conserva el núcleo del Transformador
arquitectura, modifica la entrada para acomodar datos de imagen 2D. El modelo
incluye componentes adicionales como incrustaciones de posición para conservar la información espacial
información sobre los parches, que es crucial para comprender el
Estructura de las imágenes.3. Rendimiento y Eficiencia:
Transformadores: En PNL, los Transformers se han convertido en el estándar debido a su
capacidad de escalar y tener un buen desempeño en grandes conjuntos de datos, lo que a menudo requiere
Recursos computacionales significativos.Transformadores de visión (ViT): ViT ha demostrado que un transformador puro puede lograr
Resultados competitivos en la clasificación de imágenes, a menudo superando a los métodos tradicionales.
Redes neuronales convolucionales (CNN) en términos de eficiencia y escalabilidad
cuando se entrena previamente con grandes conjuntos de datos, ViT requiere sustancialmente menos
recursos computacionales para entrenar en comparación con las CNN de última generación, lo que hace
Es una alternativa prometedora para tareas de reconocimiento de imágenes.En resumen, si bien ambas arquitecturas utilizan el marco Transformer,
Los transformadores de visión adaptan los métodos de entrada y procesamiento para
manejar datos de imágenes, demostrando ventajas significativas en rendimiento y
eficiencia de recursos en el ámbito de la visión por computadora.
En general, se puede ver que nuestro sistema RAG contextual hace un buen trabajo al generar respuestas de alta calidad para las consultas de los usuarios.
¿Por qué importar el RAG contextual?
Hemos implementado un prototipo funcional de extremo a extremo de un sistema RAG contextual con búsqueda híbrida y reclasificación. Pero, ¿por qué debería interesarle construir un sistema de este tipo? ¿Realmente vale la pena el esfuerzo? Si bien siempre debe probar y evaluar el sistema con sus propios datos, estos son los resultados de Anthropic cuando realizaron algunas evaluaciones comparativas y descubrieron que la incrustación contextual reclasificada y el BM25 contextual redujeron la tasa de fallas de recuperación de los 20 fragmentos principales en un 67 % (5.7 % → 1.9 %). Esto se muestra en la siguiente figura.
Es bastante evidente que vale la pena invertir tiempo en la búsqueda híbrida y los rerankers, independientemente de la recuperación regular o contextual, y si tienes tiempo y esfuerzo, definitivamente también deberías invertir tiempo en la recuperación contextual.
Conclusión
Si estás leyendo esto, ¡te felicito por tus esfuerzos por llegar hasta el final de esta enorme guía! Aquí, analizamos en profundidad los desafíos actuales de los sistemas RAG Naive, especialmente en lo que respecta a la fragmentación y la recuperación. Luego, analizamos en detalle qué es la búsqueda híbrida, la reclasificación, la recuperación contextual, la inspiración del trabajo reciente de Anthropic y diseñamos nuestra propia arquitectura para manejar la generación contextual, la búsqueda vectorial, la búsqueda por palabras clave, la búsqueda híbrida, la recuperación de conjuntos, la reclasificación y los unimos para construir nuestro propio sistema RAG contextual con búsqueda híbrida y reclasificación incorporadas. ¡Echa un vistazo! este cuaderno de colaboración ¡Para acceder fácilmente al código y probar a personalizar y mejorar aún más este sistema!
Preguntas frecuentes
Respuesta: Los sistemas RAG combinan la recuperación de información con modelos de lenguaje para generar respuestas basadas en el contexto relevante, a menudo a partir de conjuntos de datos personalizados.
Respuesta: Los sistemas RAG ingenuos a menudo dividen los documentos en fragmentos independientes, perdiendo el contexto y afectando la precisión de la recuperación y la calidad de la respuesta.
Respuesta: La búsqueda híbrida combina búsquedas semánticas (basadas en incrustación) y de palabras clave (BM25/TF-IDF) para mejorar la precisión de la recuperación y la relevancia del contexto.
Respuesta: La recuperación contextual enriquece los fragmentos de documentos con un contexto explicativo adicional, lo que mejora la relevancia y la coherencia en los resultados de la búsqueda.
Respuesta: La reclasificación prioriza los fragmentos de documentos recuperados en función de su relevancia, lo que mejora la calidad de las respuestas generadas por el modelo de lenguaje.
- Distribución de relaciones públicas y contenido potenciado por SEO. Consiga amplificado hoy.
- PlatoData.Network Vertical Generativo Ai. Empodérate. Accede Aquí.
- PlatoAiStream. Inteligencia Web3. Conocimiento amplificado. Accede Aquí.
- PlatoESG. Carbón, tecnología limpia, Energía, Ambiente, Solar, Gestión de residuos. Accede Aquí.
- PlatoSalud. Inteligencia en Biotecnología y Ensayos Clínicos. Accede Aquí.
- Fuente: https://www.analyticsvidhya.com/blog/2024/12/contextual-rag-systems-with-hybrid-search-and-reranking/