Module 3: Agents with create_agent
create_agent and the ReAct Loop
Capsule overview
create_agent is LangChain's main function for building autonomous agents. It takes a model and a list of tools, and returns a compiled graph that runs the full ReAct loop: the model reasons, calls tools, observes results, and repeats until it decides to answer.
In the previous capsule you saw the big picture. Now you're going to use it. You'll create your first agent, understand what it produces as output, watch how it works internally step by step, and learn to control its limits with recursion_limit. By the end, you'll be able to build working agents that solve multi-step tasks on their own.
Your first agent
The most basic way to create an agent:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
agent = create_agent("openai:gpt-4.1-mini", tools=[get_weather])
result = agent.invoke({"messages": [("user", "What's the weather in Madrid?")]})
print(result["messages"][-1].content)
# Expected output: The weather in Madrid is sunny, with a temperature of 22°C.
Three lines do all the work:
create_agent("openai:gpt-4.1-mini", tools=[get_weather])— Creates the agent with a model and a list of tools.agent.invoke({"messages": [...]})— Runs the agent with a user message.result["messages"][-1].content— Pulls out the final answer.
About the model parameter
create_agent accepts the model as a string identifier or as a direct instance:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
@tool
def greet(name: str) -> str:
"""Greet a person."""
return f"Hello, {name}!"
# Option 1: String identifier (more concise)
agent_a = create_agent("openai:gpt-4.1-mini", tools=[greet])
# Option 2: Model instance (more control)
model = init_chat_model("openai:gpt-4.1-mini", temperature=0)
agent_b = create_agent(model, tools=[greet])
# Both work the same way
result_a = agent_a.invoke({"messages": [("user", "Say hi to Ana")]})
result_b = agent_b.invoke({"messages": [("user", "Say hi to Ana")]})
print(result_a["messages"][-1].content)
# Expected output: Hello, Ana!
print(result_b["messages"][-1].content)
# Expected output: Hello, Ana!
Use string identifiers for quick prototypes. Use instances when you need to configure temperature, max_tokens, timeout, or other model parameters.
Understanding the output
agent.invoke() returns a dictionary with a "messages" key that holds the entire conversation: the user's message, the model's tool calls, the tool results, and the final answer.
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
agent = create_agent("openai:gpt-4.1-mini", tools=[calculator])
result = agent.invoke({"messages": [("user", "What's 25 * 4?")]})
for i, msg in enumerate(result["messages"]):
msg_type = type(msg).__name__
if hasattr(msg, "tool_calls") and msg.tool_calls:
print(f" [{i}] {msg_type}: tool_calls={[(tc['name'], tc['args']) for tc in msg.tool_calls]}")
else:
content_preview = msg.content[:80] if msg.content else "(no content)"
print(f" [{i}] {msg_type}: {content_preview}")
# Expected output:
# [0] HumanMessage: What's 25 * 4?
# [1] AIMessage: tool_calls=[('calculator', {'expression': '25 * 4'})]
# [2] ToolMessage: 100
# [3] AIMessage: 25 × 4 = 100.
The sequence always follows the same pattern:
| Index | Type | Content |
|---|---|---|
| 0 | HumanMessage | The user's question |
| 1 | AIMessage | Tool calls the model decided to make |
| 2 | ToolMessage | The result of running the tool |
| 3 | AIMessage | Final answer, integrating the results |
If the model needed more than one round of tools, you'll see more messages interleaved (AIMessage with tool_calls → ToolMessage → AIMessage with tool_calls → ToolMessage → ... → final AIMessage).
Pulling out the final answer
The last message is always the agent's final answer:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the web for information."""
return f"Results: LangChain was created by Harrison Chase in 2022."
agent = create_agent("openai:gpt-4.1-mini", tools=[search])
result = agent.invoke({"messages": [("user", "Who created LangChain?")]})
final_answer = result["messages"][-1].content
print(final_answer)
# Expected output: LangChain was created by Harrison Chase in 2022.
How it works under the hood
create_agent builds a LangGraph graph with two nodes in a loop:
┌──────────────────────┐
│ │
Input ──────────▶ │ Model Node │
│ (calls the LLM) │
│ │
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Any tool_calls? │
└──────────┬───────────┘
│ │
Yes No
│ │
┌──────────▼─────┐ ┌────▼─────┐
│ Tools Node │ │ Output │
│ (runs tools) │ │ (done) │
└──────────┬─────┘ └──────────┘
│
│ ToolMessages
│
└──────▶ Model Node (back to the top)
The cycle step by step:
- The Model Node takes the message list and calls the LLM.
- If the LLM responds with
tool_calls→ it moves to the Tools Node. - The Tools Node runs each tool and appends a
ToolMessageto the list. - Back to the Model Node with the results.
- If the LLM responds without
tool_calls→ done, it returns the answer.
Watching the cycle in action
You can use stream with stream_mode="updates" to see each step the agent takes:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the web for information."""
return f"Results for '{query}': Python is a programming language created by Guido van Rossum."
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
agent = create_agent("openai:gpt-4.1-mini", tools=[search, calculator])
for chunk in agent.stream(
{"messages": [("user", "Who created Python and what's 2**10?")]},
stream_mode="updates"
):
for node_name, node_output in chunk.items():
print(f"\n--- Node: {node_name} ---")
if "messages" in node_output:
for msg in node_output["messages"]:
msg_type = type(msg).__name__
if hasattr(msg, "tool_calls") and msg.tool_calls:
for tc in msg.tool_calls:
print(f" Tool call: {tc['name']}({tc['args']})")
else:
print(f" {msg_type}: {msg.content[:100]}")
# Expected output:
# --- Node: agent ---
# Tool call: search({'query': 'who created Python'})
# Tool call: calculator({'expression': '2**10'})
#
# --- Node: tools ---
# ToolMessage: Results for 'who created Python': Python is a programming language...
# ToolMessage: 1024
#
# --- Node: agent ---
# AIMessage: Python was created by Guido van Rossum. And 2^10 = 1,024.
The agent made two tool calls in parallel (search and calculator), got both results back, and produced the final answer in a single round.
Stop conditions
The agent stops when one of two conditions is met:
1. The model responds with no tool_calls
This is the natural stop. The model decides it already has everything it needs and writes a direct answer:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
agent = create_agent("openai:gpt-4.1-mini", tools=[get_weather])
# Case 1: needs a tool → calls it → answers
result = agent.invoke({"messages": [("user", "What's the weather in Madrid?")]})
print(f"With tool: {result['messages'][-1].content}")
# Expected output: With tool: The weather in Madrid is sunny, with a temperature of 22°C.
# Case 2: doesn't need a tool → answers directly
result = agent.invoke({"messages": [("user", "What is Python?")]})
print(f"Without tool: {result['messages'][-1].content}")
# Expected output: Without tool: Python is a high-level programming language...
The model decides on its own whether it needs a tool or can answer from what it already knows.
2. The recursion_limit is reached
If the agent gets stuck in a loop where it keeps calling tools forever, recursion_limit stops it. The default limit is 25 graph iterations (every step of the graph — model node or tools node — counts as one iteration):
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def always_needs_more(query: str) -> str:
"""Search for information but always suggest searching for more."""
return f"I found something about '{query}', but you need to dig up more details."
agent = create_agent("openai:gpt-4.1-mini", tools=[always_needs_more])
try:
result = agent.invoke(
{"messages": [("user", "Research artificial intelligence")]},
config={"recursion_limit": 10}
)
except Exception as e:
print(f"Error: {type(e).__name__}: {e}")
# Expected output: Error: GraphRecursionError: Recursion limit of 10 reached...
Recommended values for recursion_limit:
| Use case | Recommended value | Why |
|---|---|---|
| Simple questions (1-2 tools) | 10 | Doesn't need many rounds |
| Moderate tasks (3-5 tool calls) | 25 (default) | Enough for most tasks |
| Complex research | 50 | May need several rounds of searching |
| Debugging | 5 | To catch infinite loops fast |
Every graph step burns one unit of recursion_limit. A full model → tools cycle counts as 2. With the default of 25, the agent can do roughly 12 rounds of tool calling before it stops.
Multiple tools in action
The real power of agents shows up when they have several tools and decide on their own which to use, in what order, and how many times:
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': Mexico has 129 million people and a GDP of USD 1.3 trillion."
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression. Accepts valid Python expressions."""
return str(eval(expression))
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is cloudy, 18°C, humidity 65%"
agent = create_agent("openai:gpt-4.1-mini", tools=[search, calculator, get_weather])
result = agent.invoke({
"messages": [("user",
"What's the population of Mexico, what's its GDP per capita "
"(GDP / population), and what's the weather in its capital?"
)]
})
print(result["messages"][-1].content)
# Expected output: Mexico has 129 million people. Its GDP is USD 1.3 trillion,
# which works out to a GDP per capita of roughly USD 10,078.
# In Mexico City the weather is cloudy, 18°C with 65% humidity.
The agent did four things on its own: looked up the population, looked up the GDP, computed GDP per capita, and checked the weather. All you did was ask the question.
Comparison: manual loop vs create_agent
| Aspect | Manual loop (Module 2) | create_agent |
|---|---|---|
| Lines of code | ~15 | ~3 |
tool_map / while loop | You write it | Automatic |
ToolMessage | You build it | Automatic |
max_rounds | You control it | recursion_limit in config |
| Error handling | Manual try/except | Built in |
| Streaming | Custom implementation | agent.stream() |
| Checkpointing | Not available | checkpointer parameter |
When to use the manual loop? When you need absolute control over every step (validating args before running, custom logic between rounds), or when you're learning how tool calling works under the hood.
When to use create_agent? For everything else — prototypes, production, and any agent where the standard ReAct flow is enough.
Troubleshooting
Problem 1: "ImportError: cannot import name 'create_agent'"
Symptom: Error when importing create_agent from langchain.agents.
Cause: Your langchain version predates v1.2.
Fix:
pip install --upgrade langchain langgraph
Check the version:
import langchain
print(langchain.__version__)
# You need >= 1.2.0
Problem 2: GraphRecursionError
Symptom: The agent raises GraphRecursionError: Recursion limit of X reached.
Cause: The agent got stuck in a loop, calling tools without ever converging on an answer.
Fix: Raise recursion_limit if the task genuinely needs many rounds, or review your tools to make sure their responses are useful and lead the model to a conclusion:
result = agent.invoke(
{"messages": [("user", "complex question")]},
config={"recursion_limit": 50}
)
Problem 3: The agent doesn't use the tools
Symptom: The model answers directly without calling any tool, even when it should. Cause: The model decided it could answer without tools, or the tool's description isn't clear enough. Fix: Improve your tools' docstrings. The model picks which tool to use based on each tool's description:
# Bad — vague description
@tool
def search(q: str) -> str:
"""Search for things."""
return f"Result: {q}"
# Good — clear and specific description
@tool
def search(query: str) -> str:
"""Search the internet for up-to-date information. Use it when the user asks
about data that could have changed: news, prices, recent events,
or anything that needs real-time data."""
return f"Result: {query}"
Problem 4: "ModuleNotFoundError: No module named 'langgraph'"
Symptom: Error when creating the agent.
Cause: langgraph isn't installed. create_agent needs it internally.
Fix:
pip install langgraph
Problem 5: The agent is slow
Symptom: The agent takes a long time to answer.
Cause: Every round of the ReAct loop is a call to the LLM. If the agent does 5 rounds, that's 5 model calls.
Fix: Use a faster model (gpt-4.1-mini instead of gpt-4.1), cut down the number of available tools (fewer options = faster decision), or use streaming to give the user progressive feedback (Capsule 05).
Exercises
Exercise 1: Agent with one tool (Easy)
Create an agent with a get_population(country: str) tool that returns a country's population. Try it with "How many people live in Japan?" and print only the final answer.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_population(country: str) -> str:
"""Get the current population of a country."""
populations = {
"Japan": "125 million",
"Mexico": "129 million",
"Spain": "47 million",
"Argentina": "46 million",
"Colombia": "52 million",
}
return populations.get(country, f"I don't have population data for {country}")
agent = create_agent("openai:gpt-4.1-mini", tools=[get_population])
result = agent.invoke({"messages": [("user", "How many people live in Japan?")]})
print(result["messages"][-1].content)
# Expected output: Japan has roughly 125 million people.
What's happening: The agent takes the question, decides it needs get_population, runs it with country="Japan", gets back "125 million", and writes the final answer.
Exercise 2: Agent with multiple tools (Easy)
Create an agent with three tools: search, calculator, and get_weather. Ask three different questions and check that the agent picks the right tool for each one.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': Python was created by Guido van Rossum in 1991."
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression. Accepts valid Python expressions."""
return str(eval(expression))
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is cloudy, 18°C"
agent = create_agent("openai:gpt-4.1-mini", tools=[search, calculator, get_weather])
questions = [
"Who created Python?",
"What's 256 * 48?",
"What's the weather in Buenos Aires?",
]
for q in questions:
result = agent.invoke({"messages": [("user", q)]})
print(f"Q: {q}")
print(f"A: {result['messages'][-1].content}\n")
# Expected output:
# Q: Who created Python?
# A: Python was created by Guido van Rossum in 1991.
#
# Q: What's 256 * 48?
# A: 256 × 48 = 12,288.
#
# Q: What's the weather in Buenos Aires?
# A: The weather in Buenos Aires is cloudy, with a temperature of 18°C.
What's happening: The agent picks the right tool for each question on its own: search for the knowledge question, calculator for the math, and get_weather for the weather.
Exercise 3: Inspect the full flow (Medium)
Create an agent and, after running it, print every message in the conversation with its type and content. Count how many tool calls the agent made in total.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the internet for up-to-date information."""
return f"Results for '{query}': LangGraph is LangChain's orchestration framework."
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
agent = create_agent("openai:gpt-4.1-mini", tools=[search, calculator])
result = agent.invoke({
"messages": [("user", "What is LangGraph and what's 3**8?")]
})
total_tool_calls = 0
for i, msg in enumerate(result["messages"]):
msg_type = type(msg).__name__
if hasattr(msg, "tool_calls") and msg.tool_calls:
total_tool_calls += len(msg.tool_calls)
calls = [(tc["name"], tc["args"]) for tc in msg.tool_calls]
print(f" [{i}] {msg_type}: tool_calls={calls}")
elif msg.content:
print(f" [{i}] {msg_type}: {msg.content[:100]}")
else:
print(f" [{i}] {msg_type}: (no content)")
print(f"\nTotal tool calls: {total_tool_calls}")
# Expected output:
# [0] HumanMessage: What is LangGraph and what's 3**8?
# [1] AIMessage: tool_calls=[('search', {'query': 'what is LangGraph'}), ('calculator', {'expression': '3**8'})]
# [2] ToolMessage: Results for 'what is LangGraph': LangGraph is LangChain's orchestration...
# [3] ToolMessage: 6561
# [4] AIMessage: LangGraph is LangChain's orchestration framework. And 3^8 = 6,561.
#
# Total tool calls: 2
What's happening: We walk every message and count the tool_calls in each AIMessage. The agent made 2 tool calls in parallel (search and calculator) in a single round.
Exercise 4: Control recursion_limit (Medium)
Write a tool that always asks for more searching (simulating an infinite loop). Run the agent with recursion_limit=6 and catch the error. Then raise the limit and verify the agent eventually answers.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
call_count = 0
@tool
def deep_search(query: str) -> str:
"""Search for detailed information. Always returns partial results."""
global call_count
call_count += 1
if call_count >= 4:
return f"Complete information on '{query}': LangChain was created in 2022 by Harrison Chase."
return f"Partial result {call_count} on '{query}'. You need to search more to get the full answer."
agent = create_agent("openai:gpt-4.1-mini", tools=[deep_search])
# Test 1: low limit — should fail
call_count = 0
try:
result = agent.invoke(
{"messages": [("user", "Who created LangChain?")]},
config={"recursion_limit": 6}
)
print(f"Test 1 answered: {result['messages'][-1].content}")
except Exception as e:
print(f"Test 1 — Expected error: {type(e).__name__}")
# Expected output: Test 1 — Expected error: GraphRecursionError
# Test 2: high limit — should succeed
call_count = 0
result = agent.invoke(
{"messages": [("user", "Who created LangChain?")]},
config={"recursion_limit": 25}
)
print(f"Test 2 answered: {result['messages'][-1].content}")
# Expected output: Test 2 answered: LangChain was created by Harrison Chase in 2022.
What's happening: With recursion_limit=6, the agent doesn't have enough iterations to finish 4 rounds of searching (each round = model + tools = 2 steps). With recursion_limit=25, it has plenty of room.
Exercise 5: An agent that combines results (Advanced)
Create an agent with search and calculator tools that can answer "What's the population of Mexico and Spain, and how many people is that in total?". The agent has to look up both figures and then add them.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def search(query: str) -> str:
"""Search the internet for up-to-date information. Ideal for demographics and statistics."""
data = {
"population mexico": "The population of Mexico is 129,000,000 people.",
"population spain": "The population of Spain is 47,000,000 people.",
}
query_lower = query.lower()
for key, value in data.items():
if key in query_lower:
return value
return f"I couldn't find specific data for '{query}'."
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression. Accepts valid Python expressions."""
return str(eval(expression))
agent = create_agent("openai:gpt-4.1-mini", tools=[search, calculator])
result = agent.invoke({
"messages": [("user",
"What's the population of Mexico and Spain, "
"and how many people is that in total?"
)]
})
print(result["messages"][-1].content)
# Expected output: Mexico has 129 million people and Spain has 47 million.
# Together, the two countries add up to 176 million people.
What's happening: The agent ran several rounds: first it looked up both populations (parallel tool calls), then it called calculator to add them, and finally it pulled everything together. That's the power of the ReAct loop.
Exercise 6: An agent that answers without tools (Challenge)
Create an agent with tools available, but ask it questions that do NOT need tools. Verify the agent answers directly by checking that no message has tool_calls.
See solution
from dotenv import load_dotenv
load_dotenv()
from langchain.agents import create_agent
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
return f"The weather in {city} is sunny, 22°C"
@tool
def calculator(expression: str) -> str:
"""Evaluate a math expression."""
return str(eval(expression))
agent = create_agent("openai:gpt-4.1-mini", tools=[get_weather, calculator])
for q in ["What is photosynthesis?", "What's the capital of France?"]:
result = agent.invoke({"messages": [("user", q)]})
used_tools = any(
hasattr(m, "tool_calls") and m.tool_calls for m in result["messages"]
)
print(f"Q: {q}")
print(f" Messages: {len(result['messages'])} | Used tools: {used_tools}")
print(f" A: {result['messages'][-1].content[:80]}\n")
# Expected output:
# Q: What is photosynthesis?
# Messages: 2 | Used tools: False
# A: Photosynthesis is the process by which plants convert sunlight...
#
# Q: What's the capital of France?
# Messages: 2 | Used tools: False
# A: The capital of France is Paris.
What's happening: The model decides on its own that it doesn't need tools to answer general-knowledge questions. There are only 2 messages (HumanMessage + AIMessage), with no tool calls in between.
Summary
In this capsule you learned:
create_agent(model, tools)creates an agent that runs the full ReAct loop autonomously- The agent accepts the model as a string identifier (
"openai:gpt-4.1-mini") or as a model instance - The output is a dict with a
"messages"key holding the entire conversation (HumanMessage → AIMessage with tool_calls → ToolMessage → ... → final AIMessage) - Under the hood,
create_agentbuilds a LangGraph graph with a model node and a tools node wired in a loop - Stop conditions: the model answers with no
tool_calls(natural stop) or therecursion_limitis hit (safety net) recursion_limitgoes in config, not as acreate_agentparameter:agent.invoke(input, config={"recursion_limit": N})- The agent decides on its own which tools to use, in what order, and how many times — including parallel tool calls
- If the question doesn't need tools, the agent answers directly without calling any
- Manual loop: for total control and for learning. create_agent: for everything else
Next capsule: Static and Dynamic System Prompts — you'll learn to shape the agent's behavior with system prompts that can be fixed or change with context.
Further reading
- create_agent API Reference — Full reference with every parameter
- LangChain Agents Overview — The official conceptual guide to agents
- LangGraph Agents — How create_agent builds graphs internally
- ReAct Paper (Yao et al., 2022) — The original Reason + Act paper
- How to create a ReAct agent — Step-by-step tutorial with LangGraph
- Tool Calling — LangChain — Tool calling concepts the agent uses internally
- LangGraph Recursion Limit — Controlling iteration limits in graphs
- init_chat_model Reference — String identifiers for models
Module 3 — LangChain & LangGraph: From Chains to Agents