Home / Articles

ReAct in LangGraph: From a Hand-Built Loop to ToolNode and StateGraph

Build a ReAct agent twice: first as a manual async loop, then with LangGraph's ToolNode, tools_condition and StateGraph handling chained tool calls for you.

Publisher: REACTAPP.TOP6 min read1391 words

This article is published in English.

Sources: Agentic AI with LangChain — Part 4: ReAct Pattern in LangGraph

A basic tool-calling loop makes one pass: the model asks for tools, the results come back, an answer goes out. Tasks that need chained calls, where the second call depends on what the first returned, need an agent that can think, act, look at the outcome and act again. That cycle is the ReAct pattern. Below you get the idea, a hand-written ReAct loop and the same agent rebuilt on LangGraph with the tool node, tool messages, a conditional edge and a state graph.

The pieces LangGraph supplies for it:

  • Tool node: parses tool calls, runs the tools and returns the results.
  • Tool message: carries a tool's output back to the LLM.
  • tools_condition: a routing function that checks for tool calls and drives the conditional edge.
  • StateGraph: runs the workflow.
  • State: a typed dictionary holding the agent's changing data.
  • Nodes: Python functions that update state, a tool node being one.
  • Edges: transitions between nodes, where conditional edges allow branches and loops.

Setup

Keep secrets in a .env file at the project root:

OPENAI_API_KEY="<Your OpenAI API Key>"
TAVILY_API_KEY=<Your TAVILY API Key>

Load them through a Pydantic BaseSettings class:

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")

The project layout:

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

What ReAct means

ReAct stands for Reasoning plus Acting. An agent working on a multi-step task reasons, acts, observes and reasons again until it's finished. The basic loop allows only one round of tool execution, which is the gap ReAct fills.

One run goes through six steps:

  1. The user sends a message.
  2. The LLM reads the request, works out what information it needs and decides whether, and which, tools to call.
  3. The agent runs one or more tools, such as searching the web, querying an API or sending an email.
  4. The results come back and join the agent's working state.
  5. The LLM assesses the new information and either answers or asks for more tool calls.
  6. Once no further action is needed, the final answer goes out.

The loop is powerful because the agent can revise its plan according to what it just learned, so each decision can depend on earlier results.

A plain ReAct loop

This version lives in react_loop/weather_assistant.py and resembles the single-pass loop, with two differences:

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)

First, tool calls run asynchronously with asyncio.gather, so several can execute in parallel. Second, the tool-execution step sits inside a while loop, repeating reason-then-act until the model's response no longer contains tool calls.

The same agent in LangGraph

The manual loop works but forces you to manage the reason-act cycle, the message list, the tool dictionary and result collection yourself. LangGraph takes over each: ToolNode parses calls and executes tools, StateGraph keeps messages in graph state, and the tools_condition edge replaces the hand-written while loop and its tool-call check.

State. A typed dictionary holds the message list (in 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]

Nodes read and update messages through it, and the add_messages reducer appends whatever a node returns.

Nodes. In react_langgraph/nodes.py the setup creates a list of LangChain tools, a system prompt that tells the model how to use them, and an OpenAI model bound to that tool list:

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)

Two nodes result. The reasoning node builds the message list, calls the LLM and returns its reply. The act node is a ToolNode that parses tool calls, runs the tools and returns tool messages.

Graph. In react_langgraph/graph.py, a StateGraph registers the nodes and connects them with edges:

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

Execution follows four rules. The graph begins at the reasoning node. If the LLM's reply contains tool calls, tools_condition routes to the act node. After the tools run, control always returns to reasoning. If the reply has no tool calls, the graph ends and returns the final answer. That mirrors the ReAct cycle without a manual loop or hand-managed messages.

Compile and run. Compiling produces the agent, and a diagram of the graph can be exported as a 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())

Invoke it with a user message and the resulting message list shows the reasoning node and the tool node trading turns, which is a working multi-step flow.

Where this leaves things

Three LangGraph features do the work: ToolNode for finding and running tools, tools_condition for loop control and add_messages for automatic message handling. With ReAct understood and built, the next move is to return to a LangGraph RAG pipeline and make it fully agentic.