Agents & AgentExecutor
Give the tools from Lesson 2.6 to an agent and let LangChain run the ReAct loop - with create_agent today, AgentExecutor in older code.
What you will be able to do
- Explain the difference between a chain and an agent
- Describe the two roles: the agent decides, the executor runs the loop
- Build a weather-and-calculator agent with create_agent
- Follow one run step by step, and see what the model sees after a tool runs
- Debug an agent in Module 1 terms: descriptions, arguments, loops
- Recognise AgentExecutor in older code, and know where it lives now
The idea, in plain English
An agent is a model in a loop. It reads the conversation, decides whether to call a tool, the program runs the tool, the result goes back into the conversation, and the model decides again - until it answers without asking for a tool. That is the ReAct loop from Lesson 1.6: thought, action, observation, repeated.
Classic LangChain split this into two objects. The agent decided the next action. The AgentExecutor ran it: called the tool, fed the observation back, counted iterations, and stopped. Agent = decision maker; executor = runs the loop.
LangChain 1.x keeps the two roles but builds them with one call. create_agent(model, tools) returns a small graph with a model node and a tools node: the model node decides, the tools node executes, and the graph runtime does the executor’s job of looping and stopping. In LangChain 1.4, AgentExecutor cannot be imported from langchain.agents; it moved to the separate langchain-classic package.
Our local llama3 cannot call tools, so the runs below use a scripted model that replays the replies a tool-calling model sends. Everything around the model - the graph, the tools node, the messages, the limits - is the real LangChain 1.4.3 code.
Worked example: A tool-using agent in 15 lines.
1 - The question goes in
agent.invoke() starts the graph with one human message. The model node runs first.
"What is the weather in Hyderabad?" through the graph create_agent builds - the steps agent.stream() reported.
Chain vs agent
A chain is a path you fixed in advance. prompt | llm | parser runs the same three steps for every input. An agent is a model that chooses its next step: call a tool, call a different tool, call one again, or answer.
Use a chain when you know the steps. Use an agent when the steps depend on the question - and accept that the path, the number of model calls, and the cost now vary from run to run.
FlowChain: fixed, A -> B -> C. Agent: decided by the model at each step.Who decidesChain: you, when you write it. Agent: the model, at run time.Model callsChain: a known number. Agent: one per step, until it answers.Good forChain: workflows. Agent: tool-using tasks where the steps vary.Agent and executor
The model never runs anything. It decides; something else executes. In classic LangChain that something was the AgentExecutor: it took the agent’s chosen action, ran the tool, fed the observation back, and stopped after an answer or max_iterations.
create_agent keeps the same split inside a graph. The model node is the agent. The tools node runs the tools and appends ToolMessages. The graph runtime loops between them and stops when the model replies without a tool call, or when recursion_limit is reached.
The agentClassic: create_tool_calling_agent(llm, tools, prompt). Now: the model node.The executorClassic: AgentExecutor(agent, tools). Now: the tools node and the graph runtime.Step limitClassic: max_iterations. Now: recursion_limit in the config.Seeing the stepsClassic: verbose=True. Now: agent.stream(..., stream_mode="updates").Input / outputClassic: {"input": ...} -> {"output": ...}. Now: {"messages": [...]} in and out.ImportClassic: langchain_classic.agents. Now: langchain.agents.Inside one run
agent.stream(..., stream_mode="updates") reports each node as it finishes. For "Weather in Delhi?" there were three steps: model (a tool call), tools (Sunny, 34°C), model (the answer). Two model calls, one tool run.
On its second call the model received three messages: the question, its own tool call, and the tool result. That is the whole mechanism - the observation is not magic memory, it is a message in the list, exactly as Module 1 appended "Observation: ..." to the conversation.
The agent chooses the path
Same agent, same two tools, different questions - and different paths. A greeting needed no tool. Weather plus arithmetic needed two rounds. Two cities came back as two tool calls in one reply, run in the same step.
"Hi"No tool. One model call, then the answer."Weather in Hyderabad?"get_weather, then the answer. Four messages."Weather in Mumbai, and 47 * 89?"get_weather, then calculate, then the answer. Two tool rounds."Compare Mumbai and Delhi"Two get_weather calls in one reply; both ToolMessages, then the answer.An agent is not "fully autonomous"
The agent can only choose from the tools you pass in. When the model asked for send_email, which was not in the list, nothing ran; the tools node replied "Error: send_email is not a valid tool, try one of [get_weather, calculate]." and the model had to answer without it.
So the tool list is the agent’s capability boundary. What a tool does with its arguments - and whether it validates them - is still your code, as in Lesson 1.7.
Debugging with Module 1 in your head
When an agent misbehaves, LangChain is rarely broken. Something in the loop you built in Module 1 is going wrong, and you can find it by asking which step. Stream the run and read the messages.
One limit to set yourself: the default recursion_limit is 10,007 (Lesson 2.1). One tool round and an answer needs 4 steps - recursion_limit=3 raised GraphRecursionError, 4 completed. Allow 2 per tool round plus 2.
Wrong tool chosenThe tool names and docstrings - the model chooses from those.Wrong argumentsThe type hints and schema; the tool call’s args in the stream.Keeps loopingThe tool results - is the model getting what it needs? Set recursion_limit.Run crashes in a toolAn exception escaped the tool. Catch it and return the error as text (Lesson 1.10).Fails before the first stepThe model cannot call tools - llama3: "does not support tools".AgentExecutor in older code
Most LangChain tutorials and much existing code use AgentExecutor. In LangChain 1.4, from langchain.agents import AgentExecutor raises ImportError. It still exists in the langchain-classic package (1.0.8): from langchain_classic.agents import AgentExecutor, create_tool_calling_agent. We ran it with the same scripted model and it worked - "Invoking: get_weather with {’city’: ’Hyderabad’}", then the answer.
Read it as the same loop with older names. For new code, use create_agent.
Step-by-step code
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""
weather = {"mumbai": "Rainy, 27°C", "delhi": "Sunny, 34°C", "hyderabad": "Cloudy, 29°C"}
return weather.get(city.lower(), "Unknown city")
agent = create_agent(ChatOllama(model="llama3.1", temperature=0), tools=[get_weather])
result = agent.invoke(
{"messages": [{"role": "user", "content": "What is the weather in Hyderabad?"}]},
{"recursion_limit": 10},
)
print(result["messages"][-1].content)from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
class ScriptedModel(GenericFakeChatModel):
"""Replays the replies a tool-calling model would send (llama3 cannot call tools)."""
def bind_tools(self, tools, **kwargs):
return self
replies = iter([
AIMessage(content="", tool_calls=[{"name": "get_weather", "args": {"city": "Hyderabad"}, "id": "c1"}]),
AIMessage(content="The weather in Hyderabad is cloudy and 29°C."),
])
agent = create_agent(ScriptedModel(messages=replies), tools=[get_weather])
result = agent.invoke({"messages": [{"role": "user", "content": "What is the weather in Hyderabad?"}]})
for message in result["messages"]:
print(message.type, message.content or message.tool_calls)human What is the weather in Hyderabad?
ai [{'name': 'get_weather', 'args': {'city': 'Hyderabad'}, 'id': 'c1', 'type': 'tool_call'}]
tool Cloudy, 29°C
ai The weather in Hyderabad is cloudy and 29°C.replies = iter([
AIMessage(content="", tool_calls=[{"name": "get_weather", "args": {"city": "Delhi"}, "id": "c1"}]),
AIMessage(content="Delhi is sunny, 34°C."),
])
agent = create_agent(ScriptedModel(messages=replies), tools=[get_weather])
for update in agent.stream(
{"messages": [{"role": "user", "content": "Weather in Delhi?"}]},
stream_mode="updates",
):
for node, data in update.items():
message = data["messages"][-1]
print(node, "->", message.type, message.content or message.tool_calls)
# model -> ai [{'name': 'get_weather', 'args': {'city': 'Delhi'}, 'id': 'c1', 'type': 'tool_call'}]
# tools -> tool Sunny, 34°C
# model -> ai Delhi is sunny, 34°C.
print(list(agent.get_graph().nodes))
# ['__start__', 'model', 'tools', '__end__']
# __start__ -> model; model -> tools or __end__; tools -> modelcall 1 human What is the weather in Hyderabad?
call 2 human What is the weather in Hyderabad?
ai tool_calls: get_weather {'city': 'Hyderabad'}
tool Cloudy, 29°C <- the Observation, as a message"Hi" ai Hello! Ask me about weather or arithmetic.
"Weather in Mumbai, and 47 * 89?" ai get_weather {'city': 'Mumbai'}
tool Rainy, 27°C
ai calculate {'expression': '47 * 89'}
tool 4183
ai Mumbai is rainy, 27°C, and 47 * 89 = 4183.
"Compare Mumbai and Delhi weather" ai get_weather Mumbai + get_weather Delhi (one reply)
tool Rainy, 27°C
tool Sunny, 34°C
ai Mumbai: rainy 27°C. Delhi: sunny 34°C.
"Email my boss" ai send_email {'to': 'boss@example.com'}
tool Error: send_email is not a valid tool, try one of [get_weather, calculate].
One tool round + answer: recursion_limit=3 -> GraphRecursionError recursion_limit=4 -> ok
create_agent(ChatOllama(model="llama3"), ...)
ResponseError: registry.ollama.ai/library/llama3:latest does not support tools (status code: 400)# pip install langchain-classic
from langchain_classic.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"), # where tool calls and results go
])
agent = create_tool_calling_agent(llm, [get_weather], prompt) # decides
executor = AgentExecutor(agent=agent, tools=[get_weather], max_iterations=5, verbose=True) # runs the loop
print(executor.invoke({"input": "What is the weather in Hyderabad?"}))
# > Entering new AgentExecutor chain...
# Invoking: `get_weather` with `{'city': 'Hyderabad'}`
# Cloudy, 29°C The weather in Hyderabad is cloudy and 29°C.
# > Finished chain.
# {'input': 'What is the weather in Hyderabad?', 'output': 'The weather in Hyderabad is cloudy and 29°C.'}Tip: Stream before you guess. stream_mode="updates" shows every tool call with its arguments and every result - the same view Module 1 gave you by printing each step.
Watch out: from langchain.agents import AgentExecutor fails in LangChain 1.x. Tutorials that use it are written for the classic API; translate them to create_agent, or install langchain-classic.
Agents at a glance
create_agentBuild the model-tools loop.
create_agent(llm, tools=[get_weather, calculate])
system_promptInstructions sent before the conversation.
create_agent(llm, tools, system_prompt="...")
invokeRun to the end; returns all messages.
agent.invoke({"messages": [...]})The answerThe last message.
result["messages"][-1].content
streamOne update per node as it finishes.
agent.stream(inputs, stream_mode="updates")
recursion_limitThe step limit: 2 per tool round, plus 2.
{"recursion_limit": 10}AgentExecutorThe classic executor, now in langchain-classic.
from langchain_classic.agents import AgentExecutor
Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Give the scripted model a calculate call, then an answer, and print every message.”
“Run "Weather in Mumbai, and 47 * 89?" with stream_mode="updates" and count model calls and tool runs.”
“Script two tool rounds and find the smallest recursion_limit that completes.”
“Script a call to search_google and read the ToolMessage that comes back.”
What usually goes wrong
It was removed from the main package in LangChain 1.x.
✗ from langchain.agents import AgentExecutor✓ from langchain.agents import create_agentThe default recursion_limit is 10,007. A model stuck calling tools will run for a long time before it stops.
✗ agent.invoke(inputs)✓ agent.invoke(inputs, {"recursion_limit": 10})The agent fails before its first step. llama3 does not support tools; llama3.1 does.
✗ ChatOllama(model="llama3")✓ ChatOllama(model="llama3.1")The tool list is its capability boundary. A request for send_email gets an error message, not an email.
The last message says what the model concluded, not how. Print all messages, or stream, to see which tools ran with which arguments.
✗ print(result["messages"][-1].content)✓ for m in result["messages"]:
print(m.type, m.content or m.tool_calls)Key points
- A chain follows a fixed path; an agent chooses its next step.
- The agent decides; the executor runs the tool and loops.
- create_agent builds both: a model node, a tools node, and a loop between them.
- The loop ends when the model replies without a tool call.
- Tool results go back to the model as ToolMessages - Module 1’s observations.
- The agent can only use the tools you give it.
- AgentExecutor is the classic API, now in langchain-classic; use create_agent for new code.
Quick check before you move on
Quiz
- 1.
What stops a create_agent run?
- 2.
How many steps does one tool call plus an answer use?
- 3.
The model asks for send_email, which the agent does not have. What happens?
- 4.
Which create_agent parts correspond to AgentExecutor’s agent and executor?
Interview questions
What is an agent?
An LLM-based system that decides which action to take, uses the tools it has been given, observes the results, and repeats until it can answer.
What is AgentExecutor, and what replaced it?
The classic LangChain runtime: the agent chose actions, AgentExecutor ran them and fed back observations. In LangChain 1.x, create_agent builds the same loop as a graph; AgentExecutor lives on in langchain-classic.
Does the LLM execute Python functions?
No. It requests a tool call; the application runs the function and returns the result as a message.
How would you debug an agent that picks the wrong tool?
Stream the run to see the calls and arguments, then check the tool names and descriptions the model chose from - they are its only information about each tool.
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...
AI
System Design
Backend
- GraphQL8 modules · 69 lessons planned
- Core Python13 modules · 75 lessons planned
- FastAPI5 sections · 20 lessons
- Node.js14 modules · 206 lessons planned
- Node.js Performance7 chapters · 36 topics
- Event Loop Lifecycle6 phases · 3 scenarios
- Docker & Containerization11 modules · 144 lessons planned
- AWS for Developers14 modules · 219 lessons planned
- CI/CD & DevOps Automation10 modules · 134 lessons planned