Course Content
LangChain Mastery
7 sections · 109 lessons
How do you create a tool in LangChain for an agent?
What you need to know
What the model actually sees
When you create a tool, LangChain builds a schema like this and sends it with every model call:
1{"name": "check_stock",2 "description": "Return units in stock for a product SKU at one store...",3 "parameters": {"type": "object",4 "properties": {"sku": {"type": "string"}, "store_id": {"type": "string"}},5 "required": ["sku", "store_id"]}}The model chooses tools only from this text. It cannot read your function body. A vague description means wrong tool choices, however good the code is.
Three ways to define a tool
1from typing import Literal2from pydantic import BaseModel, Field3from langchain.tools import tool45# 1. Simple: type hints + docstring6@tool7def check_stock(sku: str, store_id: str) -> int:8 """Return units in stock for a product SKU at one store.9 Use for availability questions. Do not use for prices."""10 return inventory.count(sku, store_id)1112# 2. Strict: a Pydantic schema with descriptions and allowed values13class DeliveryArgs(BaseModel):14 pincode: str = Field(description="6-digit Indian pincode", pattern=r"^\d{6}$")15 speed: Literal["standard", "express"] = "standard"1617@tool(args_schema=DeliveryArgs)18def estimate_delivery(pincode: str, speed: str = "standard") -> str:19 """Estimate delivery days to a pincode."""20 return logistics.eta(pincode, speed)2122# 3. Async: for I/O-heavy tools23@tool24async def search_catalogue(query: str) -> list[str]:25 """Search product names and specs. Use when the user names no exact SKU."""26 return await catalogue.search(query, limit=5)Option 2 matters in production. If the model sends "pincode": "5600", Pydantic rejects it and the agent receives a validation error it can correct, instead of your logistics API receiving garbage.
StructuredTool.from_function(func=..., coroutine=...) also exists; use it when you need one tool with separate sync and async implementations, or when building tools from functions you do not own.
Using the tools
agent = create_agent(model, tools=[check_stock, estimate_delivery, search_catalogue])# or, without an agent loop:model_with_tools = model.bind_tools([check_stock])bind_tools only attaches the schemas; the model returns tool calls and you run them yourself. create_agent runs the whole loop for you.
Rules for good tools
- One job per tool.
check_stockandget_price, notproduct_info(action=...). - Say when not to use it. Overlapping tools are the top cause of wrong choices.
- Return small, readable output. A short string or a few fields, not a 40 KB JSON blob.
- Keep secrets inside the tool. API keys and user IDs come from your code or the runtime context, never from model-supplied arguments.
A real-life example
A law firm builds a contract-search assistant. The first version has one tool, search(query), over 12,000 contracts. Lawyers ask "Which vendor contracts renew in March and have no termination-for-convenience clause?" and the agent keeps running broad text searches that miss.
The team replaces it with three narrow tools: find_contracts(counterparty_type, renewal_month) backed by SQL metadata, get_clause(contract_id, clause_type) with clause_type as a Literal of 8 allowed values, and semantic_search(query) for free-text questions. Each docstring says when to prefer it. On a set of 60 test questions, correct first-tool choice goes from 58% to 90%, and the average run drops from 6 model calls to 3.
Follow-up questions to expect
- "How does the model know what arguments to pass?" — From the JSON schema built from the type hints or
args_schema, plus the field descriptions. - "How do you stop the model passing another user's ID?" — Do not make it an argument. Read it inside the tool from runtime context (
ToolRuntime) set by your authenticated code. - "
@toolorStructuredTool?" —@toolfor almost everything;StructuredTool.from_functionwhen you need both sync and async versions or are wrapping existing functions.