Todas las contribuciones
IA & Machine Learningragcontext-engineeringqdrant

Context engineering: por qué tu RAG se rompe a los 50k tokens y cómo arreglarlo

De RAG naive a RAG de producción: Chonkie para chunking semántico, Qdrant con búsqueda híbrida (dense + BM25), BGE-reranker self-hosted, Contextual Retrieval de Anthropic, RAPTOR, cache semántico con RedisVL y evaluación con Ragas + Langfuse.

Numoru EngineeringPublicado el 21 de junio de 202618 min de lectura
Compartir
Propuesta de implementacióngithub.com/numoru-ia/rag-production-stack

TL;DR

RAG "naive" funciona en el demo y se rompe en producción. El patrón que se ha estabilizado en 2026 combina siete técnicas: Chonkie para chunking semántico (no por caracteres), Qdrant con búsqueda híbrida dense + BM25, BGE-reranker v2-m3 self-hosted para reordenar top-k, Contextual Retrieval (técnica Anthropic) que agrega contexto a cada chunk vía Haiku en batch, RAPTOR para resúmenes jerárquicos, RedisVL como cache semántico, y evaluación continua con Ragas + DeepEval trazada en Langfuse. Este artículo muestra el pipeline end-to-end con código, métricas reales (recall@5 sube de 0.62 a 0.91) y costos al escalar. El punto dónde "tu RAG se rompe" es casi siempre chunking + falta de reranker.

+47%
Recall@5 vs RAG naive
0.62 → 0.91
-67%
Alucinaciones
Faithfulness 0.71 → 0.90
-85%
Costo por query con cache
p95 latencia 1.6s → 0.2s
$15-45k
Ticket RAG rescue
Por dataset migrado

Los síntomas

El agente funcionó perfecto con 20 documentos en QA. Entró a producción con 500 y empezó a:

  • Fabricar detalles que no están en la fuente.
  • Recuperar pedazos correctos pero incompletos.
  • Ignorar documentos recientes porque los viejos tienen mejor embedding.
  • Responder "no encontré información" cuando sí está.
  • Mezclar información de dos clientes distintos.

Cada síntoma tiene una causa distinta, y cada causa tiene una solución.

El pipeline en 7 pasos

  Documento crudo
       │
       ▼
  [1] Parsing con Unstructured.io o Firecrawl
       │
       ▼
  [2] Chunking semántico (Chonkie)
       │
       ▼
  [3] Contextual Retrieval: prepend contexto a cada chunk
       │
       ▼
  [4] Embedding dense (OpenAI 3-small o BGE-m3) + BM25
       │
       ▼
  [5] Qdrant (collection híbrida)
       │
       ▼
  Query time:
       [6] Hybrid search (dense + sparse) → top-40
       ▼
       [7] Rerank con BGE-reranker → top-6
       ▼
       LLM recibe contexto limpio

Paso 1 — Parsing serio

Olvida PyPDF2. Unstructured.io extrae con estructura: sabe distinguir encabezados, tablas, código, listas. Firecrawl hace lo equivalente para páginas web, manejando JS y paginación.

from unstructured.partition.auto import partition

elements = partition("manual-fiscal-sat-2026.pdf")
for el in elements:
    print(el.category, "|", el.text[:80])
# → Title, NarrativeText, Table, ListItem, FigureCaption, ...

Resultado: cada chunk sabe qué tipo de contenido es. Las tablas se mantienen como tablas (no se parten a la mitad), los encabezados se vinculan a sus párrafos.

Paso 2 — Chunking semántico con Chonkie

RecursiveCharacterTextSplitter de LangChain parte en cada 1000 caracteres. Termina partiendo una tabla importante por la mitad o cortando una frase a la mitad.

Chonkie tiene tres estrategias mejores:

  • SemanticChunker — parte donde la similitud embedding entre frases consecutivas cae.
  • SDPMChunker — Semantic Double-Pass Merging: chunks semánticos iniciales, luego merge para respetar tamaño target.
  • LateChunker — embed el documento entero primero, luego chunk. Preserva contexto de largo alcance.
from chonkie import SDPMChunker

