# 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**](https://python.langchain.com/docs/get_started/introduction) 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](https://blog.langchain.dev/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.
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710957391131/4833a0c3-621d-4e6e-ac2e-02489dbb2e64.png align="center")

# 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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710957483363/b6b80f12-11ec-4be9-92a5-b45ce9587bd5.png align="center")

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:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710957557184/09a705d3-1f13-4f76-bb12-d31e72ce6229.png align="center")

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](http://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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710957665360/056ffbe6-7ff7-4ade-a0eb-feca81d4716e.png align="center")

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 *&lt;origen&gt;* a *&lt;destino&gt;* 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.
