In our previous lesson, we crafted custom Python functions and transformed them into LangChain "tools" using the @tool decorator. We saw how type hints and docstrings generate a schema and description that an LLM can understand. However, creating a tool is only the first step. The model doesn't yet know it exists or how to use it.
Today, we will connect these two pieces. You will learn how to bind one or more tools to a chat model, empowering it to intelligently decide when to call your custom functions in response to a user's prompt. This is the core mechanism that enables LLMs to interact with external systems and is a fundamental building block for the autonomous agents we'll explore later in the course.
The Tool Calling Workflow
The process of a model using a tool involves a few distinct steps. First, we create the tool (which we did last lesson). Then, we make the model aware of the tool. When a user sends a prompt, the model analyzes it and, if appropriate, decides to use one of the tools it knows about. Instead of generating a text response, it outputs a structured "tool call" request.
The diagram below illustrates this flow. Today, we'll focus on steps 2 and 3: binding the tool to the model and observing the model's decision to call it.

Binding Tools to a Model with .bind_tools()
To make a model "tool-aware," we use the .bind_tools() method. This is a standard interface available on LangChain chat models that support tool calling (like those from OpenAI, Anthropic, and Google). It takes a list of tools and returns a new Runnable model instance with those tools "bound" to it.
When you invoke this new model instance, it will consider the provided tools alongside its internal knowledge to respond to the prompt. If it decides a tool is the best way to answer, its output will reflect that choice.
The official LangChain blog provides a concise explanation of this new, standardized approach.
This article introduces the standardized interface for tool calling in LangChain.
Please read the section titled ChatModel.bind_tools(...). Notice how it accepts a list containing various tool types, including Pydantic classes (which you've seen before) and decorated functions (which we created last lesson).
Hands-On: Invoking a Tool-Bound Model
Let's put this into practice. We'll use the mock financial tools we created in the previous lesson.
- Create a new file in your project:
src/lcel_quickstart/tool_calling.py. - Add the following code. We will import the tools from
tools.py, bind them to an OpenAI model, and invoke the model with a prompt designed to trigger their use.
# src/lcel_quickstart/tool_calling.py
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
# Import the tools we created in the previous lesson
from .tools import get_stock_price, calculate_simple_moving_average
# Load environment variables
load_dotenv()
# 1. Instantiate the base model
model = ChatOpenAI(model="gpt-4o-mini")
# 2. Define the list of tools
tools = [get_stock_price, calculate_simple_moving_average]
# 3. Create a new model instance with the tools bound to it
model_with_tools = model.bind_tools(tools)
# 4. Invoke the model with a prompt that should trigger tool use
prompt = "What is the price of TSLA and what is its 20-day SMA?"
print(f"--- Invoking model with prompt: '{prompt}' ---\n")
response = model_with_tools.invoke(prompt)
# 5. Inspect the response
print("--- Full Response Object ---")
print(response)
print("\n--- Examining Tool Calls ---")
if not response.tool_calls:
print("The model did not decide to use any tools.")
else:
for tool_call in response.tool_calls:
print(f"Tool: {tool_call['name']}")
print(f"Args: {tool_call['args']}")
print("-" * 20)
Now, execute the script from your terminal:uv run python -m src.lcel_quickstart.tool_calling
You should see an output similar to this:
--- Invoking model with prompt: 'What is the price of TSLA and what is its 20-day SMA?' ---
--- Full Response Object ---
AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_abc', 'function': {'arguments': '{"symbol":"TSLA"}', 'name': 'get_stock_price'}, 'type': 'function'}, {'id': 'call_def', 'function': {'arguments': '{"symbol":"TSLA","window_size":20}', 'name': 'calculate_simple_moving_average'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', ...}, id='...', tool_calls=[{'name': 'get_stock_price', 'args': {'symbol': 'TSLA'}, 'id': 'call_abc'}, {'name': 'calculate_simple_moving_average', 'args': {'symbol': 'TSLA', 'window_size': 20}, 'id': 'call_def'}])
--- Examining Tool Calls ---
Tool: get_stock_price
Args: {'symbol': 'TSLA'}
--------------------
Tool: calculate_simple_moving_average
Args: {'symbol': 'TSLA', 'window_size': 20}
--------------------
Notice a few critical things here:
- The
contentof theAIMessageis empty. The model's response is not a sentence, but a request to perform actions. - The
response.tool_callsattribute contains a list of dictionaries. - Each dictionary represents a single tool call, specifying the
nameof the tool and theargsit needs, extracted directly from the prompt. - The model correctly identified the need for two separate tool calls from a single prompt and even parsed the
window_sizefor the SMA function.
The official documentation provides another concise, hands-on example of this entire flow.
This official documentation page provides practical code snippets for tool calling.
Focus on the top of the Tool calling section. It shows a simple get_weather example that reinforces the pattern we just implemented: define a tool, bind it, invoke the model, and inspect the tool_calls attribute.
The Model Suggests, You Execute
This is a crucial concept to grasp, and a common point of confusion. When we invoke model_with_tools, the LLM does not run the get_stock_price function. The model itself is a pure text-in, text-out system (in this case, structured text). It cannot execute your Python code, access your file system, or call an external API.
Instead, it acts as a reasoning engine. It analyzes the prompt and the available tools, and its output is a structured suggestion or plan. It's up to our application code to receive this suggestion, execute the actual function, and (as we'll see in the next module) send the result back to the model.
The following video explains this distinction very clearly.
This video from A.I Engineering BootCamp provides a great conceptual overview of tool calling.
Watch the introduction from the beginning. The presenter diagrams the process and emphasizes that the model's job is to suggest which tool to use and extract the arguments, not to execute the function itself.
bind_tools vs. with_structured_output
In a previous lesson, you used the .with_structured_output() method to force the model to respond in a specific Pydantic schema. You might be wondering how that relates to .bind_tools(). Under the hood, for many models, with_structured_output is actually a specialized application of tool calling.
The key difference is the model's flexibility:
.with_structured_output(Schema): Use this when you want to force the model's output into a specific data structure, every single time. It's best for information extraction tasks where the desired output format is rigid. The model must return the specified schema..bind_tools([tool1, tool2]): Use this when you want to give the model options. It can choose to call one of the tools, multiple tools, or none at all and just generate a text response. This flexibility is essential for building conversational agents that can decide how to best handle a user's request.
The LangChain blog post you reviewed earlier has a short section that clarifies this distinction perfectly.
Let's revisit the blog post to clarify the use cases.
Read the section titled with_structured_output. It directly compares the two methods and their intended applications.
Conclusion
In this lesson, you've taken a significant step from simply defining tools to actively integrating them into the LLM's reasoning process. You now have the power to give a model a set of capabilities and have it intelligently decide when and how to use them.
Here are the key takeaways:
- The
.bind_tools()method attaches a list of tools to a chat model, making it "tool-aware". - When a tool-bound model is invoked, it can choose to respond with a structured tool call request instead of plain text.
- The model's response is captured in the
AIMessage.tool_callsattribute, which contains a list of tools to be called with their corresponding arguments. - Crucially, the model only suggests the tool call; your application code is responsible for the actual execution.
We've seen the model generate a plan. The next logical step is to execute that plan. In our next lesson, we will begin building our first autonomous agent. You'll learn how to use an AgentExecutor to automatically handle the tool-calling loop: invoking the model, executing the suggested tool calls, and feeding the results back to the model to generate a final, synthesized answer.