← Back to Agentic AI map
Lesson 2.7 · Building Agents with LangChain

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.

agents

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.

request flowOne run of the weather agentstep 1 / 5

1 - The question goes in

agent.invoke() starts the graph with one human message. The model node runs first.

messages
1 (human)
tools available
get_weather, calculate
next node
model
steps used
0

"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.

Chain or agent
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.

AgentExecutor and create_agent
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.

Paths through the same agent
"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.

Symptom -> where to look
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

A tool-using agent in 15 lines
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)
Running it locally - a scripted model in place of the LLM
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)
Output
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.
Watching the loop: stream and the graph
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 -> model
What the model saw on each call
call 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
Paths, limits and boundaries - checked in LangChain 1.4.3
"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)
The same agent with AgentExecutor (langchain-classic)
# 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_agent

Build the model-tools loop.

create_agent(llm, tools=[get_weather, calculate])
system_prompt

Instructions sent before the conversation.

create_agent(llm, tools, system_prompt="...")
invoke

Run to the end; returns all messages.

agent.invoke({"messages": [...]})
The answer

The last message.

result["messages"][-1].content
stream

One update per node as it finishes.

agent.stream(inputs, stream_mode="updates")
recursion_limit

The step limit: 2 per tool round, plus 2.

{"recursion_limit": 10}
AgentExecutor

The 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.

Script a path

“Give the scripted model a calculate call, then an answer, and print every message.”

Stream it

“Run "Weather in Mumbai, and 47 * 89?" with stream_mode="updates" and count model calls and tool runs.”

Find the limit

“Script two tool rounds and find the smallest recursion_limit that completes.”

Ask for a missing tool

“Script a call to search_google and read the ToolMessage that comes back.”

What usually goes wrong

Importing AgentExecutor from langchain.agents

It was removed from the main package in LangChain 1.x.

✗ from langchain.agents import AgentExecutor
✓ from langchain.agents import create_agent
Leaving the step limit at the default

The 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})
Using a model that cannot call tools

The agent fails before its first step. llama3 does not support tools; llama3.1 does.

✗ ChatOllama(model="llama3")
✓ ChatOllama(model="llama3.1")
Expecting the agent to do what it has no tool for

The tool list is its capability boundary. A request for send_email gets an error message, not an email.

Reading only the final answer

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

What is the difference between a chain and an agent?
A chain follows a predefined sequence. An agent decides at each step what to do next.
Who decides which tool to use?
The model - the agent.
Who actually executes the Python function?
Your application: the executor - in create_agent, the tools node.
What is the role of the executor?
It runs the action the agent chose, feeds the result back, and keeps looping until an answer or a limit.
What happens after a tool returns its result?
The result is added to the conversation as a ToolMessage and the model is called again to decide what to do next.
Why build the ReAct loop by hand in Module 1 first?
LangChain hides the loop, parsing and orchestration. Having built them, you can read and debug what the framework is doing.

Quiz

  1. 1.

    What stops a create_agent run?

  2. 2.

    How many steps does one tool call plus an answer use?

  3. 3.

    The model asks for send_email, which the agent does not have. What happens?

  4. 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...