Inicio / Artículos / Notas prácticas: Estrategias de recuperación en RAG: Más allá de la búsqueda básica de similitud

Notas prácticas: Estrategias de recuperación en RAG: Más allá de la búsqueda básica de similitud

Guía práctica paso a paso de las notas prácticas: Estrategias de recuperación en RAG: Más allá de la búsqueda básica de similitud; contratos, verificaciones y espacios para código listo para usar destinados a los equipos que implementan este patrón.

2161 palabras

Esta guía reconstruye el proceso desde las materias primas hasta un sistema funcional para: Estrategias de recuperación en RAG: Más allá de la búsqueda básica de similitud. El enfoque está en pasos operativos, verificaciones explícitas y código que se puede incorporar directamente a un repositorio sin tener que adivinar la intención. En la etapa de visión general, se deben definir las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Se deben registrar los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. La visibilidad temprana del costo evita facturas inesperadas cuando el proceso pasa de una demostración a entornos compartidos.

Introducción

Al trabajar en la fase de introducción, anote primero el contrato: los datos necesarios, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación ayuda a mantener honestas las futuras modificaciones del código. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Índice

Al trabajar en la etapa del índice, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Documente junto con ello el camino óptimo y el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no de mejoras posteriores. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

1. Cómo funciona la búsqueda básica de similitud →Un resumen rápido

Al trabajar en la etapa 1 “¿Qué tan básica es la similitud?”, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Prefiera unidades pequeñas y probables a scripts extensos. Cuando un paso falla, el fallo debe referirse a una sola responsabilidad y no a un proceso complicado. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente. Al trabajar en la etapa 1 “¿Qué tan básica es la similitud?”, anote primero el contrato: las entradas requeridas, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación garantiza que los cambios posteriores en el código sean transparentes. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos.

# Basic similarity search — what most RAG systems do
results = vector_store.similarity_search(query, k=3)

2. Las limitaciones de la búsqueda básica por similitud

La etapa de 2 Las limitaciones funciona mejor cuando se trata como una superficie medible. Capture un transcripto ejemplar, un caso de fallo y la nota de reversión antes de ampliar el alcance. Mantenga la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben estar en un lugar donde los operadores puedan auditarlos sin tener que leer todo el grafo. Separe la política de particionamiento del contenido de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

3. BM25 → Recuperación basada en palabras clave

La etapa 3 basada en palabras clave BM25 funciona mejor cuando se trata como una superficie medible. Capture un transcripte exitoso, un caso de fallo y la nota de reversión antes de ampliar el alcance. Documente tanto el camino óptimo como el camino de recuperación juntos. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Separe la política de fragmentación de la política de recuperación; cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

from langchain_community.retrievers import BM25Retriever
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load and split documents
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Create BM25 retriever
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 3
# Search
results = bm25_retriever.invoke("FAISS vector index")
for doc in results:
    print(doc.page_content[:200])

4. Búsqueda híbrida → Combinando lo mejor de ambos

La etapa de combinación de búsqueda híbrida 4 funciona mejor cuando se trata como una superficie medible. Capture un transcripto ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado. Separe la política de particionamiento de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad. La etapa de combinación de búsqueda híbrida 4 funciona mejor cuando se trata como una superficie medible. Capture un transcripto ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance. Registre los tiempos y el costo de tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de la versión de demostración a entornos compartidos.

from langchain_community.retrievers import BM25Retriever
from langchain_google_genai import GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.retrievers import EnsembleRetriever
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
#Load and split
loader = TextLoader("knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
#Retriever 1 — Semantic (FAISS)
embeddings = GoogleGenerativeAIEmbeddings(
    model="models/embedding-001",
    google_api_key="YOUR_KEY"
)
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 5})
#Retriever 2 — Keyword (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5
#Combine both — Hybrid Search
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]  # 60% semantic, 40% keyword
)
#Search
results = hybrid_retriever.invoke("What is FAISS and how does it store vectors?")
print(f"Retrieved {len(results)} chunks")
for i, doc in enumerate(results):
    print(f"\nChunk {i+1}: {doc.page_content[:150]}")

