---
name: rag-arquitectura
description: >
  Arquitectura de sistemas RAG (Retrieval-Augmented Generation): diseño de pipelines
  de ingestión, estrategias de chunking, selección de vector stores, reranking,
  evaluación con RAGAS y patrones de producción. Cargar cuando se diseñe o implemente
  un sistema RAG completo, se optimice la precisión de recuperación, o se evalúe
  la calidad de un pipeline existente.
version: "1.0.0"
herramientasPermitidas: [Read, Grep]
user-invocable: false
evolvable: true  # default para skill estandar
exclusiones:
  - "No cargar para construcción de agentes LangGraph con memoria y herramientas — para agentes orquestados con LangGraph cargar `langchain-langraph`."
  - "No cargar para extracción de documentos Office/PDF sin pipeline de recuperación (sin búsqueda vectorial) — para extracción puntual de documentos cargar `extraccion-documentos`."
  - "No cargar para búsqueda de texto completo en bases de datos SQL sin componente vectorial — para full-text search en PostgreSQL usar `tsvector`/`tsquery` directamente."
  - "No cargar para fine-tuning de modelos de embedding en dominios específicos — este skill cubre RAG con modelos de embedding pre-entrenados, no el proceso de ajuste de pesos."
---
# RAG — Retrieval-Augmented Generation

## Cuándo NO cargar

- La tarea es construir agentes con estado y herramientas usando LangGraph: cargar `langchain-langraph`.
- La tarea es extracción puntual de un documento (sin búsqueda vectorial posterior): cargar `extraccion-documentos`.
- La tarea es full-text search en PostgreSQL sin componente vectorial: usar `tsvector`/`tsquery` del skill `postgresql-experto`.
- La tarea es fine-tuning de modelos de embedding: usar la documentación del proveedor de embeddings.

## Arquitectura de referencia

```
Documentos → Ingestión → Chunking → Embeddings → Vector Store
                                                       ↓
                                    Usuario → Query → Retriever → Reranker → LLM → Respuesta
```

---

## Pipeline de ingestión

### Loaders por tipo de fuente

| Fuente | Loader recomendado | Notas |
|--------|-------------------|-------|
| PDF | `PyPDFLoader` / `UnstructuredPDFLoader` | Unstructured para PDFs con tablas complejas |
| Web | `WebBaseLoader` / `RecursiveUrlLoader` | Recursive para crawl de sitios completos |
| Markdown | `UnstructuredMarkdownLoader` | Preserva headers para chunking semántico |
| CSV / Excel | `CSVLoader` / `UnstructuredExcelLoader` | Cada fila como documento independiente |
| Base de datos | `SQLDatabaseLoader` | Query SQL → documentos |
| Código fuente | `GenericLoader` + `LanguageParser` | Preserva estructura de funciones/clases |

### Limpieza pre-chunking

```python
def limpiar_documento(texto: str) -> str:
    """Normalización antes de chunking."""
    import re
    # Eliminar saltos de línea excesivos
    texto = re.sub(r'\n{3,}', '\n\n', texto)
    # Normalizar espacios
    texto = re.sub(r'[ \t]+', ' ', texto)
    # Eliminar headers/footers repetitivos de PDFs
    texto = re.sub(r'Página \d+ de \d+', '', texto)
    return texto.strip()
```

---

## Estrategias de chunking

### Tabla de decisión

| Tipo de documento | Estrategia | chunk_size | overlap | Razón |
|-------------------|-----------|------------|---------|-------|
| Texto general | `RecursiveCharacterTextSplitter` | 1000 | 200 | Balance general entre contexto y precisión |
| Documentación técnica | `MarkdownHeaderTextSplitter` | — | — | Preserva estructura de secciones |
| Código fuente | `RecursiveCharacterTextSplitter(language=...)` | 500 | 50 | Funciones/clases como unidades |
| Contratos/legal | `RecursiveCharacterTextSplitter` | 1500 | 300 | Cláusulas largas necesitan más contexto |
| FAQ / Q&A | `CharacterTextSplitter(separator="\n\n")` | — | 0 | Cada pregunta-respuesta es un chunk |

### Chunking semántico (avanzado)

```python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

# Agrupa oraciones por similitud semántica
chunker = SemanticChunker(
    OpenAIEmbeddings(),
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95,
)
chunks = chunker.create_documents([texto])
```

