LangGraph con Multi-Agente

LangGraph con Multi-Agente

Durante el desarrollo de nuestro chatbot, hemos añadido varias funcionalidades. Inicialmente, solo generaba una URL para que el usuario pudiera visualizar los viajes disponibles según sus necesidades (a lo que nos referiremos como URL de viaje). Ahora, también puede responder preguntas sobre los viajes disponibles, atender consultas sobre reservamos utilizando la información de la página web y generar una URL de información sobre cualquier compra realizada por el usuario.

Para lograr lo anterior, utilizamos un Agente de Langchain que nos facilita la implementación y el uso de los modelos de OpenAI, así como los que estén disponibles. Este es muy útil para aplicaciones muy sencillas. Sin embargo, existen algunas problemáticas cuando se implementan más funcionalidades y el proyecto crece.

La problemática principal es que ahora el chatbot tiene varias funcionalidades y tiene que decidir cómo actuar dependiendo de lo que el usuario pide, disminuyendo su eficiencia al momento de responder. Nosotros, como desarrolladores, no podemos involucrarnos mucho en la manera en la que el agente toma decisiones.

Para intentar solucionar este problema, utilizamos LangGraph, una nueva herramienta que el equipo de LangChain desarrolló.

¿Qué es LangGraph?

Hace unos días, el equipo de LangChain anunció LangGraph. El objetivo de este es brindar al desarrollador un mayor control sobre el ciclo de ejecución de un Agente, pero con el uso de un grafo. Para lograrlo, se utilizan 3 elementos principales: stateGraph, Nodes y Edges.

Donde en el StateGraph se define el estado del grafo que va a viajar y mutar durante el ciclo de ejecución. Los Nodes se definen con un nombre y su función a ejecutar, y los Edges se encargan de conectar los nodos.

Los Edges se dividen en 3 grupos:

  • The Starting Edge: Edge que conecta el inicio del grafo con un nodo en particular.

  • Normal Edges: Edges donde un nodo siempre se ejecuta después de otro.

  • Conditional Edges: Edge donde se utiliza una función para determinar qué nodo será el siguiente en ejecutarse cuando existen varias conexiones.

Existen diferencias entre un Agente de LangChain y una implementación de LangGraph. La principal diferencia es que LangGraph expone parte de la lógica interna de un AgentExecutor. Una funcionalidad notable es que se puede definir un nodoprincipal que siempre se va a ejecutar al inicio del proceso. Esto hace que el Agente ya no sea 100% autónomo, por lo que el equipo de LangChain decidió nombrar la implementación del grafo como una State Machine. Donde se combina la autonomía del Agente con instrucciones específicas que el desarrollador puede definir.

Por ejemplo, tal vez quieras:

  • Forzar al agente a que ejecute una tool en específico primero.

  • Tener mas control en como se van a ejecutar las tools.

  • Tener diferentes prompts para el agente, dependiendo en el estado en que se encuente.

Implementación

Single Agent

Replicando el funcionamiento del chatbot de LangChain

Traduciendo la lógica de nuestro chatbot usando un AgentExecutor de LangChain a LangGraph, la implementación queda de la siguiente manera:

En el diagrama podemos observar los siguientes elementos:

Nodos:Agent, action, last_agent y END

El nodo Agent recibe el input del usuario y decide si necesita ejecutar alguna tool para realizar la tarea o si debe finalizar el proceso.

El nodo action se encarga de ejecutar las tools y devolver el output las veces que sean necesarias.

El nodo last_agent guarda la respuesta final en la base de datos antes de finalizar el proceso. Es importante mencionar que sin este nodo, el agente enviaría el mensaje directamente al nodo END para terminar el proceso y responder al usuario.

Modificaciones al proceso de ejecución

Un reto al que nos enfrentamos utilizando un agente de LangChain es indicar qué tools se deben ejecutar al momento de recibir la petición del usuario. Por ejemplo:

Input del usuario: Quiero viajar de Mérida a CDMX el 22 de febrero de 2024.

Para esta petición necesitamos varios elementos para generar la URL de viaje con las especificaciones del usuario. Necesitamos: origen, destino, fecha de salida, fecha de regreso y número de pasajeros.

La manera en que obtenemos estos parámetros es con una tool llamada params_extractor y esta debe ejecutarse al principio del ciclo. El problema es que el agente, al ser 100% autónomo, únicamente delimitado por el prompt del sistema, no siempre ejecuta esta tool y puede ocurrir un error al consultar los viajes y generar la URL.

Aprovechando que LangGraph te permite involucrarte en el proceso de ejecución, una modificación interesante, como antes se mencionó, es forzar al agente a primero ejecutar una tool al principio del proceso. Quedando de la siguiente manera:

En el diagrama se puede observar que hay un nuevo nodo,first_agent, que se comunica directamente con action para forzar la ejecución de la tool params_extractor y después seguir con el proceso. Con esto resolvemos el problema. Esta solución es ideal en el caso de que el chatbot solo sea capaz de consultar viajes disponibles y armar la URL de viaje.

¿Que pasaría si el chatbot tuviera mas funcionalidades?

Actualmente, el chatbot tiene varias funcionalidades:

  • Generar una URL para viajes disponibles.

  • Responder preguntas relacionadas al contenido que se encuentra en el sitio de Reservamos.com. Ej. ¿Qué es un boleto flexible?

  • Proporcionar detalles de una compra previa (soporte al cliente).

