17 de mayo de 2024
Como comentamos en esta nota, los RAG son sistemas que sirven para buscar información en una base de datos de texto plano (casi siempre) y pasarla como contexto a un LLM para aumentar sus capacidades (conocimiento o actualización). Existen muchos desafíos y decisiones a la hora de construir un RAG, que veremos a continuación.
Procesamiento de archivos
Si el objetivo es llegar a texto plano, hay que tener especial consideración en cuál es la técnica por la cual transformamos los otros tipos de archivos. En el caso de video, algunas librerías permiten hacerlo directamente sobre el video mientras que otras requieren primero descargar el audio.
Cuando procesamos audio, hay diferentes modelos con los cuales hacerlo. Todas las grandes tienen un modelo (OpenAI, Meta, Google, Amazon, Microsoft, IBM) pero también hay compañías específicas de este rubro (Deepgram, Assembly, Rev AI). En toda esta variedad encontramos diferentes modelos, precisión, costos y tiempos. Muchos incluyen la posibilidad de agregar una whitelist de palabras para ayudar al modelo a reconocerlas, lo cual suele ser muy útil para términos específicos o nombres propios.
En cuanto al procesamiento de PDF, hay infinidad de librerías con las cuales realizarlo con un nivel estándar bastante bueno. En caso de que usemos archivos escaneados, puede ser útil buscar alguna herramienta de mejor calidad, con mecanismos de OCR o de mejoramiento de las imágenes.
Por último, en relación a las tablas, existen dos enfoques para tratar esta fuente. Uno consiste en pasar toda la información de la tabla a texto plano con marcadores (por ejemplo en Markdown, JSON o XML) y el otro implicaría desarrollar un agente con capacidad de transformar la consulta a una query en SQL y ejecutarla sobre la base de datos pertinente. Sin embargo, en este caso estamos hablando de un diseño mucho más complejo de lo que hasta aquí entendemos por RAG.
Chunk y overlap
Una vez que tenemos el texto plano, llega el momento de dividirlo en fragmentos. Para eso, tenemos que definir el tamaño de los chunks (cuántas palabras o tokens va a abarcar cada uno). Si el número es muy bajo, los chunks no van a ser representativos, y si es muy largo, van a ser demasiado vagos. Además, estas decisiones van a tener impacto tanto en la latencia como en el costo del sistema. Por otro lado, también podemos jugar con el overlap. Es decir, con que una parte de los tokens se repitan entre un chunk y el siguiente. De esta manera, las divisiones no son tan estrictas y podemos probar si una misma palabra con diferentes contextos (en chunks distintos) resulta más relevante.
Para decidir el tamaño de los chunks y el overlap, podemos usar distintos enfoques. Una opción sería definir un número de tokens o una cantidad de chunks buscados. Sin embargo, el sentido de los fragmentos de texto a veces es variable. Teniendo eso en cuenta, podemos usar divisiones basadas en caracteres (por ejemplo, saltos de línea o párrafo), en oraciones (lo mismo que el anterior pero con el punto) o en sentido (analizando semánticamente cada oración). O también una combinación recursiva de todas las estrategias anteriores. Incluso podemos pensar en variar la estrategia según el tipo de archivo original de la fuente de datos. Por ejemplo, si tenemos un video de un podcast con dos interlocutores, tendría sentido que los chunks fueran fragmentos de una sola persona.
Embeddings
Cuando decidimos la mejor estrategia de división en chunks y overlap, es hora de pasar las palabras a vectores numéricos. Para eso, tenemos que elegir un encoder que pase de texto a embeddings. Podemos usar un modelo open source o privativo, podemos usar el mismo sobre el que después vamos a crear el LLM o uno distinto, podemos usar encoders genéricos o con un fine-tuning para una determinada tarea. Como hemos visto en otros casos, más allá de la precisión del encoder, debemos tener en cuenta la dimensionalidad del mismo, por cuestiones de costos y latencia.
Base de datos vectorial
Ahora que ya tenemos todos nuestros chunks (con overlap) convertidos en vectores numéricos, debemos encontrar una base de datos donde almacenarlos. Como en toda elección de base de datos, hay varios factores a tener en cuenta. Por ejemplo, el costo y la latencia para la inserción de datos pero, aún más importante, para la consulta. Si es una base de datos en memoria (tipo Redis) o en disco, con los consiguientes trade-offs (mayor velocidad a costa de menor almacenamiento y viceversa).
Por otro lado, al almacenar los embeddings también vamos a querer almacenar alguna metadata de los fragmentos (podría ser a qué sección pertenece, quién estaba hablando, en qué idioma está escrito) que nos va a servir para filtrar cuando hagamos una búsqueda. En este caso, deberíamos definir si el filtrado sucederá antes de la búsqueda de coincidencias, después o con un enfoque híbrido.
Por último, uno de los puntos más discutidos sobre el diseño de sistemas RAG tiene que ver con el mecanismo de indexación de los embeddings: Flat, HNSW, PQ, ANN o sus variantes son algunos de los más usados. Estos índices utilizan distintas estrategias para indexar los vectores según su cercanía en el espacio para facilitar la búsqueda o el retrieval posterior. Como parte del diseño del sistema, además debemos contemplar mecanismos de indexación en el caso de nuevos documentos (lo cual suponemos que en producción debería suceder bastante frecuentemente).
Query del usuario
Hasta ahora, pasamos por alto el paso inicial o disparador de todo este proceso que tiene que ver con la consulta del usuario. Podemos pensar que una parte de la eficiencia del sistema RAG tiene que ver con cómo está formulada la query para ver qué similitudes se van a encontrar en la base de datos ampliada. Sin embargo, con cierta frecuencia, las queries del usuario no son las más apropiadas o no están redactadas de una manera que facilite la búsqueda de similitudes. Por eso existen técnicas para mejorar los resultados de la búsqueda. Algunas de estas consisten en utilizar un LLM para parafrasear la query original o dividir la query en pasos y luego volver a mezclarla.
Retrieval
Más allá de que el índice que usamos para la base de datos vectorial suele estar optimizado para cierta métrica de similitud (como distancia euclidiana, Manhattan o similitud coseno), existen técnicas para mejorar la calidad de documentos recuperados por el sistema RAG. Muchas de estas técnicas consisten en reindexar los documentos originales. En algunos casos, ampliando o achicando el tamaño del chunk e incorporando más contexto, en otros casos generando texto ficticio parecido al original para aumentar la densidad del espacio de búsqueda (data augmentation) y en otros casos generando capas de indexación con resúmenes de secciones compuestas por varios chunks (o clusters de chunks), haciendo que la búsqueda suceda en varias etapas consecutivas.
Tamaño del contexto y consolidación
Otro punto que resulta relevante es cuántos documentos devolvemos en la búsqueda por similaridad. Si el número es muy chico, podemos estar perdiendo información relevante. Si el número es muy grande, puede ser demasiado vago y que el LLM se pierda a la hora de interpretarlo. A su vez, una vez que el retrieval devolvió documentos, hay un proceso que es el de interpretación y consolidación de esos distintos fragmentos. En el mejor de los casos, van a ser coincidentes y complementarios pero en caso de que no, hay que perfeccionar el mecanismo para lidiar con las posibles incongruencias.
El futuro de los RAG
Está claro que los sistemas RAG tuvieron una evolución vertiginosa desde la aparición de los LLMs hasta el momento. Gran parte de eso se explica por la necesidad de complementar el funcionamiento de los modelos con información nueva o privada. Existe un debate sobre cuánto este mecanismo va a seguir en funcionamiento a medida que se vayan ampliando las ventanas de contexto y el acceso a herramientas como la búsqueda web. Sin embargo, los RAG como sistemas de búsqueda de información relevante en bases de datos de texto probablemente encuentren otras funciones. En definitiva, lo que estamos haciendo es manipular el texto y sus representaciones vectoriales.