**Cuándo usar chunking semántico**: documentos heterogéneos donde los separadores
textuales no reflejan cambios de tema. Más costoso (requiere embedding por oración).

### Reglas estrictas de chunking

- Usar `tiktoken` para contar tokens reales, no `len(texto)`
- overlap > 0 para evitar pérdida de contexto en bordes
- chunk_size demasiado grande → ruido en recuperación
- chunk_size demasiado pequeño → fragmentos sin contexto suficiente
- Siempre incluir metadata: `source`, `page`, `section` para trazabilidad

---

## Vector stores — criterios de selección

| Vector store | Cuándo usar | Ventajas | Limitaciones |
|-------------|-------------|----------|-------------|
| **pgvector** | Ya tienes PostgreSQL, < 10M vectores | Una sola BD, SQL familiar, transacciones ACID | Rendimiento baja con > 10M vectores |
| **Chroma** | Prototipado local, desarrollo | Zero config, in-memory o persistente | No escala a producción con alta carga |
| **Pinecone** | Serverless, equipo sin DevOps | Escala automática, filtrado por metadata | Vendor lock-in, costo por volumen |
| **Weaviate** | Búsqueda híbrida vectorial + keyword | Módulos de vectorización, GraphQL API | Complejidad operativa |
| **Qdrant** | Alto rendimiento, filtrado avanzado | Filtros combinados eficientes, Rust core | Menos integraciones que Pinecone |
| **FAISS** | Batch processing, no necesitas persistencia | Ultra rápido en memoria, Facebook Research | Sin persistencia nativa, sin filtros |

### Patrón de inicialización con pgvector

```python
from langchain_postgres import PGVector

vector_store = PGVector(
    embeddings=embeddings,
    collection_name="mi_coleccion",
    connection=DATABASE_URL,
    use_jsonb=True,  # metadata como JSONB para filtros eficientes
)
```

---

## Modelos de embedding

```
text-embedding-3-small (OpenAI)     → Bajo costo ($0.02/1M tokens), suficiente para mayoría
text-embedding-3-large (OpenAI)     → Alta precisión ($0.13/1M tokens), documentos técnicos
voyage-3 (Anthropic/Voyage)         → Óptimo con Claude, buen balance
all-MiniLM-L6-v2 (HuggingFace)     → Gratuito, on-premise, calidad base aceptable
nomic-embed-text (Ollama)           → 100% local, sin costo de API, privacidad total
```

**Regla**: nunca mezclar modelos de embedding entre ingestión y búsqueda. El modelo
que generó los embeddings debe ser el mismo que transforma la query.

---

## Retrieval — estrategias de recuperación

### Búsqueda básica vs avanzada

| Estrategia | Implementación | Cuándo usar |
|-----------|---------------|-------------|
| Similarity search | `vectorstore.similarity_search(query, k=4)` | Caso base, consultas directas |
| MMR (Maximal Marginal Relevance) | `vectorstore.max_marginal_relevance_search(query, k=4, fetch_k=20)` | Evitar chunks redundantes |
| Búsqueda híbrida | Vectorial + BM25 keyword | Documentos técnicos con terminología específica |
| Multi-query | Generar N variantes de la query con LLM | Queries ambiguas o cortas |
| Parent document | Recuperar chunk hijo, devolver documento padre | Necesitas contexto amplio pero búsqueda precisa |

### Reranking

```python
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank

reranker = CohereRerank(model="rerank-v3.5", top_n=3)
retriever_con_rerank = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20}),
)
```

**Patrón retrieve-wide-rerank-narrow**: recuperar 20 chunks con similarity search,
rerankecar a los 3 más relevantes. Mejora precisión sin aumentar tokens de contexto.

---

## Evaluación con RAGAS

### Métricas obligatorias antes de producción

```python
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)

resultado = evaluate(
    dataset=dataset_evaluacion,  # mínimo 50 preguntas con ground truth
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
```

### Umbrales mínimos aceptables

| Métrica | Umbral mínimo | Qué indica si falla |
|---------|:----------:|-------------------|
| `faithfulness` | > 0.8 | El LLM alucina — revisar retriever y prompt |
| `answer_relevancy` | > 0.7 | La respuesta no atiende la pregunta — revisar prompt |
| `context_precision` | > 0.7 | Se recuperan chunks irrelevantes — ajustar chunking/k |
| `context_recall` | > 0.7 | Falta contexto — aumentar k o mejorar embeddings |