5. Reclasificación → Elegir lo mejor entre lo mejor

Para la selección de la etapa en el proceso de 5 Reranking, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido, sin tener que adivinar el estado oculto. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben encontrarse en un lugar donde los operadores puedan auditarlos sin necesidad de leer todo el sistema. Mencione las secciones del texto que sirvieron como base para la respuesta. Sin citaciones, los operadores no podrán distinguir entre alucinaciones y lagunas en el indexado.

from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain.retrievers import ContextualCompressionRetriever
#Base retriever — fetch top 20 candidates
base_retriever = vector_store.as_retriever(search_kwargs={"k": 20})
#Reranker model
reranker_model = HuggingFaceCrossEncoder(
    model_name="BAAI/bge-reranker-base"
)
#Reranker compressor — keeps only top 3 after reranking
reranker = CrossEncoderReranker(model=reranker_model, top_n=3)
#Combine base retriever + reranker
reranking_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=base_retriever
)
#Search — fetches 20, reranks, returns top 3
results = reranking_retriever.invoke("How does FAISS perform similarity search?")
print(f"Final chunks after reranking: {len(results)}")
for i, doc in enumerate(results):
    print(f"\nTop {i+1}: {doc.page_content[:200]}")

6. Máxima relevancia marginal → Evitar resultados redundantes

En la etapa 6 de Máxima Relevancia Marginal, defina las entradas, el responsable del paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Documente tanto la ruta óptima como la ruta de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores. Cite los pasajes que realmente sustentan la respuesta. Sin citas, los operadores no pueden distinguir entre alucinaciones y brechas en el indexado.

# Standard similarity search — might return redundant chunks
standard_results = vector_store.similarity_search(query, k=3)
# MMR search — returns relevant AND diverse chunks
mmr_results = vector_store.max_marginal_relevance_search(
    query,
    k=3,           # final number of chunks to return
    fetch_k=20,    # candidates to consider before selecting diverse ones
    lambda_mult=0.5  # 0 = max diversity, 1 = max relevance
)

7. Un pipeline completo de recuperación avanzada

En la etapa 7 A Complete Advanced, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Prefiera unidades pequeñas y verificables en lugar de scripts extensos. Cuando un paso falla, el error debe indicar una única responsabilidad y no un proceso complicado. Cite los pasajes que realmente sustentan la respuesta. Sin citaciones, los operadores no pueden distinguir entre alucinaciones y fallos en el indexado. En la etapa 7 A Complete Advanced, defina las entradas, el responsable de cada paso y los criterios de finalización antes de modificar el código. Los operadores deben poder volver a ejecutar el paso a partir de un punto de control conocido sin tener que adivinar el estado oculto. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando el proceso pasa de entornos de demostración a entornos compartidos.

