07

AI Agent Engineer · Módulo 7

RAG (Retrieval-Augmented Generation)

Cómo dar a un modelo el conocimiento que no memorizó —los documentos de tu empresa— recuperando en el momento de la pregunta solo los fragmentos relevantes, con citas verificables y respetando quién puede ver qué.

31 min de lectura

Por qué un modelo necesita buscar

El Módulo 1 estableció tres hechos incómodos sobre los modelos de lenguaje. Son estáticos: su conocimiento se congela en la fecha de corte del entrenamiento. Son sin estado: no recuerdan nada entre peticiones. Y alucinan: cuando no saben algo, generan la continuación más plausible, que puede ser una invención con forma de verdad. Ninguno de esos tres problemas se arregla con un mejor prompt, porque los tres nacen de cómo funciona el modelo por dentro. Un modelo no puede citar la política de vacaciones de tu empresa por la misma razón por la que no puede citar un libro que nunca leyó: no está en sus pesos.

La respuesta ingenua es meterlo todo en el contexto. Si el modelo no conoce el manual del empleado, pega el manual entero en el prompt. La razón por la que esto no escala es puro cálculo, y conviene hacerlo. Un manual de 200 páginas ronda las 100.000 palabras, que son unos 150.000 tokens. Con una ventana de contexto grande hasta cabría, pero cada pregunta que hagas arrastra esos 150.000 tokens de entrada, y si el equipo hace 1.000 preguntas al día son 150 millones de tokens diarios solo en repetir el manual. El costo es real y crece linealmente con el uso. Peor aún es la calidad: el Módulo 1 describió el fenómeno de "perdido en medio", por el cual el modelo atiende bien al principio y al final de un contexto largo pero difumina lo del centro. Enterrar la única frase relevante en la página 97 de 200 es la mejor forma de que el modelo no la use.

La analogía que ordena todo el módulo es la de un experto competente. Un buen abogado no memoriza el código civil entero; sabe que la respuesta a esta pregunta concreta está en tal artículo, va a buscarlo, lo lee y responde citándolo. RAG —generación aumentada por recuperación— es exactamente esa disciplina aplicada a un modelo. En lugar de memorizar el corpus o de volcarlo entero, el sistema recupera en el momento de la pregunta los pocos fragmentos que importan y solo esos entran al contexto. El modelo deja de responder de memoria y pasa a responder con la fuente delante. El Módulo 6 ya sembró esta idea: la disciplina de devolver solo lo relevante en una herramienta es la versión sencilla de lo que aquí se vuelve un mecanismo completo.

Pregunta de comprensión: si RAG le da al modelo la fuente correcta, ¿por qué no elimina del todo las alucinaciones?

Porque RAG no cambia la naturaleza generativa del modelo; cambia de qué depende su acierto. Sin RAG, el modelo depende de su memoria paramétrica, que es opaca e imposible de corregir sin reentrenar. Con RAG, depende de la calidad de la recuperación: si el buscador no logra traer el fragmento correcto, el modelo se queda otra vez sin la información y puede rellenar el hueco con una invención plausible. Además, aunque el fragmento correcto llegue al contexto, el modelo puede ignorarlo o mezclarlo con conocimiento previo si el prompt no lo obliga a ceñirse a la fuente. Es decir, RAG traslada el punto de fallo desde "lo que el modelo memorizó" —incontrolable— hacia "lo que el buscador recupera y cómo se ancla la respuesta" —medible y mejorable—. Por eso el resto del módulo se dedica a que la recuperación sea buena y a forzar que la respuesta se apoye en ella; sin esas dos cosas, RAG solo cambia el sabor de las alucinaciones.

El pipeline en dos tiempos

RAG tiene dos fases que ocurren en momentos distintos y conviene no confundirlas. La ingestión sucede una vez —o cada vez que cambian los documentos— y prepara el corpus. La recuperación sucede en cada pregunta y usa lo preparado. La analogía es la de una biblioteca: catalogar los libros y colocarlos en estanterías con un índice es la ingestión, lenta y ocasional; buscar en el índice cuando llega un lector es la recuperación, rápida y constante.

La ingestión encadena cuatro pasos: cargar el documento desde su origen, partirlo en fragmentos manejables, convertir cada fragmento en un vector con un modelo de embeddings, y guardar esos vectores en una base de datos vectorial junto con el texto y sus metadatos. La recuperación encadena otros cuatro: convertir la pregunta del usuario en un vector con el mismo modelo, buscar en la base los fragmentos más cercanos, insertar esos fragmentos en el prompt, y pedir al modelo que genere la respuesta anclada en ellos.

  INGESTIÓN (ocasional)                  RECUPERACIÓN (en cada pregunta)
 
  documento                              pregunta del usuario
     |                                        |
     v                                        v
  fragmentar                             embeber la pregunta
     |                                        |
     v                                        v
  embeber cada fragmento                 buscar fragmentos cercanos
     |                                        |
     v                                        v
  guardar vector + texto + metadatos     insertar en el prompt
     |                                        |
     +----> base vectorial <-----------------+
                                              v
                                        el modelo responde con la fuente

