Técnicas de Recuperación
Más Allá de la Búsqueda Básica de Similitud
En las lecciones anteriores, aprendiste a embeber documentos y buscar los fragmentos más similares usando similitud coseno. Ese enfoque básico funciona, pero tiene limitaciones. Los top-K fragmentos más similares no siempre son los fragmentos más útiles. Pueden ser redundantes (cinco fragmentos diciendo lo mismo), pueden perder coincidencias críticas de palabras clave, o incluir resultados marginalmente relevantes que diluyen el contexto.
Esta lección cubre las técnicas de recuperación que los sistemas RAG en producción usan para obtener resultados dramáticamente mejores.
Fundamentos de Búsqueda por Similitud
El enfoque básico de recuperación: embeber la consulta, calcular similitud contra todos los vectores almacenados, retornar los top-K resultados.
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
# Busqueda basica de similitud
results = vectorstore.similarity_search(
query="Cual es la politica de reembolso?",
k=5
)
# Con puntuaciones
results_with_scores = vectorstore.similarity_search_with_score(
query="Cual es la politica de reembolso?",
k=5
)
for doc, score in results_with_scores:
print(f"Puntuacion: {score:.4f} | {doc.page_content[:80]}")
Esto funciona razonablemente bien pero tiene tres modos de fallo comunes: resultados redundantes, fallos de palabras clave y ninguna diversidad en el conjunto recuperado.
Maximum Marginal Relevance (MMR)
MMR resuelve el problema de redundancia. En lugar de retornar los cinco fragmentos más similares (que podrían provenir todos de la misma sección y decir esencialmente lo mismo), MMR equilibra la relevancia con la consulta y la diversidad entre los fragmentos seleccionados.
El algoritmo funciona iterativamente: selecciona el fragmento más relevante primero, luego para cada selección subsiguiente, elige el fragmento que es más relevante a la consulta pero menos similar a los fragmentos ya seleccionados.
# Recuperacion MMR -- equilibra relevancia con diversidad
results = vectorstore.max_marginal_relevance_search(
query="Cual es la politica de reembolso?",
k=5, # Numero de resultados a retornar
fetch_k=20, # Numero de candidatos a considerar
lambda_mult=0.7 # 0 = maxima diversidad, 1 = maxima relevancia
)
El parámetro lambda_mult controla el tradeoff. En 1.0, MMR se comporta como búsqueda de similitud estándar. En 0.0, maximiza la diversidad. Un valor alrededor de 0.5-0.7 funciona bien para la mayoría de aplicaciones RAG.
Cuándo usarlo: Siempre considera MMR como reemplazo directo de la búsqueda básica de similitud. Raramente perjudica y a menudo mejora significativamente la calidad de las respuestas al dar al LLM perspectivas diversas sobre el tema.
Búsqueda Híbrida: Combinando Recuperación Densa y Dispersa
La recuperación densa (basada en embeddings) sobresale en coincidencia semántica. Pero puede perder coincidencias exactas de palabras clave que el usuario espera. Si alguien busca "código de error E-4021", una búsqueda densa podría retornar fragmentos sobre manejo general de errores en lugar del código de error específico.
La recuperación dispersa (BM25, TF-IDF) sobresale en coincidencia exacta de palabras clave pero pierde conexiones semánticas. Encontraría "código de error E-4021" perfectamente pero no coincidiría "cómo arreglar la falla de procesamiento de pago" con un documento sobre E-4021.
La búsqueda híbrida combina ambas: ejecuta una búsqueda densa y una búsqueda dispersa en paralelo, luego fusiona los resultados.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Retriever denso (semantico)
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# Retriever disperso (palabras clave)
bm25_retriever = BM25Retriever.from_documents(documents)
bm25_retriever.k = 5
# Hibrido: combinar ambos con puntuaciones ponderadas
hybrid_retriever = EnsembleRetriever(
retrievers=[dense_retriever, bm25_retriever],
weights=[0.6, 0.4] # 60% semantico, 40% palabras clave
)
results = hybrid_retriever.invoke("codigo de error E-4021 fallo de pago")
Cuándo usarlo: Aplicaciones donde los usuarios buscan tanto conceptos como términos específicos (nombres de productos, códigos de error, IDs). La búsqueda híbrida es la recomendación estándar para sistemas RAG en producción.
Re-Ranking
El re-ranking es un proceso de recuperación en dos etapas. Primero, recupera un conjunto de candidatos más grande (e.g., 20 fragmentos) usando búsqueda rápida de similitud. Luego, usa un modelo más poderoso para re-puntuar y re-ordenar esos candidatos basándose en su relevancia real a la consulta.
El modelo de re-ranking lee tanto la consulta como el texto candidato juntos, lo que le da un entendimiento mucho más profundo de la relevancia que la comparación de embeddings sola.
Cohere Rerank
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Retriever base -- obtener un conjunto grande de candidatos
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=OpenAIEmbeddings(model="text-embedding-3-small"),
)
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
# Re-ranker -- puntuar candidatos mas precisamente
reranker = CohereRerank(
model="rerank-english-v3.0",
top_n=5 # Retornar top 5 despues del re-ranking
)
# Retriever combinado
retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=base_retriever,
)
results = retriever.invoke("Cual es la politica de reembolso para productos digitales?")
Re-Ranking con Cross-Encoder (Código Abierto)
from sentence_transformers import CrossEncoder
# Cargar modelo cross-encoder
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
# Obtener fragmentos candidatos de tu retriever
query = "Cual es la politica de reembolso?"
candidates = vectorstore.similarity_search(query, k=20)
# Re-rankear con cross-encoder
pairs = [(query, doc.page_content) for doc in candidates]
scores = cross_encoder.predict(pairs)
# Ordenar por puntuacion del cross-encoder
ranked = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)
# Tomar top 5
top_results = [doc for doc, score in ranked[:5]]
Cuándo usarlo: Cuando la precisión de recuperación es crítica. El re-ranking mejora consistentemente la relevancia en benchmarks. El costo principal es latencia adicional (típicamente 100-300ms) y el costo del modelo de re-ranking. Para la mayoría de los sistemas en producción, la mejora en calidad justifica la sobrecarga.
Compresión Contextual
A veces el fragmento recuperado contiene la respuesta pero también mucho texto irrelevante. La compresión contextual extrae solo las partes de cada fragmento que son relevantes a la consulta.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Compresor que extrae solo las porciones relevantes
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor,
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 10}),
)
results = compression_retriever.invoke("Cuales son los plazos de reembolso?")
# Cada resultado ahora contiene solo la porcion relevante del fragmento original
Cuándo usarlo: Cuando tus fragmentos son grandes y contienen información mixta, o cuando necesitas minimizar el contexto enviado al LLM para ahorrar en costos de tokens.
Filtrado por Metadatos
El filtrado por metadatos reduce el espacio de búsqueda antes de que la búsqueda de similitud se ejecute. En lugar de buscar en los 100,000 fragmentos, buscas solo en los 5,000 fragmentos del departamento de "ingeniería" o los 200 fragmentos de documentos actualizados en el último mes.
# Filtrado de metadatos en ChromaDB
results = vectorstore.similarity_search(
query="proceso de despliegue",
k=5,
filter={
"$and": [
{"department": "engineering"},
{"year": {"$gte": 2024}},
]
}
)
Patrones comunes de filtrado:
- Por fuente: Solo buscar artículos de base de conocimiento, no memos internos.
- Por fecha: Priorizar documentos recientes sobre obsoletos.
- Por departamento/equipo: Limitar resultados al área del usuario.
- Por tipo de documento: Buscar solo políticas, o solo documentos técnicos.
- Por nivel de acceso: Aplicar seguridad filtrando basándose en permisos del usuario.
Recuperación Multi-Vector
En lugar de embeber cada fragmento una vez, crea múltiples embeddings por fragmento -- uno para el texto tal como está, uno para un resumen, uno para preguntas hipotéticas que el fragmento responde. Busca a través de todos los vectores y deduplica.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
def generate_hypothetical_questions(chunk_text: str) -> list[str]:
"""Generar preguntas que este fragmento podria responder."""
response = llm.invoke(
f"Genera 3 preguntas que el siguiente texto podria responder. "
f"Retorna solo las preguntas, una por linea.\n\n{chunk_text}"
)
return response.content.strip().split("\n")
# Para cada fragmento, embeber el texto Y las preguntas hipoteticas
# Almacenar todos los embeddings, mapeando de vuelta al mismo fragmento
Esto aumenta las probabilidades de que la consulta del usuario coincida con una de las representaciones del fragmento relevante.
Combinando Técnicas: Un Stack de Producción
Los mejores sistemas RAG superponen múltiples técnicas de recuperación:
- Búsqueda híbrida (densa + BM25) para lanzar una red amplia
- Filtrado por metadatos para limitar a documentos relevantes
- Re-ranking para ordenar precisamente los candidatos
- MMR para asegurar diversidad en el conjunto final
# Stack de recuperacion en produccion (pseudocodigo)
candidates = hybrid_search(query, k=30, filters=user_filters)
reranked = rerank(query, candidates, top_n=10)
final = mmr(reranked, k=5, lambda_mult=0.7)
Cada capa aborda un modo de fallo diferente. Juntas, producen un pipeline de recuperación que es robusto, preciso y diverso.
En la siguiente lección, ensamblarás todos estos componentes en un pipeline RAG completo y funcional de principio a fin.