Skip to content

Agent Tools

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

Tool definition (LangChain)

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.

Tool schema (what the LLM sees)

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"]
# }

Tool categories

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

Read-only tools

@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)

Write tools (with confirmation)

@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}"

Code execution tools

@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]

Composite tools (tool that calls tools)

@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

Example 1 — Tool set for a data analyst agent

@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]

Example 2 — Tool with validation

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

Pattern: tool registry

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)

Pattern: tool middleware (logging)

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}"

Pattern: tool versioning

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.