Que ambas fases usen el mismo modelo de embeddings no es un detalle: es la condición para que la comparación tenga sentido. Buscar la cercanía entre un vector producido por un modelo y otro producido por un modelo distinto es como comparar coordenadas de dos mapas con proyecciones diferentes. Cambiar el modelo de embeddings obliga a re-embeber todo el corpus.

Fragmentación

Antes de embeber hay que partir. La analogía es la de resumir un libro en fichas: no cabe el libro entero en una ficha, así que lo cortas en trozos, y dónde cortas determina lo útil que será cada ficha. Un corte a mitad de una frase produce una ficha que no se entiende sola; un corte demasiado grande mete tres temas distintos en la misma ficha y la vuelve borrosa.

Hay tres razones técnicas para fragmentar. La primera es que un embedding representa una cantidad acotada de significado: comprimir un capítulo entero en un solo vector promedia tantas ideas que la señal se pierde. La segunda es la granularidad de la recuperación: quieres devolver el párrafo que responde, no el documento de cincuenta páginas donde ese párrafo vive. La tercera es el presupuesto de contexto del Módulo 1: cuanto más pequeño y preciso el fragmento, menos tokens gastas al insertarlo.

Las estrategias van de simple a sofisticada. La fragmentación por tamaño fijo corta cada N tokens; es trivial y a menudo suficiente. La fragmentación con solapamiento repite los últimos tokens del fragmento anterior al inicio del siguiente, para que una idea que cruza el corte no se pierda —como repetir la última frase de una página al empezar la siguiente para no perder el hilo—. La fragmentación estructural corta por los límites naturales del documento: títulos, secciones, párrafos, lo que respeta la unidad de sentido. La semántica va más allá y corta donde el tema cambia, detectado por la propia distancia entre embeddings de frases consecutivas.

Pongamos números, porque las decisiones se toman con ellos. Un fragmento de 512 tokens es un tamaño habitual: cabe con holgura en el límite del modelo de embeddings y contiene un par de párrafos, suficiente para un pensamiento completo. Con un solapamiento de 50 tokens, el avance efectivo entre fragmentos es de 462 tokens. Un documento de 150.000 tokens produce entonces alrededor de 150000 dividido entre 462, unos 325 fragmentos. Si el fragmento fuera de 128 tokens, con el mismo solapamiento de 50 tendrías cerca de 1.900 fragmentos: recuperación más precisa pero un índice más caro y respuestas más troceadas. Si fuera de 2.000 tokens, unos 75 fragmentos: índice barato pero cada recuperación arrastra mucho texto irrelevante al contexto. No hay un número mágico; hay un compromiso que depende de tus documentos, y se afina midiendo.

Pregunta de comprensión: el solapamiento mejora la recuperación en los bordes de los fragmentos, pero ¿qué cuesta y cuándo es exagerado?

El solapamiento duplica texto: cada token repetido se almacena, se embebe y puede aparecer dos veces entre los resultados recuperados. Un solapamiento de 50 tokens sobre fragmentos de 512 infla el índice en torno a un 10 por ciento en almacenamiento y en llamadas al modelo de embeddings, y provoca que dos fragmentos vecinos recuperados juntos repitan un fragmento de su contenido, gastando tokens de contexto en decir lo mismo dos veces. Es un seguro barato cuando las ideas cruzan los cortes con frecuencia, como en prosa continua. Se vuelve exagerado cuando el solapamiento se acerca al tamaño del fragmento —un solapamiento de 400 sobre 512 significa que casi todo se almacena por partida doble, multiplicando el costo sin mejorar la señal— o cuando el documento ya tiene límites naturales claros, como una tabla de preguntas frecuentes, donde cortar por estructura hace innecesaria toda repetición. La regla práctica: solapa lo justo para no partir ideas, no por defecto.

Ejercicio 1: dimensionar y construir un fragmentador

Tienes una colección de 40 documentos, cada uno de unos 20.000 tokens de media. Vas a fragmentar con tamaño 512 y solapamiento 64.

Primero calcula, sin código: cuántos fragmentos produce un documento medio y cuántos la colección entera. Luego implementa un fragmentador por tamaño fijo con solapamiento que reciba una lista de tokens y devuelva los fragmentos, y verifica que el número real coincide con tu cálculo.

