31

RAG: leer antes de responder

¿Cómo se le da conocimiento propio a un modelo sin reentrenarlo?

13 minRuta ejecutiva

RAG: leer antes de responder

Con voz. Puedes pausar y saltar a cualquier paso.

Lección completa

Subtítulos

Un cliente le pregunta al chatbot de una tienda de bicicletas eléctricas: «¿cuánto dura la garantía de la batería?». La respuesta está en el manual de la tienda.

Voz

Sin voz: la lección avanza sola con subtítulos.

La voz de esta lección está en preparación: por ahora avanza sola con subtítulos.

0:00 / 0:00

Qué lograrás

  • Explicar qué hace un sistema RAG: preparar una vez la biblioteca (fragmentar e indexar) y, con cada pregunta, recuperar, armar el prompt con fuentes y responder citando.
  • Predecir qué fragmentos recupera una pregunta y si el correcto llega al prompt según el tamaño del fragmento y el valor de k.
  • Reconocer las tres fallas típicas (no encontrar, contradicciones, instrucciones escondidas) y medir la recuperación con preguntas propias.

Ideas clave

  1. RAG no reentrena el modelo: con cada pregunta busca los fragmentos de tus documentos que más se parecen y los pega en el prompt, junto con la pregunta.
  2. Se prepara una vez (fragmentar, convertir en vectores, indexar) y, con cada pregunta, se recupera, se arma el prompt con fuentes numeradas y se responde citando.
  3. El tamaño del fragmento y el número k de fragmentos que se pegan son decisiones con costo: se eligen probando, no por intuición. Un reordenador puede mejorar el orden, pero no siempre.
  4. La búsqueda por palabras falla con las paráfrasis; los embeddings comparan significados y rescatan muchas, no todas.
  5. Fallas típicas: no encontrar lo correcto, documentos que se contradicen e instrucciones escondidas en el texto. La calidad depende sobre todo de la recuperación: mídela con preguntas propias.

Lo que mucha gente cree

Creencia común RAG «entrena» al modelo con tus documentos: después de leerlos, el modelo los sabe.

Lo correcto El modelo no aprende nada: sus pesos no cambian. Con cada pregunta se buscan los fragmentos y se pegan en el prompt; si la búsqueda no los trae, el modelo no los ve.

Compruébalo

Cuatro preguntas. Sin nota ni cuenta: sirven para ver si la idea quedó.

Concepto¿Qué hace un sistema RAG antes de que el modelo responda?
ConceptoSi los fragmentos son demasiado pequeños, ¿qué suele pasar?
CálculoEn diez preguntas de prueba, el puesto del fragmento correcto fue: 1, 1, 2, 1, 4, 1, no apareció, 3, 1 y 6. Con k = 3, ¿en cuántas llega el fragmento correcto al prompt?
AplicaciónUna aseguradora quiere un asistente que responda con base en las condiciones de sus pólizas, que cambian cada trimestre, y que muestre de dónde salió cada dato. ¿Qué conviene probar primero?

Por dentroPara quien quiere las fórmulas y el código. No hace falta para seguir el curso.

TF-IDF y similitud coseno

Así se calcula la búsqueda por palabras de la escena. Primero se quitan las palabras muy comunes («de», «la», «que»…) y se recorta cada palabra a su raíz, de forma sencilla («baterías» y «batería» cuentan igual). Con eso, el manual de la lección tiene 228 términos distintos: cada fragmento es un vector de 228 números.

Dados NN fragmentos, ct,dc_{t,d} es cuántas veces aparece el término tt en el fragmento dd, y df(t)\mathrm{df}(t) en cuántos fragmentos aparece. Con las variantes de la lección:

tf(t,d)=1+ln⁡ct,didf(t)=ln⁡N+1df(t)+1+1wt,d=tf(t,d)⋅idf(t)\mathrm{tf}(t,d)=1+\ln c_{t,d}\qquad \mathrm{idf}(t)=\ln\frac{N+1}{\mathrm{df}(t)+1}+1\qquad w_{t,d}=\mathrm{tf}(t,d)\cdot\mathrm{idf}(t)

