Construyendo un agente LangGraph desde cero

El término “agente de IA” es uno de los más populares en este momento. Surgieron después del revuelo de los LLM, cuando la gente se dio cuenta de que las últimas capacidades de los LLM son impresionantes, pero que sólo pueden realizar tareas para las que han sido capacitados explícitamente. En ese sentido, los LLM normales no cuentan con herramientas que les permitan hacer algo fuera de su alcance de conocimiento.

TRAPO

Para abordar esto, más tarde se introdujo la generación aumentada de recuperación (RAG) para recuperar contexto adicional de fuentes de datos externas e inyectarlo en el mensaje, de modo que el LLM tenga en cuenta más contexto. Podemos decir aproximadamente que RAG hizo que el LLM tuviera más conocimientos, pero para problemas más complejos, el enfoque LLM + RAG aún fallaba cuando la ruta de solución no se conocía de antemano.

oleoducto RAG

Agentes

Los agentes son un concepto notable construido en torno a los LLM que introducen el estado, la toma de decisiones y la memoria. Se puede considerar a los agentes como un conjunto de herramientas predefinidas para analizar resultados y almacenarlos en la memoria para su uso posterior antes de producir la respuesta final.

LangGraph

LangGraph es un marco popular utilizado para crear agentes. Como sugiere el nombre, los agentes se construyen utilizando gráficos con nodos y aristas.

Los nodos representan el estado del agente, que evoluciona con el tiempo. Los bordes definen el flujo de control especificando reglas y condiciones de transición entre nodos.

Para comprender mejor LangGraph en la práctica, veremos un ejemplo detallado. Si bien LangGraph puede parecer demasiado detallado para el problema siguiente, generalmente tiene un impacto mucho mayor en problemas complejos con gráficos grandes.

Primero, necesitamos instalar las bibliotecas necesarias.

langgraph==1.0.5 langchain-community==0.4.1 jupyter==1.1.1 notebook==7.5.1 langchain[openai]

Luego importamos los módulos necesarios.

importar sistema operativo desde dotenv importar load_dotenv importar json importar aleatorio desde pydantic importar BaseModel desde escribir importar Opcional, Lista, Dict, Cualquiera desde langgraph.graph importar StateGraph, START, END desde langchain_openai importar ChatOpenAI desde langchain_core.messages importar HumanMessage desde langchain.chat_models importar init_chat_model desde langchain.tools importar herramienta desde IPython.display importar imagen, mostrar

También necesitaríamos crear un archivo .env y agregar OPENAI_API_KEY allí:

OPENAI_API_KEY=…

Luego, con load_dotenv(), podemos cargar las variables de entorno en el sistema.

cargar_dotenv()

Funcionalidades adicionales

La siguiente función nos será útil para mostrar visualmente gráficos construidos.

def display_graph(graph): devolver display(Imagen(graph.get_graph().draw_mermaid_png()))

Agente

Inicialicemos un agente basado en GPT-5-nano usando un comando simple:

llm = init_chat_model("openai:gpt-5-nano")

Estado

En nuestro ejemplo, construiremos un agente capaz de responder preguntas sobre fútbol. Su proceso de pensamiento se basará en las estadísticas recuperadas sobre los jugadores.

Para hacer eso, necesitamos definir un estado. En nuestro caso, será una entidad que contiene toda la información que un LLM necesita sobre un jugador. Para definir un estado, necesitamos escribir una clase que herede de pydantic.BaseModel:

clase PlayerState(BaseModel): pregunta: str herramientas_seleccionadas: Opcional[Lista[str]] = Ninguno nombre: Opcional[str] = Ninguno club: Opcional[str] = Ninguno país: Opcional[str] = Ninguno número: Opcional[int] = Ninguno calificación: Opcional[int] = Ninguno goles: Opcional[List[int]] = Ninguno minutos_jugados: Opcional[List[int]] = Ninguno resumen: Opcional[str] = Ninguno

Al moverse entre nodos LangGraph, cada nodo toma como entrada una instancia de PlayerState que especifica cómo procesar el estado. Nuestra tarea será definir cómo se procesa exactamente ese estado.

Herramientas

Primero, definiremos algunas de las herramientas que puede utilizar un agente. Una herramienta puede considerarse aproximadamente como una función adicional a la que un agente puede llamar para recuperar la información necesaria para responder la pregunta de un usuario.

