Empezar aquí
Índice de la sección

RAG

Referencia de la generación aumentada por recuperación, con indexación, búsqueda, inyección de fragmentos en el prompt, parámetros clave y fallos típicos.

Publicado 3 min de lectura

RAG (Retrieval-Augmented Generation) es la técnica de buscar los fragmentos relevantes de tus documentos en el momento de la pregunta e incluirlos en el prompt, para que el modelo responda con esa información y no solo con lo que aprendió en su entrenamiento. Se describió en Lewis et al., 2020, Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Es la forma estándar de dar al modelo conocimiento privado, extenso o que cambia.

Este artículo es la referencia de las piezas. El aprendizaje paso a paso está en Nivel 30 · Contexto, memoria y RAG y en el curso RAG a fondo.

Las dos fases

flowchart LR
  subgraph Indexar
    D[Documentos] --> C[Trocear] --> E[Embeddings] --> V[(Índice)]
  end
  subgraph Consultar
    P[Pregunta] --> B[Buscar] --> R[Reordenar] --> G[Modelo + fragmentos]
  end
  V --> B
FaseCuándoPasos
IndexaciónUna vez, y cada vez que cambian los documentosLimpiar, trocear (Chunking), calcular Embeddings, guardar con metadatos
ConsultaEn cada preguntaBuscar, opcionalmente reordenar, construir el prompt, generar, verificar

Componentes

ComponenteQué haceReferencia
Troceado (chunking)Divide documentos en fragmentos con sentido propioChunking: cortar sin romper el sentido, Trocear documentos con solapamiento
EmbeddingsConvierte texto en vectores comparablesEmbeddings: texto convertido en vectores
Vector storeGuarda vectores y busca los más cercanosSimilitud coseno
Búsqueda léxicaCoincidencia de palabras exactas con BM25Búsqueda híbrida: vectorial + BM25
Búsqueda híbridaCombina vectorial y léxicaBúsqueda híbrida con BM25 y embeddings
RerankingReordena los candidatos con un modelo más precisoReranking: reordenar antes de responder
Generación con citasResponde citando fragmentos y se abstiene si no hay baseResponder con citas verificables

Anthropic no ofrece un modelo de embeddings propio: usa el proveedor que prefieras. Mezclar embeddings de modelos distintos en el mismo índice no funciona; si cambias de modelo, reindexa.

Parámetros que más influyen

ParámetroEfectoCómo ajustarlo
Tamaño del fragmentoPequeño: preciso pero sin contexto. Grande: más contexto, más ruidoEmpieza por párrafos o secciones naturales del documento y mide
SolapamientoEvita cortar una idea entre dos fragmentosUna o dos frases suelen bastar
k (fragmentos recuperados)Más k: más cobertura, más tokens y más ruidoRecupera de más y reordena a menos
MetadatosPermiten filtrar (fecha, producto, idioma) antes de buscarGuarda origen, sección y fecha en cada fragmento
Contexto del fragmentoUn fragmento suelto puede ser ambiguo («el plazo es de 30 días»)Añade al fragmento una frase que lo sitúe: Recuperación contextual

La recuperación contextual consiste en anteponer a cada fragmento un breve contexto generado a partir del documento completo antes de indexarlo. Anthropic la describe en Introducing Contextual Retrieval (2024).

Ejemplo completo

Un RAG mínimo en memoria. embed() es abstracta: conéctala a tu proveedor.

import math
import anthropic

MODEL = "claude-sonnet-5-5"
client = anthropic.Anthropic()

def embed(textos: list[str]) -> list[list[float]]:
    """Usa el proveedor de embeddings que prefieras."""
    raise NotImplementedError

def coseno(a: list[float], b: list[float]) -> float:
    num = sum(x * y for x, y in zip(a, b))
    return num / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))

# Indexación (una vez)
FRAGMENTOS = [
    {"id": "dev-1", "fuente": "politicas.md#devoluciones", "texto": "Las devoluciones se aceptan hasta 30 días después de la entrega."},
    {"id": "env-1", "fuente": "politicas.md#envios", "texto": "Los envíos peninsulares tardan entre 48 y 72 horas laborables."},
]
VECTORES = embed([f["texto"] for f in FRAGMENTOS])

def recuperar(pregunta: str, k: int = 4) -> list[dict]:
    q = embed([pregunta])[0]
    puntuados = sorted(zip(FRAGMENTOS, VECTORES), key=lambda fv: coseno(q, fv[1]), reverse=True)
    return [f for f, _ in puntuados[:k]]