Con N=25N=25: «ciclos» aparece en 1 fragmento (idf≈3.56\mathrm{idf}\approx 3.56) y «garantía» en 6 (idf≈2.31\mathrm{idf}\approx 2.31); por eso «ciclos» pesa más. Cada vector se normaliza, d^=d/∥d∥\hat{\mathbf d}=\mathbf d/\lVert\mathbf d\rVert, y la pregunta recibe el mismo tratamiento con los idf\mathrm{idf} del índice. La similitud coseno queda como un producto punto:

cos⁡(q,d)=q^⋅d^∈[0,1]\cos(\mathbf q,\mathbf d)=\hat{\mathbf q}\cdot\hat{\mathbf d}\in[0,1]

La idea de pesar por rareza es de Karen Spärck Jones (1972): un término que aparece en pocos documentos los distingue mejor.

BM25, la otra puntuación por palabras

BM25 (Robertson y Zaragoza, 2009) puntúa cada fragmento DD con una frecuencia que se satura y una corrección por longitud:

score(D,Q)=∑t∈Qidf(t) f(t,D) (k1+1)f(t,D)+k1 (1−b+b ∣D∣/avgdl)\mathrm{score}(D,Q)=\sum_{t\in Q}\mathrm{idf}(t)\,\frac{f(t,D)\,(k_1+1)}{f(t,D)+k_1\,(1-b+b\,|D|/\mathrm{avgdl})}

con 1.2<k1<21.2<k_1<2 y 0.5<b<0.80.5<b<0.8 como valores que, según sus autores, suelen funcionar bien. Es un punto de partida difícil de superar solo con palabras, y muchos sistemas lo combinan con embeddings (búsqueda híbrida).

Embeddings y búsqueda aproximada

Con embeddings (lección 14) el vector tiene unos cientos de números densos y no hace falta que coincidan las palabras: la similitud coseno compara significados. Comparar la pregunta con los NN vectores cuesta O(N⋅D)O(N\cdot D); con millones de fragmentos se usan índices aproximados como HNSW (Malkov y Yashunin), que cambian un poco de exactitud por mucha velocidad. En el artículo que dio nombre a RAG (Lewis et al., 2020), Wikipedia se cortó en trozos de 100 palabras, 21 millones en total, que se indexaron con FAISS y HNSW; al entrenar se recuperaban k=5k=5 o k=10k=10 por pregunta.

Reordenar

Un recuperador denso (bi-encoder), como DPR (Karpukhin et al., 2020), el que usa RAG, codifica la pregunta y el fragmento por separado, así que los fragmentos se convierten en vectores una sola vez. Un reordenador (cross-encoder) lee la pregunta y el fragmento juntos en una pasada del modelo y devuelve una puntuación: es más fino, pero cuesta una pasada por candidato, así que solo se aplica a los primeros de la lista. Nogueira y Cho (2019) reordenaban con BERT los 1 000 primeros pasajes que traía BM25.

En la lección, las cifras de embeddings y de reordenador se midieron aparte (ver build-dense.py) con dos modelos abiertos: intfloat/multilingual-e5-small (Wang et al., 2024) y cross-encoder/mmarco-mMiniLMv2-L12-H384-v1, entrenado con mMARCO (Bonifacio et al., 2021). El reordenador se aplica a los 5 primeros candidatos.

Cómo se mide la recuperación

Para cada pregunta de prueba se anota el fragmento correcto y su puesto rir_i en la lista ordenada (ri=0r_i=0 si no aparece). Con QQ preguntas:

