Skip to content

Tools & the ReAct Agent

1. Why this matters

Tools (web search, calculators, databases, APIs) are how LLMs reach the outside world. An agent runs them in a loop — call tool, see result, decide next step — until the LLM produces a final answer.

LangGraph's create_react_agent gives you that loop in one line, with all the state/checkpointing/streaming benefits of LangGraph baked in.

2. Mental model

A ReAct agent is a 2-node cycle:

flowchart TD
    START([START]) --> A[Agent Node<br/>LLM with tools bound]
    A -->|tool_calls present| T[ToolNode<br/>executes each tool_call]
    T --> A
    A -->|no tool_calls| END([END])

Conditional edge after the agent node: if the LLM emitted tool calls, route to tools; otherwise end.

3. Architecture / Flow

What lives in state:

flowchart LR
    M[messages list<br/>HumanMessage,<br/>AIMessage with tool_calls,<br/>ToolMessage,<br/>AIMessage final] -->|fed every loop iter| LLM
    LLM -->|tool_calls or content| M

State is just messages with the add_messages reducer — every loop iteration appends to it. Each iteration the LLM sees the full trace and decides what's next.

4. Core concepts

  • create_react_agent(model, tools, prompt=..., checkpointer=..., interrupt_before=...) — factory.
  • tools — list of LangChain tools (@tool decorators, StructuredTools, pre-built tools like TavilySearchResults).
  • prompt — system message (string or callable). Tells the agent its role and policies.
  • checkpointer — same idea as elsewhere; gives the agent memory.
  • interrupt_before=["tools"] — pause before any tool runs (great for sensitive ops).
  • recursion_limit — pass in invoke config; caps loop iterations.
  • ToolNode — the standalone node that executes tools. Used directly when building custom agent graphs.
  • MessagesState — pre-built state schema {messages: Annotated[list, add_messages]} — the foundation of every agent.