/p>
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.retrievers import BM25Retriever
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain.retrievers import EnsembleRetriever, ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
# Setup
llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key="YOUR_KEY")
embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001", google_api_key="YOUR_KEY")
# Load and split documents
loader = TextLoader("your_knowledge_base.txt")
documents = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
# Build retrievers
vector_store = FAISS.from_documents(chunks, embeddings)
semantic_retriever = vector_store.as_retriever(search_kwargs={"k": 10})
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 10
# Hybrid retriever
hybrid_retriever = EnsembleRetriever(
    retrievers=[semantic_retriever, bm25_retriever],
    weights=[0.6, 0.4]
)
# Add reranking on top
reranker = CrossEncoderReranker(
    model=HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base"),
    top_n=3
)
final_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=hybrid_retriever
)
# RAG prompt
prompt = ChatPromptTemplate.from_messages([
    ("system", """Answer the question using only the context below.
    If the answer is not in the context, say "I don't have that information."

    Context: {context}"""),
    ("human", "{question}")
])
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
# Complete chain
rag_chain = (
    {"context": final_retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
# Use it
answer = rag_chain.invoke("Your question here")
print(answer)

8. ¿Qué estrategia debería utilizar?

Al trabajar en la etapa de “8. ¿Qué estrategia debería utilizar?”, anote primero el contrato: los datos de entrada necesarios, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación ayuda a mantener honestas las futuras modificaciones en el código. Guarde la configuración fuera del código de la aplicación. Los archivos de entorno, los almacenes de datos secretos y las banderas de funcionalidad deben encontrarse en un lugar donde los operadores puedan auditarlos sin tener que leer todo el sistema. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

Just starting out / simple use case?
    → Basic similarity search is fine
Documents have specific technical terms or product names?
    → Add BM25 → use Hybrid Search
Answer quality is critical, wrong answers are costly?
    → Add Reranking on top of any retriever
Documents have lots of repeated content?
    → Use MMR instead of standard similarity search
Production system with high quality requirements?
    → Hybrid Search + Reranking together

Puntos clave

Al trabajar en la etapa de conclusiones clave, anote primero el contrato: los datos necesarios, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Documente junto con ello el camino óptimo y el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no de mejoras posteriores. Mida la capacidad de recuperación con un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente.

¿Qué sigue? Vista previa del blog #32

Al trabajar en la etapa del blog “What’s Next”, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Prefiera unidades pequeñas y verificables a scripts extensos. Cuando un paso falla, el fallo debe apuntar a una sola responsabilidad y no a un proceso complicado. Mida el rendimiento en un conjunto fijo de preguntas antes de ajustar los prompts. El cambio constante de prompts rara vez soluciona un sistema de recuperación deficiente. Al trabajar en la etapa del blog “What’s Next”, anote primero el contrato: los datos de entrada requeridos, la señal de éxito y qué ocurre en caso de fallo parcial. Esa lista de verificación mantiene honestas las futuras modificaciones del código. Registre los tiempos de ejecución y el costo en tokens o consultas junto con los resultados funcionales. Tener visibilidad del costo desde el principio evita facturas inesperadas cuando se pasa de entornos de demostración a entornos compartidos.

Lista de verificación operativa

La etapa de lista de verificación operativa funciona mejor cuando se trata como una métrica cuantificable. Consiga un registro ideal, un caso de fallo y la nota de reversión antes de ampliar el alcance.

Considere esta etapa como un contrato entre las entradas y los resultados validados. Asigne nombres a los artefactos, defina las verificaciones de éxito y rechace las completaciones parciales silenciosas.

Separe la política de fragmentación de la política de recuperación. Cambiar una no debe obligar a reescribir la otra cuando cambian las métricas de calidad.

Evalúe por separado las respuestas de una sola interacción y las trayectorias de múltiples interacciones. La agregación de puntuaciones de chat oculta los fallos en el ciclo del herramienta.

Escriba un manual breve: cómo rotar claves, cómo vaciar la cola y cómo revertir la última incorporación.

Documente tanto el camino óptimo como el camino de recuperación. Las reintentos, los controles humanos y el manejo de mensajes no entregados forman parte del producto, no son mejoras posteriores.

Antes de promocionar el stack, congele las versiones, capture una transcripción de referencia para la ruta crítica y confirme los pasos de reversión. Los entornos compartidos requieren límites de velocidad, verificaciones de tenencia y un responsable claro para la rotación de secretos. Prefiera una fiabilidad sólida a demostraciones ingeniosas pero puntuales.

Nota para el lote 61fc728ec048: mantenga las claves del proveedor fuera del repositorio, establezca un límite para tokens por sesión y almacene las transcripciones junto a los fixtures de evaluación para que los cambios posteriores en el modelo sigan siendo comparables.

Lecturas relacionadas

  • Notas prácticas: RAG es más que recuperación — es búsqueda y juicio — Guía paso a paso de las Notas prácticas: RAG es más que recuperación — es búsqueda y juicio: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.