Tool Calling#
Large Language Models are powerful text processors, but they have inherent limitations: They can’t perform precise calculations, access real-time information, or interact with external systems directly. Tool calling solves this by allowing LLMs to invoke Python functions that extend their capabilities beyond text generation.
With tool calling, you can build applications where LLMs can:
Perform accurate mathematical calculations
Retrieve information from databases or APIs
Interact with external services (weather, search engines, etc.)
Execute custom business logic
Access and process real-time data
How Tool Calling Works#
The tool calling workflow creates a bridge between the LLM and your Python functions:
You define Python functions and make them available as tools
The LLM analyzes user queries and decides when to use these tools
The LLM generates appropriate function calls with parameters
Your code executes the functions and returns results
The LLM incorporates these results into its final response
This pattern enables LLMs to go beyond their training data and perform actions in the real world.
Hint
Tool calling is the foundation for building agents (covered in the next recipe), which can use multiple tools iteratively to accomplish complex, multi-step tasks.
Let’s start by defining our first tool!
from dotenv import find_dotenv, load_dotenv
load_dotenv(find_dotenv())
True
What Are Tools?#
In LangChain, a tool is a Python function that an LLM can invoke. When you make a function available as a tool, the LLM receives a schema describing the function’s structure, which it uses to decide when and how to call it.
Each tool consists of four key components:
Name: An identifier that the model uses to reference the tool (typically derived from the function name)
Description: A natural language explanation that guides the LLM on when and how to use the tool
Parameters: The function’s arguments, including their types and purposes
Return type: The type of value the function returns
For tools to work effectively, LLMs need clear information about what each tool does and how to use it. This is where Python’s documentation features become critical:
Docstrings: The first line of your function’s docstring becomes the tool’s description. This is what the LLM reads to understand when to use your tool. Clear, specific descriptions help the model make better decisions.
Type hints: Type annotations on parameters and return values define the tool’s schema. They tell the LLM what kind of arguments to provide and what to expect back. Without type hints, the LLM may not know how to properly invoke your tool.
Note
Think of docstrings and type hints as the “instructions” you’re giving to the LLM. The more precise and descriptive they are, the better the LLM will be at using your tools correctly!
Let’s see how to create a tool in practice.
Defining Your First Tool#
LangChain provides the @tool decorator to convert ordinary Python functions into tools that LLMs can invoke. Let’s create a simple multiplication tool:
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers.
Always use this tool when trying to multiply numbers.
"""
return a * b
# The decorator turns the function into a Tool object
print(f"Type: {type(multiply)}")
print(f"Tool name: {multiply.name}")
print(f"Tool description: {multiply.description}")
print(f"Tool parameters: {multiply.args}")
Type: <class 'langchain_core.tools.structured.StructuredTool'>
Tool name: multiply
Tool description: Multiply two numbers.
Always use this tool when trying to multiply numbers.
Tool parameters: {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
When we apply the @tool decorator, LangChain automatically extracts metadata from our function:
The function name (
multiply) becomes the tool’s identifierThe docstring (
"Multiply two numbers.") becomes the description the LLM seesThe type hints (
a: int, b: int -> int) define the parameter schema
The args property shows the JSON schema that describes the tool’s parameters to the LLM. This schema is what enables the model to generate valid function calls with the correct argument types.
Hint
Keep your tool descriptions concise but specific. You can include information on the appropriate time to use this tool as part of this description!
Binding Tools to Models#
Creating a tool is only the first step. To make it available to an LLM, we need to bind it to the model using the bind_tools() method. This will ensure that the tool’s schema (name, description, and parameter definitions) is send to to the model as part of any invocation:
from langchain_dartmouth.llms import ChatDartmouth
model = ChatDartmouth(model_name="openai.gpt-oss-120b", temperature=0.0)
tools = [multiply]
model_with_tools = model.bind_tools(tools)
Now the model is aware of the multiply tool and its capabilities. When you send a message to model_with_tools, the LLM can choose to:
Respond directly with text if it can answer without tools
Call the tool if it needs to perform a calculation or action
The key insight is that the model decides when to use tools based on the user’s query and the tool descriptions. The LLM won’t actually execute the tool-that’s still your responsibility. Tool binding simply makes the model aware that these functions exist and how to call them.
Note
Not all models support tool calling. You can check which models have this capability by running ChatDartmouth.list() and looking for "tool_calling" in the capabilities list for each model.
Let’s see the complete workflow in action!
The Tool Calling Workflow#
Now let’s walk through the complete tool calling process step-by-step. The workflow involves a conversation between the user, the model, and the tool:
User sends a message requiring tool use
Model analyzes the query and decides to invoke a tool
Extract the tool call from the model’s response
Execute the tool with the provided arguments
Send the tool result back to the model as a new message
Model synthesizes a final natural language response
Let’s implement this workflow:
from langchain_core.messages import HumanMessage
# Step 1: Create initial message
messages = [HumanMessage("What is 5 times 3?")]
# Step 2: Model responds with tool call
response = model_with_tools.invoke(messages)
messages.append(response)
# Step 3 & 4: Execute tool if called
if tool_calls := response.tool_calls:
for tool_call in tool_calls:
# Find the matching tool
fn, *_ = [tool for tool in tools if tool.name == tool_call["name"]]
# Execute it and create tool message
tool_msg = fn.invoke(tool_call)
messages.append(tool_msg)
# Step 5 & 6: Get final response
final_response = model_with_tools.invoke(messages)
messages.append(final_response)
# Display the conversation
for message in messages:
message.pretty_print()
================================ Human Message =================================
What is 5 times 3?
================================== Ai Message ==================================
Tool Calls:
multiply (chatcmpl-tool-ab9be1d1c78266a0)
Call ID: chatcmpl-tool-ab9be1d1c78266a0
Args:
a: 5
b: 3
================================= Tool Message =================================
Name: multiply
15
================================== Ai Message ==================================
5 × 3 = 15.
Let’s examine the conversation flow that just occurred:
Human Message: The user’s question (
"What is 5 times 3?")AI Message (first): Contains the tool call with arguments
a=5, b=3—the model decided to use themultiplytoolTool Message: The result of executing
multiply(5, 3), which is15AI Message (final): The natural language response incorporating the tool result
Notice that the model doesn’t just return the raw number “15”. Instead, it contextualizes the result in natural language.
Hint
The tool_calls attribute on the AI message contains a list of tool invocations. A model can request multiple tool calls in a single response! We use the walrus operator (:=) to both check if the list exists and assign it to a variable in one line.
Working with Multiple Tools#
When you bind multiple tools to a model, the LLM analyzes the user’s query and selects the most appropriate tool based on the tool descriptions. This allows you to build systems with specialized capabilities for different tasks.
Let’s create a calculator with multiple operations:
@tool
def add(a: int, b: int) -> int:
"""Add two numbers together. Always use this tool when trying to add numbers."""
return a + b
@tool
def subtract(a: int, b: int) -> int:
"""Subtract the second number from the first. Always use this tool when trying to subtract numbers."""
return a - b
@tool
def divide(a: float, b: float) -> float:
"""Divide the first number by the second. Always use this tool when trying to divide numbers."""
if b == 0:
return "Error: Division by zero"
return a / b
# Bind all tools
tools = [add, subtract, multiply, divide]
model_with_tools = model.bind_tools(tools)
Now let’s test the model’s ability to select the correct tool for different queries:
# Test with various math questions
test_queries = [
"What is 35 plus 27?",
"Calculate 100 divided by 4",
"What is 50 minus 18?",
]
for query in test_queries:
print(f"\nQuery: {query}")
print("-" * 50)
messages = [HumanMessage(query)]
response = model_with_tools.invoke(messages)
messages.append(response)
if tool_calls := response.tool_calls:
for tool_call in tool_calls:
print(f"Tool called: {tool_call['name']}")
print(f"Arguments: {tool_call['args']}")
fn, *_ = [tool for tool in tools if tool.name == tool_call["name"]]
tool_msg = fn.invoke(tool_call)
messages.append(tool_msg)
final_response = model_with_tools.invoke(messages)
messages.append(final_response)
print(f"Final answer: {final_response.content}")
Query: What is 35 plus 27?
--------------------------------------------------
Tool called: add
Arguments: {'a': 35, 'b': 27}
Final answer: The sum of 35 and 27 is **62**.
Query: Calculate 100 divided by 4
--------------------------------------------------
Tool called: divide
Arguments: {'a': 100, 'b': 4}
Final answer: The result of \(100 \div 4\) is **25**.
Query: What is 50 minus 18?
--------------------------------------------------
Tool called: subtract
Arguments: {'a': 50, 'b': 18}
Final answer: The result is **32**.
The model successfully chooses the correct tool for each query:
“plus” triggers the
addtool“divided by” triggers the
dividetool“minus” triggers the
subtracttool
The model’s tool selection is based on semantic understanding of the query combined with the tool descriptions. Clear, descriptive tool names and docstrings help the model make accurate choices.
Connecting the Concepts#
Tool calling is a foundational pattern that connects to several other concepts in this cookbook:
Agents: Agents use tools iteratively to accomplish complex, multi-step tasks. While this recipe shows single tool invocations, agents can chain multiple tool calls together to solve problems that require reasoning and planning.
Chains: Tools can be incorporated into LangChain chains, allowing you to build sophisticated pipelines that combine LLM reasoning with external computations and data retrieval.
Structured Output: Both tool calling and structured output use schemas to constrain LLM outputs. Tool calling focuses on function invocation, while structured output focuses on data formatting.
In the next recipe, we’ll see how agents build on tool calling to create autonomous systems that can use multiple tools strategically to accomplish user goals.
Summary#
In this recipe, we learned how to extend LLM capabilities through tool calling.
Tool calling allows LLMs to invoke Python functions
The
@tooldecorator converts Python functions into LangChain tools by extracting metadata from function signatures and docstringsbind_tools()makes tools available to a model by sending their schemas (names, descriptions, parameters) with every messageThe tool calling workflow involves: user query → model decides to call tool → tool execution → model synthesizes response
Clear docstrings and type hints are essential because they guide the LLM’s understanding of when and how to use tools
Multiple tools enable specialized capabilities, with the model selecting appropriate tools based on semantic understanding
Error handling should return descriptive messages rather than raising exceptions
Tool calling is the foundation for building agents and other advanced LLM applications. In the next recipe, we’ll explore how agents use tools iteratively to accomplish complex tasks.