Tools let models act and fetch fresh data; they are the foundation of every agent.
A tool is a function plus a schema the model can read. The @tool decorator builds it from the function's name, type hints and docstring:
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city."""The docstring becomes the description and the type hints become the argument schema, so write both for the model. tool.name, tool.description and tool.args show what the model will see. Tools are runnables: get_weather.invoke({"city": "Oslo"}).
Tool calling works in steps:
1. model.bind_tools([get_weather]) sends tool schemas with each request. 2. The model may respond with an AIMessage whose tool_calls list contains {"name", "args", "id"} entries instead of (or besides) text. 3. Your code runs each tool and appends a ToolMessage(content=result, tool_call_id=id). Invoking a tool with the whole tool call (tool.invoke(call)) returns a ready ToolMessage. 4. Call the model again with the extended history so it can use the results.
tool_choice="any" or a tool name forces tool use. Models may request several tools at once (parallel tool calls).
Agents automate this loop. Writing it once by hand shows exactly what they do. Tool errors should be returned as informative text rather than crashing, so the model can recover.
Example
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
print(multiply.name, multiply.args)
# multiply {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
model = init_chat_model("openai:gpt-5-mini").bind_tools([multiply])
messages = [HumanMessage("What is 12.5% of 3 times 48? Use the tool for 3 x 48.")]
ai = model.invoke(messages)
messages.append(ai)
for call in ai.tool_calls: # e.g. {'name': 'multiply', 'args': {'a': 3, 'b': 48}, ...}
messages.append(multiply.invoke(call)) # returns a ToolMessage with tool_call_id
final = model.invoke(messages)
print(final.text) # 3 x 48 = 144, and 12.5% of that is 18.Remember
@toolbuilds a schema from the name, type hints and docstring- Bound models return
tool_calls; you answer each with a ToolMessage - Invoking a tool with a tool call returns a ready ToolMessage
Watch out for
- Writing vague docstrings like 'does stuff', so the model calls the wrong tool or passes wrong arguments.