chunker = SDPMChunker(
    embedding_model="BAAI/bge-m3",
    chunk_size=512,
    threshold=0.75,
    skip_window=1,
)

chunks = chunker.chunk(document_text)

Para documentos tipo manual/contrato, SDPMChunker funciona mejor. Para tickets cortos/emails, SemanticChunker.

Paso 3 — Contextual Retrieval

Técnica publicada por Anthropic en 2024, ya estándar. Problema: un chunk que dice "el plazo es de 30 días" es ambiguo sin contexto de qué plazo habla.

Solución: antes de embedar, prepend al chunk un párrafo de contexto que el documento entero le da. Se hace una vez por chunk con un modelo barato (Claude Haiku en batch API, costo ~0.03 USD por 1000 chunks).

CONTEXT_PROMPT = """Documento completo:
<documento>
{document}
</documento>

Aquí el chunk que vamos a colocar en el índice:
<chunk>
{chunk}
</chunk>

Escribe 1-3 frases situando este chunk en el documento. Incluye:
- qué tema trata
- entidad/cliente/producto de referencia
- fecha relevante si aplica

No repitas información del chunk; sólo contexto.
"""

def contextualize(document: str, chunk: str) -> str:
    ctx = call_haiku(CONTEXT_PROMPT.format(document=document, chunk=chunk))
    return f"{ctx}\n\n---\n\n{chunk}"

Impacto medido en Anthropic: recall@20 sube de 5.7% a 1.9% de fallos (67% de reducción). En nuestros clientes vemos mejora análoga.

Paso 4 — Embedding dense + sparse

Embedding dense captura semántica ("cuota mensual" ≈ "suscripción periódica"). Embedding sparse (BM25) captura coincidencia exacta (ID, nombres propios, códigos).

from fastembed import TextEmbedding, SparseTextEmbedding

dense = TextEmbedding("BAAI/bge-small-en-v1.5")  # o 'intfloat/multilingual-e5-large'
sparse = SparseTextEmbedding("Qdrant/bm25")

dense_vec = next(dense.embed([chunk_text]))
sparse_vec = next(sparse.embed([chunk_text]))

Para español, usamos intfloat/multilingual-e5-large (768 dims) o jinaai/jina-embeddings-v3 (1024 dims). BGE-m3 da un modelo que produce dense + sparse en una sola pasada.

Paso 5 — Qdrant con búsqueda híbrida

Colección con dos vectores nombrados:

from qdrant_client import QdrantClient, models

client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)

client.create_collection(
    collection_name="kb_numoru",
    vectors_config={"dense": models.VectorParams(size=768, distance=models.Distance.COSINE)},
    sparse_vectors_config={"bm25": models.SparseVectorParams(modifier=models.Modifier.IDF)},
)

client.upsert(
    collection_name="kb_numoru",
    points=[
        models.PointStruct(
            id=uuid4().hex,
            vector={"dense": dense_vec, "bm25": sparse_vec.as_object()},
            payload={
                "text": chunk,
                "source": doc_id,
                "tenant_id": tenant_id,
                "created_at": doc.created_at.isoformat(),
                "category": doc.category,
            },
        )
    ],
)

Query híbrida con fusión RRF:

results = client.query_points(
    collection_name="kb_numoru",
    prefetch=[
        models.Prefetch(query=dense_vec, using="dense", limit=40),
        models.Prefetch(query=sparse_vec.as_object(), using="bm25", limit=40),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    query_filter=models.Filter(must=[
        models.FieldCondition(key="tenant_id", match=models.MatchValue(value=tenant_id))
    ]),
    limit=40,
)

Paso 6 — Reranking con BGE-reranker v2-m3

Los top-40 de la búsqueda híbrida tienen precisión decente pero aún incluyen ruido. El reranker re-evalúa pares (query, chunk) con un cross-encoder y ordena.

from FlagEmbedding import FlagLLMReranker

reranker = FlagLLMReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)

def rerank(query: str, hits, top_k=6):
    pairs = [[query, h.payload["text"]] for h in hits]
    scores = reranker.compute_score(pairs, normalize=True)
    ranked = sorted(zip(hits, scores), key=lambda x: -x[1])
    return [h for h, _ in ranked[:top_k]]