Si `faithfulness < 0.6` después de optimizar → evaluar fine-tuning del retriever.

---

## Patrones de producción

### Caching de embeddings

```python
from langchain.storage import LocalFileStore
from langchain.embeddings import CacheBackedEmbeddings

store = LocalFileStore("./cache/embeddings/")
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
    underlying_embeddings=embeddings,
    document_embedding_cache=store,
    namespace=embeddings.model,  # invalida cache si cambia el modelo
)
```

### Streaming de respuestas

```python
async for chunk in cadena_rag.astream({"question": pregunta}):
    yield chunk  # Enviar al cliente vía SSE o WebSocket
```

### Guardrails obligatorios

- Rate limiting por usuario (máx. 10 queries/min para APIs públicas)
- Límite de tokens de contexto: `k` entre 3 y 6 chunks
- Timeout en llamadas al LLM: 30s máximo
- Logging de query + contexto recuperado + respuesta para auditoría
- Nunca incluir PII en embeddings sin cifrar o consentimiento

---

## RAG con grafo de conocimiento (patrón LightRAG)

### Arquitectura híbrida: Vector + Knowledge Graph

A diferencia del RAG tradicional (solo vectores), LightRAG combina:

```
Documentos → Chunking → [Extracción de entidades/relaciones] → Knowledge Graph
                       → [Embeddings]                         → Vector Store
                                                                    ↓
Consulta → Keywords → [Retrieval híbrido: KG + Vector] → Reranker → LLM → Respuesta
```

**Ventaja clave**: el KG captura relaciones explícitas entre entidades que la búsqueda
vectorial sola no puede representar. Ejemplo: "Bug X fue causado por Patrón Y que
se adoptó en ADR-003" — esta cadena causal se pierde en similaridad de embeddings.

### 5 modos de retrieval (adoptar según caso de uso)

| Modo | Estrategia | Cuándo usar |
|------|-----------|-------------|
| **local** | Keywords de bajo nivel → entidades cercanas → relaciones | Preguntas específicas sobre un componente |
| **global** | Keywords de alto nivel → relaciones agregadas/comunidades | Preguntas amplias sobre patrones o tendencias |
| **hybrid** | Local + global fusionados | Cuando no se sabe el nivel de especificidad |
| **naive** | Solo similarity search (sin KG) | Búsqueda rápida cuando se conoce el texto |
| **mix** | KG + vector + reranking (recomendado) | Default para producción — mejor precisión |

### Extracción de entidades y relaciones

```python
async def extract_entities(chunk: str, llm) -> dict:
    """LLM extrae entidades y relaciones del chunk."""
    # El LLM identifica:
    # - Entidades: personas, tecnologías, componentes, conceptos
    # - Relaciones: causa, dependencia, uso, implementación
    # - Keywords: alto nivel (temas) y bajo nivel (términos específicos)
    prompt = f"""Extrae entidades y relaciones del siguiente texto.
    Formato: ENTITY|||TYPE|||DESCRIPTION y RELATION|||SOURCE|||TARGET|||DESCRIPTION
    Texto: {chunk}"""
    return await llm(prompt)
```

**Gleaning**: ejecutar extracción 2-3 veces con "¿falta algo?" para mejorar cobertura.

### Configuración recomendada

```python
# Parámetros optimizados para proyectos de software
chunk_token_size = 1200        # Balance contexto/precisión
chunk_overlap_token_size = 100  # Continuidad entre chunks
top_k = 60                     # Entidades/relaciones a recuperar
chunk_top_k = 20               # Chunks post-reranking
max_entity_tokens = 6000       # Límite tokens por entidad
max_relation_tokens = 8000     # Límite tokens por relación
max_total_tokens = 30000       # Límite total de contexto
cosine_threshold = 0.2         # Mínimo de similaridad para incluir
```

### Cuándo usar RAG + KG vs RAG solo vectores