Criterio de éxito: tu cálculo a mano y el conteo del código difieren en menos de un fragmento por documento; el fragmentador respeta el solapamiento exacto; y sabes explicar en una frase qué le pasaría a la recuperación si subieras el tamaño a 2.048.

Embeddings y similitud, aplicados

El Módulo 1 introdujo los embeddings como la traducción de un texto a un punto en un espacio de muchas dimensiones, donde la cercanía significa parecido de significado, y la similitud coseno como la medida de esa cercanía. Aquí ese concepto deja de ser teoría y se convierte en el motor de la búsqueda.

Recordemos la mecánica con números pequeños. La similitud coseno mide el ángulo entre dos vectores: vale 1 cuando apuntan en la misma dirección, 0 cuando son perpendiculares, y baja hacia valores negativos cuando se oponen. La fórmula es el producto punto dividido entre el producto de las magnitudes.

cos(A, B) = (A · B) / (|A| · |B|)
 
A = pregunta embebida
B = fragmento embebido

En la práctica, un modelo de embeddings como text-embedding-3-large de Azure OpenAI produce vectores de 3.072 dimensiones —reducibles a menos si aceptas algo menos de precisión a cambio de un índice más ligero—. Nadie calcula ese coseno a mano, pero la intuición se ve con un ejemplo minúsculo. Supón que la pregunta "cuántos días de vacaciones tengo" se embebe, y comparas con dos fragmentos: uno sobre la política de vacaciones anuales y otro sobre el estacionamiento de la oficina. El coseno con el primero podría dar 0,86 y con el segundo 0,31. Ese contraste es toda la magia: sin compartir ni una palabra exacta, el sistema sabe que "días libres" y "vacaciones anuales" viven cerca en el espacio de significado, mientras que "estacionamiento" queda lejos.

La búsqueda por similitud consiste en calcular esa cercanía entre la pregunta y cada fragmento del corpus y quedarse con los k más cercanos —el top-k—. Con k igual a 5, recuperas los cinco fragmentos más pertinentes y solo esos entran al prompt. La elección de k es otro compromiso: pocos fragmentos arriesgan dejar fuera la respuesta; muchos inflan el contexto y reintroducen el problema de "perdido en medio".

Pregunta de comprensión: un fragmento contiene la palabra exacta de la pregunta y aun así obtiene un coseno bajo. ¿Cómo es posible y qué anticipa ese fenómeno?

El embedding no representa las palabras superficiales sino el significado que el modelo les atribuye en conjunto. Un fragmento puede contener la palabra "cargo" refiriéndose a un puesto de trabajo mientras la pregunta usa "cargo" como un adeudo bancario; comparten la cadena de caracteres pero el modelo las sitúa lejos porque su sentido difiere, y el coseno cae. A la inversa, la búsqueda vectorial acierta con sinónimos pero puede fallar con términos que solo importan por su forma exacta: un código de producto como "AX-7742", el acrónimo de una norma interna o el apellido de una persona pueden quedar diluidos en el vector porque el modelo no les asocia un significado rico. Ese punto ciego —la búsqueda semántica ignora coincidencias exactas que sí importan— es justo lo que motiva la búsqueda híbrida de la sección siguiente: hace falta combinar el "entiendo lo que quieres decir" del vector con el "coincido con la palabra exacta" de la búsqueda por palabras clave.

Bases de datos vectoriales y búsqueda aproximada

Comparar la pregunta con cada fragmento del corpus funciona con mil fragmentos, pero no con diez millones. La búsqueda exacta es de orden lineal: cada consulta recorre todos los vectores. Pongamos números. Con un millón de fragmentos de 3.072 dimensiones, una sola pregunta exige del orden de tres mil millones de multiplicaciones para calcular todos los cosenos. Repetir eso por cada consulta, con decenas de usuarios a la vez, es insostenible. La analogía es buscar una palabra leyendo el diccionario entero de la primera a la última página: correcto pero absurdo cuando existe un índice alfabético.

Una base de datos vectorial es ese índice. Su truco es la búsqueda aproximada de vecinos más cercanos —ANN por sus siglas—: renuncia a garantizar el vecino perfecto a cambio de encontrar uno casi perfecto muchísimo más rápido. El algoritmo más común, HNSW, construye un grafo de "amigos de amigos": cada vector se conecta con unos pocos cercanos, y la búsqueda salta de nodo en nodo acercándose al objetivo, como quien pregunta direcciones saltando de conocido en conocido en vez de tocar todas las puertas de la ciudad. En lugar de tres mil millones de comparaciones, visita unos cientos de nodos y devuelve el top-k en milisegundos, con una precisión que en la práctica ronda el acierto perfecto.