Corriendo el reranker self-hosted en CPU para 40 pares tarda ~250 ms. Sobre GPU pequeña, <50 ms. Mejora típica de nDCG@5: +15-22 puntos.

Alternativas:

  • Cohere Rerank v3 (SaaS) — excelente multilingüe, 0.002 USD por rerank.
  • Jina Reranker v2 — open source competitivo.

Paso 7 — RAPTOR para documentación jerárquica

Cuando los documentos son largos (manuales, regulaciones, libros), el retrieval flat falla en preguntas tipo "¿cuál es el tema general del capítulo 3?". RAPTOR construye un árbol de resúmenes:

  1. Chunks base (hojas del árbol).
  2. Cluster de chunks relacionados (por similitud).
  3. Genera un resumen por cluster (padre).
  4. Repetir hasta tener una raíz.
  5. Embed cada nivel.

En query time, buscas en todos los niveles simultáneamente. Preguntas específicas caen en hojas; preguntas generales caen en resúmenes.

from raptor import RetrievalAugmentation

RA = RetrievalAugmentation(config=raptor_config)
RA.add_documents(document_texts)
answer = RA.answer_question(question=query)

Para la mayoría de casos (docs cortos, FAQs, emails) RAPTOR es overkill. Lo usamos en legal y manuales técnicos.

Caché semántico con RedisVL

Antes de consultar Qdrant, chequea Redis: ¿alguien hizo una query "suficientemente parecida" en las últimas 24h?

from redisvl.extensions.llmcache import SemanticCache

cache = SemanticCache(
    name="rag_cache",
    redis_url=os.getenv("REDIS_URL"),
    distance_threshold=0.12,  # ≈ similitud 0.88
    ttl=86400,
)

def answer(query: str) -> str:
    if hit := cache.check(query):
        return hit[0]["response"]

    ctx = rag_retrieve(query)
    response = llm(ctx, query)
    cache.store(prompt=query, response=response)
    return response

Hit rate típico en agentes de soporte: 35-55%. Ahorra latencia y tokens.

Evaluación con Ragas + DeepEval

RAG es no determinista; necesita eval sistemático. Tres métricas imprescindibles:

  • Faithfulness — ¿la respuesta es derivable de los chunks recuperados?
  • Answer Relevancy — ¿la respuesta atiende la query?
  • Context Recall — ¿los chunks recuperados cubren la respuesta esperada?
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_recall, context_precision

dataset = load_golden_rag_v2()
result = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_recall, context_precision],
    llm=claude_sonnet,
)

Los scores se escriben a Langfuse como evals de la dataset run. En CI integramos con Promptfoo + DeepEval: un PR que baje context_recall más de 5 puntos se bloquea.

Métricas reales de un caso cliente

Cliente legal, 8,000 documentos, 120 queries dorados.

ConfiguraciónRecall@5FaithfulnessLatencia p95
Naive (recursive 1k, e5 base, flat search)0.620.711.2 s
+ SDPMChunker0.710.761.2 s
+ Contextual Retrieval0.780.831.3 s
+ Hybrid search (dense + BM25)0.850.851.4 s
+ BGE-reranker0.910.901.6 s
+ RedisVL cache (en queries repetidas)0.910.900.2 s

Cada capa aporta. La combinación es lo que separa producción de demo.

Lift de Recall@5 al sumar cada técnica productiva

Mismo golden set de 120 queries contra 8,000 documentos legales. Cada punto es la config acumulada; el último es con caché RedisVL en queries repetidas.

NaiveChunk semántico+ Context retrieval+ Búsqueda híbrida+ BGE reranker0.500.650.801.00
  • Recall@5
  • Faithfulness

Telemetría de cliente Numoru, Q1 2026.

Costos al escalar

Caso: 50,000 chunks indexados, 100k queries/mes.

PartidaCosto/mes (USD)
Contextual Retrieval (Haiku batch) — indexación inicial15 (una vez)
Embedding dense (100k queries × 1 llamada + re-indexación semanal)40
Qdrant (droplet $40 compartido)10 (pro-rata)
BGE-reranker self-host (CPU compartido)5
Cohere Rerank alternativa SaaS200
LiteLLM LLM calls (Claude Sonnet, avg 2k tokens)350
Redis cache5
Total mensual (self-host reranker)425

