Hello! Welcome back to our hands-on exploration of LangGraph.
In the last lesson, we successfully built a complete ReAct agent from scratch. You learned how to structure a StateGraph with nodes and a conditional edge to create the fundamental "think, act, observe" loop. Our agent could decide if it needed to use its single tool.
Today, we'll focus on making our agent more capable by adding tool-calling capabilities. Specifically, we will equip our agent with multiple tools, forcing it to reason not just about whether to act, but also which specific action to take. We will also streamline our code by leveraging some of LangGraph's pre-built components, a common practice in real-world development.
By the end of this lesson, you will be able to add any number of custom or pre-built tools to a LangGraph agent, a critical skill for building applications that can interact with diverse external systems.
The Mechanics of Tool Calling
Before we modify our code, let's solidify our understanding of how an LLM interacts with tools. It's a three-step process:
- Tool Definition: You create a function (e.g., in Python) and describe it to the LLM. The most critical part is the description, or
docstring. The LLM never sees your code; it only sees the function name, its arguments, and the description you provide. This is its "API documentation." - Tool Binding: You "bind" the list of available tools to the model. This essentially tells the model, "In addition to generating text, you are now allowed to respond with a request to call one of these specific functions."
- Tool Execution: When the model decides to use a tool, it doesn't execute the function itself. Instead, it outputs a structured message (a
tool_callsobject) containing the name of the tool and the arguments. It's up to your code to catch this message, execute the corresponding function, and pass the result back to the model.
Let's start with a practical guide on defining and adding tools.
Building AI Agents With LangGraph Part 2/4: Adding Tools
The article 'Building AI Agents With LangGraph Part 2/4: Adding Tools' provides an excellent, focused explanation of this process. We'll use it to guide our implementation.
Please read the sections 'Creating tools' and 'Adding web search to your LangGraph agent'. Pay attention to: How the @tool decorator is used and the importance of the Google-style docstring. The distinction between creating a custom tool (like get_weather) and using a pre-made one (DuckDuckGoSearchResults). The model.bind_tools() method, which is the key to making the model aware of the available tools.
From Custom Logic to ToolNode
In our last lesson, we wrote a call_tool function to handle step 3 (Tool Execution). It worked, but it involved writing boilerplate code: looping through the model's tool_calls, looking up the right function, invoking it, and packaging the result into a ToolMessage.
As a software engineer, you know it's best to use battle-tested library components when they're available. LangGraph provides exactly that with the pre-built ToolNode. This node abstracts away all the execution logic. You simply give it a list of your tools, and it handles the rest.
Let's see how this works in practice.
Building AI Agents With LangGraph Part 2/4: Adding Tools
Let's return to the 'Adding Tools' article to see how to integrate ToolNode.
Read the short section titled 'ToolNode'. Notice how it simplifies the process. Instead of a custom function, you just instantiate the ToolNode class with your list of tools and add it to the graph.
Let's Code: Upgrading Our Agent
Now, let's apply these concepts to the agent we built in the previous lesson. We'll modify it to:
- Use two tools: our custom
get_weathertool and a pre-built web search tool. - Replace our custom
call_toolfunction with the more robustToolNode.
Here is the full, updated code. You can replace the code in your main.py file. I'll highlight the key changes below.
First, make sure to add the required dependency for the web search tool:uv pip install langchain-community
import os
from dotenv import load_dotenv
from typing import Annotated, Sequence, TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import BaseMessage, HumanMessage
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchResults # New import
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode # New import
# Load environment variables
load_dotenv()
# --- 1. Define the Agent's State ---
class AgentState(TypedDict):
"""The state of the agent."""
messages: Annotated[Sequence[BaseMessage], add_messages]
# --- 2. Define the Tools ---
# Custom tool from the last lesson
@tool
def get_weather(city: str):
"""Call to get the weather in a specific city."""
if "san francisco" in city.lower():
return "It is currently 20 degrees Celsius and sunny in San Francisco."
elif "tokyo" in city.lower():
return "It is currently 25 degrees Celsius and rainy in Tokyo."
else:
return f"I don't have the weather for {city}."
# New pre-built tool for web search
search_tool = DuckDuckGoSearchResults(max_results=2)
search_tool.name = "web_search" # Giving it a more descriptive name
# Create a list of all tools
tools = [get_weather, search_tool]
# --- 3. Define the Nodes and Graph ---
# Initialize the model and bind the tools
llm = ChatOpenAI(model="gpt-4o", api_key=os.getenv("OPENAI_API_KEY"))
model_with_tools = llm.bind_tools(tools)
# Agent/Reasoner Node (same as last lesson)
def call_model(state: AgentState):
"""Calls the LLM to decide the next step or generate a final response."""
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
# Conditional Edge (same as last lesson)
def should_continue(state: AgentState):
"""Determines the next step: continue with tools or end."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools" # Changed from "continue" to match node name
return "end"
# --- 4. Assemble the Graph ---
# Define a new graph with our state
workflow = StateGraph(AgentState)
# Add the agent node
workflow.add_node("agent", call_model)
# **** CHANGE: Use the pre-built ToolNode ****
tool_node = ToolNode(tools)
workflow.add_node("tools", tool_node)
# Set the entrypoint
workflow.set_entry_point("agent")
# Add the conditional edge
workflow.add_conditional_edges(
"agent",
should_continue,
{
"tools": "tools", # Route to our new ToolNode
"end": END
},
)
# Add the edge that creates the loop
workflow.add_edge("tools", "agent")
# Compile the graph
graph = workflow.compile()
# --- 5. Run the Agent ---
def run_agent(query: str):
inputs = {"messages": [HumanMessage(content=query)]}
print(f"--- Running agent with query: '{query}' ---\n")
for output in graph.stream(inputs, {"recursion_limit": 5}):
for key, value in output.items():
print(f"Output from node '{key}':")
print("---")
print(value)
print("\n---\n")
# Run with a query that should use the weather tool
run_agent("What is the weather in San Francisco?")
# Run with a query that should use the search tool
run_agent("What is LangGraph?")
Key Changes:
- Imports: We imported
DuckDuckGoSearchResultsandToolNode. - Tool List: We now have a
toolslist containing both our customget_weatherfunction and an instance of thesearch_tool. ToolNode: We replaced our entirecall_toolfunction with a single line:tool_node = ToolNode(tools). This node is then added to the graph under the name"tools".- Routing: In
add_conditional_edges, the keycontinuewas changed totoolsto match the name of our new node.
When you run this script, you will see two separate executions. The first will correctly route to the get_weather tool, and the second will use the web_search tool, demonstrating that the LLM is successfully choosing the right tool for the job.
This diagram illustrates the flow we have just built. The LLM decides if a tool is needed, and if so, which one. The graph then routes to the ToolNode to execute it.

To see this multi-tool capability in action, the following video provides a great demonstration.
Building Effective Agents with LangGraph
The 'Building Effective Agents with LangGraph' video from the LangChain team shows a clear example of an agent using multiple tools in sequence to solve a problem. This will reinforce what we've just built.
Watch the segment from 27:32 to 29:02. The presenter gives the agent a task: 'add three and four then take the output and multiply by four'. Observe the stream of messages: The agent first calls the add tool. It receives the result (7). It then uses that result to call the multiply tool. Finally, it provides the final answer. This demonstrates how the agent maintains context through the messages in its state to perform multi-step tasks.
Test your understanding!
Imagine you want to add a third tool to our agent: a simple calculator for multiplication.
@tool
def multiply(a: int, b: int) -> int:
"""Multiplies two numbers."""
return a * b
What are the two essential places in the code you would need to modify to integrate this new multiply tool into the agent's workflow?
Show answer
You would need to modify:
-
The
toolslist: You must add the newmultiplyfunction to this list.tools = [get_weather, search_tool, multiply] -
This list is then used in two places:
- When binding tools to the model:
model_with_tools = llm.bind_tools(tools) - When creating the tool executor node:
tool_node = ToolNode(tools)
Because both
bind_toolsandToolNodereference the sametoolslist, you only need to update the list itself. The rest of the graph structure remains unchanged, highlighting the modularity of this design. - When binding tools to the model:
Conclusion
You have now successfully upgraded your agent with robust tool-calling capabilities. You can give it access to multiple tools, and it can intelligently decide which one to use based on the user's request.
Key Takeaways:
- An agent's power comes from the tools it can access. You can use a mix of custom-built functions and pre-built tools from libraries like LangChain.
- The LLM relies on the tool's
docstringto understand its purpose and how to use it. Clear, descriptive docstrings are essential. - LangGraph's pre-built
ToolNodedrastically simplifies the code required to execute tool calls, handling the necessary boilerplate logic for you. - By adding multiple tools to the
toolslist passed tobind_toolsandToolNode, you expand the agent's capabilities without needing to change the core graph structure.
In our next lesson, we will move beyond the simple "tool or end" decision. We will explore conditional routing, which allows us to create more complex, non-linear workflows where the agent's path through the graph can change dynamically based on its state. This will enable us to build agents that can, for example, route to a human approval step only under certain conditions—a common requirement for production-ready systems.