A medida que se agregan nuevas funcionalidades, la lista de tools va creciendo, reduciendo la eficiencia del chatbot. Esto se debe a que, cuanto más funcionalidades tenga, aumenta el margen de error al momento de decidir qué tools ejecutar.

Un enfoque interesante es utilizar LangGraph Multi-Agent.

Multi-Agent

Un Chatbot con el enfoque Single Agent puede funcionar de manera efectiva usualmente utilizando un grupo de tools dentro de un único dominio, pero incluso utilizando modelos poderosos como gpt-4, puede ser menos efectivo al usar muchas tools.

Una manera de abordar tareas complicadas es a través de un enfoque de "divide y vencerás": crear un agente especializado para cada tarea o dominio y dirigir las tareas al "experto" correcto.

Ahora cada nodo es un agente con sus propias tools y se definen utilizando la clase AgentExecutor. Al seguir con este enfoque, nos podemos encontrar nuevamente con las limitantes de un Agente de LangChain. ¿Cómo puedo involucrarme en el proceso de cada agente? Para lograr esto, sería necesario que cada agente sea un subgrafo con sus propios flujos de ejecución en lugar de utilizar un AgentExecutor. Lo que genera nuevos puntos a considerar. Por ejemplo:

  • ¿Cuántos tokens va a consumir?

  • ¿Cuánto tiempo va a tardar en responder?

  • ¿Qué tan preciso es?

Es importante tomar en cuenta las preguntas anteriores, ya que tenemos que asegurar que la eficiencia del chatbot se mantenga o mejore, mientras que el tiempo y costo de procesamiento no aumenten drásticamente.

Al ejecutar pruebas manuales, podemos notar un comportamiento interesante. Iniciamos estas pruebas con peticiones sencillas como: "Quiero viajar de <origen> a <destino> el 23 de marzo". Resultando en una respuesta correcta con un tiempo de ejecución dentro de un rango aceptable.

Mensaje inicial - Hola!

Recordemos que tenemos un chatbot y sabemos que normalmente los usuarios inician con un Hola y no con la petición que quieren. Entonces, el siguiente paso es observar el comportamiento del supervisor con un primer input saludando al chatbot. Resulta que no contesta a esos mensajes. Ya que, la responsabilidad del supervisor es delegar la tarea a uno de sus expertos y al no coincidir ninguno con el input, este decide terminar la ejecución.

Un primer enfoque para resolver esta situación puede ser agregar un agente nuevo que se encargue de contestar conversaciones casuales hasta que el usuario ingrese la información suficiente para ejecutar otro agente y dar una respuesta final. Lo cual parece la opción ideal, pero el supervisor no entiende que la respuesta que devuelve el nuevo agente no es un mensaje del usuario y la ejecución termina en un bucle.

Una forma de evitar esto es modificar manualmente el nodo responsable de llamar de nuevo al supervisor o terminar el proceso. Cuando recibe una respuesta de uno de sus agentes, devuelve la respuesta al usuario. Esto no es ideal ya que el propósito de este enfoque es que los agentes puedan interactuar entre ellos para generar una respuesta más precisa. Sin embargo, por ahora, esto solo es posible para peticiones específicas y no para mantener una conversación.

Aún así, podemos aprovechar esta idea de tener varios "expertos", dividir las responsabilidades y al mismo tiempo, mantener una conversación casual. ¿Cómo? Regresando al primer enfoque SingleAgent Chatbot Graph con algunas modificaciones. La idea es mantener la estructura de un Agente que tiene una lista de tools para ejecutar dependiendo del input del usuario. Pero ahora, estas tools las vamos a ver como "expertos", cada uno de ellos con un nombre y descripción de lo que pueden hacer. Cuando se invoquen, estas se encargarán de inicializar un Agente siendo un subgrafo con sus propios nodos y tools a ejecutar.

Con este nuevo enfoque logramos lo siguiente:

  • El grafo principal puede mantener conversaciones casuales.

  • Cada subgrafo tiene su propio flujo, sus nodos y hasta podemos utilizar diferentes modelos de IA para realizar sus tareas.

Con estos resultados, podemos afirmar que hemos logrado nuestro objetivo.

  1. Como desarrolladores, podemos involucrarnos en el proceso de ejecución del Agente.

  2. El chatbot es capaz de mantener una conversación casual sin consumir más memoria.

  3. Los tiempos de respuesta y el consumo de tokens son similares a nuestro Agente de LangChain.

  4. Implementamos la arquitectura multi-agente, lo que nos ofrece flexibilidad para implementar varios modelos de IA simultáneamente y experimentar con ellos.

Puntos a considerar

Para saber si es viable implementar LangGraph con Multi-Agent, hay una serie de puntos a considerar con sus respectivos criterios:

  1. Asegurar que el chatbot responde de manera correcta a la petición del usuario.

    1. Ejecutando pruebas unitarias donde definimos una serie de preguntas que nos ayudan a validar el comportamiento del chatbot.
  2. Mantener el tiempo de respuesta promedio del chatbot.

    1. Ejecutando las pruebas unitarias múltiples veces para calcular el promedio de tiempo de respuesta y compararlo.
  3. Mantener el consumo de tokens por request.

    1. Ejecutando las pruebas unitarias múltiples veces para calcular el promedio de consumo de tokens por request y compararlo.
  4. Analizar si mejora la escalabilidad del proyecto.

    1. Es importante mencionar que la complejidad del proyecto puede aumentar con el uso de LangGraph, ya que cada nodo puede tener su propio flujo de ejecución. Por lo tanto, un buen diseño y documentación son cruciales para mantener el proyecto manejable y escalable.