How to Build a ReAct Agent from Scratch with LangGraph (Step-by-Step)
You ask your LLM a question that needs live data. It tries to wing it from training data and gets the answer wrong. Now imagine if the model could pause, fire off a web search, read what comes back, and then respond. That’s exactly what a ReAct agent does. In this guide, you’ll assemble one from scratch in LangGraph and understand every moving part.
Let’s start with the big picture before writing a single line of code.
A user types a question. It goes to the agent node, which is basically your LLM with a thin wrapper. The model looks at the question and realizes it needs outside data. So it emits a tool call — a structured request, not a text response. That request travels to a tool node, which runs the actual function and ships the result back as an “observation.”
Now the model reads that observation. Does it have enough to answer? If yes, it writes a response and the graph finishes. If no, it fires another tool call and the loop goes around again.
The whole thing boils down to: think → act → observe → repeat. One conditional edge between the agent and tool nodes is all it takes to create this loop. We’ll construct each piece — state, agent node, tool node, routing logic, and the final compiled graph — one at a time.
What Is the ReAct Pattern?
The name stands for Reasoning + Acting. A 2022 paper by Yao et al. showed that LLMs produce much better answers when they alternate between reasoning and taking actions, rather than answering in a single shot.
Here’s the idea boiled down: the model figures out what data it needs, uses a tool to fetch it, checks what came back, and keeps going until it can give a solid answer.
A normal LLM call works like this:
User question → LLM → Answer (possibly wrong)
A ReAct agent works like this:
User question → Think → Act (call tool) → Observe → Think → Act → ... → Final answer
Because the model gets several rounds to collect information, ReAct agents crush factual queries, multi-step research, and real-time lookups that would stump a one-shot prompt.
Key Insight: There’s no upfront plan. The agent takes one step, looks at what happened, and picks the next action based on what it just learned. If a tool returns garbage, it can try a different approach. That’s the power of the loop.
Before You Start
-
Python: 3.10+
-
Packages: langgraph 0.4+, langchain-openai 0.3+, langchain-core 0.3+
-
Install:
pip install langgraph langchain-openai langchain-core -
API key: Set
OPENAI_API_KEYin your environment. See OpenAI’s docs to create one. -
Time: About 35 minutes
-
Background: Basic LangGraph concepts (nodes, edges, state) from earlier posts in this series.
Start by pulling in the imports — the chat model class, the different message types, the tool decorator, and all the LangGraph building blocks.
import os
import json
from typing import Annotated, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage,
AIMessage,
ToolMessage,
SystemMessage,
)
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, create_react_agent
Step 1 — How Do You Set Up the State?
Every LangGraph graph runs on a state object. For a ReAct agent, the state is simple: a growing list of messages that holds everything — user questions, AI outputs, and tool results.
LangGraph comes with MessagesState out of the box. It has one field called messages and uses the add_messages reducer. That reducer appends new messages rather than wiping the old ones. Perfect for ReAct, where the conversation gets longer with each think-act-observe pass.
# MessagesState is equivalent to writing:
# class AgentState(TypedDict):
# messages: Annotated[list, add_messages]
#
# We use MessagesState directly — no custom state needed.
Do you need a custom state class? Not for a basic ReAct agent. MessagesState tracks all the things you care about — user input, model reasoning, tool calls, and tool output. Custom fields only make sense if you want extras like a loop counter or a running cost total.
Quick check: Why does add_messages matter? Regular assignment (messages = new_list) would erase all prior messages on every update. The reducer appends instead. Without it, your agent forgets its entire history after each step — a guaranteed disaster.
Step 2 — How Do You Create the Tools?
Tools let the agent reach beyond the LLM’s training data. We’ll make two: one for weather lookups and one for math. In a real app, these would call live APIs. We’ll use hardcoded data here so nothing breaks without an internet connection.
LangChain’s @tool decorator wraps any Python function as a tool the model can ask for. The key detail: the model reads your docstring when choosing tools. A clear docstring means better tool selection.
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
weather_data = {
"new york": "72°F, Partly Cloudy",
"london": "58°F, Rainy",
"tokyo": "80°F, Sunny",
"paris": "65°F, Overcast",
}
city_lower = city.lower()
if city_lower in weather_data:
return f"Weather in {city}: {weather_data[city_lower]}"
return f"Weather data not available for {city}"
Simple lookup logic: match the city name against our dictionary. Found it? Return the weather. Didn’t find it? Say so. The model handles either outcome gracefully.
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression. Use Python syntax."""
try:
result = eval(expression)
return f"Result: {result}"
except Exception as e:
return f"Error evaluating '{expression}': {e}"
tools = [get_weather, calculator]
Warning: eval() is a security hazard — fine for demos, dangerous in production. Swap it for a safe math library like numexpr or asteval before deploying. Arbitrary Python execution is not something you want in a user-facing system.
Step 3 — How Do You Build the Agent Node?
This is the brain of the operation. The agent node grabs the current state, feeds the full message history to the LLM, and collects whatever comes back. Sometimes the model replies with text. Other times it replies with tool_calls — structured requests that say “run this function with these inputs.”
Before any of that works, we need to tell the model about our tools via bind_tools(). This attaches the tool schemas (names, descriptions, parameter types) so the model knows what it can ask for.
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools(tools)
def agent_node(state: MessagesState) -> dict:
"""Call the LLM with the current message history."""
messages = state["messages"]
response = model_with_tools.invoke(messages)
return {"messages": [response]}
That’s all the logic — four lines. The model scans every message in the history and produces the next one. The interesting part is what form that response takes: plain text means the model is done, while tool_calls means it wants to gather more data first.
Want to steer how the agent talks or behaves? Add a system prompt. Just slip a SystemMessage in front of the conversation before each LLM call.
SYSTEM_PROMPT = (
"You are a concise research assistant. "
"Use tools when you need facts. "
"Answer in 2-3 sentences maximum."
)
def agent_node_with_prompt(state: MessagesState) -> dict:
"""Agent node with a system prompt prepended."""
messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
response = model_with_tools.invoke(messages)
return {"messages": [response]}
We don’t save the SystemMessage in the state. We add it fresh before each call. That keeps the state clean — only user messages, AI responses, and tool results pile up.
Tip: The system prompt guides the agent across all loop passes. Add rules like “never call the same tool twice with the same arguments” to prevent endless loops.
Step 4 — How Do You Build the Tool Node?
When the model asks for a tool call, something has to run that function and return the output. That’s the tool node.
LangGraph provides ToolNode — a built-in node that handles this. It reads tool_calls from the last AI message, runs the matching function, and sends back a ToolMessage.
tool_node = ToolNode(tools)
One line. ToolNode takes the same tools list. When the graph reaches it, it pulls tool call requests from the latest AI message, runs each one, and adds the results to the conversation.
What’s going on under the hood? Here’s a simplified version:
def manual_tool_node(state: MessagesState) -> dict:
"""Execute tool calls from the last AI message."""
last_message = state["messages"][-1]
tool_results = []
tool_map = {t.name: t for t in tools}
for call in last_message.tool_calls:
tool_fn = tool_map[call["name"]]
result = tool_fn.invoke(call["args"])
tool_results.append(
ToolMessage(content=str(result), tool_call_id=call["id"])
)
return {"messages": tool_results}
Each ToolMessage carries a tool_call_id that links it to the original request. The LLM uses this to match each result with the call that asked for it.
Step 5 — How Does the Conditional Edge Create the Loop?
This is the part that makes a ReAct agent tick. After the agent node runs, we need to decide: did the model ask to call a tool, or did it write a final answer?
If the last message has tool_calls, we route to the tool node. If not, we go to END.
def should_continue(state: MessagesState) -> Literal["tools", "end"]:
"""Decide whether to call tools or finish."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return "end"
This simple check is the engine behind the loop. The agent thinks → calls a tool → reads the result → thinks again → calls another tool or wraps up. The cycle continues as long as the model keeps asking for tools.
Key Insight: The conditional edge IS the ReAct loop. Without it, you have a one-shot LLM call. With it, you have an agent that reasons across multiple steps. The whole design rests on this one routing function.
Step 6 — How Do You Wire and Compile the Graph?
We have all four pieces: state, agent node, tool node, and routing function. Time to connect them in a StateGraph.
The graph mirrors the ReAct loop. START sends the first message to the agent. The conditional edge picks the next step. If tools are needed, the tool node runs and feeds results back to the agent. If not, the graph exits.
workflow = StateGraph(MessagesState)
# Add nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
# Add edges
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
{"tools": "tools", "end": END},
)
workflow.add_edge("tools", "agent")
# Compile
react_agent = workflow.compile()
Three things to notice. First, START connects to "agent" — every conversation starts at the LLM. Second, add_conditional_edges maps the router’s return values to node names. Third, "tools" always routes back to "agent" — that’s the cycle. After a tool runs, the agent sees the result and decides what to do next.
How Do You Run and Test Your ReAct Agent?
Let’s try it. We’ll ask about the weather in Tokyo — a question that needs a tool call.
We pass a HumanMessage to the graph. The agent should spot that it needs get_weather, call it, read the result, and write a natural-language answer.
result = react_agent.invoke(
{"messages": [HumanMessage(content="What's the weather in Tokyo?")]}
)
for msg in result["messages"]:
print(f"{msg.type}: {msg.content[:80] if msg.content else '[tool call]'}")
The output shows four messages:
human: What's the weather in Tokyo?
ai: [tool call]
tool: Weather in Tokyo: 80°F, Sunny
ai: The weather in Tokyo is currently 80°F and sunny!
Follow the flow. The human message goes in. The AI’s first response is a tool call (no text — it chose to act). The tool sends back “80°F, Sunny.” Then the AI uses that result to write a final answer.
What about a question that needs several tool calls?
result = react_agent.invoke(
{"messages": [HumanMessage(
content="What's the weather in New York and London? "
"Also, what's 72 minus 58?"
)]}
)
for msg in result["messages"]:
if msg.content:
print(f"{msg.type}: {msg.content[:100]}")
The agent calls get_weather for both cities and calculator for the subtraction. It might batch the calls or do them one at a time. Either way, it gathers all the facts before writing the final answer. The temperature gap should be 14°F.
How Do You Trace Agent Steps for Debugging?
When your agent gives odd results, you need to see what happened at each step. The stream method shows each node’s output as it runs.
inputs = {"messages": [HumanMessage(content="What's the weather in Paris?")]}
for step in react_agent.stream(inputs, stream_mode="updates"):
for node_name, node_output in step.items():
print(f"\n--- {node_name} ---")
for msg in node_output["messages"]:
if hasattr(msg, "tool_calls") and msg.tool_calls:
for tc in msg.tool_calls:
print(f" Tool call: {tc['name']}({tc['args']})")
if msg.content:
print(f" Content: {msg.content}")
The trace shows three steps. First, the agent node sends a get_weather call for Paris. Second, the tools node runs it and returns “65°F, Overcast.” Third, the agent node reads the result and writes the answer. If the agent loops in a weird way, this trace tells you exactly where things went off track.
Tip: For production debugging, use LangSmith. Set LANGCHAIN_TRACING_V2=true and LANGCHAIN_API_KEY to record every node run with timing. You can replay any agent session and inspect state at each step.
What Is create_react_agent() and When Should You Use It?
Everything we built by hand — agent node, tool node, conditional edge, graph wiring — is such a common pattern that LangGraph wraps it in one function: create_react_agent().
Pass it a model and a list of tools, and you get a compiled graph that works the same as our hand-built version. You can also add a prompt to tweak the behavior.
prebuilt_agent = create_react_agent(
model="openai:gpt-4o-mini",
tools=tools,
prompt="You are a helpful assistant. Be concise.",
)
result = prebuilt_agent.invoke(
{"messages": [HumanMessage(content="What's 15 * 23?")]}
)
print(result["messages"][-1].content)
The agent calls the calculator with 15 * 23 and returns 345. Three lines to get a working ReAct agent. The prebuilt version has the same routing, tool handling, and message tracking inside.
Manual Build vs create_react_agent — Which Should You Pick?
Why build from scratch when a one-liner exists? The manual approach gives you control that the shortcut doesn’t.
| Feature | Manual Build | create_react_agent() |
|---|---|---|
| Custom state fields | Add anything you need | Limited to MessagesState plus schema |
| Node-level logic | Full control over every step | Hooks via pre_model_hook / post_model_hook |
| Routing logic | Any custom condition | Fixed: tool_calls → tools, else → end |
| Lines of code | ~30 | ~3 |
| Best for | Custom agents, production | Prototypes, standard use cases |
Use create_react_agent() when you want a standard agent fast. Build by hand when you need custom routing, extra state fields, or non-standard node logic.
Here’s a real example: a manual build with a step counter that stops runaway loops.
class AgentStateWithCounter(MessagesState):
tool_call_count: int
def should_continue_with_limit(
state: AgentStateWithCounter,
) -> Literal["tools", "end"]:
"""Stop after 5 tool calls to prevent runaway loops."""
last_message = state["messages"][-1]
if last_message.tool_calls and state.get("tool_call_count", 0) < 5:
return "tools"
return "end"
This kind of guard — counting calls, capping them — needs a custom state. The prebuilt helper can’t do it on its own.
Note: LangGraph’s create_react_agent moves fast. Version 0.4+ added pre_model_hook, post_model_hook, and response_format. Check the official docs for the latest API.
How Do You Fix Common Agent Loop Problems?
ReAct agents can misbehave. Here are the three issues I see most often.
The agent loops forever
The model calls the same tool with the same arguments over and over, getting the same result each time.
Why: The system prompt doesn’t tell the model to stop when it has enough data. Or the tool returns vague results the model can’t use.
Fix: Add clear rules to your system prompt.
system_prompt = """You are a helpful assistant with access to tools.
Rules:
- Call a tool ONLY if you need information you don't have.
- NEVER call the same tool with the same arguments twice.
- Once you have enough information, respond directly.
"""
The agent never calls tools
You ask something that needs a tool, but the model answers from memory.
Why: The model’s training data already has the answer (or it thinks so). It sees no reason to use a tool.
Fix: Be specific in your prompt: “Always use the get_weather tool for weather questions. Do not guess.”
Tool errors crash the agent
A tool throws an exception and the whole graph goes down.
Why: APIs fail — network errors, rate limits, bad inputs. Without error handling, one failure kills the graph.
Fix: Wrap tool code in try/except and return the error as a string. The model reads it and adjusts.
@tool
def safe_weather(city: str) -> str:
"""Get weather for a city, with error handling."""
try:
if not city.strip():
raise ValueError("City name cannot be empty")
weather_data = {"new york": "72°F, Partly Cloudy"}
city_lower = city.lower()
if city_lower in weather_data:
return f"Weather in {city}: {weather_data[city_lower]}"
return f"No weather data for '{city}'"
except Exception as e:
return f"Error looking up weather: {e}"
Warning: LangGraph’s default recursion limit is 25 steps. Go past that and you get a GraphRecursionError. Raise it with config={"recursion_limit": 50}. But if your agent needs 50+ steps, the real problem is usually the prompt or tool design — not the limit.
Real-World Example — How Would You Build a Research Assistant?
Let’s build something useful: a research agent that looks up facts and does math. This shows how a ReAct agent handles questions where each step depends on the one before it.
We add a mock search tool next to our existing calculator.
@tool
def search(query: str) -> str:
"""Search for information about a topic."""
knowledge = {
"python popularity": (
"Python is #1 on the TIOBE index as of 2025, "
"with a rating of approximately 23%."
),
"javascript popularity": (
"JavaScript is #6 on the TIOBE index as of 2025, "
"with a rating of approximately 3.5%."
),
"earth population": (
"Earth's population is approximately 8.1 billion."
),
}
query_lower = query.lower()
for key, value in knowledge.items():
if key in query_lower:
return value
return f"No results found for: {query}"
The research agent uses both tools — search for facts, calculator for math. We ask it a question that needs two searches and one calculation.
research_tools = [search, calculator]
research_agent = create_react_agent(
model="openai:gpt-4o-mini",
tools=research_tools,
prompt=(
"You are a research assistant. Use search for facts "
"and calculator for math. Cite what you find."
),
)
result = research_agent.invoke(
{"messages": [HumanMessage(
content="How much more popular is Python than JavaScript "
"according to TIOBE? Give me the ratio."
)]}
)
print(result["messages"][-1].content)
The agent figures this out on its own. It searches for Python’s TIOBE score (23%), searches for JavaScript’s (3.5%), divides 23 by 3.5 to get roughly 6.57, and reports that Python is about 6.6x more popular. Each step builds on the last — just like a human researcher would do it.
When Does ReAct Work Well — and When Doesn’t It?
ReAct is the most widely used agent pattern, but it’s not always the best fit.
ReAct is great for:
-
Fact lookups — when the answer lives inside a tool
-
Multi-step reasoning — when you need info from several sources
-
Open-ended tasks — when you don’t know how many steps it’ll take
-
Error recovery — the agent sees a failed tool call and tries something else
ReAct struggles with:
-
Fixed workflows — if the steps are always the same (extract → transform → load), use a static graph. ReAct adds extra LLM calls and makes things less predictable.
-
High-stakes actions — the model might skip a key step. Add human approval for anything with real consequences.
-
Tight budgets — each loop pass is an LLM call. A five-step loop costs five times what a single call does. If most answers are simple, ReAct wastes money.
-
Long tasks — the message history grows each pass. After 10+ steps, you risk hitting token limits.
Key Insight: Pick ReAct when you don’t know the number of steps in advance. If you can draw the workflow as a fixed flowchart, you don’t need it. ReAct’s strength is flexibility. Its weakness is that the LLM might do something unexpected.
What Are the Most Common Mistakes When Building ReAct Agents?
Mistake 1: Forgetting to bind tools to the model
Wrong:
def agent_node(state):
response = model.invoke(state["messages"]) # No tools bound
return {"messages": [response]}
Why it breaks: The model has no idea tools exist. It answers from memory, never calling a tool. No error appears — you just get bad answers.
Right:
model_with_tools = model.bind_tools(tools)
def agent_node(state):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
Mistake 2: No edge from tools back to agent
Wrong:
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent", should_continue, {"tools": "tools", "end": END}
)
# Missing: workflow.add_edge("tools", "agent")
Why it breaks: After the tool runs, there’s nowhere to go. The graph raises a runtime error. The ReAct loop needs the full cycle: agent → tools → agent.
Right:
workflow.add_edge("tools", "agent")
Mistake 3: Wrong return type from a node
Wrong:
def agent_node(state):
response = model_with_tools.invoke(state["messages"])
return response # Returns AIMessage, not a dict
Why it breaks: Nodes must return a dict that matches the state schema. The messages key with add_messages expects a list. A bare message object causes a type error.
Right:
def agent_node(state):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
Tip: A handy debug trick: print inside your agent node. Show the message count and whether tool calls are present. You’ll see the loop in action without any extra tooling.
Complete Code
Click to expand the full script (copy-paste and run)
# Complete code from: Build a ReAct Agent from Scratch with LangGraph
# Requires: pip install langgraph langchain-openai langchain-core
# Python 3.10+
# Set OPENAI_API_KEY environment variable before running
import os
import json
from typing import Annotated, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage,
AIMessage,
ToolMessage,
SystemMessage,
)
from langchain_core.tools import tool
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, create_react_agent
# --- Tools ---
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
weather_data = {
"new york": "72°F, Partly Cloudy",
"london": "58°F, Rainy",
"tokyo": "80°F, Sunny",
"paris": "65°F, Overcast",
}
city_lower = city.lower()
if city_lower in weather_data:
return f"Weather in {city}: {weather_data[city_lower]}"
return f"Weather data not available for {city}"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression. Use Python syntax."""
try:
result = eval(expression)
return f"Result: {result}"
except Exception as e:
return f"Error evaluating '{expression}': {e}"
tools = [get_weather, calculator]
# --- Model ---
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
model_with_tools = model.bind_tools(tools)
# --- Nodes ---
def agent_node(state: MessagesState) -> dict:
"""Call the LLM with the current message history."""
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
tool_node = ToolNode(tools)
# --- Routing ---
def should_continue(state: MessagesState) -> Literal["tools", "end"]:
"""Decide whether to call tools or finish."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return "end"
# --- Graph Assembly ---
workflow = StateGraph(MessagesState)
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
workflow.add_edge(START, "agent")
workflow.add_conditional_edges(
"agent",
should_continue,
{"tools": "tools", "end": END},
)
workflow.add_edge("tools", "agent")
react_agent = workflow.compile()
# --- Test ---
print("=== Single Tool Call ===")
result = react_agent.invoke(
{"messages": [HumanMessage(content="What's the weather in Tokyo?")]}
)
for msg in result["messages"]:
if msg.content:
print(f" {msg.type}: {msg.content}")
print("\n=== Multi-Step Query ===")
result = react_agent.invoke(
{"messages": [HumanMessage(
content="What's the weather in New York and London? "
"Also, what's 72 minus 58?"
)]}
)
for msg in result["messages"]:
if msg.content:
print(f" {msg.type}: {msg.content}")
print("\n=== Prebuilt Agent ===")
prebuilt = create_react_agent(
model="openai:gpt-4o-mini",
tools=tools,
prompt="You are a helpful assistant. Be concise.",
)
result = prebuilt.invoke(
{"messages": [HumanMessage(content="What's 15 * 23?")]}
)
print(f" Answer: {result['messages'][-1].content}")
print("\nScript completed successfully.")
Summary
You’ve built a ReAct agent from the ground up and seen how every part works. The pattern is simpler than it sounds: an LLM node that can ask for tool calls, a tool node that runs them, and a conditional edge that ties the loop together.
Here are the big takeaways:
-
ReAct = Reason + Act. The agent switches between thinking and doing, gathering info step by step.
-
The conditional edge is the loop. It checks for
tool_calls— if present, loop. If not, exit. -
MessagesStateholds the conversation. Theadd_messagesreducer appends each entry, building the full history. -
create_react_agent()does it all in one call. Use it for quick prototypes. Build by hand for custom behavior. -
Debug with streaming. Watch each node’s output to trace the agent’s choices step by step.
Practice exercise: Add a lookup_population(country: str) tool that returns population data. Ask a question that needs both population and calculator (e.g., “What’s the combined population of India and China?”).
Solution
@tool
def lookup_population(country: str) -> str:
"""Look up the approximate population of a country."""
populations = {
"india": "1.44 billion",
"china": "1.43 billion",
"usa": "335 million",
"brazil": "216 million",
}
country_lower = country.lower()
if country_lower in populations:
return f"Population of {country}: {populations[country_lower]}"
return f"Population data not available for {country}"
extended_agent = create_react_agent(
model="openai:gpt-4o-mini",
tools=[get_weather, calculator, lookup_population],
prompt="Use tools for facts. Use calculator for math.",
)
result = extended_agent.invoke(
{"messages": [HumanMessage(
content="What's the combined population of India and China?"
)]}
)
print(result["messages"][-1].content)
The agent calls lookup_population twice, then uses calculator to add 1.44 + 1.43 billion, giving roughly 2.87 billion.
Frequently Asked Questions
Can a ReAct agent call multiple tools at once?
Yes. Models like GPT-4o can send several tool_calls in one response. LangGraph’s ToolNode runs them all and returns the results together. The agent sees everything at once in the next pass. You don’t need to change any code — it’s built in.
How do I limit loops to control costs?
Pass recursion_limit in the config: agent.invoke(inputs, config={"recursion_limit": 10}). This caps the total number of node runs. For tighter control, track tool call counts in a custom state field and check the count in your routing function.
Can I use Anthropic, Google, or local models instead of OpenAI?
Yes. Swap ChatOpenAI for ChatAnthropic, ChatGoogleGenerativeAI, ChatOllama, or any LangChain-compatible chat model. Everything else stays the same. The model just needs to support tool calling.
What’s the difference between LangGraph’s and LangChain’s create_react_agent?
They’re different functions in different packages. LangChain’s version (in langchain.agents) creates a legacy AgentExecutor. LangGraph’s version (in langgraph.prebuilt) builds a StateGraph with streaming, persistence, and human-in-the-loop support. Use the LangGraph version for new projects.
How do I give the agent memory across conversations?
Add a checkpointer when compiling: workflow.compile(checkpointer=MemorySaver()). Then pass a thread_id in config: agent.invoke(inputs, config={"configurable": {"thread_id": "user-123"}}). The agent keeps all messages in that thread. Our persistence article covers this in detail.
References
-
Yao, S. et al. — “ReAct: Synergizing Reasoning and Acting in Language Models” (2022). arXiv:2210.03629
-
LangGraph documentation — How to create a ReAct agent from scratch. Link
-
LangGraph API Reference —
create_react_agent. Link -
LangGraph documentation — StateGraph and MessagesState. Link
-
LangChain documentation — Tool calling with chat models. Link
-
LangGraph prebuilt module — ToolNode reference. Link
-
IBM — What is a ReAct Agent? Link
-
Wei, J. et al. — “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models” (2022). arXiv:2201.11903
Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.
Start Free Course →