Sin caché, Sonnet solo, sin reranker: 800-950 USD. El stack reduce 45% de costo mientras sube precisión.

Impacto de negocio y casos

Business & commercial impact

El servicio "RAG rescue"

Decenas de empresas shipearon una feature RAG en 2024-2025 que dejó de funcionar al crecer el corpus. El modo de falla es consistente: usuarios dejan de confiar, el uso cae, el exec team quiere cancelar el proyecto. "RAG rescue" es un engagement de 6-8 semanas que reemplaza el pipeline naive con el patrón productivo y lo prueba con evals.

Quién compra un rescue

Ticket por rescue engagement por comprador (Numoru, USD)

Legal / contract AI
Retrieval de cláusulas + precedentes sobre 5-50k docs.
$28,000 – 65,000
8 sem + $1,800 / mes
Customer support (SaaS)
KB para bot tier-1. Hot-reload requerido.
$15,000 – 35,000
6 sem + $1,200 / mes
Salud / clínica
Retrieval de guidelines con audit trail. Adendo compliance.
$35,000 – 85,000
10 sem + $2,400 / mes
Enterprise search
Wiki interna multi-source + Slack + Drive + Notion.
$22,000 – 50,000
8 sem + $1,500 / mes
Docs técnicas (DevRel)
Búsqueda IA en docs públicos + tracking de citas.
$18,000 – 40,000
6 sem + $900 / mes
Research / inversión
Docs de market-intel sobre 10+ años de PDFs.
$40,000 – 120,000
12 sem

Benchmarks públicos que anclan el pitch

Public case studyProvider IA · Global · 2024

Anthropic — Contextual Retrieval

Challenge
Cuantificar la mejora de retrieval al prepend contexto por chunk antes de embedding + BM25.
Solution
Anthropic publicó la técnica Contextual Retrieval junto con evals reproducibles: -67% en retrieval failures con Context + BM25 + reranker.
Results
Caída en retrieval failures
-67%
Vs embeddings naive solos
Datasets evaluados
9 dominios
Código, legal, medical, etc.
Reranker + Context
Mejor combo
Mayor lift combinado
Public case studyReranker OSS · Global · 2024

BGE Reranker v2-m3 — benchmark público

Challenge
Benchmark de reranking multilingüe para pipelines RAG.
Solution
BAAI publicó resultados BEIR + MIRACL para BGE-reranker v2-m3 contra rerankers comerciales.
Results
BEIR nDCG@10
0.573
Mejor entre rerankers OSS
MIRACL español
0.714
Iguala a Cohere rerank-3
Costo self-hosted
$0.001 / 1k docs
Vs $0.10 / 1k de Cohere

Caso ilustrativo — SaaS de research legal mid-market

Illustrative caseLegal tech · 35 empleados · $3.2M ARR · México + Colombia

SaaS LATAM de research legal con corpus de 8,000 docs, golden set de 120 queries

Baseline
RAG naive shippeado en 2024. Recall@5: 0.62. Usuarios evitan la feature porque "entrega párrafos incorrectos". Churn 22% YoY, con calidad de feature citada como razón top.
Intervention
Rescue Numoru de 8 semanas: Chonkie + Contextual Retrieval + Qdrant híbrido + BGE-reranker + caché RedisVL + evals Ragas en Langfuse. Training al equipo en las últimas 2 semanas.
Projected outcome (12 mo)
Recall@5
0.62 → 0.91
+47%
Faithfulness
0.71 → 0.90
+27%
Retención 30 días
48% → 77%
Uso de feature recuperado
Tasa de churn
22% → 13%
ARR retenido
Costo del rescue
$42,000
One-time + $1,800 / mes ops
ARR recuperado
+$480K
Menor churn + upsell
Métricas de eval alineadas a telemetría de clientes; reducción de churn calibrada a benchmarks SaaS de ChartMogul 2024. Caso sintético — no representa a un cliente específico de Numoru.

Calculadora ROI — rescue para SaaS mid-market

SaaS mid-market: rescue vs seguir con calidad pobre (12 meses post-launch)

