Tools are the actions an agent can take — API calls, code execution, database queries, file operations. Well-designed tools are the difference between an agent that works and one that fails. This covers how to define, design, and organize tools.
Table of Contents
Installation / Setup
pip install langchain-core # or: uv add langchain-core
Core Syntax
from langchain_core.tools import tool
@tool
def get_weather(city: str, units: str = "celsius") -> str:
"""Get the current weather for a city.
Args:
city: The city name, e.g. "London"
units: Temperature units, "celsius" or "fahrenheit"
"""
return f"Weather in {city}: 22° {units}"
The docstring is critical — the LLM reads it to decide when to use the tool.
print(get_weather.name) # "get_weather"
print(get_weather.description) # "Get the current weather for a city..."
print(get_weather.args_schema.model_json())
# {
# "type": "object",
# "properties": {
# "city": {"type": "string", "description": "The city name, e.g. London"},
# "units": {"type": "string", "description": "Temperature units"}
# },
# "required": ["city"]
# }
| Category |
Examples |
Risk level |
| Read-only |
Search, lookup, read file |
Low |
| Data transform |
Calculate, format, parse |
Low |
| Write |
Create file, send email |
Medium |
| External API |
Book flight, make payment |
High |
| Code execution |
Run Python, shell commands |
High |
| System |
Deploy, delete, modify config |
Critical |
Risk levels and guardrails
| Level |
Guardrails |
| Low |
No restrictions |
| Medium |
Log and confirm |
| High |
Human approval required |
| Critical |
Never auto-execute; always require approval |
Common Operations
@tool
def search_web(query: str) -> str:
"""Search the web for information about a topic."""
# call a search API
return f"Results for: {query}"
@tool
def read_database(query: str) -> str:
"""Read data from the database using SQL SELECT."""
# only allow SELECT statements
if not query.strip().upper().startswith("SELECT"):
return "Error: only SELECT queries allowed"
return execute_sql(query)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a recipient.
Args:
to: Email address
subject: Email subject line
body: Email body text
"""
# in production, add confirmation step
return f"Email sent to {to}: {subject}"
@tool
def run_python(code: str) -> str:
"""Execute Python code and return the output.
Args:
code: Python code to execute
"""
import io, contextlib
f = io.StringIO()
with contextlib.redirect_stdout(f):
try:
exec(code, {"__builtins__": __builtins__})
except Exception as e:
return f"Error: {e}"
return f.getvalue()[:2000]
@tool
def research(query: str) -> str:
"""Research a topic by searching and summarizing."""
# this tool orchestrates other tools internally
search_results = search_web.invoke({"query": query})
summary = summarize.invoke({"text": search_results})
return summary
Practical Examples
@tool
def query_sql(sql: str) -> str:
"""Query the database with SQL. Only SELECT statements allowed."""
if not sql.strip().upper().startswith("SELECT"):
return "Error: only SELECT queries permitted"
return str(pd.read_sql(sql, connection))
@tool
def create_chart(code: str) -> str:
"""Create a matplotlib chart from Python code."""
exec(code) # carefully sandboxed in production
return "Chart created"
@tool
def save_csv(data: str, filename: str) -> str:
"""Save data to a CSV file."""
pd.read_json(data).to_csv(filename, index=False)
return f"Saved to {filename}"
tools = [query_sql, create_chart, save_csv]
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
query: str = Field(description="Search query")
max_results: int = Field(default=5, description="Max results to return", ge=1, le=20)
@tool(args_schema=SearchInput)
def search(query: str, max_results: int = 5) -> str:
"""Search the web for information."""
return f"Top {max_results} results for: {query}"
Example 3 — Safe code execution
@tool
def safe_execute(code: str) -> str:
"""Execute Python code in a restricted environment."""
import ast, io, contextlib
# parse and validate AST
try:
tree = ast.parse(code)
except SyntaxError as e:
return f"Syntax error: {e}"
# block dangerous operations
dangerous = {"exec", "eval", "import os", "subprocess", "open"}
code_lower = code.lower()
for d in dangerous:
if d in code_lower:
return f"Blocked: '{d}' not allowed"
f = io.StringIO()
with contextlib.redirect_stdout(f):
exec(code, {"__builtins__": {}})
return f.getvalue()[:2000]
Common Patterns
class ToolRegistry:
def __init__(self):
self.tools = {}
def register(self, tool):
self.tools[tool.name] = tool
def get(self, name):
return self.tools.get(name)
def list_tools(self):
return list(self.tools.values())
registry = ToolRegistry()
registry.register(search_web)
registry.register(query_sql)
import functools
def log_tool(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"TOOL CALL: {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"TOOL RESULT: {result[:200]}")
return result
return wrapper
@log_tool
@tool
def search(query: str) -> str:
"""Search the web."""
return f"Results for: {query}"
search_v1 = tool(lambda q: f"v1: {q}", name="search", description="Search v1")
search_v2 = tool(lambda q: f"v2: {q}", name="search_v2", description="Search v2 (better)")
# agent uses v2 by default, can fall back to v1
Best Practices
- Docstrings are everything — the LLM decides based on them; make them precise.
- Use type hints — they generate better schemas for the LLM.
- Keep tools focused — one tool, one job (not "do_everything").
- Validate inputs — use Pydantic schemas for complex tools.
- Truncate outputs — cap at 2000 chars to prevent context overflow.
- Add error handling — return descriptive error messages, not crashes.
- Log tool calls — agents are opaque; logs help debug.
- Risk-rate tools — read-only = safe, write = careful, delete = dangerous.
Common Mistakes
| Mistake |
Fix |
| Vague tool descriptions |
Write specific, actionable descriptions |
| No output truncation |
Cap outputs at 2000 chars |
| Too many tools (>15) |
Group into categories; give agent a "tool search" tool |
| No error handling |
Return error strings, don't raise exceptions |
| Dangerous tools without guardrails |
Add human approval for write/delete operations |
| Ignoring tool output format |
Return clean text, not raw JSON dumps |
Troubleshooting
| Symptom |
Cause / Fix |
| Agent never uses a tool |
Description is vague — rewrite it |
| Agent uses wrong tool |
Descriptions overlap — make them more distinct |
| Tool crashes agent |
Add try/except; return error message |
| Agent overwhelmed by tool outputs |
Truncate outputs; reduce top-K |
| Agent can't find the right tool |
Too many tools — add a "list tools" meta-tool |
Official Documentation
Last Updated
2026-09-06 — Initial version.