Construye un agente que decide entre múltiples herramientas

Video
25 min~12 min lectura

Reproductor de video

Introducción: La Esencia de un Agente Decisor

En el núcleo de un agente de IA verdaderamente autónomo y útil se encuentra la capacidad de tomar decisiones. No se trata solo de ejecutar una secuencia predefinida de pasos, sino de evaluar el contexto, el estado actual y los objetivos para seleccionar la herramienta o el camino de acción más apropiado. Hasta ahora, has trabajado con agentes que pueden usar herramientas, pero en esta lección avanzada daremos el salto crucial hacia la construcción de un agente que decide cuál herramienta usar, cuándo usarla e incluso si debe finalizar su tarea. Este es el componente que transforma un simple flujo de trabajo en un sistema inteligente adaptativo.

La lógica condicional en LangGraph se implementa principalmente a través de los nodos condicionales o routers. Estos nodos no realizan trabajo por sí mismos, sino que actúan como desvíos en el grafo, dirigiendo el flujo de ejecución hacia una rama u otra en función del estado del sistema. Dominar este patrón es fundamental para crear agentes que puedan manejar consultas complejas y multifacéticas, como un asistente que debe decidir entre buscar información en una base de datos, realizar un cálculo o generar un resumen, todo dentro de la misma conversación.

Concepto Clave: El Router como Centro de Control

Imagina que eres el jefe de una oficina de correos muy eficiente. Los paquetes (las solicitudes del usuario) llegan a tu escritorio. Tú no los abres ni los entregas personalmente. En su lugar, inspeccionas cada paquete: su tamaño, etiqueta, destino y urgencia. Basándote en esta inspección, decides a qué departamento enviarlo: el departamento de paquetes pequeños para cartas, el de logística internacional para envíos al extranjero, o el de almacén para objetos grandes. Tu función es puramente de enrutamiento. En LangGraph, el nodo condicional es ese jefe. Examina el estado compartido del grafo (el paquete) y, basándose en una función de enrutamiento que tú defines, devuelve el nombre del siguiente nodo (el departamento) al que debe ir el flujo de ejecución.

La potencia de este modelo radica en su separación de preocupaciones. La lógica de decisión está encapsulada en una función pequeña y enfocada (el router), mientras que la lógica de ejecución específica reside en nodos especializados (las herramientas o subgrafos). Esto hace que el sistema sea más fácil de depurar, mantener y extender. Puedes modificar la lógica de enrutamiento sin tocar cómo funciona cada herramienta, y viceversa. Es el principio de diseño de software "separación de intereses" aplicado a la construcción de agentes de IA.

Un concepto avanzado que emerge de esto es el de ciclos controlados por condiciones. El agente no solo decide una vez, sino que puede entrar en un ciclo donde, después de ejecutar una herramienta, vuelve al nodo router para evaluar el nuevo estado. "¿La respuesta de la herramienta contestó la pregunta? Si no, ¿debo usar otra herramienta? ¿O ya puedo dar una respuesta final?" Este ciclo es la base de agentes que realizan razonamiento iterativo, acercándose paso a paso a la solución óptima.

Cómo Funciona en la Práctica: Anatomía de un Flujo Decisor

Vamos a desglosar el proceso paso a paso para construir un agente que decide entre tres herramientas: un buscador web, un calculador y un generador de resúmenes. El agente debe interpretar la pregunta del usuario y elegir la herramienta correcta, o finalizar si ya tiene la respuesta.

Paso 1: Definir el Estado. Todo comienza con el estado. Necesitamos un esquema que contenga, como mínimo, los mensajes de la conversación (historial), la pregunta actual, y cualquier resultado intermedio de herramientas. Usaremos un StateGraph con un TypedDict de Pydantic para tener tipado y autocompletado.

Paso 2: Crear las Herramientas (Nodos). Cada herramienta es un nodo en el grafo. Cada nodo es una función que recibe el estado, realiza una acción (como una búsqueda o un cálculo), y actualiza el estado devolviendo un diccionario con los cambios. Por ejemplo, el nodo "calculadora" leería una pregunta del estado, ejecutaría un LLM con una cadena de pensamiento matemática, y añadiría la respuesta al historial de mensajes.