En el ecosistema del curso, el almacén natural es Azure AI Search, que integra el índice vectorial, la búsqueda por palabras clave y el reranking en un solo servicio, y se autentica con la identidad de Entra ID del Módulo 2. Una alternativa que reaprovecha lo ya montado es pgvector, la extensión que añade tipos y operadores vectoriales a PostgreSQL: el mismo servidor del Módulo 5 puede alojar el índice, aunque la ingestión necesitará un rol con permiso de escritura distinto del rol de solo lectura que usa el agente.

from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
from azure.identity import DefaultAzureCredential
 
client = SearchClient(endpoint=ENDPOINT, index_name="documentos-empresa",
                      credential=DefaultAzureCredential())
 
def recuperar(vector_pregunta, k=5):
    consulta_vectorial = VectorizedQuery(
        vector=vector_pregunta, k_nearest_neighbors=k, fields="vector")
    resultados = client.search(
        search_text=None, vector_queries=[consulta_vectorial],
        select=["id", "texto", "fuente"])
    return [{"texto": r["texto"], "fuente": r["fuente"], "score": r["@search.score"]}
            for r in resultados]

Advertencia honesta: tanto Azure AI Search como los SDK de acceso evolucionan con rapidez, y los nombres de clases como VectorizedQuery, los parámetros de la consulta y la forma de declarar el índice han cambiado entre versiones. El código ilustra el patrón —cliente, consulta vectorial con k, proyección de campos—, pero antes de fijar una versión conviene contrastar la firma exacta con la documentación oficial de Azure AI Search y del paquete azure-search-documents instalado. La forma del patrón es estable; los detalles de la API, no siempre.

Lo que el vector no ve: búsqueda híbrida y reranking

La sección de embeddings dejó un cabo suelto: la búsqueda vectorial es ciega a las coincidencias exactas que solo importan por su forma. Un usuario que busca la norma "ISO-27001" o el código de proyecto "AX-7742" necesita que el sistema encuentre esa cadena literal, no un párrafo semánticamente parecido sobre seguridad en general.

La defensa es la búsqueda por palabras clave, cuya representante clásica es BM25, un algoritmo que puntúa un documento por cuántas veces aparecen los términos de la consulta, ponderando los raros por encima de los comunes. Es lo contrario del vector: no entiende significado, pero clava la coincidencia exacta. La analogía junta a dos bibliotecarios: uno entiende lo que quieres decir aunque uses otras palabras; el otro recuerda exactamente en qué signatura está el libro cuyo título mencionaste. Ninguno solo es suficiente; juntos son muy buenos.

La búsqueda híbrida ejecuta ambas y fusiona sus rankings. La técnica habitual es la fusión recíproca de rangos —RRF—, que combina las dos listas premiando a los fragmentos que aparecen arriba en cualquiera de ellas. Así, el párrafo semánticamente pertinente y el que contiene el código exacto sobreviven ambos a la fusión.

Queda un segundo refinamiento. La recuperación —vectorial, por palabras o híbrida— optimiza traer muchos candidatos buenos de forma barata, pero no ordena con finura cuál es el mejor. Ahí entra el reranking. Un reranker es un modelo más lento y más caro que lee la pregunta junto a cada fragmento candidato y emite una puntuación de pertinencia mucho más precisa que un coseno. La analogía es un cribado en dos pasos: primero pasas la grava por una criba gruesa que retiene muchos candidatos sin esfuerzo, y luego inspeccionas a mano solo los finalistas. En números: recuperas barato los 50 fragmentos más prometedores por búsqueda híbrida, el reranker relee esos 50 y devuelve los 5 mejores ordenados con precisión. Azure AI Search ofrece este paso como su reranker semántico integrado. El costo extra se paga solo sobre 50 fragmentos, no sobre todo el corpus, y suele ser la mejora de calidad más rentable de un sistema RAG.

Pregunta de comprensión: si ya recuperas el top-5 por búsqueda vectorial, ¿qué añade recuperar el top-50 y reordenar a 5 con un reranker?

La diferencia es entre recuerdo (recall) y precisión. La búsqueda vectorial con k igual a 5 optimiza el recuerdo de forma barata: trae rápido cinco candidatos plausibles, pero su orden se basa en un coseno que solo aproxima la pertinencia real, así que el fragmento que de verdad responde a la pregunta puede estar en la posición 23 y quedar fuera del corte. Recuperar 50 en vez de 5 amplía la red para que el fragmento correcto casi con seguridad esté entre los candidatos —sube el recuerdo— sin apenas costo, porque la búsqueda aproximada es barata. Entonces el reranker, que lee pregunta y fragmento juntos en lugar de comparar dos vectores por separado, reordena esos 50 con una noción de pertinencia mucho más fina y sube a los primeros puestos el que un simple coseno había dejado hundido —sube la precisión—. El resultado es un embudo de dos etapas: la primera es barata y generosa, la segunda es cara y exigente, y solo se aplica a los finalistas. Saltarse el reranking ahorra un paso, pero deja la calidad final a merced del orden aproximado de la primera etapa.