Para definir una herramienta, necesitamos escribir una función con un decorador @tool. Es importante utilizar nombres de parámetros y cadenas de documentación de funciones claros, ya que el agente los considerará al decidir si llama a la herramienta en función del contexto de entrada.

Para simplificar nuestros ejemplos, utilizaremos datos simulados en lugar de datos reales recuperados de fuentes externas, como suele ser el caso de las aplicaciones de producción.

En la primera herramienta, devolveremos información sobre el club y el país de un jugador por nombre.

@tool def fetch_player_information_tool(name: str): """Contiene información sobre el club de fútbol de un jugador y su país""" data = { 'Haaland': { 'club': 'Manchester City', 'country': 'Noruega' }, 'Kane': { 'club': 'Bayern', 'country': 'Inglaterra' }, 'Lautaro': { 'club': 'Inter', 'país': 'Argentina' }, 'Ronaldo': { 'club': 'Al-Nassr', 'país': 'Portugal' } } if nombre en los datos: print(f"Información del jugador que regresa: {datos[nombre]}") devuelve datos[nombre] else: return { 'club': 'desconocido', 'país': 'desconocido' } def fetch_player_information(estado: PlayerState): return fetch_player_information_tool.invoke({'nombre': estado.nombre})

Quizás se pregunte por qué colocamos una herramienta dentro de otra función, lo que parece un exceso de ingeniería. De hecho, estas dos funciones tienen responsabilidades diferentes.

La función fetch_player_information() toma un estado como parámetro y es compatible con el marco LangGraph. Extrae el campo de nombre y llama a una herramienta que opera en el nivel de parámetro.

Proporciona una clara separación de preocupaciones y permite una fácil reutilización de la misma herramienta en múltiples nodos de gráficos.

Luego tenemos una función análoga que recupera el número de camiseta de un jugador:

@tool def fetch_player_jersey_number_tool(name: str): "Devuelve el número de camiseta del jugador" data = { 'Haaland': 9, 'Kane': 9, 'Lautaro': 10, 'Ronaldo': 7 } if nombre en los datos: print(f"Número de jugador que regresa: {data[name]}") return {'number': data[name]} else: return {'number': 0} def fetch_player_jersey_number(estado: PlayerState): devuelve fetch_player_jersey_tool.invoke({'nombre': estado.nombre})

Para la tercera herramienta, obtendremos la clasificación FIFA del jugador:

@tool def fetch_player_rating_tool(name: str): "Devuelve la calificación del jugador en FIFA" data = { 'Haaland': 92, 'Kane': 89, 'Lautaro': 88, 'Ronaldo': 90 } if nombre en los datos: print(f"Devuelve datos de calificación: {data[name]}") return {'rating': data[name]} else: return {'rating': 0} def fetch_player_rating(estado: PlayerState): return fetch_player_rating_tool.invoke({'nombre': estado.nombre})

Ahora, escribamos varias funciones de nodos de gráficos más que recuperarán datos externos. No los etiquetaremos como herramientas como antes, lo que significa que no serán algo que el agente decida llamar o no.

def retrieve_goals(estado: PlayerState): nombre = estado.nombre datos = { 'Haaland': [25, 40, 28, 33, 36], 'Kane': [33, 37, 41, 38, 29], 'Lautaro': [19, 25, 27, 24, 25], 'Ronaldo': [27, 32, 28, 30, 36] } si el nombre está en los datos: devolver {'objetivos': datos[nombre]} más: devolver {'objetivos':[0]}

Aquí hay un nodo gráfico que recupera la cantidad de minutos jugados durante las últimas temporadas.

def recuperar_minutos_jugados(estado: PlayerState): nombre = estado.nombre datos = { 'Haaland': [2108, 3102, 3156, 2617, 2758], 'Kane': [2924, 2850, 3133, 2784, 2680], 'Lautaro': [2445, 2498, 2519, 2773], 'Ronaldo': [3001, 2560, 2804, 2487, 2771] } si el nombre está en los datos: devolver {'minutos_jugados': datos[nombre]} más: devolver {'minutos_jugados':[0]}

A continuación se muestra un nodo que extrae el nombre de un jugador de una pregunta de usuario.

