En este artículo, mi objetivo es explicar cómo y por qué es beneficioso utilizar un modelo de lenguaje grande (LLM) para la recuperación de información basada en fragmentos.
Utilizo el modelo GPT-4 de OpenAI como ejemplo, pero este enfoque se puede aplicar con cualquier otro LLM, como los de Hugging Face, Claude y otros.
Todos pueden acceder a esto. artículo gratis.
Consideraciones sobre la recuperación de información estándar
El concepto principal implica tener una lista de documentos (trozos de texto) almacenado en una base de datos, que podría recuperarse según algunos filtros y condiciones.
Normalmente, se utiliza una herramienta para habilitar la búsqueda híbrida (como Azure AI Search, LlamaIndex, etc.), que permite:
- realizar una búsqueda basada en texto utilizando algoritmos de frecuencia de términos como TF-IDF (por ejemplo, BM25);
- realizar una búsqueda basada en vectores, que identifica conceptos similares incluso cuando se utilizan términos diferentes, calculando distancias de vectores (normalmente similitud de coseno);
- combinando elementos de los pasos 1 y 2, ponderándolos para resaltar los resultados más relevantes.
La Figura 1 muestra el proceso de recuperación clásico:
- el usuario hace una pregunta al sistema: “Me gustaría hablar de París”;
- el sistema recibe la pregunta, la convierte en un vector de incrustación (utilizando el mismo modelo aplicado en la fase de ingesta) y encuentra los fragmentos con las distancias más pequeñas;
- el sistema también realiza una búsqueda textual basada en la frecuencia;
- los fragmentos devueltos por ambos procesos se someten a una evaluación adicional y se reordenan según una fórmula de clasificación.
Esta solución logra buenos resultados pero tiene algunas limitaciones:
- no siempre se recuperan todos los fragmentos relevantes;
- En ocasiones, algunos fragmentos contienen anomalías que afectan la respuesta final.
Un ejemplo de un problema de recuperación típico
Consideremos la matriz “documentos”, que representa un ejemplo de una base de conocimientos que podría conducir a una selección de fragmentos incorrecta.
documents = [
"Chunk 1: This document contains information about topic A.",
"Chunk 2: Insights related to topic B can be found here.",
"Chunk 3: This chunk discusses topic C in detail.",
"Chunk 4: Further insights on topic D are covered here.",
"Chunk 5: Another chunk with more data on topic E.",
"Chunk 6: Extensive research on topic F is presented.",
"Chunk 7: Information on topic G is explained here.",
"Chunk 8: This document expands on topic H. It also talk about topic B",
"Chunk 9: Nothing about topic B are given.",
"Chunk 10: Finally, a discussion of topic J. This document doesn't contain information about topic B"
]
Supongamos que tenemos un sistema RAG, que consta de una base de datos vectorial con capacidades de búsqueda híbrida y un mensaje basado en LLM, al que el usuario plantea la siguiente pregunta: “Necesito saber algo sobre el tema B”.
Como se muestra en la Figura 2, la búsqueda también devuelve un fragmento incorrecto que, si bien es semánticamente relevante, no es adecuado para responder la pregunta y, en algunos casos, podría incluso confundir al LLM encargado de proporcionar una respuesta.
En este ejemplo, el usuario solicita información sobre “tema B”, y la búsqueda devuelve fragmentos que incluyen “Este documento amplía el tema H. También habla del tema B.” y “Las ideas relacionadas con el tema B se pueden encontrar aquí.” así como el fragmento que dice “No se da nada sobre el tema B.”.
Si bien este es el comportamiento esperado de la búsqueda híbrida (como hacen referencia los fragmentos “tema B”), no es el resultado deseado, ya que el tercer fragmento se devuelve sin reconocer que no es útil para responder la pregunta.
La búsqueda no produjo el resultado deseado, no sólo porque la búsqueda del BM25 encontró el término “tema B” en el tercer Chunk, sino también porque la búsqueda de vectores arrojó una alta similitud de coseno.
Para comprender esto, consulte la Figura 3, que muestra los valores de similitud de coseno de los fragmentos en relación con la pregunta, utilizando el modelo text-embedding-ada-002 de OpenAI para incrustaciones.
Es evidente que el valor de similitud del coseno para el “fragmento 9” se encuentra entre los más altos, y que entre este fragmento y el fragmento 10, que hace referencia a “tema B”, también está el fragmento 1, que no menciona “tema B”.
Esta situación permanece sin cambios incluso cuando se mide la distancia utilizando un método diferente, como se ve en el caso de la distancia de Minkowski.
Utilización de LLM para la recuperación de información: un ejemplo
La solución que describiré está inspirada en lo publicado en mi repositorio de GitHub. https://github.com/peronc/LLMRetriever/.
La idea es que el LLM analice qué fragmentos son útiles para responder la pregunta del usuario, no clasificando los fragmentos devueltos (como en el caso de RankGPT) sino evaluando directamente todos los fragmentos disponibles.
En resumen, como se muestra en la Figura 4, el sistema recibe una lista de documentos para analizar, que pueden provenir de cualquier fuente de datos, como almacenamiento de archivos, bases de datos relacionales o bases de datos vectoriales.
Los fragmentos se dividen en grupos y se procesan en paralelo mediante un número de subprocesos proporcional a la cantidad total de fragmentos.
La lógica de cada hilo incluye un bucle que recorre los fragmentos de entrada y llama a un mensaje de OpenAI para que cada uno verifique su relevancia con la pregunta del usuario.
El mensaje devuelve el fragmento junto con un valor booleano: verdadero si es relevante y FALSO si no lo es.
Vamos a codificar 😊
Para explicar el código, lo simplificaré usando los fragmentos presentes en el documentos array (en las conclusiones haré referencia a un caso real).
En primer lugar, importo las bibliotecas estándar necesarias, incluidas os, langchain y dotenv.
import os
from langchain_openai.chat_models.azure import AzureChatOpenAI
from dotenv import load_dotenv
A continuación, importo mi clase LLMRetrieverLib/llm_retrieve.py, que proporciona varios métodos estáticos esenciales para realizar el análisis.
from LLMRetrieverLib.retriever import llm_retriever
Después de eso, necesito importar las variables necesarias para utilizar el modelo Azure OpenAI GPT-4o.
load_dotenv()
azure_deployment = os.getenv("AZURE_DEPLOYMENT")
temperature = float(os.getenv("TEMPERATURE"))
api_key = os.getenv("AZURE_OPENAI_API_KEY")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
api_version = os.getenv("API_VERSION")
A continuación, procedo con la inicialización del LLM.
# Initialize the LLM
llm = AzureChatOpenAI(api_key=api_key, azure_endpoint=endpoint, azure_deployment=azure_deployment, api_version=api_version,temperature=temperature)
Estamos listos para comenzar: el usuario hace una pregunta para recopilar información adicional sobre Tema B.
question = "I need to know something about topic B"
En este punto comienza la búsqueda de fragmentos relevantes y para ello utilizo la función llm_retrieve.process_chunks_in_parallel desde LLMRetrieverLib/retriever.py biblioteca, que también se encuentra en el mismo repositorio.
relevant_chunks = LLMRetrieverLib.retriever.llm_retriever.process_chunks_in_parallel(llm, question, documents, 3)
Para optimizar el rendimiento, la función llm_retrieve.process_chunks_in_parallel Emplea subprocesos múltiples para distribuir el análisis de fragmentos en múltiples subprocesos.
La idea principal es asignar a cada hilo un subconjunto de fragmentos extraídos de la base de datos y hacer que cada hilo analice la relevancia de esos fragmentos en función de la pregunta del usuario.
Al final del procesamiento, los fragmentos devueltos son exactamente los esperados:
['Chunk 2: Insights related to topic B can be found here.',
'Chunk 8: This document expands on topic H. It also talk about topic B']
Finalmente, le pido al LLM que responda a la pregunta del usuario:
final_answer = LLMRetrieverLib.retriever.llm_retriever.generate_final_answer_with_llm(llm, relevant_chunks, question)
print("Final answer:")
print(final_answer)
A continuación se muestra la respuesta del LLM, que es trivial ya que el contenido de los fragmentos, si bien es relevante, no es exhaustivo sobre el tema B:
Topic B is covered in both Chunk 2 and Chunk 8.
Chunk 2 provides insights specifically related to topic B, offering detailed information and analysis.
Chunk 8 expands on topic H but also includes discussions on topic B, potentially providing additional context or perspectives.
Escenario de puntuación
Ahora intentemos hacer la misma pregunta pero utilizando un enfoque basado en la puntuación.
Le pido al LLM que asigne una puntuación del 1 al 10 para evaluar la relevancia entre cada fragmento y la pregunta, considerando solo aquellos con una relevancia superior a 5.
Para hacer esto, llamo a la función llm_retriever.process_chunks_in_parallelpasando tres parámetros adicionales que indican, respectivamente, que se aplicará puntuación, que el umbral para que se considere válido debe ser mayor o igual a 5 y que quiero una copia impresa de los fragmentos con sus respectivas puntuaciones.
relevant_chunks = llm_retriever.process_chunks_in_parallel(llm, question, documents, 3, True, 5, True)
La fase de recuperación con puntuación produce el siguiente resultado:
score: 1 - Chunk 1: This document contains information about topic A.
score: 1 - Chunk 7: Information on topic G is explained here.
score: 1 - Chunk 4: Further insights on topic D are covered here.
score: 9 - Chunk 2: Insights related to topic B can be found here.
score: 7 - Chunk 8: This document expands on topic H. It also talk about topic B
score: 1 - Chunk 5: Another chunk with more data on topic E.
score: 1 - Chunk 9: Nothing about topic B are given.
score: 1 - Chunk 3: This chunk discusses topic C in detail.
score: 1 - Chunk 6: Extensive research on topic F is presented.
score: 1 - Chunk 10: Finally, a discussion of topic J. This document doesn't contain information about topic B
Es lo mismo que antes, pero con una puntuación interesante 😊.
Finalmente, vuelvo a pedirle al LLM que dé una respuesta a la pregunta del usuario, y el resultado es similar al anterior:
Chunk 2 provides insights related to topic B, offering foundational information and key points.
Chunk 8 expands on topic B further, possibly providing additional context or details, as it also discusses topic H.
Together, these chunks should give you a well-rounded understanding of topic B. If you need more specific details, let me know!
Consideraciones
Este enfoque de recuperación ha surgido como una necesidad después de algunas experiencias previas.
He notado que las búsquedas puramente basadas en vectores producen resultados útiles, pero a menudo son insuficientes cuando la incrustación se realiza en un idioma que no es el inglés.
El uso de OpenAI con oraciones en italiano deja claro que la tokenización de términos suele ser incorrecta; por ejemplo, el término “canción metódica”, que significa “canción”en italiano, se divide en dos palabras distintas: “poder” y “zona”.
Esto lleva a la construcción de una matriz de incrustación que está lejos de lo que se pretendía.
En casos como este, la búsqueda híbrida, que también incorpora el recuento de frecuencia de términos, conduce a mejores resultados, pero no siempre son los esperados.
Entonces, esta metodología de recuperación se puede utilizar de las siguientes maneras:
- como método de búsqueda principal: donde se consulta la base de datos para todos los fragmentos o un subconjunto basándose en un filtro (por ejemplo, un filtro de metadatos);
- como refinamiento en el caso de la búsqueda híbrida: (este es el mismo enfoque utilizado por RankGPT) de esta manera, la búsqueda híbrida puede extraer una gran cantidad de fragmentos y el sistema puede filtrarlos para que solo los relevantes alcancen el LLM y al mismo tiempo cumplir con el límite de token de entrada;
- como alternativa: en situaciones en las que una búsqueda híbrida no produce los resultados deseados, se pueden analizar todos los fragmentos.
Analicemos los costos y el rendimiento.
Por supuesto, no es oro todo lo que reluce, ya que hay que tener en cuenta los tiempos de respuesta y los costes.
En un caso de uso real, recuperé los fragmentos de una base de datos relacional que consta de 95 segmentos de texto divididos semánticamente usando mi LLMChunkizerLib/chunkizer.py biblioteca a partir de dos documentos de Microsoft Word, con un total de 33 páginas.
El análisis de la relevancia de los 95 fragmentos para la pregunta se realizó llamando a las API de OpenAI desde una PC local con un ancho de banda no garantizado, con un promedio de alrededor de 10 Mb, lo que resultó en tiempos de respuesta que variaron de 7 a 20 segundos.
Naturalmente, en un sistema en la nube o utilizando LLM locales en GPU, estos tiempos se pueden reducir significativamente.
Creo que las consideraciones sobre los tiempos de respuesta son muy subjetivas: en algunos casos es aceptable tardar más en dar una respuesta correcta, mientras que en otros es fundamental no hacer esperar demasiado a los usuarios.
De manera similar, las consideraciones sobre los costos también son bastante subjetivas, ya que se debe adoptar una perspectiva más amplia para evaluar si es más importante brindar respuestas lo más precisas posible o si algunos errores son aceptables.
En ciertos campos, el daño a la reputación causado por respuestas incorrectas o faltantes puede superar el gasto en tokens.
Además, aunque los costos de OpenAI y otros proveedores han ido disminuyendo constantemente en los últimos años, aquellos que ya tienen una infraestructura basada en GPU, tal vez debido a la necesidad de manejar datos sensibles o confidenciales, probablemente preferirán utilizar un LLM local.
Conclusiones
En conclusión, espero haber aportado mi perspectiva sobre cómo se puede abordar la recuperación.
Al menos mi objetivo es ser útil y tal vez inspirar a otros a explorar nuevos métodos en su propio trabajo.
Recuerde, el mundo de la recuperación de información es vasto y, con un poco de creatividad y las herramientas adecuadas, podemos descubrir conocimientos de formas que nunca imaginamos.