def responder(pregunta: str) -> str:
    fragmentos = recuperar(pregunta)
    contexto = "\n".join(
        f'<fragmento id="{f["id"]}" fuente="{f["fuente"]}">{f["texto"]}</fragmento>' for f in fragmentos
    )
    resp = client.messages.create(
        model=MODEL,
        max_tokens=600,
        system=(
            "Responde solo con la información de <fragmentos>. Cita el id entre corchetes, p. ej. [dev-1]. "
            "Si los fragmentos no contienen la respuesta, di que no lo sabes."
        ),
        messages=[{"role": "user", "content": f"<fragmentos>\n{contexto}\n</fragmentos>\n\nPregunta: {pregunta}"}],
    )
    return "".join(b.text for b in resp.content if b.type == "text")
import Anthropic from "@anthropic-ai/sdk";

const MODEL = "claude-sonnet-5-5";
const client = new Anthropic();

type Fragmento = { id: string; fuente: string; texto: string };

async function embed(textos: string[]): Promise<number[][]> {
  // Usa el proveedor de embeddings que prefieras.
  throw new Error("No implementado");
}

function coseno(a: number[], b: number[]): number {
  const num = a.reduce((s, x, i) => s + x * b[i], 0);
  const na = Math.sqrt(a.reduce((s, x) => s + x * x, 0));
  const nb = Math.sqrt(b.reduce((s, x) => s + x * x, 0));
  return num / (na * nb);
}

const FRAGMENTOS: Fragmento[] = [
  { id: "dev-1", fuente: "politicas.md#devoluciones", texto: "Las devoluciones se aceptan hasta 30 días después de la entrega." },
  { id: "env-1", fuente: "politicas.md#envios", texto: "Los envíos peninsulares tardan entre 48 y 72 horas laborables." },
];
const VECTORES = await embed(FRAGMENTOS.map((f) => f.texto));

async function recuperar(pregunta: string, k = 4): Promise<Fragmento[]> {
  const [q] = await embed([pregunta]);
  return FRAGMENTOS.map((f, i) => ({ f, s: coseno(q, VECTORES[i]) }))
    .sort((a, b) => b.s - a.s)
    .slice(0, k)
    .map((x) => x.f);
}

async function responder(pregunta: string): Promise<string> {
  const fragmentos = await recuperar(pregunta);
  const contexto = fragmentos
    .map((f) => `<fragmento id="${f.id}" fuente="${f.fuente}">${f.texto}</fragmento>`)
    .join("\n");
  const resp = await client.messages.create({
    model: MODEL,
    max_tokens: 600,
    system:
      "Responde solo con la información de <fragmentos>. Cita el id entre corchetes, p. ej. [dev-1]. " +
      "Si los fragmentos no contienen la respuesta, di que no lo sabes.",
    messages: [{ role: "user", content: `<fragmentos>\n${contexto}\n</fragmentos>\n\nPregunta: ${pregunta}` }],
  });
  return resp.content.map((b) => (b.type === "text" ? b.text : "")).join("");
}

Para producción faltan tres cosas: un índice persistente, búsqueda híbrida con reordenado y verificar las citas (que cada id citado exista entre los recuperados y que el texto lo respalde). Todo eso está en RAG con citas verificables.

RAG como herramienta

En lugar de recuperar siempre antes de llamar al modelo, puedes darle una herramienta buscar_docs y dejar que decida cuándo y qué buscar, incluso varias veces con consultas distintas. Es más flexible y más caro; encaja en agentes (Uso de herramientas). El RAG fijo es más predecible y fácil de evaluar.

Errores frecuentes

  • Evaluar solo la respuesta final. Mide por separado si la recuperación trae el fragmento correcto y si el modelo lo usa bien: Evaluar un RAG.
  • Solo búsqueda vectorial. Falla con códigos, nombres propios y términos exactos. Añade BM25.
  • Fragmentos sin contexto ni metadatos. El modelo recibe «el plazo es de 30 días» sin saber de qué.
  • No permitir la abstención. Si el prompt no dice qué hacer cuando no hay base, el modelo rellena: Alucinación.
  • Indexar una vez y olvidarse. Los documentos cambian; el índice también debe cambiar.
  • Tratar los documentos como confiables. Un documento indexado puede contener instrucciones: Prompt injection.

Ver también