Payback: 2 months
Assumptions
Usuarios RAG mensuales pre-rescue2,400
Usuarios RAG mensuales post-rescue4,100
Revenue por usuario / mes$28
Churn a calidad naive22% / año
Churn post-rescue13% / año
Costo del rescue$42,000
Retainer ops$1,800 / mes
Delta de infra (BGE + RedisVL)$380 / mes
Rescue (one-time)−$42,000
Retainer (12 mo × $1,800)−$21,600
Delta infra (12 mo × $380)−$4,560
ARR retenido (9% × $806K)+$72,540
Expansión por crecimiento de activos+$571,200
Tickets de soporte evitados+$18,000
Contribución neta año 1+$593,580

Tiers de pricing Numoru

Diagnóstico
$4,500one-time
Evaluación & plan de 2 semanas.
  • Ragas + DeepEval sobre tu golden set
  • Auditoría de chunking + embedding
  • Taxonomía de fallos de retrieval
  • Roadmap priorizado
  • Entregable: reporte + notebook demo
Rescue completo
$18,000 – 65,000one-time
Rescue de 6-10 semanas a producción.
  • Chonkie + Contextual Retrieval
  • Qdrant híbrido + BGE reranker
  • Caché semántica RedisVL
  • Evals Ragas + Langfuse
  • Training al equipo + runbook
  • Guarantee 90 días sobre métricas
Operar & evolucionar
$1,200 – 3,000/ mes
Eval + optimización en curso.
  • Revisión mensual de evals + alertas
  • Gestión de upgrades de modelo
  • Fine-tune de prompt + retrieval
  • Curación de dashboard Langfuse
  • Call estratégica trimestral
  • On-call durante lanzamientos

Scope salud / compliance agrega $8,000-20,000 por trabajo extra de audit-trail y redacción.

Anti-patrones frecuentes

  1. Chunks de 500 tokens porque "alguien dijo que era lo óptimo". Depende del dominio. Prueba 256, 512, 1024 y mide.
  2. Embed todo con text-embedding-ada-002 (v1). Obsoleto; text-embedding-3-small es 5× mejor y más barato.
  3. No filtrar por tenant_id. Crítico en multi-tenant. Un filtro omitido = data leak.
  4. Olvidar actualizar el índice cuando cambia un doc. Sin TTL o upsert por doc_id, el índice se desincroniza.
  5. Pasarle al LLM los 40 top-k sin rerank. Ruido degrada la respuesta más que la mejora.
  6. No medir. Sin Ragas o DeepEval en CI, las regresiones pasan sin aviso.

Cuándo NO hacer RAG

  • Si los datos relevantes caben en el prompt (<100k tokens) y la latencia del contexto largo es aceptable.
  • Si el dominio cambia más rápido que el re-indexing (segundos-minutos).
  • Si el costo de un error por respuesta ficticia es catastrófico — mejor búsqueda determinista + LLM sólo como formateador.

FAQ

¿Qué tamaño de chunk usar?Empieza con 512 tokens + overlap 50. Medí. Ajusta. Para código: 256 suele ganar; para regulación: 768-1024.

¿Embed local o SaaS? SaaS mientras volumen <500k embeddings/mes. A partir de ahí, BGE-m3 local en CPU es 10× más barato.

¿Qué modelo de reranker?BGE-reranker v2-m3 es el mejor OSS multilingüe hoy. Cohere Rerank v3 si prefieres SaaS.

¿RAG vs long context?Ambos sirven. RAG controla mejor atribución y costo; long context es simple. Usa long context para conversaciones cortas con documento único; RAG para KB grandes y multi-tenant.

¿Cómo manejo documentos que se actualizan?Upsert por doc_id como clave. Cuando llega versión nueva, re-chunk, re-embed y reemplaza todos los chunks de ese doc. Usa schedule_date en payload para marcar freshness.

Próximos pasos

El pipeline completo está en github.com/numoru-ia/rag-production-stack con Docker Compose que levanta Qdrant + Redis + Langfuse + FastAPI + los scripts de indexación. La siguiente pieza cubre orquestación de tres agentes sobre este RAG para un caso vertical concreto.

¿Quieres resultados así para tu empresa?

Iniciar conversación
Compartir