RAG que funciona: patrones reales vs tutoriales basura
TL;DR
- El RAG básico (embed → search → generate) funciona en demos y poco más.
- Lo que marca la diferencia: chunking inteligente, reranking, y evaluación.
- Si tu RAG no tiene evaluación automática, estás debugueando a ciegas.
El problema del RAG básico
El 90% de tutoriales de RAG siguen este patrón:
- Parte tu documento en chunks de 512 tokens
- Embed cada chunk con un modelo de embeddings
- Guarda en una vector DB
- Cuando el usuario pregunta, busca los K chunks más similares
- Pasa esos chunks al LLM como contexto
Esto funciona para demos. En producción falla por:
- Chunks irrelevantes: El embedding captura similitud semántica, no relevancia para la pregunta.
- Contexto roto: Partir texto en 512 tokens corta ideas a mitad.
- Sin evaluación: No sabes si las respuestas son correctas.
- Sin feedback loop: El sistema no mejora con el uso.
Patrón 1: Chunking inteligente
No uses tamaño fijo
El chunking por tamaño fijo es la peor opción. Usa chunking semántico:
from semantic_text_splitter import TextSplitter
splitter = TextSplitter(max_chunk_size=1000)
chunks = splitter.split_text(document)
O mejor aún, chunking por estructura:
- Markdown: un chunk por sección (## headers).
- Código: un chunk por función/clase.
- HTML: un chunk por sección semántica.
- PDF: un chunk por página o sección con heading.
Metadata en cada chunk
Cada chunk debe llevar metadata:
chunk = {
"content": "texto del chunk...",
"source": "docs/api-reference.md",
"section": "Authentication",
"chunk_type": "code_example",
"last_updated": "2026-05-01",
}
Esto permite filtrado previo al embedding search: “solo busca en la sección de Authentication”.
Patrón 2: Hybrid search
Embedding search solo captura similitud semántica. Añade keyword search (BM25):
# En lugar de solo embedding search
results = vector_db.search(query_embedding, top_k=20)
# Usa hybrid: embedding + BM25
embedding_results = vector_db.search(query_embedding, top_k=20)
bm25_results = bm25_index.search(query_keywords, top_k=20)
# Combina con Reciprocal Rank Fusion
combined = reciprocal_rank_fusion(
[embedding_results, bm25_results],
k=60 # constante RRF
)
Hybrid search mejora recall un 15-30% sobre embedding solo.
Patrón 3: Reranking
El paso que más marca la diferencia. Después de recuperar 20-50 candidatos, reordena con un modelo de reranking:
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
# Rerankear top candidates
pairs = [(query, chunk["content"]) for chunk in candidates]
scores = reranker.predict(pairs)
# Ordenar por score y quedarse con top 5
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])[:5]
Impacto: Reranking mejora precisión en top-5 entre un 20-40%. Es el single biggest improvement que puedes hacer.
Coste: El reranking es rápido (<10ms por query) y barato (modelo local).
Patrón 4: Query transformation
El usuario pregunta cosas ambiguas. Transforma la query antes de buscar:
Multi-query
Genera 3-5 variaciones de la pregunta y busca con todas:
from openai import OpenAI
client = OpenAI()
def generate_queries(original_query):
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": f"Genera 3 variantes de esta pregunta para búsqueda: {original_query}"
}]
)
return [original_query] + response.choices[0].message.content.split('\n')
HyDE (Hypothetical Document Embedding)
El LLM genera una respuesta hipotética, y buscas embeddings de ESA respuesta:
hypothetical_answer = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": f"Responde brevemente: {query}"}]
).choices[0].message.content
# Buscar usando la respuesta hipotética, no la pregunta
results = vector_db.search(embed(hypothetical_answer), top_k=10)
Funciona porque la respuesta hipotética es semánticamente más cercana a los documentos reales que la pregunta.
Patrón 5: Evaluación
Sin evaluación, no sabes si tu RAG funciona. Implementa méctricas automatizadas:
Dataset de test
Crea 50-100 pares pregunta-respuesta-correcta:
{
"question": "¿Cómo reseteo la contraseña?",
"expected_answer": "Ve a Settings > Security > Reset Password",
"relevant_chunks": ["docs/security.md#password-reset"],
"source": "docs/security.md"
}
Métricas automáticas
def evaluate_rag(test_cases):
results = {"faithfulness": [], "relevance": [], "correctness": []}
for case in test_cases:
answer = rag_pipeline.query(case["question"])
# Faithfulness: ¿la respuesta se basa en los chunks recuperados?
results["faithfulness"].append(
check_faithfulness(answer, retrieved_chunks)
)
# Relevance: ¿los chunks recuperados son relevantes?
results["relevance"].append(
check_relevance(case["question"], retrieved_chunks)
)
# Correctness: ¿la respuesta es correcta?
results["correctness"].append(
check_correctness(answer, case["expected_answer"])
)
return {k: sum(v)/len(v) for k, v in results.items()}
LLM-as-judge para evaluación
Usa un modelo mejor como juez:
def check_faithfulness(answer, chunks):
prompt = f"""Dada esta respuesta:
{answer}
Y estos chunks de referencia:
{chunks}
¿La respuesta está fielmente basada en los chunks? Responde solo: YES o NO.
Si la respuesta incluye información no presente en los chunks, responde NO."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content.strip() == "YES"
Patrón 6: Context window vs RAG
En 2026, con modelos de 128K-1M tokens de contexto, ¿tiene sentido RAG?
Sí, para:
- Knowledge bases que cambian frecuentemente
- Documentación que no cabe ni en 1M tokens
- Coste: meter 100K tokens de contexto en cada llamada es caro
No, para:
- Documentos estáticos pequeños (<50K tokens)
- Prototipos rápidos
- Cuando la latencia del retrieval empeora el UX
El approach híbrido: usa contexto largo para el documento principal y RAG para el knowledge base externo.
Stack recomendado
| Componente | Herramienta | ¿Por qué |
|---|---|---|
| Embeddings | text-embedding-3-small (OpenAI) o bge-m3 (local) | Calidad/precio |
| Vector DB | Qdrant o Chroma | Open-source, fácil de usar |
| Reranker | cross-encoder/ms-marco-MiniLM-L-12-v2 | Rápido, local |
| Framework | Sin framework. Código custom | Más control, menos abstracción |
Evita frameworks de RAG (LangChain, LlamaIndex) para producción. Añaden complejidad sin aportar valor. El código custom es más simple, más mantenible y más fácil de debuguear.
Conclusión
RAG que funciona = hybrid search + reranking + evaluación. Todo lo demás son optimizaciones menores.
Si solo implementas una mejora, que sea reranking. Es el ROI más alto por esfuerzo invertido.
Fuentes: Pinecone learning center, Qdrant docs,经验 propia con RAG en producción.