RAG en Producción: Cómo Darle Memoria Privada a Cualquier LLM Sin Fine-Tuning

Retrieval-Augmented Generation (RAG) resuelve el problema más común en implementaciones de IA empresarial: el modelo no sabe nada de tu negocio. No conoce tus contratos, tu documentación interna, tus procedimientos ni el historial de tus clientes. Fine-tuning es una opción, pero tiene un costo alto en tiempo, dinero y mantenimiento. RAG es la alternativa pragmática — y en la mayoría de los casos, la correcta.

Esta guía cubre la arquitectura completa de un pipeline RAG en producción: desde la ingesta de documentos hasta la evaluación de resultados, con privacidad total y datos que nunca salen de tu infraestructura.

¿Qué es RAG y cuándo usarlo?

RAG es una técnica que le da al modelo acceso a información externa en tiempo de inferencia. En lugar de memorizar datos durante el entrenamiento (fine-tuning), el modelo consulta una base de conocimiento justo antes de responder.

Criterio Fine-Tuning RAG
Información que cambia frecuentemente ❌ Requiere re-entrenamiento ✅ Actualiza solo el vector store
Costo de implementación Alto ($3–$500 por run) Bajo (solo infra)
Privacidad de datos Datos van al proveedor ✅ Pueden quedarse en tu infra
Trazabilidad (¿de dónde viene la respuesta?) ❌ Opaco ✅ Citas y fuentes explícitas
Cambio de comportamiento del modelo ✅ Profundo (tono, formato, persona) ❌ No modifica el modelo base

Regla práctica: si tu problema es "el modelo no sabe X", usa RAG. Si tu problema es "el modelo no se comporta como quiero", considera fine-tuning. Y si ambos problemas existen, puedes combinar las dos técnicas.

La Arquitectura Completa de un Pipeline RAG

┌──────────────────────────────────────────────────────┐
│                  PIPELINE DE INGESTA                 │
│  Documentos → Chunking → Embeddings → Vector Store   │
└──────────────────────────────────────────────────────┘
                          ↓ (offline, batch)
┌──────────────────────────────────────────────────────┐
│                PIPELINE DE CONSULTA                  │
│  Query → Embed → Buscar → Contexto → LLM → Respuesta │
└──────────────────────────────────────────────────────┘
                          ↓ (online, tiempo real)
┌──────────────────────────────────────────────────────┐
│                PIPELINE DE EVALUACIÓN                │
│  Relevancia → Faithfulness → Latencia → Feedback     │
└──────────────────────────────────────────────────────┘

Cada pipeline tiene su propia lógica, sus propias fallas posibles y sus propias métricas de calidad. Vamos por partes.

Paso 1: Ingesta y Chunking de Documentos

El chunking es donde más pipelines RAG fallan silenciosamente. Un chunk mal diseñado produce retrieval irrelevante, y el modelo responde con confianza sobre la base equivocada — peor que no saber nada.

Estrategias de chunking y cuándo usar cada una

Estrategia Cómo funciona Ideal para
Fixed-size (tokens) Divide cada N tokens con overlap Documentos sin estructura clara
Sentence-based Divide por oraciones completas FAQs, artículos, contenido web
Semantic chunking Agrupa por similitud semántica Documentos técnicos, manuales
Hierarchical (parent-child) Chunks pequeños para retrieval, grandes para contexto Contratos, documentación extensa
Structure-aware Respeta headers, secciones, tablas PDFs estructurados, wikis, Notion

Implementación con LangChain y parámetros recomendados

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader, DirectoryLoader

# Cargar documentos desde directorio local
loader = DirectoryLoader(
    "./docs",
    glob="**/*.pdf",
    loader_cls=PyPDFLoader
)
documents = loader.load()

# Chunking con overlap para no perder contexto en los bordes
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # ~750 palabras por chunk
    chunk_overlap=200,      # 20% overlap — crucial para preguntas en bordes
    length_function=len,
    separators=["\n\n", "\n", ".", " "]
)
chunks = splitter.split_documents(documents)

# Agregar metadata relevante a cada chunk
for i, chunk in enumerate(chunks):
    chunk.metadata["chunk_id"] = i
    chunk.metadata["source_file"] = chunk.metadata.get("source", "unknown")
    chunk.metadata["ingested_at"] = datetime.now().isoformat()

print(f"Total chunks: {len(chunks)}")

Paso 2: Embeddings — Elegir el Modelo Correcto

Los embeddings convierten texto en vectores numéricos que capturan significado semántico. La calidad del embedding determina directamente la calidad del retrieval.

Modelo Dimensiones Costo Privacidad Cuándo usarlo
OpenAI text-embedding-3-small 1536 $0.02/1M tokens ❌ Datos a OpenAI Prototipado rápido
OpenAI text-embedding-3-large 3072 $0.13/1M tokens ❌ Datos a OpenAI Alta precisión, sin restricción de privacidad
nomic-embed-text (Ollama) 768 $0 (local) ✅ 100% local Datos sensibles, privacidad total
BAAI/bge-large-en-v1.5 1024 $0 (self-hosted) ✅ 100% local Inglés, alto rendimiento en MTEB
multilingual-e5-large 1024 $0 (self-hosted) ✅ 100% local Documentos en múltiples idiomas (incluyendo español)
# Opción 1: OpenAI (rápido, no privado)
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Opción 2: Ollama local (privado, gratis)
from langchain_community.embeddings import OllamaEmbeddings
embeddings = OllamaEmbeddings(
    model="nomic-embed-text",
    base_url="http://localhost:11434"
)

