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 (@tooldecorators,StructuredTools, pre-built tools likeTavilySearchResults).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_agentwithlanggraph.prebuilt.create_react_agent. The former is the legacy version paired withAgentExecutor(deprecated). Always import fromlanggraph.prebuiltfor 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 (
ToolNodethat executes the requested tool). They loop via a conditional edge. -
Q: How does the agent decide when to stop? A: The LLM emits an
AIMessagewith notool_calls. The conditional edge then routes toEND. -
Q: Difference between
langgraph.prebuilt.create_react_agentandlangchain.agents.create_react_agent? A: The LangGraph one is the modern, recommended version. The LangChain one is deprecated and tied toAgentExecutor. -
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
checkpointertocreate_react_agentand athread_idin 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
Set a recursion_limit on the agent (currently it could loop forever)
Expected: True
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
@tooland pass the list tocreate_react_agent(model, tools=[my_tool]) - Subclass
BaseTool - Modify the model
- Use a vector store
Why:
@toolwraps 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.