| Criterio | Solo vectores | Vector + KG |
|----------|:-------------:|:-----------:|
| Preguntas factuales directas | Suficiente | Overkill |
| Relaciones causales entre documentos | Pierde relaciones | Captura cadenas causales |
| Corpus < 100 documentos | Óptimo | KG no aporta mucho |
| Corpus > 1000 documentos | Pierde contexto global | KG organiza conocimiento |
| Preguntas multi-hop (A→B→C) | Falla frecuente | Sigue relaciones en el grafo |
| Costo de ingestión | Bajo (solo embeddings) | Alto (LLM extrae entidades) |

---

## Reglas estrictas

- **Evaluar con RAGAS** antes de desplegar — mínimo 50 preguntas con ground truth
- **Nunca mezclar** modelos de embedding entre ingestión y búsqueda
- **Siempre incluir metadata** en chunks: source, page, section
- **Reranking obligatorio** cuando k > 5 en producción
- **Cachear embeddings** para evitar recálculos en re-ingestión
- **Nunca hacer embedding en loops** — usar `embed_documents(chunks)` en lote
- **Monitorear drift**: re-evaluar con RAGAS cada 2 semanas o tras cambios en el corpus
- **KG: gleaning obligatorio** — mínimo 2 pasadas de extracción para cobertura ≥ 80%
- **KG: merge de entidades** — entidades con nombre similar deben fusionarse automáticamente
- **KG: caché de LLM** — habilitar para reducir costo en re-ingestión de documentos sin cambios

---

## Gotchas / Errores comunes no obvios

**`similarity_search(query, k=4)` devuelve documentos con score > 0.9 pero la respuesta del LLM alucina información no presente en ningún chunk**: el score de similitud coseno mide afinidad semántica del embedding, no relevancia para la pregunta. Un chunk sobre "autenticación JWT" con alta similitud a la query "cómo funciona OAuth2" puede no contener la respuesta real. Causa: los embeddings capturan temas relacionados, no respuestas precisas; la similitud semántica y la completitud factual son dimensiones independientes. Fix: agregar un paso de reranking con un modelo cross-encoder (Cohere rerank, Jina reranker) después del retrieval vectorial. El cross-encoder evalúa relevancia query→documento directamente, no por similitud de embedding.

**`RecursiveCharacterTextSplitter` con `chunk_overlap=200` en documentos de soporte técnico crea chunks que empiezan con la respuesta de la pregunta anterior pero sin la pregunta, haciendo que el retriever recupere contextos semánticamente incoherentes**: un chunk que comienza con "...solución: actualizar el driver a la versión 4.2" sin el contexto del problema tiene alto overlap con queries generales sobre drivers pero no responde ninguna pregunta concreta correctamente. Causa: el overlap de caracteres corta oraciones sin considerar la estructura semántica del documento. Fix: para documentos de Q&A o tickets de soporte, usar `CharacterTextSplitter(separator="\n\n", chunk_overlap=0)` con cada par pregunta-respuesta como chunk atómico, añadiendo la pregunta como prefijo en el metadata para boosting en el retrieval.

**`CacheBackedEmbeddings` con `LocalFileStore` produce inconsistencias cuando dos procesos del servidor escriben simultáneamente al mismo archivo de cache porque `LocalFileStore` no usa file locking**: bajo carga concurrente en un servidor uvicorn con múltiples workers, el cache de embeddings se corrompe ocasionalmente generando embeddings parciales que fallan silenciosamente en la búsqueda vectorial (el vector store retorna k=0 resultados para esas queries). Causa: `LocalFileStore` usa escrituras directas sin sincronización entre procesos. Fix: en producción con múltiples workers, usar Redis como backend de cache de embeddings en lugar de `LocalFileStore`, o limitar la app a un solo worker y usar async concurrente en lugar de multiprocessing.

**RAGAS reporta `faithfulness > 0.85` en el test set pero el sistema alucina en producción porque el test set fue generado con el mismo LLM que ahora se evalúa**: el LLM genera preguntas cuyas respuestas están claramente en los documentos de entrenamiento, sesgando los scores hacia arriba. Causa: la distribución de queries reales de usuarios es diferente al test set sintético — los usuarios hacen preguntas sobre brechas, casos edge y contradicciones que el LLM generador evita. Fix: mezclar el test set sintético (50%) con queries reales anonimizadas de usuarios (50%). Si no hay queries reales disponibles, usar personas adversariales explícitas en la generación: "genera preguntas que el sistema probablemente no pueda responder correctamente".
