Tecnicas de Recuperacion
Mas Alla de la Busqueda Basica de Similitud
En las lecciones anteriores, aprendiste a embeber documentos y buscar los fragmentos mas similares usando similitud coseno. Ese enfoque basico funciona, pero tiene limitaciones. Los top-K fragmentos mas similares no siempre son los fragmentos mas utiles. Pueden ser redundantes (cinco fragmentos diciendo lo mismo), pueden perder coincidencias criticas de palabras clave, o incluir resultados marginalmente relevantes que diluyen el contexto.
Esta leccion cubre las tecnicas de recuperacion que los sistemas RAG en produccion usan para obtener resultados dramaticamente mejores.
Fundamentos de Busqueda por Similitud
El enfoque basico de recuperacion: 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 mas similares (que podrian provenir todos de la misma seccion 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 mas relevante primero, luego para cada seleccion subsiguiente, elige el fragmento que es mas 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 parametro lambda_mult controla el tradeoff. En 1.0, MMR se comporta como busqueda de similitud estandar. En 0.0, maximiza la diversidad. Un valor alrededor de 0.5-0.7 funciona bien para la mayoria de aplicaciones RAG.
Cuando usarlo: Siempre considera MMR como reemplazo directo de la busqueda basica de similitud. Raramente perjudica y a menudo mejora significativamente la calidad de las respuestas al dar al LLM perspectivas diversas sobre el tema.
Busqueda Hibrida: Combinando Recuperacion Densa y Dispersa
La recuperacion densa (basada en embeddings) sobresale en coincidencia semantica. Pero puede perder coincidencias exactas de palabras clave que el usuario espera. Si alguien busca "codigo de error E-4021", una busqueda densa podria retornar fragmentos sobre manejo general de errores en lugar del codigo de error especifico.
La recuperacion dispersa (BM25, TF-IDF) sobresale en coincidencia exacta de palabras clave pero pierde conexiones semanticas. Encontraria "codigo de error E-4021" perfectamente pero no coincidiria "como arreglar la falla de procesamiento de pago" con un documento sobre E-4021.
La busqueda hibrida combina ambas: ejecuta una busqueda densa y una busqueda 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")
Cuando usarlo: Aplicaciones donde los usuarios buscan tanto conceptos como terminos especificos (nombres de productos, codigos de error, IDs). La busqueda hibrida es la recomendacion estandar para sistemas RAG en produccion.
Re-Ranking
El re-ranking es un proceso de recuperacion en dos etapas. Primero, recupera un conjunto de candidatos mas grande (e.g., 20 fragmentos) usando busqueda rapida de similitud. Luego, usa un modelo mas poderoso para re-puntuar y re-ordenar esos candidatos basandose 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 mas profundo de la relevancia que la comparacion 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 (Codigo 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]]
Cuando usarlo: Cuando la precision de recuperacion es critica. El re-ranking mejora consistentemente la relevancia en benchmarks. El costo principal es latencia adicional (tipicamente 100-300ms) y el costo del modelo de re-ranking. Para la mayoria de los sistemas en produccion, la mejora en calidad justifica la sobrecarga.
Compresion Contextual
A veces el fragmento recuperado contiene la respuesta pero tambien mucho texto irrelevante. La compresion 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
Cuando usarlo: Cuando tus fragmentos son grandes y contienen informacion 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 busqueda antes de que la busqueda de similitud se ejecute. En lugar de buscar en los 100,000 fragmentos, buscas solo en los 5,000 fragmentos del departamento de "ingenieria" o los 200 fragmentos de documentos actualizados en el ultimo 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 articulos de base de conocimiento, no memos internos.
- Por fecha: Priorizar documentos recientes sobre obsoletos.
- Por departamento/equipo: Limitar resultados al area del usuario.
- Por tipo de documento: Buscar solo politicas, o solo documentos tecnicos.
- Por nivel de acceso: Aplicar seguridad filtrando basandose en permisos del usuario.
Recuperacion Multi-Vector
En lugar de embeber cada fragmento una vez, crea multiples embeddings por fragmento -- uno para el texto tal como esta, uno para un resumen, uno para preguntas hipoteticas que el fragmento responde. Busca a traves 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 Tecnicas: Un Stack de Produccion
Los mejores sistemas RAG superponen multiples tecnicas de recuperacion:
- Busqueda hibrida (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 recuperacion que es robusto, preciso y diverso.
En la siguiente leccion, ensamblaras todos estos componentes en un pipeline RAG completo y funcional de principio a fin.