Las tecnologías GenAI son sin duda un elemento de tendencia en 2023 y 2024, y como trabajo para Tikalque publica su propio informe anual radar tecnológico y tendencias report, LLM y genAI no escaparon a mi atención. Como desarrollador, a menudo consulto chatbots de IA generativa para que me ayuden a resolver todo tipo de errores de TypeScript y misteriosos problemas de linting, utilizo herramientas de asistencia de genAI en mi IDE y para mejorar mis PR. Esta tecnología puede cambiarnos la vida.
Como técnicos y, en definitiva, como desarrolladores de software, esta nueva tendencia nos brinda la oportunidad de integrar estas capacidades en todos los proyectos en los que trabajamos, y veo que mis amigos y colegas exploran estas opciones, lo que me llevó a tomar la decisión: ¡yo también debería hacerlo!
Y tenía justo el proyecto:
A menudo me pregunto cómo pueden los artistas aficionados explorar el amplio mundo de los eventos culturales locales y mundiales para llegar a conseguir esa deseada invitación para actuar. No tenemos los recursos, las conexiones y el conocimiento de todo lo disponible. Claro que hay motores de búsqueda y sitios web especializados, pero hay que saber qué y cómo buscar, así que decidí utilizar genAI para obtener recomendaciones.
Paso 1 - ¿Se puede hacer?
Comprobar la viabilidad de un motor de recomendación utilizando uno de los LLM, incluía abrir cuentas en varios servicios de chat genAI y hacerles la misma pregunta:
Somos una aficionado Danza folclórica israelí grupo, incluidos bailarines en sillas de ruedas. Buscamos cultural y folclore eventos y festivales en Europa la posibilidad de que nos inviten a actuar, siempre y cuando cubrir nuestros gastos. ¿Podría recomendarme algunos?
Los resultados en H1 de 2024 variaron entre los distintos servicios de chat:
-
- Me remitieron a sitios web especializados que podía consultar para obtener resultados
- Me dio resultados reales
De entre los que devolvieron resultados, califiqué la calidad de los resultados por su relevancia y precisión, y acabé con OpenAI GPT-3 como opción.
Paso 2 - ¿Es suficiente?
Recordando que incluso uno de los asistentes de chat del paso 1 me sugirió que consultara otros sitios web, ¿qué pasaría si pudiera incluir algunos de esos datos en los resultados?
Teniendo en cuenta que también dependo de quién y cuándo se entrenó el modelo, quería que mis recomendaciones se basaran en más fuentes de datos y sabía que se podía hacer con RAG. En primer lugar, ¿qué es RAG?
Generación de Recuperación Aumentada (RAG)
RAG es el proceso de enriquecer y optimizar los resultados que recibe de LLM añadiendo datos "externos". Si puedo añadir resultados basados en la misma búsqueda en fuentes de datos externas (de sitios web dedicados) puedo ampliar la variedad de los resultados que proporcionará mi aplicación.
Para ello necesitarás:
-
- Fuentes de datos externas - Para mi experimento creé una cuenta de prueba para API de eventos de predictHQ
- Almacenar mis datos externos es un motor que permite la búsqueda por similitud y no una coincidencia exacta
Accesibilidad de los datos para el GAR
Una vez que haya terminado de examinar los datos, su aspecto y las características que contienen, es hora de que seleccione las características de los datos que le gustaría utilizar y hacerlas utilizables para el GAR.
Para permitir una búsqueda de similitudes, tendríamos que transformar nuestros datos en un formato que permita realizar búsquedas y comparable. Dado que no buscamos coincidencias exactas, sino coincidencias similares, existen dos técnicas muy comunes para ello:
Técnica RAG | Detalles |
Búsqueda vectorial (también conocida como RAG común) | Los datos y la pregunta se transforman en vectores de números (puntos flotantes).
Se utilizan cálculos matemáticos para determinar la similitud entre la pregunta y los datos. |
GraphRAG | Los datos y la pregunta se transforman en gráfico vértices y aristas.
Las relaciones gráficas se comparan por similitud |
El proceso de creación de la representación de los datos se denomina incrustaciónEn este artículo nos centraremos en la búsqueda vectorial.
Métrica de similitud
Hay 3 opciones comunes (en pocas palabras):
-
- Producto de puntos: Cálculo de la similitud a partir del producto de los valores de cada vector.
- Coseno: Basado en el ángulo entre los vectores
- L2_norm: Distancia euclidiana entre los vectores, basada en el ángulo entre los vectores y la longitud de cada vector.
Más información opciones de similitud vectorial.
Paso 3 - ¿Cómo lo hago?
Antes de sumergirnos en cómo vamos a hacer esto y en algo de código real y capturas de pantalla, veamos cómo se construiría tal arquitectura, y cómo Couchbase entra en escena:
Lo que esto significa en la práctica es:
-
- Ingestión app to:
- Obtener datos de la API externa
- Crear incrustaciones vectoriales
- Cargar datos en una colección Couchbase
- Crear un índice de búsqueda vectorial en Couchbase
- Solicitud inmediata a:
- Pedir resultados a los datos de Couchbase
- Añadir los resultados de la búsqueda vectorial a la consulta LLM como contexto
- Devolver los resultados cohesionados a los usuarios
- Ingestión app to:
Aplicación por ingestión
Este proceso fue probablemente el más largo, pasé tiempo creando incrustaciones en distintos campos y formatos. Por simplicidad, al final opté por utilizar únicamente la información geográfica que había recopilado:
1 2 3 4 5 |
de langchain_openai importar OpenAIEmbeddings modelo_incrustaciones = OpenAIEmbeddings(modelo="text-embedding-3-small") texto = f"Información geográfica: {row['geo_info']}" incrustación = modelo_incrustaciones.embed_query(texto) |
Para crear la incrustación elegí utilizar incrustaciones textuales como punto de partida, es decir, "comparar" representación de texto con representación de texto. La incrustación en sí incluye aproximadamente 1500 números (que es el pequeño).
El código en sí no es extremadamente complejo, sin embargo puede llevar mucho tiempo, crear 1 incrustación para 5000 eventos me llevó aproximadamente 1 hora en mi MacBook pro M1 de 16 GB.
El código completo utilizando pandas2 puede encontrarse en este repositorio.
Colección Couchbase e índice de búsqueda
Para poder buscar datos similares entre la pregunta y los resultados que hemos preparado basándonos en una API externa, vamos a...
-
- Crear una colección Couchbase
- Sube los datos preparados a una colección de Couchbase incluidas las incrustaciones
- Crear un índice de búsqueda en los campos de incrustación eligiendo el algoritmo de similitud vectorial para comparar vectores.
Nueva colección Couchbase
Para mi aplicación elegí utilizar el servicio alojado Couchbase - Capella, la configuración es muy fácil. Me registré, elegí el servicio Cloud y creé un nuevo proyecto.
Al hacer clic en mi proyecto y navegar hasta la pestaña Herramientas de datos, ahora puedo crear una nueva colección para los datos que he preparado:
Para subir los datos que preparé hay varias opciones: como el tamaño del archivo era bastante grande opté por utilizar la opción cbimport utilidad de hacerlo.
1 |
./cbimport json --grupo sofás cama:// --username --password --bucket --scope-collection-exp "." --dataset for_collection.json --generate-key '%id%' --cacert --format lines |
Observe que he elegido el ID de los documentos JSON para que sea el documento clave en la colección.
Recuerde que antes de hacerlo, es necesario:
-
- Cree un usuario/contraseña de acceso a la base de datos con privilegios de escritura como mínimo
- Abrir el cluster para llamadas desde su host
- Descargar un certificado para el clúster
El esquema del documento inferido muestra que el incrustación con el tipo de matriz de números:
Para permitir la búsqueda por similitud vectorial, creemos el índice de búsqueda accediendo a la pestaña Búsqueda.
Obviamente debemos seleccionar el campo de incrustación para el índice de búsqueda, pero observe que hay más parámetros que configurar:
Ya hemos discutido lo que la métrica de similitud es, sólo tenga en cuenta que Couchbase apoyo l2_norm (es decir, la distancia euclidiana) y el producto punto, elegí "producto punto"que puede ser más beneficioso para mi sistema de recomendación.
El siguiente paso consiste en elegir campos adicionales de los documentos que se devolverían siempre que se atenúe un vector similar a la pregunta:
Si no añade al menos un campo, su aplicación fallará porque no habrá ningún dato devuelto.
Ahí está, la selección de campos índice:
Hemos llegado a un punto crucial en nuestro proyecto, ahora podemos empezar a ejecutar la búsqueda de similitud en los datos que hemos preparado, pero puede que no tengas una búsqueda de similitud que funcione en tu primer intento. Le daré algunos consejos para obtener resultados de su búsqueda de similitud o para comprobar por qué no obtiene resultados:
-
- Asegúrese de que su técnica de incrustación, al crear los datos y preparar una búsqueda son idénticos
- Comience con un formato sencillo y predecible para la información que desea comparar. Por ejemplo , , .
- Asegúrese de que no tiene información adicional que se añade accidentalmente a los datos que está creando incrustaciones para (por ejemplo, tuve saltos de línea)
- Asegúrese de que la búsqueda de coincidencia exacta funciona:
- Búsqueda de los datos exactos para los que creó incrustaciones
- Compare el vector de incrustación para asegurarse de que se crean incrustaciones idénticas en la parte de generación y búsqueda (la depuración será útil aquí). Si hay alguna diferencia, vuelva a los pasos 1-3.
Una vez que tenga una búsqueda por similitud que funcione, añada gradualmente más campos, cambie formatos, incrustaciones y cualquier otra cosa que considere que falta.
Recuerde que cualquier cambio en las incrustaciones significa:
-
- Recrear las incrustaciones
- Carga de los datos de los cambios en una colección truncada
- Cambiar el índice de búsqueda si es necesario
- Es necesario cambiar el código
Estos pasos pueden llevar mucho tiempo, especialmente la creación de incrustaciones, por lo que es posible que desee comenzar con:
-
- Una pequeña parte de sus documentos
- Una técnica de incrustación pequeña/rápida
LLM y solicitud de RAG
Lo que nuestra aplicación necesita hacer es:
-
- Pedir a Couchbase que encuentre resultados similares a la pregunta del usuario
- Añade los resultados al contexto de la pregunta al LLM
- Haz una pregunta al LLM
Para simplificar he creado este código en Python como un cuaderno Jupyter que se puede encontrar en este repositorio. Para ello he utilizado las siguientes bibliotecas:
-
- Couchbase: Conectar y autenticar a mi clúster Capella
- Cadena LangChainun marco para el desarrollo de aplicaciones basadas en grandes modelos lingüísticos (LLM), para:
- Incrustaciones
- Uso de Couchbase como almacén de vectores
- "Chateando" con OpenAI
- LangGraph: Un marco para la construcción de aplicaciones LLM con estado y multiactores, para la creación de un flujo de la aplicación LLM.
Si has estado leyendo sobre, e incluso intentando construir tu propia aplicación LLM probablemente estés algo familiarizado con LangChain, es un conjunto de librerías que te permiten escribir, construir, desplegar y monitorizar una aplicación, tiene muchos agentes y extensiones que te permiten integrar diferentes partes en tu código, como una API de terceros, una base de datos, una búsqueda web y muchas más.
Últimamente, también aprendí sobre LangGraph del hogar de LangChain, que te permite como desarrollador construir topologías más complejas de la aplicación LLM con condiciones, bucles (¡el grafo no tiene que ser un DAG!), interacción del usuario, y quizás la característica más buscada: Mantener el estado.
Antes de ver el código echemos un vistazo al archivo de entorno (.env) para ver qué credenciales y otros datos confidenciales necesitamos:
1 2 3 4 5 6 7 8 9 10 11 |
LANGSMITH_KEY=langsmithkey OPENAI_API_KEY=openaikey LANGCHAIN_PROJECT=miproyecto COUCHBASE_CONNECTION_STRING=couchbase://mycluster.com COUCHBASE_USER=miusuario COUCHBASE_PASS=mypass CUBO_CAMA=mybucket ÁMBITO DE LA BASE DE SOFÁ=myscope COUCHBASE_COLLECTION=micolección COUCHBASE_SEARCH_INDEX=mysearchindex LANGCHAIN_API_KEY=langchainapikey |
El estado de cada nodo del grafo es:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
de langógrafo.gráfico importar añadir_mensajes, StateGraph de extensiones_de_tipado importar TypedDict de escribiendo importar Anotado de langógrafo.punto de control.sqlite importar SqliteSaver clase Estado(TypedDict): # Los mensajes tienen el tipo "lista". La función `add_messages # en la anotación define cómo debe actualizarse esta clave de estado # (en este caso, añade mensajes a la lista, en lugar de sobrescribirlos) mensajes: Anotado[lista, añadir_mensajes] tipo_evento: str ubicación: str etiquetas: str graficador = StateGraph(Estado) |
Es importante tener en cuenta que a menos que se defina un reductor el estado se sobrescribirá entre cada nodo del gráfico, el miembro messages de la clase state tiene un reductor que añadirá los nuevos mensajes a la lista.
Para conectarnos a Couchbase y usarlo como almacén de vectores para la aplicación LLM, nos autenticamos en el cluster, y pasamos la conexión del cluster al objeto LangChain para el almacén de vectores:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
de langchain_openai importar OpenAIEmbeddings importar os de couchbase.grupo importar Grupo de couchbase.opciones importar ClusterOptions de couchbase.auth importar PasswordAuthenticator de langchain_couchbase importar CouchbaseVectorStore COUCHBASE_CONNECTION_STRING = os.environ["COUCHBASE_CONNECTION_STRING"] COUCH_USER = os.environ["COUCHBASE_USER"] COUCH_PASS = os.environ["COUCHBASE_PASS"] NOMBRE_CUBO = os.environ["COUCHBASE_BUCKET"] SCOPE_NAME = os.environ["COUCHBASE_SCOPE"] NOMBRE_COLECCIÓN = os.environ["COUCHBASE_COLLECTION"] NOMBRE_ÍNDICE_BÚSQUEDA = os.environ["COUCHBASE_SEARCH_INDEX"] auth = PasswordAuthenticator(COUCH_USER, COUCH_PASS) opciones = ClusterOptions(auth) grupo = Grupo(COUCHBASE_CONNECTION_STRING, opciones) incrustación = OpenAIEmbeddings(modelo="text-embedding-3-small") vector_store = CouchbaseVectorStore( grupo=grupo, nombre_cubo=NOMBRE_CUBO, nombre_ámbito=SCOPE_NAME, nombre_colección=NOMBRE_COLECCIÓN, incrustación=incrustación, nombre_índice=NOMBRE_ÍNDICE_BÚSQUEDA, ) |
Hay dos detalles importantes a tener en cuenta:
-
- La integración en la aplicación debe ser idéntico al utilizado en la parte de ingestión
- El nombre por defecto del campo de incrustación es 'incrustación', si el nombre del campo respectivo es diferente en su índice de búsqueda necesita establecerlo durante la instanciación de CouchbaseVectorStore (embedding_key)
En este momento, estás listo para escribir tu aplicación LangGraph y usar Couchbase como almacén de vectores. Vamos a montarlo: cada grafo necesita nodos, punto de inicio y aristas dirigidas.
Nuestro gráfico obtendrá datos del almacén de vectores y continuará añadiendo esta información al contexto de la solicitud LLM.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
de langchain_core.solicita importar ChatPromptTemplate de langchain_core.output_parsers importar StrOutputParser de langchain_openai importar ChatOpenAI llm = ChatOpenAI(modelo="gpt-3.5-turbo") plantilla = """Eres un bot útil que sirve para encontrar eventos para artistas que buscan salas en EE.UU.. Si no puedes responder basándote en el contexto proporcionado, responde con un genérico contesta. Responde a la pregunta con la mayor sinceridad posible utilizando el contexto que aparece a continuación: {contexto} Por favor, formatee también el resultado en formato Markdown. Pregunta: {pregunta}""" consulte = ChatPromptTemplate.desde_plantilla(plantilla) cadena_generacional = consulte | llm | StrOutputParser() def chatbot(estado: Estado): respuesta = cadena_generacional.invoque({"contexto": estado[mensajes], "pregunta": f"Somos un grupo de aficionados de {estado['tipo_de_evento']} que busca festivales de {estado['etiquetas']} en {estado['ubicación']}, ¿podría recomendarnos algunos?".}) estado[mensajes].añadir(respuesta) devolver estado def base_de_busqueda(estado: Estado): consulta = f"Geo Info: {state['location']}" retriever = vector_store.as_retriever() resultados = retriever.invoque(consulta) para resultado en resultados: texto = f"Title {result.metadata['title']}/{result.metadata['alternate_titles_flat']} - {result.metadata['description']} desde {result.metadata['start']} hasta {result.metadata['end']}, ubicación {result.metadata['geo_info']}. Etiquetas {result.metadata['labels_flat']}, categoría {result.metadata['category']}". estado[mensajes].añadir(texto) devolver estado graficador.añadir_nodo("vector_search", base_de_busqueda) graficador.añadir_nodo("chatbot", chatbot) graficador.set_entry_point("vector_search") graficador.añadir_borde("vector_search", "chatbot") graficador.set_finish_point("chatbot") memoria = SqliteSaver.from_conn_string(":memoria:") gráfico = graficador.compilar(puntero de control=memoria) |
Se traduce en el código anterior a dos nodos:
-
- vector_search (punto de entrada)
- chatbot (punto final)
Como una imagen vale más que mil palabras, he utilizado el siguiente código para visualizar el gráfico:
1 2 3 4 5 6 7 8 9 10 |
de IPython.mostrar importar Imagen, mostrar de langchain_core.ejecutables.gráfico importar CurveStyle, MermaidDrawMethod, NodeStyles mostrar( Imagen( gráfico.get_graph().dibujar_sirena_png( método_dibujo=MermaidDrawMethod.API, ) ) ) |
Lo que dio lugar al siguiente dibujo:
Para más opciones de visualización en langGraph, véase este Cuaderno Jupyter de LangGraph.
Preguntar al almacén vectorial significa buscar datos con una ubicación similar, puede observar que el formato de la consulta es el mismo que en el texto incrustado, los resultados se añaden al estado para ser utilizados en el siguiente nodo.
El nodo del chatbot toma la información de los mensajes y la incrusta en la pregunta dirigida al LLM.
Tenga en cuenta que el estado se guarda en la base de datos en memoria sqlite. Para utilizar el gráfico no dude en utilizar el siguiente ejemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
de al azar importar randint de IPython.núcleo.mostrar importar Markdown session_id = randint(1, 10000) config = {"configurable": {"thread_id": session_id}} ubicación_entrada = "kansas" categoría_entrada = "jaz" etiquetas_entrada = "grange" # Transmitir el gráfico, cada salida se imprimirá cuando esté listo para evento en gráfico.flujo({"tipo_evento": categoría_entrada, "localización": ubicación_entrada, "etiquetas": etiquetas_entrada}, config): para valor en evento.valores(): si len(valor[mensajes]) > 0: mostrar(Markdown(valor[mensajes][-1])) |
Y ya está, has creado una aplicación LLM para recomendar eventos culturales a grupos de aficionados a los que pedir invitaciones.
Resumen
Comenzar con las aplicaciones LLM es emocionante y, en mi humilde opinión, como una rampa divertida, emocionante y factible debido a su pronta naturaleza, sin embargo, hacer que nuestra aplicación sea mejor y más robusta esconde más desafíos.
En este artículo, me centré en el reto de aprovechar el conocimiento de nuestro modo con datos externos a través de la técnica o RAG y cómo se puede aprovechar Couchbase para hacerlo.
Es importante recordar que la creación de incrustaciones que la aplicación LLM encontrará en la búsqueda vectorial, puede no funcionar en su primer intento. Compruebe el formato, intente empezar con incrustaciones sencillas y utilice la depuración en la medida de lo posible.
También demostré las capacidades de LangGraph de LangChain que permite crear decisiones y flujos complejos en la aplicación LLM.
Disfruta de tu viaje con las solicitudes de LLM.