Creating Tools in LangChain with the @tool Decorator
In LangChain, a tool is a function plus a schema that tells the model what the function does and what arguments it takes. The @tool decorator builds both from an ordinary Python function: the name becomes the tool name, the docstring becomes the description, and the type hints become the argument schema. Good tools are the largest factor in whether an agent works.
Your first tool
from langchain_core.tools import tool
@tool
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
print(add.name)
print(add.description)
print(add.args)
print(add.invoke({"a": 2, "b": 3}))
This prints the tool name add, its description, a dictionary describing arguments a and b as integers, and finally 5. A tool is itself a Runnable, so you call it with invoke and a dictionary of arguments. You can test tools like this without any model.
Write docstrings for the model
The model reads the description to decide when to use the tool. Be specific about what the tool does and when to use it. Compare a vague docstring with a useful one:
@tool
def lookup_order(order_id: str) -> str:
"""Look up the status of a customer order.
Use this when the user asks where their order is or whether it shipped.
The order_id looks like "ORD-1234".
"""
fake_db = {"ORD-1234": "shipped on Monday", "ORD-5678": "being packed"}
return fake_db.get(order_id, "order not found")
Mentioning the id format helps the model pass the right value. Returning a clear message for the not-found case, instead of raising, lets the model recover and tell the user.
Use a Pydantic schema for richer arguments
When you need descriptions per argument or constraints, pass an args_schema:
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class SearchInput(BaseModel):
query: str = Field(description="Search words")
limit: int = Field(default=3, ge=1, le=10, description="Number of results")
@tool(args_schema=SearchInput)
def search_docs(query: str, limit: int = 3) -> list[str]:
"""Search the documentation and return matching titles."""
titles = ["Install guide", "Quick start", "Deploy guide", "API reference"]
return [t for t in titles if query.lower() in t.lower()][:limit]
The constraints ge=1 and le=10 appear in the schema the model sees, and invalid calls fail validation before your function runs.
Bind tools to a model
The model does not run tools. It only asks for them. bind_tools sends the schemas along with each request, and the reply may contain tool calls:
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
load_dotenv()
model = init_chat_model("anthropic:claude-sonnet-5-5")
model_with_tools = model.bind_tools([add, lookup_order])
ai_message = model_with_tools.invoke("What is 17 plus 25?")
print(ai_message.tool_calls)
The output is a list such as [{'name': 'add', 'args': {'a': 17, 'b': 25}, 'id': '...'}]. Your code, or an agent loop, must run the tool and send the result back. The next post builds an agent that does this for you.
Tool design checklist
- One clear job per tool, with a verb in the name.
- Type hints on every argument; they become the schema.
- Return short, useful strings or JSON-friendly values, not huge dumps.
- Return readable error messages instead of raising for expected failures.
- Keep side effects (sending email, deleting data) in separate tools and gate them with approval.
- Unit test each tool without a model.
Why the description matters so much
When a model decides which tool to call, it sees only the tool name, the description and the argument schema. It does not see your code. A tool with a poor description is a tool the model will misuse or ignore. Think of the docstring as a short manual written for a new colleague who cannot read the source. Say what the tool returns, when it should be used and when it should not. If two tools overlap, explain the difference in both descriptions. A few sentences here often fix problems that people try to solve by rewriting the system prompt.
Names matter too. A name such as get_order_status tells the model more than lookup. If an argument accepts a fixed set of values, use a Literal type so the schema lists the allowed values and the model cannot invent a new one.
Handling errors inside tools
Tools fail in ordinary ways: a network timeout, a missing record, a bad argument. If a tool raises an exception, the run may stop. Often it is better to catch the expected failures and return a message the model can act on, such as "order not found, ask the user to check the id". Reserve exceptions for real bugs you want to see. This one habit makes agents noticeably more robust, because the model can retry, ask the user a question or explain the problem instead of crashing the run.
Comments
No comments yet. Have a question or a tip? Be the first to share it.