def extract_name(state: PlayerState): question = state.question Prompt = f""" Eres un asistente de extracción de nombres de fútbol. Tu objetivo es simplemente extraer el apellido de un futbolista en la siguiente pregunta. Pregunta del usuario: {pregunta} Solo tienes que generar una cadena que contenga una palabra: apellido del futbolista. """ respuesta = llm.invoke([HumanMessage(content=prompt)]).content print(f"Nombre del jugador: ", respuesta) return {'nombre': respuesta}

Ahora es el momento en que las cosas se ponen interesantes. ¿Recuerdas las tres herramientas que definimos anteriormente? Gracias a ellos, ahora podemos crear un planificador que le pedirá al agente que elija una herramienta específica a la que llamar en función del contexto de la situación:

def planificador(estado: PlayerState): pregunta = estado.Pregunta rápida = f""" Eres un asistente de resumen de jugadores de fútbol. Tienes las siguientes herramientas disponibles: ['fetch_player_jersey_number', 'fetch_player_information', 'fetch_player_rating'] Pregunta de usuario: {question} Decide qué herramientas son necesarias para responder. Devuelve una lista JSON de nombres de herramientas, por ejemplo, ['fetch_player_jersey_number', 'fetch_rating'] """ respuesta = llm.invoke([HumanMessage(content=prompt)]).content prueba: selected_tools = json.loads(respuesta) excepto: selected_tools =[]devolver {'herramientas_seleccionadas': herramientas_seleccionadas}

En nuestro caso, le pediremos al agente que cree un resumen de un futbolista. Decidirá por sí solo a qué herramienta llamar para recuperar datos adicionales. Las cadenas de documentación en herramientas desempeñan un papel importante: proporcionan al agente contexto adicional sobre las herramientas.

A continuación se muestra nuestro nodo de gráfico final, que tomará múltiples campos recuperados de pasos anteriores y llamará al LLM para generar un resumen final.

def write_summary(state: PlayerState): question = state.question data = { 'name': state.name, 'country': state.country, 'number': state.number, 'rating': state.rating, 'goals': state.goals, 'minutos_jugados': estado.minutos_jugados, } aviso = f""" Eres un asistente de reportero de fútbol. Teniendo en cuenta los siguientes datos y estadísticas del jugador de fútbol, tendrás que crear un resumen de rebajas de ese jugador Datos del jugador: {json.dumps(data, indent=4)} El resumen de rebajas debe incluir la siguiente información: – Nombre completo del jugador (si solo se proporciona el nombre o apellido, intente adivinar el nombre completo) – País del jugador (también agregue emoji de bandera) – Número de jugador (también agregue el número en el formulario emoji(-s)) – Clasificación FIFA – Número total de goles en las últimas 3 temporadas – Número promedio de minutos necesarios para marcar un gol – Respuesta a la pregunta del usuario: {pregunta} """ respuesta = llm.invoke([HumanMessage(content=prompt)]).content return {"summary": respuesta}

Construcción de gráficos

Ahora tenemos todos los elementos para construir un gráfico. En primer lugar, inicializamos el gráfico utilizando el constructor StateGraph. Luego, agregamos nodos a ese gráfico uno por uno usando el método add_node(). Se necesitan dos parámetros: una cadena utilizada para asignar un nombre al nodo y una función invocable asociada con el nodo que toma un estado de gráfico como único parámetro.

Graph_builder = StateGraph(PlayerState) Graph_builder.add_node('extract_name', extract_name) Graph_builder.add_node('planner', planificador) Graph_builder.add_node('fetch_player_jersey_number', fetch_player_jersey_number) Graph_builder.add_node('fetch_player_information', fetch_player_information) Graph_builder.add_node('fetch_player_rating', fetch_player_rating) Graph_builder.add_node('retrieve_goals', retrieve_goals) Graph_builder.add_node('retrieve_minutos_jugados', recuperar_minutos_jugados) Graph_builder.add_node('write_summary', escribir_resumen)

En este momento, nuestro gráfico consta solo de nodos. Necesitamos agregarle bordes. Los bordes en LangGraph se orientan y agregan mediante el método add_edge(), especificando los nombres de los nodos inicial y final.

Lo único que debemos tener en cuenta es el planificador, que se comporta de forma ligeramente diferente al resto de nodos. Como se muestra arriba, puede devolver el campo selected_tools, que contiene de 0 a 3 nodos de salida.

Para eso, necesitamos usar el método add_conditional_edges() tomando tres parámetros:

