LangChainLangChain 1.4 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

bind_tools: asking for a tool

bind_tools is a model method that gives the model a list of tools and returns a copy that can answer with a request to run one, delivered as tool_calls.

Last updated: 27 Sep, 2026 · LangChain 1.4

Lesson 3's model could only say it had no way to look an order up. To fetch a real answer it needs three things: somewhere to keep the tools it is given, a reply that asks for one, and a way to read the tool's result when it comes back.

The bind_tools method

python
model_with_tools = model.bind_tools([my_tool])   # a copy that knows the tools
reply = model_with_tools.invoke(messages)        # reply.tool_calls holds any requests
result = my_tool.invoke(reply.tool_calls[0])     # run the tool, get a ToolMessage back

The imports

Start with the imports the model class needs.

python
import re

from langchain.chat_models import BaseChatModel
from langchain.messages import AIMessage, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResult

Adding bind_tools to the model

The class keeps the tools it was given. bind_tools returns a copy holding the list, the same method every hosted chat model has, and _generate hands the decision to a method called decide.

python
class ShopModel(BaseChatModel):
    tools: list = []                       # the tools bind_tools was given

    @property
    def _llm_type(self):
        return "shop"

    def bind_tools(self, tools, **kwargs):
        return self.model_copy(update={"tools": tools})   # a copy holding the tools

    def _generate(self, messages, stop=None, run_manager=None, **kwargs):
        message = self.decide(messages)    # decide picks the reply
        return ChatResult(generations=[ChatGeneration(message=message)])

Reading tool results

The start of decide checks for tool results. When the conversation ends with ToolMessages, the model answers with their text instead of asking again.

python
    def decide(self, messages):
        results = []
        for m in reversed(messages):
            if not isinstance(m, ToolMessage):
                break
            results.insert(0, m.text)
        if results:
            return AIMessage(" ".join(results))   # tool results are back: answer with them

Building the tool call

The rest of decide is the new idea. When the message names an order and a matching tool was bound, the reply has no text and one tool call per order: the tool's name, its arguments, and an id.

python
        text = messages[-1].text
        orders = re.findall(r"\b[A-Z]\d+\b", text)          # order ids like A17
        tool = "refund_order" if "refund" in text.lower() else "lookup_order"
        if orders and tool in [t.name for t in self.tools]:
            calls = [{"name": tool, "args": {"order_id": o}, "id": f"call_{o}"}
                     for o in orders]
            return AIMessage("", tool_calls=calls)           # ask for the tool, no text
        if orders:
            return AIMessage(f"I have no way to look up {orders[0]} yet.")
        return AIMessage("Hello. Which order is this about?")

Save this as shop_model.py, replacing lesson 3's version, so the run below imports the tool-aware model.

One round trip by hand

Bind one tool and send a question. The model replies with a request, not text.

Example
from langchain.messages import HumanMessage
from shop_model import ShopModel
from tools import lookup_order

model = ShopModel().bind_tools([lookup_order])
question = HumanMessage("Where is my order A17?")

request = model.invoke([question])
print(request.tool_calls)

No text, one tool call: the model asked for lookup_order with order_id set to A17, and it is up to your code to run it.

Now run that tool and send its result back for the answer.

Example
call = request.tool_calls[0]
result = lookup_order.invoke(call)
print(type(result).__name__, result.text)

answer = model.invoke([question, request, result])
print(answer.text)

Reading the tool call and answer

  • bind_tools returns a copy of the model that holds the tool list; the model itself is unchanged.
  • When the message names an order and a matching tool is bound, the reply has empty text and one tool call per order: name, args, and id.
  • Invoking a tool with the whole tool call, not only its arguments, returns a ToolMessage tagged with the call's id.
  • Sent back with the question and the request, the ToolMessage gives the model what it needs to write the answer.

A text reply vs a tool call

Text replyTool call
reply.textThe answer in wordsEmpty
reply.tool_callsEmpty listOne dict per tool: name, args, id
Your code thenShows it to the userRuns the tool and calls the model again

When to bind tools

  • Any assistant that must fetch data before answering: an order status, a price, a record.
  • Letting the model pick which of several tools to call, and with what arguments.
Watch out. Invoke the tool with the whole tool call, not only its arguments. The returned ToolMessage carries the call's id, and the model matches its answer to the request by that id.
Try it yourself
  • Bind no tools and invoke the same question. Which reply do you get?
  • Ask "Where are A17 and C40?" and count the tool calls.
  • Print result.tool_call_id and compare it with call["id"].

You understood something today that you didn't yesterday.