Hoy en día, la RAG a veces tiene mala fama: algunos creen que es completamente trivial —lo es al principio, pero no tanto al escalarla— y otros piensan que los “sistemas agénticos” la han superado, aunque en muchos casos, al examinar la superficie, enseguida empiezan a parecerse mucho a la RAG…
En este artículo presentamos un par de ejemplos prácticos para mostrar cómo abordamos algunos desafíos comunes, en concreto:
Manejar datos mixtos de texto y números y por qué hacen fallar una RAG básica: las palabras clave coinciden y los números carecen de significado semántico.
Por qué conviene diseñar embeddings basados primero en resúmenes: generar un breve resumen descriptivo de cada fragmento y usarlo para crear embeddings y hacer consultas.
Cómo generar resúmenes contextuales: incluir el contexto del documento principal para distinguir estadísticas con formatos similares.
Cuándo recurrir al código y a los modelos de Pydantic: cuando importa conservar el contenido literal, combinar código personalizado o un modelo de Pydantic con llamadas a LLM para aumentar la confiabilidad.
Conceptos básicos
Los sistemas RAG impulsan desde bots de soporte hasta asistentes internos de conocimiento.
Por lo general, el proceso interno consiste en:
Dividir los documentos fuente en fragmentos
Convertir cada fragmento en un embedding dentro de un espacio vectorial
Recuperar los K fragmentos principales al realizar la consulta
Generar una respuesta condicionada por esos fragmentos
Herramientas populares como LangChain, LlamaIndex y Filestore de OpenAI vuelven estos pasos casi triviales. Pero en los flujos de trabajo reales aparecen datos que no son solo texto denso, y una RAG básica puede tener dificultades. En las siguientes secciones mostraremos ejemplos concretos de problemas con los datos y construiremos la solución gradualmente a medida que aumente la complejidad.
Cuando los datos no son solo texto (algo bastante común)
Consideremos el siguiente fragmento de datos en el contexto de un videojuego:
JSON
Los embeddings funcionan gracias a las relaciones aprendidas entre las palabras mediante su significado semántico y la gramática. En los datos anteriores se mezclan texto y números; fuera de ese contexto preciso, los números no guardan relación con las palabras. Podríamos decir que este fragmento de datos es básicamente una combinación de palabras más o menos descriptivas seguidas de números aleatorios.
Esto no sería un problema si fuera el único tipo de datos disponible, porque aún podríamos recuperarlos mediante los embeddings de las pocas palabras descriptivas disponibles (o simplemente usar text-to-SQL). Pero ¿qué ocurre si este fragmento está oculto entre muchos fragmentos de texto denso donde también aparecen esas palabras? Por ejemplo:
JSON
Ahora imaginemos que queremos consultar: “¿Cuál es el alcance de ataque con Ascensión dracónica?”Es muy probable que no podamos recuperar el fragmento pertinente porque está perdido entre el ruido de otros fragmentos que contienen las mismas palabras clave.
El problema de fondo es que no podemos distinguir bien estos fragmentos de datos, aunque contengan tipos de información diferentes sobre el mismo tema. ¿Podríamos enriquecer o mejorar esos datos de algún modo? Claro que sí :smile:
Enriquecer los datos mediante resúmenes; sí, leíste bien
En lugar de crear directamente el embedding del fragmento, primero podemos generar un resumen que describa los datos y luego crear el embedding y realizar la recuperación a partir de ese resumen. En la etapa de generación seguiríamos usando los datos originales vinculados al resumen.
Para los dos ejemplos de fragmentos anteriores, generaríamos resúmenes como estos:
Estadísticas de alcance, velocidad y daño de ataque (predeterminadas y con Ascensión dracónica).
Descripción y detalles de la Ascensión dracónica, incluidas sus condiciones de activación, efectos visuales e historia.
Después, también ampliamos la consulta para “alinearla” con el resumen. Por ejemplo, transformaríamos “¿Cuál es el alcance de ataque con Ascensión dracónica?”en “¿Cuáles son las estadísticas del alcance de ataque con Ascensión dracónica?”.Esto es especialmente importante cuando la consulta de recuperación proviene de usuarios ajenos a áreas técnicas, que preguntan en un lenguaje ~~“libre”~~ humano normal, pues no tienen por qué saber ni preocuparse por cómo funciona una RAG para maximizar la precisión y la exhaustividad.