Anclar la respuesta y reducir alucinaciones

Recuperar bien no sirve de nada si el modelo, con los fragmentos delante, sigue respondiendo de memoria o inventando lo que falta. El último paso del pipeline es construir el prompt de forma que la respuesta quede anclada en la fuente. La analogía es un examen a libro abierto con una regla estricta: solo puedes afirmar lo que esté en el libro, debes indicar de qué página lo sacaste, y si el libro no lo dice, la respuesta correcta es "no lo sé".

En la práctica, el prompt de generación intercala los fragmentos recuperados con instrucciones explícitas. Se numeran las fuentes para que el modelo pueda citarlas, se le ordena responder únicamente a partir de ellas y se le autoriza —se le exige— admitir la ausencia de información en lugar de rellenarla.

def construir_prompt(pregunta, fragmentos):
    contexto = "\n\n".join(
        f"[Fuente {i+1}: {f['fuente']}]\n{f['texto']}"
        for i, f in enumerate(fragmentos))
    return (
        "Responde la pregunta usando SOLO el contexto siguiente. "
        "Cita la fuente entre corchetes tras cada afirmación. "
        "Si el contexto no contiene la respuesta, di exactamente: "
        "'No encuentro esa información en los documentos disponibles.'\n\n"
        f"Contexto:\n{contexto}\n\nPregunta: {pregunta}"
    )

Este anclaje es donde RAG paga su promesa de reducir alucinaciones: al obligar a citar y a admitir el vacío, se recorta el espacio para inventar. Pero la instrucción es solo un prompt, y el Módulo 6 dejó claro que un prompt es una sugerencia, no un muro. El modelo puede citar mal, mezclar fuentes o afirmar algo que la fuente no dice. Por eso el anclaje se refuerza con una capa fuera del modelo: la detección de anclaje —la funcionalidad de groundedness de la seguridad de contenido del Módulo 2 y su patrón de LLM como juez— compara la respuesta generada contra las fuentes y marca las afirmaciones no respaldadas. La instrucción reduce el problema; la verificación posterior lo detecta cuando la instrucción no basta.

Pregunta de comprensión: has ordenado al modelo "responde solo con el contexto y cita la fuente". ¿Por qué no basta y qué capa lo respalda?

No basta porque esa orden vive dentro del prompt, y el prompt es texto que el modelo pondera junto con todo lo demás, no una restricción que se imponga por sí sola. El modelo puede combinar un dato del contexto con conocimiento previo y presentarlo como si viniera de la fuente, puede citar la fuente 2 cuando la afirmación salió de la 4, o puede, ante una recuperación pobre, ceder a la tentación de completar el hueco. La misma lógica de capas del Módulo 6 aplica aquí: la seguridad y la fiabilidad se implementan fuera del modelo. La capa que respalda el anclaje es la verificación de groundedness, que toma cada afirmación de la respuesta y comprueba, con un segundo modelo actuando como juez, si está respaldada por alguna de las fuentes recuperadas; las afirmaciones sin respaldo se marcan, se descartan o se convierten en una advertencia al usuario. La instrucción empuja al modelo en la dirección correcta; la verificación es el control determinista que atrapa las veces en que el empujón no alcanza.

Filtrado por metadatos y seguridad

Hasta aquí hemos tratado el corpus como si todo el mundo pudiera ver todo. En una empresa real, no. El documento de salarios es visible para recursos humanos y para nadie más; el plan estratégico, para la dirección; el manual del empleado, para todos. Un sistema RAG que ignore esto es una máquina de fugas: basta preguntar con las palabras adecuadas para que un fragmento confidencial suba al top-k y aterrice en la respuesta de quien no debía verlo.

La analogía es una biblioteca con salas restringidas. El carné de cada lector abre unas salas y no otras, y —esto es lo importante— el propio catálogo oculta los libros de las salas a las que ese lector no tiene acceso. No es que el lector vea el título y se le niegue el préstamo; es que para él ese título no existe en la búsqueda. Trasladado a RAG, esto es el filtrado por metadatos, también llamado security trimming: cada fragmento se guarda con metadatos sobre quién puede verlo —grupos, roles, listas de control de acceso heredadas de SharePoint—, y la consulta de recuperación aplica un filtro por la identidad del usuario antes de calcular el top-k, de modo que los fragmentos prohibidos ni siquiera compiten.