5. Code — minimal working example

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers."""
    return a * b

agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o-mini", temperature=0),
    tools=[add, multiply],
)

result = agent.invoke({"messages": [("user", "What is (3 + 4) * 5?")]})
print(result["messages"][-1].content)   # "(3 + 4) * 5 = 35"

6. Code — real-world pattern

Production agent: persistent memory + web search + weather tool:

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.tools import tool
import sqlite3, requests

@tool
def get_weather(city: str) -> str:
    """Get current weather for a city. Use ONLY for weather questions."""
    r = requests.get(f"https://wttr.in/{city}?format=3", timeout=5)
    return r.text

search = TavilySearchResults(max_results=4)

cp = SqliteSaver(sqlite3.connect("agent.db", check_same_thread=False))

agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o-mini", temperature=0),
    tools=[search, get_weather],
    prompt=(
        "You are a research assistant. Use tools when you need fresh data. "
        "Cite sources. If unsure, say so."
    ),
    checkpointer=cp,
)

cfg = {"configurable": {"thread_id": "user-42"},
       "recursion_limit": 10}

# Turn 1
print(agent.invoke(
    {"messages": [("user", "What's the weather in Paris and the latest news?")]},
    config=cfg,
)["messages"][-1].content)

# Turn 2 — same thread, remembers context
print(agent.invoke(
    {"messages": [("user", "And in Tokyo?")]},
    config=cfg,
)["messages"][-1].content)

Stream the agent's intermediate steps (great for debugging or chat UIs):

for event in agent.stream(
    {"messages": [("user", "Multiply 17 by 23, then add 100.")]},
    config=cfg,
    stream_mode="updates",
):
    for node, update in event.items():
        for m in update["messages"]:
            print(f"[{node}] {m.type}: {m.content[:100]}")

HITL agent — pause before any tool runs:

agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o-mini"),
    tools=[delete_user, send_email],
    checkpointer=cp,
    interrupt_before=["tools"],         # pause before EVERY tool execution
)

# After invoke pauses, inspect:
snap = agent.get_state(cfg)
last = snap.values["messages"][-1]
print("About to call:", last.tool_calls)

# Approve and resume
agent.invoke(None, config=cfg)

Build a custom agent from ToolNode (when create_react_agent is too rigid):

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode

llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([add, multiply])

def agent_node(state: MessagesState):
    return {"messages": [llm.invoke(state["messages"])]}

def should_continue(state: MessagesState):
    return "tools" if state["messages"][-1].tool_calls else END

b = StateGraph(MessagesState)
b.add_node("agent", agent_node)
b.add_node("tools", ToolNode([add, multiply]))
b.add_edge(START, "agent")
b.add_conditional_edges("agent", should_continue)
b.add_edge("tools", "agent")        # loop back

custom_agent = b.compile(checkpointer=MemorySaver())

That's exactly what create_react_agent builds internally.

7. Common pitfalls

  • Confusing langchain.agents.create_react_agent with langgraph.prebuilt.create_react_agent. The former is the legacy version paired with AgentExecutor (deprecated). Always import from langgraph.prebuilt for new code.
  • No recursion_limit — buggy agents run forever. Always cap iterations.
  • Bad tool descriptions. The LLM picks tools from descriptions. "Searches" beats "Search tool", but "Searches the web for current news; do NOT use for math" is best.
  • Returning massive blobs from tools. Each tool result lands in messages and is fed back to the LLM next turn. Truncate/summarize.
  • 15+ tools. Accuracy drops. Use a router/supervisor pattern (Chapter 12) instead.
  • No interrupt before destructive tools. Send-email / delete-row tools need a human gate.
  • No tracing. Agents go off the rails subtly. LangSmith is basically mandatory.

8. When to use vs not use

Use an agent when Use a chain when
Steps to solve aren't known in advance Pipeline is fixed
LLM must choose if/which tool You always call tools X then Y
Tasks span multiple domains Single domain
You want exploratory behavior Predictable cost/latency

Use create_react_agent unless you need to depart from the standard loop. Then build your own graph with ToolNode.

9. Cheatsheet

from langgraph.prebuilt import create_react_agent, ToolNode
from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

# Build agent
agent = create_react_agent(
    model=ChatOpenAI(model="gpt-4o-mini", temperature=0),
    tools=[tool_a, tool_b],
    prompt="System instructions string OR a callable returning messages",
    checkpointer=MemorySaver(),
    interrupt_before=["tools"],         # optional HITL pause
    debug=False,
)

# Run
agent.invoke(
    {"messages": [("user", "...")]},
    config={"configurable": {"thread_id": "t1"},
            "recursion_limit": 15},
)

# Stream intermediate steps
for event in agent.stream({"messages": [...]}, config=cfg, stream_mode="updates"):
    ...

# Inspect / replay
agent.get_state(cfg)
list(agent.get_state_history(cfg))
agent.update_state(cfg, {"messages": [...]})

10. Q&A — recall test

  • Q: What two nodes are inside a basic ReAct agent? A: Agent (LLM with tools bound) and Tools (ToolNode that executes the requested tool). They loop via a conditional edge.

  • Q: How does the agent decide when to stop? A: The LLM emits an AIMessage with no tool_calls. The conditional edge then routes to END.

  • Q: Difference between langgraph.prebuilt.create_react_agent and langchain.agents.create_react_agent? A: The LangGraph one is the modern, recommended version. The LangChain one is deprecated and tied to AgentExecutor.

  • Q: How do you protect destructive tools? A: interrupt_before=["tools"] — the graph pauses before any tool runs; you inspect the pending tool calls and approve/edit/reject.

  • Q: How do you give an agent memory across user sessions? A: Pass a checkpointer to create_react_agent and a thread_id in the invoke config.

  • Q: Practical cap on tool count before quality drops? A: Around 10–15. Beyond that, use a supervisor / hierarchical pattern (Chapter 12).

Practice

What does this print?

Expected: True

# ReAct loop in LangGraph: agent → tool → agent → tool → ... → answer
has_loop = True
print(has_loop)

Set a recursion_limit on the agent (currently it could loop forever)

Expected: True

config = {}                    # bug: no recursion_limit set
safe = "recursion_limit" in config
print(not safe)

Quiz — Quick check

What you remember

Q1. What does the prebuilt create_react_agent give you?

  • A ready-made graph with agent + tool-calling loop + state — just pass model + tools
  • A standalone model
  • A specific LLM
  • A vector store

Why: ReAct agent is a common pattern. The prebuilt saves you from wiring up the loop manually. Add custom nodes, edges, or state as needed.

Q2. What's the simplest way to add a tool to a LangGraph agent?

  • Decorate a function with @tool and pass the list to create_react_agent(model, tools=[my_tool])
  • Subclass BaseTool
  • Modify the model
  • Use a vector store

Why: @tool wraps any function (with type hints and docstring) into a callable Tool. The agent's LLM reads the docstring to know when to call it.

Q3. Where should recursion_limit be set?

  • In the config passed to .invoke(): config={"recursion_limit": 25}
  • In the tool definition
  • As a state field
  • Globally for all graphs

Why: Pass it via config. Default is 25; raise it if your agent legitimately needs more steps. Always set it for production to bound runaway costs.

Common doubts

When should I build my own ReAct loop instead of create_react_agent?

When you need custom behavior — specific routing logic, custom error handling, multiple specialized agents, or non-standard state. The prebuilt is great for ~80% of cases; reach for custom when you outgrow it.

How do I make the agent stop after answering, not call more tools?

The agent stops when the LLM responds without proposing more tool calls. To enforce: in your system prompt, instruct it when to stop ("Once you have the answer, respond directly without calling tools"). Or use a router that exits to END when sufficient information has been gathered.

Can I mix LangGraph agents and LangChain LCEL chains?

Yes — LCEL chains are Runnables; you can use them as nodes inside a LangGraph. Common pattern: LCEL for the "happy path" (retrieve + generate), LangGraph wrapping it with HITL/retries/branching.