LangChain Mastery

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:

JSON
{"name": "check_stock", "description": "Return units in stock for a product SKU at one store...", "parameters": {"type": "object",   "properties": {"sku": {"type": "string"}, "store_id": {"type": "string"}},   "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

Python
from typing import Literalfrom pydantic import BaseModel, Fieldfrom langchain.tools import tool# 1. Simple: type hints + docstring@tooldef check_stock(sku: str, store_id: str) -> int:    """Return units in stock for a product SKU at one store.    Use for availability questions. Do not use for prices."""    return inventory.count(sku, store_id)# 2. Strict: a Pydantic schema with descriptions and allowed valuesclass DeliveryArgs(BaseModel):    pincode: str = Field(description="6-digit Indian pincode", pattern=r"^\d{6}$")    speed: Literal["standard", "express"] = "standard"@tool(args_schema=DeliveryArgs)def estimate_delivery(pincode: str, speed: str = "standard") -> str:    """Estimate delivery days to a pincode."""    return logistics.eta(pincode, speed)# 3. Async: for I/O-heavy tools@toolasync def search_catalogue(query: str) -> list[str]:    """Search product names and specs. Use when the user names no exact SKU."""    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

Python
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_stock and get_price, not product_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.
  • "@tool or StructuredTool?" — @tool for almost everything; StructuredTool.from_function when you need both sync and async versions or are wrapping existing functions.