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
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 backThe imports
Start with the imports the model class needs.
import re
from langchain.chat_models import BaseChatModel
from langchain.messages import AIMessage, ToolMessage
from langchain_core.outputs import ChatGeneration, ChatResultAdding 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.
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.
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 themBuilding 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.
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.
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.
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_toolsreturns 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
ToolMessagetagged with the call's id. - Sent back with the question and the request, the
ToolMessagegives the model what it needs to write the answer.
A text reply vs a tool call
| Text reply | Tool call | |
|---|---|---|
reply.text | The answer in words | Empty |
reply.tool_calls | Empty list | One dict per tool: name, args, id |
| Your code then | Shows it to the user | Runs 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.
ToolMessage carries the call's id, and the model matches its answer to the request by that id.Related
- Previous: Tools: a function the model can call
- Next: create_agent: the agent loop
- Reference: Tool calling
- 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_idand compare it withcall["id"].
You understood something today that you didn't yesterday.