@tool decorator: wrapping a Python function
Why this matters
Agents need to know what tools exist, what they do, and what arguments they accept. The <code>@tool</code> decorator automatically extracts function signatures and docstrings, making your code discoverable to LLM-powered agents without manual schema definitions.
Explanation
What it is: The @tool decorator from langchain_core.tools wraps a Python function so that LLM agents can see it, understand its purpose, and call it with the right arguments. It reads your function's name, docstring, and type hints to build tool metadata. How it works mechanically: When you decorate a function with @tool, LangChain introspects the function's signature and docstring. The first line of the docstring becomes the tool description, and parameter type hints become the schema. This metadata is bundled into a StructuredTool object that agents can bind to and invoke during execution. When to use it: Use @tool whenever an agent (like ReAct or LangGraph) needs to call custom logic: database queries, API calls, calculations, or real-time data retrieval. The decorator handles all the schema generation, keeping your code DRY and your agent's tool definitions synchronized with actual function behavior.
Analogy
Think of <code>@tool</code> as labeling a tool in a toolbox. The decorator is the label: it tells the LLM (the worker) what the tool does, what inputs it needs (hammer head size, nail length), and what output it produces. Without the label, the LLM has no idea the tool exists or how to use it.
Code
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import json
@tool
def calculate_shipping_cost(weight_kg: float, distance_km: float) -> float:
"""Calculate shipping cost based on weight and distance.
Uses $0.50 per kg plus $0.10 per km.
"""
return weight_kg * 0.50 + distance_km * 0.10
@tool
def get_user_by_id(user_id: int) -> dict:
"""Fetch user details from database by user ID.
Returns a dict with name, email, and account_status.
"""
mock_users = {
1: {"name": "Alice", "email": "alice@example.com", "account_status": "active"},
2: {"name": "Bob", "email": "bob@example.com", "account_status": "inactive"},
}
return mock_users.get(user_id, {"error": "User not found"})
if __name__ == "__main__":
print("Tool 1: calculate_shipping_cost")
print(f"Name: {calculate_shipping_cost.name}")
print(f"Description: {calculate_shipping_cost.description}")
print(f"Args schema: {calculate_shipping_cost.args}")
print(f"Direct call: {calculate_shipping_cost.invoke({'weight_kg': 5.0, 'distance_km': 100.0})}")
print()
print("Tool 2: get_user_by_id")
print(f"Name: {get_user_by_id.name}")
print(f"Description: {get_user_by_id.description}")
print(f"Args schema: {get_user_by_id.args}")
print(f"Direct call: {get_user_by_id.invoke({'user_id': 1})}")
print()
print("Binding tools to LLM:")
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [calculate_shipping_cost, get_user_by_id]
llm_with_tools = llm.bind_tools(tools)
print(f"LLM now has {len(tools)} tools bound.")
print(f"Tool names: {[t.name for t in tools]}") Tool 1: calculate_shipping_cost
Name: calculate_shipping_cost
Description: Calculate shipping cost based on weight and distance.
Uses $0.50 per kg plus $0.10 per km.
Args schema: {'weight_kg': {'title': 'Weight Kg', 'type': 'number'}, 'distance_km': {'title': 'Distance Km', 'type': 'number'}}
Direct call: 52.0
Tool 2: get_user_by_id
Name: get_user_by_id
Description: Fetch user details from database by user ID.
Returns a dict with name, email, and account_status.
Args schema: {'user_id': {'title': 'User Id', 'type': 'integer'}}
Direct call: {'name': 'Alice', 'email': 'alice@example.com', 'account_status': 'active'}
Binding tools to LLM:
LLM now has 2 tools bound.
Tool names: ['calculate_shipping_cost', 'get_user_by_id'] What just happened?
The <code>@tool</code> decorator inspected each function's signature, type hints, and docstring, then created a <code>StructuredTool</code> object with metadata (name, description, argument schema). We then invoked each tool directly using <code>.invoke()</code> with a dict of arguments, and bound both tools to an LLM instance so the model can later decide to call them during agent execution.
Common gotcha
The docstring format matters: only the first line (before any blank line) becomes the tool description. Everything after the first blank line is ignored. Many developers write multi-line docstrings and assume all of it becomes the description, then are confused when the agent only sees the first sentence. Also, type hints are required: @tool cannot infer argument types without them, and the LLM needs explicit types to construct valid calls.
Error recovery
TypeError: 'NoneType' object is not subscriptableMissing type hints warningTool not appearing in agentExperienced dev note
The @tool decorator is pure metadata generation: it does not add retry logic, error handling, or rate limiting. In production, wrap your decorated function in a try/except and return structured errors (e.g., {"error": "message"}) so the agent can handle failures gracefully. Also, be explicit about what your docstring promises: if it says 'fetches from the database,' make sure it actually does, because the LLM will call it based on that contract. Broken tool promises lead to agent hallucination.
Check your understanding
You have a function decorated with @tool that takes three parameters: user_id: int, limit: int, and include_archived: bool. The LLM calls this tool during agent execution. What determines whether the LLM will include include_archived in the call, and why might the agent fail to construct the correct schema if you forget one parameter's type hint?
Show answer hint
A correct answer explains that the docstring and type hints tell the LLM what parameters exist and what types they expect; the LLM uses this schema to decide which parameters to include in each invocation. Missing or ambiguous type hints break the schema, making the tool unreliable because the LLM cannot infer the correct argument structure.
@tool was in langchain.tools. Since 0.3.0 (langchain-core), import from langchain_core.tools. The API is identical, but the import location changed as part of the core/community split.