def recuperar_con_permisos(vector_pregunta, grupos_usuario, k=5):
    filtro = " or ".join(f"grupos/any(g: g eq '{g}')" for g in grupos_usuario)
    consulta = VectorizedQuery(vector=vector_pregunta, k_nearest_neighbors=k, fields="vector")
    return client.search(search_text=None, vector_queries=[consulta],
                         filter=filtro, select=["id", "texto", "fuente"])

El punto crítico es dónde se aplica el filtro. Debe aplicarse en la capa de recuperación, no pidiéndole al modelo que ignore los fragmentos que no le corresponden. La razón es la misma que atraviesa el curso: una vez que un fragmento entra al contexto, el modelo ya lo leyó, y una instrucción de "no uses este documento" es una sugerencia que una inyección indirecta puede anular. La seguridad es un permiso que se hace cumplir fuera del modelo, no una instrucción que se le confía. En la práctica, esto obliga a que la ingestión capture los permisos de cada documento —en el proyecto, las pertenencias a grupos que SharePoint expone vía Microsoft Graph— y los guarde como metadatos filtrables, y a que la recuperación resuelva la identidad y los grupos del usuario que pregunta y filtre por ellos siempre.

Un detalle de higiene en el código anterior: los nombres de grupo se interpolan directamente en la expresión del filtro. Aunque provienen de Microsoft Graph y no del usuario, la capa determinista también merece código defensivo: escapa las comillas simples duplicándolas —la convención de OData— antes de construir el filtro, para que un nombre de grupo con un apóstrofo no rompa la consulta ni abra una grieta.

Y queda una trampa silenciosa: los permisos cambian. Un empleado sale del grupo de recursos humanos, un documento confidencial se comparte con un equipo nuevo, otro se elimina de SharePoint. Si los metadatos del índice no se actualizan, el recorte por permisos hace cumplir los permisos de ayer: el ex miembro de recursos humanos seguiría recuperando el documento de salarios, con el filtro funcionando a la perfección sobre datos obsoletos. La ingestión, por tanto, no es un evento único sino una sincronización: las consultas delta de Microsoft Graph permiten detectar qué documentos cambiaron —contenido o permisos— desde la última pasada, re-embeber y re-etiquetar solo esos, y propagar los borrados eliminando sus fragmentos del índice. Un documento borrado del origen pero vivo en el índice es una fuga con retardo. La ejecución programada de esta sincronización —un proceso nocturno disparado por un temporizador— es exactamente el tipo de tarea por evento que el Módulo 10 resuelve con Azure Functions.

Pregunta de comprensión: ¿por qué el filtro de permisos debe ejecutarse en la recuperación y no instruyendo al modelo para que descarte lo que el usuario no puede ver?

Porque el daño ocurre en el momento en que el fragmento entra al contexto, no cuando el modelo decide usarlo. Si recuperas un fragmento confidencial y luego le dices al modelo "no lo uses", el modelo ya lo ha leído: puede citarlo por error, parafrasearlo, o ser inducido por una inyección indirecta a revelarlo pese a la instrucción. La única defensa robusta es que ese fragmento nunca llegue al contexto, y eso solo se garantiza aplicando el filtro de permisos dentro de la consulta de recuperación, de forma que los fragmentos prohibidos queden excluidos del conjunto candidato antes de calcular el top-k. Es exactamente el principio del Módulo 6 llevado a RAG: la seguridad vive en una capa de permisos fuera del modelo, no en una instrucción dentro de él. Confiar la confidencialidad a un prompt equivale a poner un cartel de "prohibido el paso" en una puerta abierta; el filtro en la recuperación es cerrar la puerta con llave.

Transformación de consultas y RAG agéntico

Dos mejoras cierran el panorama. La primera reconoce que la pregunta del usuario no siempre es la mejor consulta. Alguien escribe "y lo de las vacaciones del año pasado?", que como cadena de búsqueda es pobre. La transformación de consultas usa un modelo para reescribir la pregunta en una o varias consultas mejores antes de recuperar: la reescritura la limpia, la expansión multi-consulta genera varias variantes para ampliar el recuerdo, y técnicas como HyDE generan una respuesta hipotética y buscan por su embedding, bajo la idea de que una respuesta plausible se parece más al fragmento correcto que la pregunta escueta. Cada variante cuesta una llamada extra al modelo, así que se usan donde la calidad lo justifica.

La segunda mejora conecta este módulo con los dos siguientes. Hasta ahora el sistema recupera siempre; pero no toda pregunta necesita el corpus. "Qué hora es" o "reformula este texto" no requieren buscar nada, y recuperar por reflejo gasta latencia y tokens. El RAG agéntico convierte la recuperación en una herramienta —en el sentido exacto del Módulo 6— que el agente decide invocar o no según la pregunta. El modelo evalúa si necesita conocimiento externo, y solo entonces llama a la herramienta de recuperación, quizá varias veces con consultas distintas, quizá razonando entre una búsqueda y la siguiente. Esta idea de la recuperación como una capacidad más que un agente orquesta es el puente hacia el Módulo 8, donde varios agentes se reparten herramientas y responsabilidades.