Paso 3: El Corazón: La Función Router. Esta es la función de lógica condicional. Recibe el estado completo. Típicamente, se usa un LLM para clasificar la intención del usuario basándose en el último mensaje. La función debe devolver un string que coincida exactamente con el nombre de uno de los nodos siguientes disponibles (incluyendo un nodo especial como "__end__" para terminar). Esta decisión puede ser simple (basada en palabras clave) o compleja (usando un LLM para clasificación).

Paso 4: Ensamblar el Grafo. Se crea un StateGraph, se añaden todos los nodos (herramientas y un nodo para generar la respuesta final). Luego, se añade el nodo condicional usando el método .add_conditional_edges(). Este método requiere especificar el nodo de origen (a menudo el inicio), la función router, y un mapeo de los posibles destinos. Finalmente, se define un punto de entrada y se compila el grafo en una aplicación ejecutable.

Código en Acción: Agente Multihabilidad con Memoria

A continuación, un ejemplo completo y funcional de un agente que decide entre buscar, calcular o resumir. Requiere tener configuradas las claves API de OpenAI y Tavily.

from typing import TypedDict, List, Annotated
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_core.messages import HumanMessage, AIMessage
import operator
import json

# --- 1. DEFINICIÓN DEL ESTADO ---
class AgentState(TypedDict):
    messages: Annotated[List, operator.add]  # Historial de chat
    query: str                               # Consulta actual
    tool_result: str                         # Último resultado de herramienta

# --- 2. CONFIGURACIÓN DE HERRAMIENTAS Y MODELO ---
llm = ChatOpenAI(model="gpt-4-turbo", temperature=0)
search_tool = TavilySearchResults(max_results=2)

# Herramienta de cálculo (simulada con LLM)
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
    """Evalúa una expresión matemática. Ej: '2 + 3 * 4'."""
    try:
        # Precaución: eval() es peligroso en producción. Usar biblioteca segura.
        result = eval(expression)
        return f"El resultado de '{expression}' es {result}."
    except Exception as e:
        return f"Error al calcular '{expression}': {e}"

@tool
def text_summarizer(long_text: str) -> str:
    """Genera un resumen conciso de un texto largo."""
    summary_prompt = f"Resume el siguiente texto en 2 oraciones:\n\n{long_text}"
    summary_msg = llm.invoke(summary_prompt)
    return summary_msg.content

tools = [search_tool, calculator, text_summarizer]
llm_with_tools = llm.bind_tools(tools)

# --- 3. NODOS DEL GRAFO ---
def call_model(state: AgentState):
    """Nodo que llama al LLM para decidir la siguiente acción."""
    messages = state['messages']
    response = llm_with_tools.invoke(messages)
    return {"messages": [response]}

tool_node = ToolNode(tools)  # Nodo que ejecuta automáticamente la herramienta seleccionada

def final_response(state: AgentState):
    """Nodo final que devuelve la respuesta al usuario."""
    last_message = state['messages'][-1]
    # Si el último mensaje es del asistente y no es una llamada a herramienta, ya es la respuesta.
    if isinstance(last_message, AIMessage) and not last_message.tool_calls:
        return {"messages": [last_message]}
    # Si no, generamos una respuesta final basada en el historial.
    final_msg = llm.invoke(state['messages'])
    return {"messages": [final_msg]}

# --- 4. FUNCIÓN ROUTER (LÓGICA CONDICIONAL) ---
def router(state: AgentState) -> str:
    """Examina el último mensaje del LLM y decide el próximo paso."""
    last_message = state['messages'][-1]
    
    # Si no hay llamadas a herramienta, es una respuesta final.
    if not hasattr(last_message, 'tool_calls') or not last_message.tool_calls:
        return "__end__"
    
    # Si hay llamadas a herramienta, enruta al nodo de herramientas.
    # Podemos ser más específicos aquí si quisiéramos rutas diferentes por herramienta.
    # Por ahora, todas las herramientas van al mismo nodo 'tools'.
    return "tools"

