Chapter 8: Tool Calling
Up until now, our Large Language Models have been isolated brains floating in a void. They can read the State, reason about it, and extract structured data, but they cannot interact with the world.
If you ask an LLM for the current stock price of Apple, it will hallucinate because its training data is static. It cannot browse the internet, it cannot query your live database, and it cannot execute mathematical equations reliably.
In this chapter, we will give the LLM arms and legs. We will explore how to define tools, how to bind them to a model, and how to execute those tools inside a LangGraph node.
8.1 Why Agents Need Tools
An Agent is simply an LLM equipped with tools and Control Flow.
When an LLM is given tools, the fundamental way it interacts with you changes. Instead of immediately returning a string answer, the LLM can pause, analyze the tools it has available, and return a "Tool Call Request."
It is saying: "I don't know the answer yet, but if you execute this specific tool with these specific arguments and give me the result, I can figure it out."
8.2 Defining Tools
A tool in LangChain is just a Python function wrapped in a schema. The easiest way to create a tool is using the @tool decorator.
from langchain_core.tools import tool
# The docstring and type hints are MANDATORY.
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers together."""
return a * b
tools = [multiply]
What Does The LLM Actually Receive?
The LLM does not execute your Python code, and it cannot read your source code. When you bind a tool, the LLM simply receives a JSON schema describing the tool.
Conceptually, the LLM sees this:
{
"name": "multiply",
"description": "Multiply two integers together.",
"parameters": {
"a": "integer",
"b": "integer"
}
}
This is why explicit docstrings and strict type hints are non-negotiable. The LLM relies entirely on that schema to understand what the tool does and how to request it.
8.3 bind_tools()
Once you have defined your tools, you must explicitly bind them to your model so it receives the schemas.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
# Bind the tools!
llm_with_tools = llm.bind_tools(tools)
8.4 Understanding AIMessage.tool_calls
Let's see what happens when we invoke the bound model with a question that requires a tool.
response = llm_with_tools.invoke("What is 453 multiplied by 34?")
# Notice that the content string is EMPTY!
print("Content:", response.content) # Output: ""
# Instead, the model returns a structured payload!
print("Tool Calls:", response.tool_calls)
# Output:
# [{'name': 'multiply', 'args': {'a': 453, 'b': 34}, 'id': 'call_abc123'}]
The LLM did not do the math. It returned a structured payload instructing us to do the math.
Multiple Tool Calls: Modern models are highly intelligent. If you ask, "What is the weather in Paris and Tokyo?", the model can return an array containing two separate tool calls in a single response.
8.4.5 The ToolMessage
To complete the conversation loop, we must execute the requested Python function and hand the result back to the LLM. We do this by wrapping the result in a ToolMessage.
This completes the message hierarchy we learned in Chapter 6:
HumanMessage: User speaksAIMessage(withtool_calls): Model reasons and requests a toolToolMessage: Application executes tool and returns the resultAIMessage(withcontent): Model reads the result and gives the final answer.
8.5 The ToolNode
In a basic Python script, you would write a manual for loop to extract the tool calls, run the functions, create the ToolMessages, and invoke the LLM again.
LangGraph automates this entirely using the pre-built ToolNode.
When the Graph routes to the ToolNode, it automatically inspects the latest model response, extracts any tool calls, executes the corresponding Python functions, and automatically appends the ToolMessage results back into the State.
from langgraph.prebuilt import ToolNode
# Create the node
tool_node = ToolNode(tools=[multiply])
8.6 Structured Output vs Tool Calling
Before we build our graph, we must establish a critical distinction. Beginners frequently confuse .with_structured_output() (Chapter 7) with .bind_tools().
Structured Output (with_structured_output)
- Goal: Return Data.
- Example: Extracting a customer's name from an email.
- Result: A fully instantiated Pydantic object (e.g.,
Customer(name="Sarah")).
Tool Calling (bind_tools)
- Goal: Take Action.
- Example: Sending an email, searching the web, or querying a database.
- Result: A
tool_callspayload requesting the application to do work.
8.7 Building a Web Search & Calculator Agent
Let's build a functional multi-tool agent graph. We will give the agent the ability to multiply numbers and search the web (using a mock function).
Step 1: Tools and State
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
from langchain_core.messages import AnyMessage
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
@tool
def search_web(query: str) -> str:
"""Search the web for real-time information."""
return "The capital of France is Paris."
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
tools = [search_web, multiply]
llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools)
Step 2: The Nodes
from langgraph.prebuilt import ToolNode
# The LLM Node
def call_model(state: AgentState):
response = llm.invoke(state["messages"])
return {"messages": [response]}
# The Tool Node
tool_node = ToolNode(tools=tools)
Step 3: The Conditional Router
We need an edge that decides if the LLM requested a tool, or if it generated a final text answer.
def should_continue(state: AgentState):
last_message = state["messages"][-1]
# If the LLM payload contains tool_calls, route to the tools!
if last_message.tool_calls:
return "tools"
# Otherwise, stop!
return END
Step 4: Build the Topology
from langgraph.graph import StateGraph, START, END
builder = StateGraph(AgentState)
builder.add_node("agent", call_model)
builder.add_node("tools", tool_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue)
# AFTER the tools execute, loop BACK to the agent so it can read the results!
builder.add_edge("tools", "agent")
app = builder.compile()
Step 5: Execute
from langchain_core.messages import HumanMessage
result = app.invoke({
"messages": [HumanMessage(content="What is 12 multiplied by 5?")]
})
print("\n--- Final Messages ---")
for msg in result["messages"]:
msg_type = msg.__class__.__name__
print(f"[{msg_type}]: {msg.content}")
Trace Output:
[HumanMessage]: What is 12 multiplied by 5?[AIMessage]: (Empty string, but requests 'multiply(12, 5)')[ToolMessage]: 60[AIMessage]: 12 multiplied by 5 is 60.
8.8 Common Tool Failures
As you begin building Agents with tools, watch out for these failures:
- Hallucinated Arguments: The LLM might try to call
search_web(query="cats", engine="google"), but your Python function only acceptsquery. It will crash. Ensure your docstrings are perfectly clear. - Missing Tool Implementations: If the LLM requests a tool named
"search_wikipedia", but you forgot to pass it to theToolNode, the node will throw an error. - Tool Output Too Large: If a tool returns a 50,000-word Wikipedia article inside a
ToolMessage, it will blow out the LLM's context window on the next loop.
8.9 Summary
By binding tools to the LLM and routing execution through LangGraph's cyclic topology, the LLM is no longer just answering questions—it is independently formulating plans, taking actions, observing results, and synthesizing final conclusions.
However, you still do not fully understand why the loop works, or how to handle multi-step reasoning failures. In the next chapter, we will master Agent Loops and ReAct Workflows.