RAG

Primero, el nombre. RAG es la abreviatura de generación aumentada por recuperación: buscas el texto relevante tú mismo y se lo pasas al modelo en el momento de la pregunta, para que responda basándose en los hechos que suministraste en lugar de hacerlo desde la memoria congelada. Este capítulo es sobre cómo funciona ese ciclo de búsqueda-y-respuesta y dónde falla.
El patrón recuperar-luego-generar
Un modelo no conoce los documentos de ayuda de tu empresa, los detalles de tu producto, o nada que haya sucedido después de que fue entrenado. Pregúntale al respecto y o bien admitirá que no puede ayudarte o, peor, inventará algo que suena correcto. RAG arregla esto pasándole al modelo el texto relevante en el momento de la pregunta.
La idea se materializa una vez que tienes embeddings, el truco que convierte texto en vectores que puedes comparar por cercanía (la similitud del coseno mide qué tan cerca apuntan dos vectores). La generación aumentada por recuperación son dos pasos enganchados a una llamada normal al modelo:
- Recuperar. Toma la pregunta del usuario, busca en tus documentos los fragmentos más relevantes y recopila las coincidencias principales.
- Generar. Pon esos fragmentos en la solicitud como contexto, luego pide al modelo que responda usando solo ese contexto.
La capacidad general del modelo para el lenguaje hace la escritura; el texto que recuperaste suministra los hechos. Obtienes respuestas fluidas que están fundamentadas en tu contenido y pueden mantenerse actuales, porque controlas lo que entra.
Ayuda ver por qué esto funciona tan bien, en términos del ciclo de predicción. Sin RAG, le estás pidiendo al modelo que recuerde un hecho de los patrones congelados en sus parámetros, que puede no tener, lo que lleva a una adivinanza segura de sí misma. Con RAG, el hecho está justo ahí en el contexto, entonces el trabajo del modelo cambia de "recuerda esto" a "lee lo que tienes delante y responde basándote en ello". La recuperación mueve el trabajo de la memoria a la lectura. La imagen clara es un examen: un examen de libro cerrado invita a adivinar, mientras que RAG lo convierte en un examen de libro abierto donde la página relevante está abierta en el escritorio.
Chunking
No incrustas documentos completos. Los divides en fragmentos más pequeños primero, un párrafo o pocas oraciones cada uno, e incrustas esos. Dos razones. Primero, la recuperación es más precisa: extraes el párrafo relevante en lugar de un manual completo de 40 páginas. Segundo, los fragmentos encajan en la ventana de contexto, donde un documento gigante podría no hacerlo.
# un fragmentador pequeño: divide en párrafos
def chunk(text):
return [c.strip() for c in text.split("\n\n") if c.strip()]El tamaño del fragmento es un equilibrio, y la razón se remonta a embeddings. Cada fragmento se convierte en un vector único que resume su significado completo. Haz un fragmento demasiado grande y cubre varios temas, así que su vector único es un promedio borroso de todos ellos que no coincide bien con ninguna pregunta específica. Hazlo demasiado pequeño y pierdes el significado circundante que necesitaba para tener sentido, así que el vector apunta a un fragmento. Los fragmentos de tamaño de párrafo cada uno contienen una idea coherente. Ajustas desde ahí basándote en cómo funciona tu recuperación.
Dónde viven los vectores
Para un puñado de fragmentos, puedes mantener los vectores en memoria y compararlos con la función de similitud del coseno del capítulo embeddings, que es lo que el ejemplo posterior hace. Para miles o millones de fragmentos, eso se vuelve lento, y recurres a un almacén de vectores: una base de datos construida para encontrar los vectores más cercanos rápidamente a escala.
No necesitas uno para aprender RAG, y no necesitas elegir uno ahora. Varios existen como servicios alojados y bibliotecas; el concepto es lo que importa: un lugar para almacenar incrustaciones y buscarlas rápidamente. Comienza en memoria, pasa a un almacén cuando la colección la sobrepase.
Fundamentación y citación
El paso generar es una solicitud, y cómo la escribes decide si RAG realmente reduce respuestas inventadas. Dos instrucciones hacen el trabajo pesado: dile al modelo que responda solo del contexto proporcionado, y dile que diga así cuando el contexto no contenga la respuesta.
context_text = "\n\n".join(retrieved_chunks)
system_prompt = f'''Responde la pregunta usando SOLO el contexto a continuación.
Si el contexto no contiene la respuesta, di "No tengo esa información."
Cita la parte relevante del contexto en tu respuesta.
Contexto:
"""
{context_text}
"""'''Esto junta dos lecciones anteriores. Los delimitadores y el explícito "responde solo de esto" vienen de prompting. El permiso para decir "No tengo esa información" es la defensa de alucinación: un modelo fundamentado con una salida de emergencia inventa mucho menos que uno dejado para adivinar. Fundamentación más una salida de emergencia es lo que corta respuestas inventadas. Pedirle que cite la fuente también da a los usuarios algo para verificar.
Más allá del vecino más cercano: búsqueda híbrida y re-clasificación
La búsqueda de vectores puros coincide en significado, que es lo que quieres la mayoría del tiempo. Pero tiene un punto ciego: puede perder una palabra exacta que importa. Busca un código de error como E_4021 o un nombre de producto específico, y una búsqueda basada en significado puede devolver fragmentos que son sobre el mismo tema mientras se salta el que contiene la cadena exacta.
La solución tiene un nombre, búsqueda híbrida: combina búsqueda basada en significado con búsqueda de palabras clave antigua, para que términos exactos y significado general ambos obtengan voto. No necesitas construir esto para comenzar con RAG. Recurre a ello cuando términos exactos importan y los vectores puros sigan perdiéndolos.
En práctica
Una respuesta RAG mínima, reutilizando los ayudantes search y embed del capítulo embeddings:
def answer_from_docs(question, index):
# 1. recuperar: los fragmentos que mejor coinciden para la pregunta
query_vector = embed(question)
scored = [{"text": item["text"], "score": cosine_similarity(query_vector, item["vector"])} for item in index]
top = sorted(scored, key=lambda x: x["score"], reverse=True)[:3]
# 2. generar: responde usando solo esos fragmentos
context = "\n\n".join(t["text"] for t in top)
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": f'Responde usando SOLO este contexto. Si no está aquí, di que no sabes.\n\n"""{context}"""'},
{"role": "user", "content": question},
],
)
return response.choices[0].message.contentRecupera los tres fragmentos más cercanos, suéltalo en la solicitud, y deja que el modelo responda desde ellos. Ese es el pipeline RAG completo: la búsqueda embeddings alimentando una llamada del modelo, con una solicitud fundamentada sosteniéndolo todo. Casi toda la calidad vive en recuperar los fragmentos correctos.
Cuándo no necesitas RAG
RAG no es gratis, y no siempre es la respuesta. Omítelo cuando:
- Los datos son pequeños y caben en la solicitud. Si tu base de conocimiento completa es unos pocos párrafos, pégalos directamente. RAG es para cuando hay demasiado para enviar todo a la vez.
- Un modelo de contexto largo puede sostenerlo todo. Algunos modelos aceptan entradas muy grandes, lo suficiente para tomar un documento completo a la vez, que puede ser menos trabajo que construir recuperación.
Y recuerda el límite real: La calidad de RAG es principalmente calidad de recuperación. Si el paso de búsqueda devuelve los fragmentos equivocados, incluso una solicitud perfecta no puede salvar la respuesta. La mayoría del esfuerzo en sistemas RAG reales va en recuperar el texto correcto, no en la llamada del modelo final.
Las incrustaciones y RAG dan al modelo conocimiento en el momento de la pregunta, sin nunca cambiar el modelo a sí mismo. El próximo capítulo, Fine-tuning, cubre la otra opción: cambiar el modelo con tus propios ejemplos, y cuándo eso vale la pena sobre prompting o RAG.