# --- 5. CONSTRUCCIÓN DEL GRAFO ---
workflow = StateGraph(AgentState)

# Añadir nodos
workflow.add_node("agent", call_model)   # Nodo que decide/llama a herramientas
workflow.add_node("tools", tool_node)    # Nodo que ejecuta herramientas
workflow.add_node("final", final_response) # Nodo de respuesta final

# Definir flujo principal: El agente siempre actúa primero.
workflow.set_entry_point("agent")

# Añadir aristas condicionales DESPUÉS del nodo 'agent'.
# Dependiendo de lo que diga el agente (si llama a herramienta o no), vamos a 'tools' o a 'final'.
workflow.add_conditional_edges(
    "agent",
    router,
    {
        "tools": "tools",  # Si el router devuelve "tools", ve al nodo 'tools'.
        "__end__": "final" # Si el router devuelve "__end__", ve al nodo 'final'.
    }
)

# Arista fija: después de ejecutar herramientas, volvemos al agente para que procese el resultado.
workflow.add_edge("tools", "agent")

# Arista fija del nodo final al fin del grafo.
workflow.add_edge("final", END)

# Compilar la aplicación
app = workflow.compile()

# --- 6. EJECUCIÓN DE PRUEBA ---
if __name__ == "__main__":
    # Ejemplo 1: Búsqueda
    print("--- Consulta de Búsqueda ---")
    inputs1 = {"messages": [HumanMessage(content="¿Cuál es la noticia más reciente sobre inteligencia artificial?")], "query": ""}
    for event in app.stream(inputs1, stream_mode="values"):
        event['messages'][-1].pretty_print()
    
    # Ejemplo 2: Cálculo
    print("\n--- Consulta de Cálculo ---")
    inputs2 = {"messages": [HumanMessage(content="Calcula la raíz cuadrada de 144 multiplicada por 5.")], "query": ""}
    for event in app.stream(inputs2, stream_mode="values"):
        event['messages'][-1].pretty_print()
    
    # Ejemplo 3: Resumen
    print("\n--- Consulta de Resumen ---")
    long_text = "LangGraph es un marco de trabajo para construir aplicaciones de IA con estado y flujos de trabajo complejos. Permite crear ciclos, bifurcaciones y nodos condicionales, facilitando la construcción de agentes robustos que pueden usar herramientas y mantener memoria a lo largo de una sesión."
    inputs3 = {"messages": [HumanMessage(content=f"Resume este texto: {long_text}")], "query": ""}
    for event in app.stream(inputs3, stream_mode="values"):
        event['messages'][-1].pretty_print()

Este código define un agente completo con un ciclo agente-herramienta-agente. El nodo agent (call_model) es donde el LLM, armado con las definiciones de las herramientas, decide si necesita llamar a una y cuál. La función router luego interpreta la salida del LLM: si hay una llamada a herramienta, envía el flujo al nodo tools para su ejecución; si no la hay, envía el flujo al nodo final para dar una respuesta. La arista fija de tools de vuelta a agent crea el ciclo esencial para el procesamiento iterativo.

Tip Crítico: La función router debe ser determinista y rápida. Evita usar llamadas a LLM dentro del router a menos que sea estrictamente necesario, ya que esto añade latencia y costo. Para decisiones complejas, a menudo es mejor que el LLM principal en el nodo 'agent' tome la decisión (mediante tool_calls) y que el router simplemente interprete esa elección ya tomada, como se hace en el ejemplo.

Extensión: Router Inteligente con Clasificación

Para un control más fino, podríamos tener un router que decida entre múltiples nodos de herramienta específicos. Esto es útil si cada herramienta requiere un pre-procesamiento diferente.