# Opción 3: HuggingFace local (mejor calidad, requiere GPU)
from langchain_community.embeddings import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-large-en-v1.5",
    model_kwargs={'device': 'cuda'}
)

Paso 3: Vector Store — Dónde Viven los Embeddings

El vector store almacena los embeddings y permite búsqueda por similitud semántica a alta velocidad. La elección correcta depende de tu escala y requisitos de privacidad.

Vector Store Hosting Escala Setup
Qdrant Self-hosted / Cloud Millones de vectores Docker en minutos
Chroma Local / Self-hosted Miles–cientos de miles pip install, cero config
Weaviate Self-hosted / Cloud Millones de vectores Docker + config YAML
pgvector (PostgreSQL) Self-hosted Cientos de miles Extensión PostgreSQL
Pinecone Solo cloud Billones de vectores API key, sin infra

Para la mayoría de casos enterprise con requisito de privacidad, Qdrant self-hosted es la recomendación: rendimiento excelente, Docker en minutos, y los datos nunca salen de tu red.

# Levantar Qdrant con Docker
docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

# Conectar e ingestar los chunks
from langchain_community.vectorstores import Qdrant

vectorstore = Qdrant.from_documents(
    documents=chunks,
    embedding=embeddings,
    url="http://localhost:6333",
    collection_name="empresa_docs",
    force_recreate=False   # True solo en el primer run
)

print(f"Ingested {len(chunks)} chunks into Qdrant")

Paso 4: Pipeline de Consulta — Del Query a la Respuesta

Aquí es donde el RAG cobra vida. El usuario hace una pregunta → se convierte a embedding → se buscan los chunks más similares → se construye el contexto → el LLM genera la respuesta citando las fuentes.

from langchain_anthropic import ChatAnthropic
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# Definir el LLM
llm = ChatAnthropic(
    model="claude-sonnet-4-6",
    anthropic_api_key="sk-ant-xxx",
    temperature=0   # 0 para respuestas más deterministas en RAG
)

# Retriever con configuración de búsqueda
retriever = vectorstore.as_retriever(
    search_type="mmr",           # MMR reduce redundancia entre chunks recuperados
    search_kwargs={
        "k": 5,                  # Recuperar los 5 chunks más relevantes
        "fetch_k": 20,            # Candidatos previos al reranking
        "lambda_mult": 0.7        # Balance relevancia vs diversidad
    }
)

# Prompt con instrucción explícita de citar fuentes
prompt_template = """Usa ÚNICAMENTE la siguiente información para responder.
Si la respuesta no está en el contexto, di exactamente: "No tengo esa información en la documentación disponible."
No inventes ni uses conocimiento externo.

CONTEXTO:
{context}

PREGUNTA: {question}

RESPUESTA (cita el documento fuente cuando sea relevante):"""

PROMPT = PromptTemplate(
    template=prompt_template,
    input_variables=["context", "question"]
)

# Pipeline completo
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT},
    return_source_documents=True   # Retorna los chunks usados
)

# Consulta
result = qa_chain.invoke({"query": "¿Cuál es la política de reembolso para clientes enterprise?"})
print(result["result"])
print("\nFuentes usadas:")
for doc in result["source_documents"]:
    print(f  "- {doc.metadata['source_file']} (chunk {doc.metadata['chunk_id']})")

Paso 5: Técnicas Avanzadas de Retrieval

El retrieval básico (buscar por similitud semántica) falla en casos como preguntas muy específicas, términos técnicos poco comunes o queries que no coinciden semánticamente con la respuesta. Estas técnicas lo resuelven.

Hybrid Search: Semántico + BM25

Combina búsqueda semántica (embeddings) con búsqueda léxica (BM25/TF-IDF). El híbrido supera a cualquiera de los dos en aislamiento para la mayoría de casos reales.

from langchain.retrievers import EnsembleRetriever, BM25Retriever

# Retriever léxico (BM25)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5

# Retriever semántico (vectorstore)
semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# Ensemble: 40% léxico + 60% semántico
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, semantic_retriever],
    weights=[0.4, 0.6]
)

Reranking con Cross-Encoder

Después de recuperar candidatos, un cross-encoder los reordena por relevancia real. Mejora la precisión ~15-20% en benchmarks estándar.

from langchain.retrievers import ContextualCompressionRetriever
from langchain_community.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# Modelo de reranking local (no envía datos a terceros)
reranker_model = HuggingFaceCrossEncoder(
    model_name="BAAI/bge-reranker-large"
)
compressor = CrossEncoderReranker(model=reranker_model, top_n=3)

# Pipeline: busca 10 → reranker selecciona los 3 mejores
reranking_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=ensemble_retriever
)