El nombre del nodo planificador; Se debe llamar a una función invocable que toma un nodo LangGraph y devuelve una lista de cadenas que indican la lista de nombres de nodos; Un diccionario que asigna cadenas desde el segundo parámetro a los nombres de los nodos.

En nuestro caso, definiremos el nodo route_tools() para que simplemente devuelva el campo state.selected_tools como resultado de una función de planificación.

def route_tools(estado: PlayerState): devuelve estado.selected_tools o[]

Entonces podemos construir nodos:

Graph_builder.add_edge(START, 'extract_name') Graph_builder.add_edge('extract_name', 'planner') Graph_builder.add_conditional_edges( 'planner', route_tools, { 'fetch_player_jersey_number': 'fetch_player_jersey_number', 'fetch_player_information': 'fetch_player_information', 'fetch_player_rating': 'fetch_player_rating' } ) Graph_builder.add_edge('fetch_player_jersey_number', 'retrieve_goals') Graph_builder.add_edge('fetch_player_information', 'retrieve_goals') Graph_builder.add_edge('fetch_player_rating', 'retrieve_goals') Graph_builder.add_edge('retrieve_goals', 'retrieve_minutos_jugados') Graph_builder.add_edge('retrieve_minutos_jugados', 'write_summary') Graph_builder.add_edge('write_summary', FIN)

INICIO y FIN son constantes de LangGraph que se utilizan para definir los puntos inicial y final del gráfico.

El último paso es compilar el gráfico. Opcionalmente podemos visualizarlo usando la función auxiliar definida anteriormente.

gráfico = graph_builder.compile() display_graph(gráfico)

Obteniendo la primera experiencia con LangGraph
Diagrama gráfico

Ejemplo

¡Ahora finalmente podemos usar nuestro gráfico! Para hacerlo, podemos usar el método invoke y pasar un diccionario que contenga el campo de pregunta con una pregunta de usuario personalizada:

result = graph.invoke({ 'question': '¿Podrá Haaland ganar la Copa Mundial de la FIFA para Noruega en 2026 según su desempeño y estadísticas recientes?' })

¡Y aquí hay un resultado de ejemplo que podemos obtener!

{'question': '¿Podrá Haaland ganar la Copa Mundial de la FIFA para Noruega en 2026 según su desempeño y estadísticas recientes?', 'selected_tools': ['fetch_player_information', 'fetch_player_rating'], 'name': 'Haaland', 'club': 'Manchester City', 'country': 'Noruega', 'rating': 92, 'goals': [25, 40, 28, 33, 36], 'minutos_jugados': [2108, 3102, 3156, 2617, 2758], 'summary': '- Nombre completo: Erling Haalandn- País: Noruega 🇳🇴n- Número: N/A – Clasificación FIFA: 92n- Total de goles en las últimas 3 temporadas: 97 (28 + 33 + 36)n- Promedio de minutos por gol (últimas 3 temporadas): 87,95 minutos por goln- ¿Ganará Haaland la Copa Mundial de la FIFA para Noruega en 2026 según el rendimiento y las estadísticas recientes?n – Respuesta corta: No garantizado. Haaland sigue estando entre los mejores delanteros del mundo (92 de valoración, producción goleadora de élite) y podría ser un factor clave para Noruega. Sin embargo, el éxito en la Copa Mundial es un logro del equipo que depende de la calidad general del equipo, la profundidad, las tácticas, las lesiones y el contexto del torneo de Noruega. Basándonos únicamente en las estadísticas, refuerza las posibilidades de Noruega, pero no se puede predecir con certeza un título de la Copa del Mundo en 2026.'}

Lo interesante es que podemos observar todo el estado del gráfico y analizar las herramientas que el agente ha elegido para generar la respuesta final. ¡El resumen final tiene una pinta estupenda!

Conclusión

En este artículo, hemos examinado los agentes de IA que han abierto un nuevo capítulo para los LLM. Equipados con herramientas y toma de decisiones de última generación, ahora tenemos un potencial mucho mayor para resolver tareas complejas.

Un ejemplo que vimos en este artículo nos presentó LangGraph, uno de los marcos más populares para crear agentes. Su sencillez y elegancia permiten construir cadenas de decisión complejas. Si bien, para nuestro ejemplo simple, LangGraph puede parecer excesivo, resulta extremadamente útil para proyectos más grandes donde las estructuras de estado y gráficos son mucho más complejas.

Recursos

Todas las imágenes, a menos que se indique lo contrario, son del autor.