Evaluar la recuperación

No se puede mejorar lo que no se mide, y RAG tiene dos puntos de fallo que se miden por separado: la recuperación puede no traer el fragmento correcto, o la generación puede no usarlo bien. Confundirlos lleva a arreglar lo que no está roto.

La recuperación se evalúa con métricas de ranking sobre un conjunto de preguntas cuyas respuestas correctas conoces. El recuerdo en k —recall@k— mide qué fracción de las veces el fragmento correcto aparece entre los k recuperados: si de 100 preguntas el fragmento correcto está en el top-5 en 88, el recall@5 es 0,88. La precisión en k mide qué proporción de los recuperados son realmente pertinentes. El rango recíproco medio —MRR— premia que el fragmento correcto aparezca lo más arriba posible. Estas métricas te dicen si el problema está antes del modelo: si el recall@5 es 0,60, el modelo no puede responder bien el 40 por ciento de las veces porque nunca recibe la fuente, y ninguna mejora del prompt lo arreglará.

La generación se evalúa con el patrón de LLM como juez del Módulo 2: un segundo modelo puntúa el anclaje —si cada afirmación de la respuesta está respaldada por las fuentes—, la pertinencia —si la respuesta contesta la pregunta— y la completitud. Estas métricas te dicen si el problema está en el modelo: fuentes correctas recuperadas pero respuesta que las ignora o las contradice. La evaluación sistemática de estos dos frentes es el oficio del Módulo 9; aquí basta con saber que un sistema RAG sin medición es un sistema que no sabes si funciona.

Ejercicio 2: recuperar, anclar y detectar el vacío

Monta una recuperación mínima sobre una decena de fragmentos de prueba, al menos uno de los cuales responda a una pregunta que harás y otro que sea un distractor plausible. Embebe la pregunta, recupera el top-3, construye el prompt de anclaje con citas y genera la respuesta. Luego repite con una pregunta cuya respuesta no esté en ningún fragmento.

Criterio de éxito: en la primera pregunta, la respuesta cita la fuente correcta; en la segunda, el modelo responde exactamente con la fórmula de vacío que le indicaste y no inventa; y sabes decir, mirando los scores del top-3, si un fallo vendría de la recuperación o de la generación.

Ejercicio 3: medir el recuerdo antes de creer

Prepara 15 preguntas etiquetadas sobre tu corpus de prueba: para cada una sabes de antemano qué fragmento la responde. Incluye al menos 3 preguntas que dependan de una coincidencia exacta —un código de producto, un acrónimo interno, un identificador tipo AX-7742— y el resto formuladas con sinónimos, sin reutilizar las palabras del fragmento.

Ejecuta las 15 preguntas dos veces: primero con búsqueda solo vectorial y luego con búsqueda híbrida más reranker. Para cada configuración calcula el recall@5 —en cuántas de las 15 el fragmento correcto apareció entre los cinco recuperados— y anota en qué posición quedó.

Criterio de éxito: produces la tabla comparativa de ambas configuraciones y puedes explicar, señalando preguntas concretas, en qué tipo de consulta la híbrida marcó la diferencia. Lo esperable es que las preguntas de coincidencia exacta sean las que la búsqueda vectorial pierda y la híbrida rescate; si tu corpus no lo muestra, entender por qué es parte del ejercicio.

Proyecto del módulo: RAG sobre los documentos de la empresa

El objetivo es dotar al Employee AI Assistant de memoria documental: que responda preguntas sobre las políticas, los procedimientos y los documentos de la empresa alojados en SharePoint, con citas verificables y respetando quién puede ver cada documento. Se construye sobre las herramientas de Microsoft Graph del Módulo 6 y se expone como una herramienta más del agente.

Parte A: ingestión desde SharePoint

  1. Usando el acceso a Microsoft Graph del Módulo 6, lista y descarga los documentos de una biblioteca de SharePoint, capturando por cada uno su texto, su URL de origen y las pertenencias a grupos que determinan quién puede verlo.
  2. Fragmenta cada documento con tamaño 512 y solapamiento 64, embebe cada fragmento con text-embedding-3-large de Azure OpenAI y guarda en Azure AI Search —o en pgvector sobre el PostgreSQL del Módulo 5— el vector, el texto, la URL de origen y los grupos autorizados como metadato filtrable.
  3. Verifica el conteo de fragmentos contra tu cálculo del Ejercicio 1 para confirmar que la fragmentación es la esperada.