hit@k=1Q∑i1[1≤ri≤k]MRR=1Q∑i{1/riri>00ri=0\mathrm{hit@}k=\frac1Q\sum_i \mathbf 1[1\le r_i\le k]\qquad \mathrm{MRR}=\frac1Q\sum_i\begin{cases}1/r_i & r_i>0\\ 0 & r_i=0\end{cases}

En las 10 preguntas de la escena, con k=3k=3: por palabras, 7 de 10; con embeddings, 9 de 10; con embeddings y reordenador, 8 de 10 (en ambos idiomas). Diez preguntas son muy pocas para sacar conclusiones generales: el punto es el método.

Esto mide solo la primera mitad. La segunda, si la respuesta se apoya en lo recuperado, se evalúa con casos de prueba y con revisión humana o con un modelo como juez (lección 34). Ragas (Es et al., 2023) la separa en tres medidas: fidelidad (lo que dice la respuesta se deduce del contexto), pertinencia de la respuesta y pertinencia del contexto.

Actualizar sin reentrenar

En el artículo de RAG se probó cambiar solo el índice: con 82 gobernantes que cambiaron entre 2016 y 2018, el modelo acertó el 70 % con el índice de Wikipedia de 2016 para los de 2016, y el 68 % con el de 2018 para los de 2018; con el índice que no correspondía, solo el 12 % y el 4 %. Para actualizar lo que sabe, se cambian documentos, no pesos.

En código

Un buscador TF-IDF completo, sin librerías. Es la misma receta de la lección, sin recortar las palabras a su raíz:

import math, re
from collections import Counter

STOP = {"la", "el", "de", "los", "a", "en", "tu", "o", "es", "cuanto", "dura"}

def terms(text):
    return [w for w in re.findall(r"[a-z0-9]+", text.lower()) if w not in STOP]

chunks = [
    "la bateria tiene 18 meses de garantia o 500 ciclos de carga",
    "puedes cancelar tu compra durante los 30 dias posteriores a la entrega",
    "el envio es gratis en compras mayores a 1500",
    "carga la bateria a temperatura ambiente",
]

N = len(chunks)
bags = [Counter(terms(c)) for c in chunks]
df = Counter(t for b in bags for t in b)
idf = {t: math.log((N + 1) / (df[t] + 1)) + 1 for t in df}

def vector(bag):
    v = {t: (1 + math.log(c)) * idf[t] for t, c in bag.items() if t in idf}
    norm = math.sqrt(sum(x * x for x in v.values())) or 1.0
    return {t: x / norm for t, x in v.items()}

index = [vector(b) for b in bags]

def search(question, k=2):
    q = vector(Counter(terms(question)))
    scores = [sum(w * d.get(t, 0.0) for t, w in q.items()) for d in index]
    return sorted(enumerate(scores), key=lambda p: -p[1])[:k]

for i, s in search("cuanto dura la garantia de la bateria"):
    print(f"{s:.2f}  {chunks[i]}")
# 0.47  la bateria tiene 18 meses de garantia o 500 ciclos de carga
# 0.27  carga la bateria a temperatura ambiente

Lo que viene

Aquí el modelo solo lee. En la lección 32 puede además usar herramientas: buscar, calcular, escribir. La inyección de instrucciones se retoma en la lección 35, y la evaluación completa de sistemas, en la 34.

Lente de decisión

Qué significa esta idea si tu trabajo es decidir, no programar.

  • RAG da respuestas con tus datos y con fuentes que se pueden revisar, sin reentrenar: para actualizar lo que sabe se cambian documentos, no el modelo. En el artículo original, cambiar el índice de Wikipedia de 2016 por el de 2018 bastó para responder sobre los gobernantes nuevos. Pregunta cómo se actualiza el índice y cada cuánto.
  • La calidad se decide en la recuperación: si el fragmento correcto no llega al prompt, ningún modelo puede citarlo. Pide la tasa de acierto de la recuperación medida con tus propias preguntas, no con una demostración.
  • Gobierna los documentos: versión, fecha, dueño y permisos. Lo que el sistema puede recuperar, lo puede decir: versiones viejas que contradicen a las nuevas, y datos que no todas las personas deberían ver.
  • Todo texto recuperado es de confianza limitada: una reseña, una página web o un correo pueden traer instrucciones escondidas. Limita lo que el sistema puede hacer con lo que lee y registra qué fuentes usó en cada respuesta.

Términos de la lección

Transcripción completa
ComentariosConversación pública de esta lección: dudas, datos por revisar, ideas.

Los comentarios viven en GitHub Discussions, en el repositorio público del curso; para escribir se necesita una cuenta de GitHub. Se cargan desde giscus.app al abrir esta sección. Las personas que revisan el curso los leen, y si un cambio se acepta, se aplica en los dos idiomas. Abrir el hilo en GitHub

Fuentes