def advanced_router(state: AgentState) -> str:
    """Un router que decide la herramienta específica o finaliza."""
    last_msg = state['messages'][-1].content.lower()
    
    # Lógica heurística simple (en la práctica, usarías un LLM clasificador pequeño)
    if "buscar" in last_msg or "noticia" in last_msg or "actual" in last_msg:
        return "use_search"
    elif "calcula" in last_msg or "raíz" in last_msg or "multiplica" in last_msg or "+" in last_msg:
        return "use_calculator"
    elif "resume" in last_msg or "resumen" in last_msg:
        return "use_summarizer"
    elif "gracias" in last_msg or "adiós" in last_msg:
        return "__end__"
    else:
        # Por defecto, dejar que el agente principal lo maneje
        return "agent_decision"

# En el grafo, tendrías que añadir nodos separados para cada herramienta
# y mapear las claves del router ("use_search", etc.) a esos nodos específicos.

Errores Comunes y Cómo Evitarlos

1. Router Devuelve un Valor No Mapeado: El error más común es que la función router devuelva un string (ej: "buscar") que no coincide exactamente con ninguna de las claves en el diccionario de mapeo proporcionado a add_conditional_edges. LangGraph lanzará una excepción. Solución: Usa constantes o un Enum para los nombres de destino y asegúrate de que el router y el mapeo usen los mismos valores. Verifica con un print lo que devuelve tu router durante el desarrollo.

2. Estado No Sincronizado Entre Nodos: Cada nodo debe recibir y devolver el estado de forma compatible. Si un nodo espera un campo que otro no produce, tendrás valores None o faltantes. Solución: Define claramente el esquema TypedDict inicial. Asegúrate de que cada nodo devuelva un diccionario que actualice solo los campos necesarios, usando Annotated con operator.add para listas que acumulan.

3. Ciclos Infinitos por Mala Condición de Salida: Un agente que siempre decide llamar a una herramienta creará un bucle infinito. Solución: Implementa un mecanismo de límite de pasos (max_steps). Puedes añadir un campo step_count al estado, incrementarlo en cada ciclo, y hacer que el router devuelva "__end__" cuando se supere un umbral. O usa la configuración interrupt_before/interrupt_after para supervisión externa.

4. Lógica de Router Demasiado Compleja o Costosa: Implementar un LLM completo dentro de la función router para cada decisión es ineficiente. Solución: Sigue el patrón del ejemplo principal: deja que el nodo agente principal (con el LLM) tome la decisión de alto nivel mediante tool_calls, y usa el router solo como un intérprete de esa decisión. Para clasificación simple, usa heurísticas de texto o un modelo de clasificación pequeño y rápido.

5. Olvidar el Punto de Entrada o el Fin del Grafo: Un grafo sin set_entry_point() no sabrá por dónde empezar. Sin una ruta hacia END, el grafo puede terminar en un nodo sin salida. Solución: Siempre define un entry point explícito y asegúrate de que todas las ramas posibles eventualmente lleven a END, ya sea directamente o a través de un nodo que tenga una arista hacia END.

Checklist de Dominio

Antes de considerar esta lección completa, verifica que puedes realizar o explicar cada uno de estos puntos:

  • Puedo definir un esquema de estado TypedDict que incluya campos para mensajes, consulta y resultados intermedios.
  • Sé crear y añadir nodos funcionales a un StateGraph que leen y actualizan correctamente el estado compartido.
  • Comprendo la diferencia entre una arista fija (add_edge) y una arista condicional (add_conditional_edges) y cuándo usar cada una.
  • Puedo escribir una función router que, basándose en el estado, devuelva un string que dirija el flujo a diferentes nodos, incluyendo "__end__".
  • Soy capaz de diseñar un ciclo básico agente-herramienta-agente donde el agente procesa iterativamente los resultados de las herramientas.
  • Puedo explicar el riesgo de los ciclos infinitos y describir al menos una estrategia para mitigarlo (ej: contador de pasos).
  • He implementado un grafo con al menos dos herramientas diferentes donde la elección entre ellas es dinámica, no estática.
  • Puedo depurar un flujo de LangGraph identificando en qué nodo falla y verificando el contenido del estado en ese punto.
De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión