ReAct en LangGraph: De un bucle creado manualmente a ToolNode y StateGraph
Crea un agente ReAct dos veces: primero como un bucle asíncrono manual, y luego con ToolNode de LangGraph, tools_condition y StateGraph que gestionan las llamadas a herramientas en cadena por ti.
Fuentes: Agentic AI with LangChain — Part 4: ReAct Pattern in LangGraph
Un bucle básico de llamada a herramientas realiza una sola pasada: el modelo solicita las herramientas, llegan los resultados y se emite una respuesta. Las tareas que requieren llamadas en cadena, donde la segunda llamada depende de lo devuelto por la primera, necesitan un agente capaz de pensar, actuar, analizar el resultado y volver a actuar. Ese ciclo es el patrón ReAct. A continuación se presenta la idea, un bucle ReAct escrito a mano y el mismo agente reconstruido en LangGraph con el nodo de herramienta, mensajes de herramienta, un borde condicional y un grafo de estado.
Los componentes que proporciona LangGraph para ello:
- Nodo de herramienta: analiza las llamadas a herramientas, ejecuta dichas herramientas y devuelve los resultados.
- Mensaje de herramienta: transmite la salida de una herramienta de vuelta al LLM.
- tools_condition: una función de enrutamiento que verifica las llamadas a herramientas y activa el borde condicional.
- StateGraph: ejecuta el flujo de trabajo.
Configuración
Guarde los secretos en un archivo .env en la raíz del proyecto:
OPENAI_API_KEY="<Your OpenAI API Key>"
TAVILY_API_KEY=<Your TAVILY API Key>
Cargúelos mediante una clase BaseSettings de Pydantic:
class BaseConfig(BaseSettings):
OPENAI_API_KEY: Optional[str]
PINECONE_API_KEY: Optional[str]
TAVILY_API_KEY: Optional[str]
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
La estructura del proyecto:
project/
├── react_loop/
│ └── weather_assistant.py # implement a vanilla react loop
│
├── react_langgraph # implement ReAct by LangGraph workflow
│ ├── nodes.py # graph nodes. Defined as Python functions
│ ├── graph.py # graph workflow
│ ├── consts.py # define node names as constants
│ └── state.py # typed dictionary to store graph states
|
├── tools
│ ├── tool_call.py # implement simple tool-calling loop
| └── weather.py # get_weather LangChain tool using open source APIs
|
|── # implement tool-calling loop using LangChain tool
├── config.py # pydantic BaseConfig
├── utils.py # utility method to print messages
├── .env # environment variable definition
├── .gitignore
└── requirements.txt # package requirements
Qué significa ReAct
ReAct es la abreviatura de Reasoning plus Acting. Un agente que realiza una tarea en varios pasos razona, actúa, observa y vuelve a razonar hasta completarla. El bucle básico solo permite una sola ejecución de herramientas, y eso es lo que ReAct soluciona.
Cada ejecución consta de seis pasos:
- El usuario envía un mensaje.
- El LLM lee la solicitud, determina qué información necesita y decide si debe usar herramientas y cuáles.
- El agente ejecuta una o más herramientas, como buscar en la web, consultar una API o enviar un correo electrónico.
- Los resultados regresan y se integran al estado de trabajo del agente.
- El LLM evalúa la nueva información y ya sea responde o solicita más ejecuciones de herramientas.
- Una vez que no se necesita ninguna acción adicional, se emite la respuesta final.
El bucle es poderoso porque el agente puede revisar su plan según lo que acaba de aprender, por lo que cada decisión puede depender de resultados anteriores.
Un bucle ReAct simple
Esta versión se encuentra en react_loop/weather_assistant.py y se asemeja al bucle de un solo paso, con dos diferencias:
async def chat(self, message: str):
# Add user message
self.messages.append(HumanMessage(content=message))
# loop to handle serial tool calls
while True:
# Get AI response with potential tool calls
response = await self.llm_with_tools.ainvoke(self.messages)
self.messages.append(response)
# Check if there is any tools to call
if not response.tool_calls:
# no tools to call
break
# Execute each tool call
# process tool calls asynchronously
results = await asyncio.gather(*[self.tools[tc["name"]].ainvoke(tc) for tc in response.tool_calls])
self.messages.extend(results)
Primero, las llamadas a herramientas se ejecutan de forma asíncrona con asyncio.gather, por lo que varias pueden realizarse en paralelo. Segundo, el paso de ejecución de la herramienta se encuentra dentro de un bucle while, repitiendo el proceso de razonamiento y acción hasta que la respuesta del modelo ya no contenga llamadas a herramientas.
El mismo agente en LangGraph
El bucle manual funciona, pero obliga a gestionar personalmente el ciclo de razonamiento y acción, la lista de mensajes, el diccionario de herramientas y la recopilación de resultados. LangGraph se encarga de cada uno de estos aspectos: ToolNode analiza las llamadas y ejecuta las herramientas, StateGraph almacena los mensajes en el estado del grafo, y el enlace tools_condition reemplaza al bucle while escrito a mano y su verificación de llamadas a herramientas.
Estado. Un diccionario tipado almacena la lista de mensajes (en react_langgraph/state.py):
from typing import TypedDict, Annotated
from langgraph.graph.message import AnyMessage, add_messages
class ReactState(TypedDict):
"""
Define the state schema for our hotel assistant.
Attributes:
messages: list of messages
"""
messages: Annotated[list[AnyMessage], add_messages]
Los nodos leen y actualizan mensajes a través de él, y el reductor add_messages agrega todo lo que devuelve un nodo.
Nodos. En react_langgraph/nodes.py la configuración crea una lista de herramientas de LangChain, un prompt del sistema que indica al modelo cómo utilizarlas, y un modelo de OpenAI vinculado a esa lista de herramientas:
tools = [TavilySearch(max_results=3, tavily_api_key=TAVILY_API_KEY), get_weather]
SYSTEM_PROMPT = f"""You are an assistant for question-answering tasks with access to multiple tools.
use these tools to retrieve context to answer the question. If you don't know the answer,
just say that you don't know. Use three sentences maximum and keep the answer concise.
Tool Use Instructions:
- Use tools thoughtfully—only when they add value to the response
- Chain tools when needed (e.g., search for info, then use that to search
for relevant hotels)
- Be conversational and helpful
"""
system_message = SystemMessage(content=SYSTEM_PROMPT)
llm = ChatOpenAI(api_key=api_key, model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)
async def run_agent_reasoning(state: ReactState) -> dict:
"""
run agent reasoning node
:param state: ReactState, manage graph state
:return: dict containing new message
"""
print("----AGENT REASONING------")
messages = [system_message] + state["messages"]
if isinstance(messages[-1], ToolMessage):
print_message(messages[-1])
response = await llm_with_tools.ainvoke(messages)
print_message(response)
return {"messages": [response]}
tool_node = ToolNode(tools)
Se obtienen dos nodos. El nodo de razonamiento crea la lista de mensajes, llama al LLM y devuelve su respuesta. El nodo de acción es un ToolNode que analiza las llamadas a herramientas, ejecuta dichas herramientas y devuelve los mensajes correspondientes.
Gráfico. En react_langgraph/graph.py, un StateGraph registra los nodos y los conecta mediante aristas:
workflow = StateGraph(ReactState)
workflow.add_node(AGENT_REASON, run_agent_reasoning)
workflow.add_node(ACT, tool_node)
workflow.add_edge(START, AGENT_REASON)
workflow.add_edge(ACT, AGENT_REASON)
workflow.add_conditional_edges(
AGENT_REASON,
tools_condition,
{"tools": ACT, END: END}
)tion is invoked first
La ejecución sigue cuatro reglas. El grafo comienza en el nodo de razonamiento. Si la respuesta del LLM contiene llamadas a herramientas, tools_condition redirige al nodo de acción. Después de que las herramientas se ejecutan, el control siempre vuelve al razonamiento. Si la respuesta no contiene llamadas a herramientas, el grafo finaliza y se devuelve la respuesta final. Esto refleja el ciclo ReAct sin un bucle manual ni mensajes gestionados a mano.
Compilar y ejecutar. La compilación genera el agente, y se puede exportar un diagrama del grafo como PNG:
react_agent = workflow.compile()
react_agent.get_graph().draw_mermaid_png(output_file_path="graphs.png")
question = """
tell me the first hotel you find in Boston if
its temperature is lower than 30 degree."""
async def main():
print("hello ReAct agent by Langraph")
message = HumanMessage(content=question)
res = await react_agent.ainvoke({"messages": [message]})
if __name__ == "__main__":
asyncio.run(main())
Úsalo con un mensaje del usuario y la lista de mensajes resultante mostrará los turnos entre el nodo de razonamiento y el nodo de herramienta, lo que constituye un flujo multi-paso funcional.
Donde deja esto las cosas
Tres características de LangGraph realizan esta tarea: ToolNode para encontrar y ejecutar herramientas, tools_condition para el control de bucles y add_messages para el manejo automático de mensajes. Una vez comprendido y desarrollado ReAct, el siguiente paso es volver a un pipeline RAG de LangGraph y hacerlo completamente agente.