No sacar las cosas de contexto (un consejo aplicable a la vida en general)
Veamos ahora otro escenario: trabajar con una enorme cantidad de fragmentos de datos que se parecen entre sí, como los siguientes:
Plain Text
Si mantenemos el mismo enfoque, imaginemos que preguntamos: “¿Cuál es el alcance de ataque del personaje X?”Con los resúmenes que acabamos de generar, estaríamos jugando a adivinar y dependiendo de la suerte, porque también serían muy similares. Entonces, ¿cómo podríamos diferenciarlos?
La respuesta sencilla es aportar contexto. Podríamos incluir en el fragmento de datos una referencia a su documento principal; por ejemplo, {“personaje”: “X”} en este caso. Así podríamos recuperar con precisión los datos correctos del personaje X, aunque también tuviéramos los mismos datos de los personajes Y y Z.
Sin embargo, un enfoque mejor y más generalizable sería generar un resumen contextual del fragmento. Es decir, en vez de resumir únicamente el fragmento de datos, podríamos proporcionar tanto el documento principal como el fragmento para generar un resumen contextual general que explique cómo encaja el fragmento en su documento principal. Por ejemplo:
Este fragmento ofrece estadísticas detalladas de… para el personaje X. El fragmento encaja en el documento completo porque muestra la fortaleza de X en velocidad de ataque…
Este fragmento ofrece estadísticas detalladas de… para el personaje Y. El fragmento encaja en el documento completo porque muestra las estadísticas mejoradas de Y al usar su habilidad especial…
Este fragmento ofrece estadísticas detalladas de… para el personaje Z. El fragmento encaja en el documento completo porque muestra que las estadísticas de Z son ideales para desempeñarse como tanque en partidas en equipo…
Este método (inspirado en parte por Anthropic) puede parecer excesivo para el ejemplo anterior, pero resulta muy eficaz con fragmentos que podrían malinterpretarse “fuera de contexto”. Además, ofrece un enfoque unificado que funciona con todos los fragmentos y mantiene ordenado el flujo de ingeniería.


Cuando hay que ser ~~obsesivo con el control~~ riguroso
Por lo general, recibimos datos completos y los dividimos en fragmentos para un sistema RAG. En este ejemplo mostramos algo distinto: datos divididos en fragmentos deficientes. Son secciones aleatorias de un fragmento lógico que, en realidad, deben volver a agruparse. Un fragmento lógico es un bloque de contenido que naturalmente debe permanecer unido, como una subsección de un documento o un párrafo coherente.


Nuestro primer intento consiste en introducir todos los datos en una llamada a un LLM, pedirle que los agrupe como considere adecuado y solicitarle que devuelva el contenido agrupado. Un LLM debería hacer esto bastante bien, ¿cierto? Bueno, sí y no.
En varias ocasiones descubrimos que los LLM tienden a tomar atajos y no son confiables cuando se necesita el contenido completo y exacto, sobre todo si el contexto es extenso. Lo cual tiene mucho sentido. Pero eso era un impedimento decisivo para este caso de uso, porque necesitábamos conservar el contenido exacto, palabra por palabra: sin resúmenes ni omisiones del contenido original. No podemos omitir ningún detalle.
La parte positiva, por supuesto, fue que comprendió muy bien la semántica y la estructura de los fragmentos divididos. Siempre y cuando no se negara a reproducir el contenido exacto. Maldición :/
Entonces, ¿cómo podíamos aprovechar aquello en lo que destaca un LLM y evitar aquello en lo que no es confiable? Recurrimos a nuestro viejo amigo: el código (es decir, una función personalizada de Python). Y a un modelo de Pydantic “imposible de simplificar más”. Esta es la solución:
Recorrer las secciones mientras se mantiene un fragmento lógico actual
En cada sección, preguntar al LLM: ¿esta sección pertenece al fragmento lógico actual? Debe responder sí o no, siguiendo el modelo de Pydantic.
Si responde que sí, adjuntar la sección al fragmento; si responde que no, guardar el fragmento lógico actual, pues ya está completo, e iniciar uno nuevo con esa sección.


Por supuesto, aquí usamos algunos tokens más que al procesar todo el contenido de una sola vez. Sin embargo, en este caso de uso, donde conservar el contenido exacto era la máxima prioridad, el pequeño costo adicional valía la pena.
Es una solución muy sencilla, pero sigue un principio importante: cuando se necesita rigor, no conviene depender únicamente de los LLM porque, al fin y al cabo, son probabilísticos.
El código y las funciones personalizadas, junto con los modelos de Pydantic, permiten obtener resultados predecibles y confiables sin dejar de aprovechar las capacidades de los LLM.
Crear una solución de IA generativa supone tanto un desafío de ingeniería como uno de IA. Esperamos que estos ejemplos te inspiren a abordar tus propios desafíos. Para obtener más información sobre soluciones de IA generativa centradas en la ingeniería, consulta nuestro artículo sobre el diseño de sistemas agénticos basados en enrutadores.