Paso 6: Integración con n8n para Automatización

Una vez que el pipeline RAG está funcionando como servicio (FastAPI, por ejemplo), integrarlo con n8n es trivial. Esto te permite usar RAG dentro de cualquier workflow de automatización.

# FastAPI wrapper para el pipeline RAG
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class QueryRequest(BaseModel):
    query: str
    collection: str = "empresa_docs"
    top_k: int = 5

@app.post("/query")
async def query_rag(request: QueryRequest):
    result = qa_chain.invoke({"query": request.query})
    sources = [
        {"file": d.metadata["source_file"], "chunk": d.metadata["chunk_id"]}
        for d in result["source_documents"]
    ]
    return {"answer": result["result"], "sources": sources}

# Desde n8n: HTTP Request a http://tu-servidor:8000/query
# Body: {"query": "{{ $json.userMessage }}", "collection": "empresa_docs"}

Paso 7: Evaluación del Pipeline RAG

Un pipeline RAG sin evaluación es un pipeline que falla silenciosamente. Las tres métricas más importantes:

Métrica Qué mide Señal de problema
Context Relevance ¿Los chunks recuperados son relevantes para la pregunta? Si es bajo: problema de chunking o embeddings
Faithfulness ¿La respuesta está basada en el contexto recuperado? Si es bajo: el modelo está alucinando fuera del contexto
Answer Relevance ¿La respuesta responde la pregunta original? Si es bajo: problema en el prompt o en la lógica de contexto
# Evaluación con RAGAS (framework estándar)
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_relevancy
from datasets import Dataset

# Dataset de evaluación: preguntas con respuestas esperadas
eval_data = {
    "question": ["¿Cuál es el SLA para clientes enterprise?"],
    "answer": [result["result"]],
    "contexts": [[d.page_content for d in result["source_documents"]]],
    "ground_truth": ["El SLA enterprise garantiza 99.9% de uptime..."]
}

dataset = Dataset.from_dict(eval_data)
scores = evaluate(
    dataset,
    metrics=[faithfulness, answer_relevancy, context_relevancy]
)
print(scores)
# Output: {'faithfulness': 0.92, 'answer_relevancy': 0.88, 'context_relevancy': 0.85}

Arquitectura de Privacidad Total: El Stack Completamente Local

Si los datos son altamente sensibles (legal, médico, financiero), el stack puede ser 100% local — ningún dato sale de tu infraestructura:

Stack completamente local:

LLM:         Ollama + Qwen3-32B o Llama 4 Maverick
Embeddings:  Ollama + nomic-embed-text o multilingual-e5-large
Vector Store: Qdrant self-hosted (Docker)
Orquestación: LangChain + FastAPI
Workflow:    n8n self-hosted

Costo de infra: ~$50/mes (VPS con GPU A10 o RTX 3090 en RunPod)
Costo por consulta: $0 (sin tokens de API)
Datos que salen de tu red: NINGUNO

Los 5 Errores Más Comunes en RAG (y Cómo Evitarlos)

  1. Chunks demasiado grandes o demasiado pequeños. Un chunk de 5,000 tokens incluye demasiado ruido. Uno de 50 tokens pierde contexto. El rango óptimo para la mayoría de documentos es 500-1,500 tokens con 10-20% de overlap.
  2. No agregar metadata a los chunks. Sin metadata (fuente, fecha, sección) no puedes filtrar por relevancia temporal o por dominio, y el modelo no puede citar la fuente con precisión.
  3. Usar solo búsqueda semántica. Los embeddings fallan con términos muy específicos (IDs de producto, códigos, nombres propios poco comunes). Hybrid search resuelve este problema.
  4. No evaluar el pipeline. Un pipeline que devuelve respuestas con confianza incorrecta es peor que no tener RAG. Mide faithfulness y context relevance desde el día uno.
  5. No actualizar el índice cuando cambian los documentos. Un vector store desactualizado produce respuestas con información obsoleta. Implementa un proceso de re-ingesta incremental o por detección de cambios.

Costos Reales del Stack RAG

Componente Cloud (OpenAI/Anthropic) Self-hosted
LLM (10K consultas/mes) $20–$60 (Claude Sonnet) $0 (Qwen3-32B local)
Embeddings (1M docs) $0.02–$0.13 (OpenAI) $0 (nomic-embed-text)
Vector Store $70+ (Pinecone) $10/mes (VPS Qdrant)
Total estimado $90–$150/mes $10–$50/mes

Conclusión: RAG es la Base, No el Destino

RAG resuelve el problema de conocimiento de dominio de forma pragmática, actualizable y trazable. Es la herramienta correcta para el 80% de los casos donde un cliente dice "quiero que la IA sepa de mi empresa".

El camino correcto es: empieza con RAG, mide la calidad del retrieval, evalúa si la faithfulness es suficiente para tu caso de uso, y solo entonces considera si el fine-tuning agrega algo que RAG no puede darte. En la mayoría de los casos, no lo necesitarás.

Y si la privacidad es no negociable: el stack completamente local (Ollama + Qdrant + n8n) es hoy una opción real de producción, no un experimento académico.