Caso de prueba: ingesta un documento conocido y confirma que se puede recuperar por una pregunta que use sinónimos, no palabras exactas del documento; eso valida que el embedding captura significado.

Parte B: recuperación híbrida anclada

  1. Implementa la recuperación híbrida: combina búsqueda vectorial y por palabras clave, recupera un conjunto amplio y aplica el reranker semántico para quedarte con los mejores 5.
  2. Construye el prompt de anclaje con fuentes numeradas, obligando a citar y a admitir el vacío, y genera la respuesta.
  3. Expón todo esto como una herramienta buscar_en_documentos(pregunta) que el agente pueda invocar, de modo que la recuperación sea agéntica y no un reflejo en cada turno.

Caso de prueba: pregunta algo cuya respuesta no esté en el corpus y confirma que el asistente responde con la fórmula de vacío y no inventa. Pregunta algo respondible y confirma que la respuesta incluye una cita a la URL de SharePoint correcta.

Parte C: recorte por permisos y su verificación

  1. Resuelve, para el usuario que pregunta, sus pertenencias a grupos vía Microsoft Graph, y aplica ese filtro en la consulta de recuperación para que solo compitan los fragmentos que el usuario puede ver.
  2. Registra en cada recuperación qué filtro se aplicó y qué fuentes se devolvieron, dejando la traza lista para el Módulo 9.

Caso de prueba de seguridad: crea un documento confidencial visible solo para el grupo de recursos humanos y añádelo al índice. Con la identidad de un usuario que no pertenece a ese grupo, formula una pregunta redactada para que ese documento sea el más pertinente semánticamente. Verifica que el fragmento confidencial no aparece entre los recuperados y que la respuesta admite el vacío. El criterio de éxito es que la defensa no dependa de ninguna instrucción en el prompt: el documento se excluye en la capa de recuperación por el filtro de grupos, de modo que aunque el modelo quisiera usarlo, nunca lo recibe. Como contraprueba, ejecuta la misma pregunta con la identidad de un miembro de recursos humanos y confirma que entonces sí se recupera y se cita.

Resumen

Este módulo resolvió los tres límites estructurales que el Módulo 1 dejó planteados —conocimiento congelado, ausencia de memoria y alucinación— no cambiando el modelo, sino cambiando de qué depende su acierto: en lugar de responder de memoria, el modelo responde con la fuente recuperada delante. Vimos que volcar el corpus entero al contexto es inviable por costo y por el fenómeno de "perdido en medio", y que la alternativa es un pipeline de dos tiempos: una ingestión que fragmenta, embebe y almacena, y una recuperación que en cada pregunta embebe, busca, ancla y genera. La fragmentación resultó ser un compromiso medible entre precisión y costo; los embeddings y la similitud coseno del Módulo 1 se convirtieron en el motor de la búsqueda; y las bases vectoriales con búsqueda aproximada hicieron esa búsqueda viable sobre millones de fragmentos.

Sobre esa base añadimos las capas que separan un RAG de juguete de uno serio. La búsqueda híbrida cubrió el punto ciego del vector ante las coincidencias exactas, y el reranking convirtió una recuperación generosa y barata en una selección precisa mediante un embudo de dos etapas. El anclaje del prompt, reforzado por la verificación de groundedness, materializó la promesa de reducir alucinaciones exigiendo citas y admisión del vacío. Y el filtrado por metadatos llevó a RAG el principio rector del curso: la confidencialidad se hace cumplir excluyendo el documento en la recuperación, no confiándosela a una instrucción, porque un fragmento que llega al contexto ya fue leído. La transformación de consultas y el RAG agéntico afinaron cuándo y cómo buscar, y las métricas de recuperación y de generación mostraron cómo distinguir un fallo del buscador de un fallo del modelo.

Cada hilo se tiende hacia adelante. La recuperación como herramienta que un agente decide invocar es el punto exacto donde este módulo se engancha con el Módulo 8, donde varios agentes especializados se reparten herramientas, corpus y responsabilidades para tareas que un solo agente no abarca. La distinción entre evaluar la recuperación y evaluar la generación es la materia del Módulo 9, que convierte estas métricas en un proceso sistemático. Y el índice, los embeddings y el servicio de búsqueda que montaste piden el despliegue robusto del Módulo 10. El Employee AI Assistant ya no solo actúa sobre la empresa: ahora la conoce, cita sus documentos y respeta sus permisos.

¿Te sirvió esta lección?

El curso es gratis y así seguirá. Un café ayuda a que salga el siguiente módulo.

Invítame un café

¿Quieres llevar esto a tu negocio?

En Burnisoft convertimos estas ideas en apps, web y automatizaciones a la medida. Cuéntanos tu reto, sin compromiso.

Contáctanos