Tool use is the "brain" of AI agents. The LLM decides WHEN and HOW to use tools.

Core Concept

User: "What's the weather in Berlin?"
   ↓
LLM: "I need get_weather tool with city=Berlin"
   ↓
System: Calls get_weather("Berlin")
   ↓
Result: "20°C, sunny"
   ↓
LLM: "In Berlin it's 20°C and sunny"
   ↓
User: "Thanks!"

That's tool use: LLM thinks, code acts.

Tool Schema Definition

Tools are JSON schema + description.

Basic Tool

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name (e.g., Berlin, New York)"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["city"]
        }
    }
}

Field Explanation:

  • name: Unique tool ID (snake_case)
  • description: What it does (2-3 sentences)
  • parameters: JSON Schema for inputs
  • required: Required fields (can be empty)

Claude Tool Use

from anthropic import Anthropic

client = Anthropic()

tools = [
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "Perform math calculations",
            "parameters": {
                "type": "object",
                "properties": {
                    "operation": {
                        "type": "string",
                        "enum": ["add", "subtract", "multiply", "divide"]
                    },
                    "a": {"type": "number"},
                    "b": {"type": "number"}
                },
                "required": ["operation", "a", "b"]
            }
        }
    }
]

def execute_calculator(operation: str, a: float, b: float) -> str:
    if operation == "add":
        return str(a + b)
    elif operation == "multiply":
        return str(a * b)
    # ... etc

def solve_math(problem: str) -> str:
    messages = [{"role": "user", "content": problem}]

    while True:
        response = client.messages.create(
            model="claude-3-5-sonnet-20241022",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )

        if response.stop_reason == "end_turn":
            for content in response.content:
                if hasattr(content, "text"):
                    return content.text

        elif response.stop_reason == "tool_use":
            tool_results = []

            for content in response.content:
                if content.type == "tool_use":
                    result = execute_calculator(**content.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": content.id,
                        "content": result
                    })

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

solve_math("What is 123 * 456?")

Parallel Tool Calling

Modern LLMs call multiple tools simultaneously:

# Claude calls get_stock_price AND get_news at same time
tool_calls = []

for content in response.content:
    if content.type == "tool_use":
        tool_calls.append(content)

# Execute all in parallel
results = []
for tool_call in tool_calls:
    result = execute_tool(tool_call.name, tool_call.input)
    results.append({
        "type": "tool_result",
        "tool_use_id": tool_call.id,
        "content": result
    })

# Send all results at once
messages.append({"role": "user", "content": results})

Best Practices

1. Clear Descriptions (CRITICAL)

Bad:

"description": "Tool"

Good:

"description": "Search products by price, category, and availability in our catalog"

LLM uses this to decide WHETHER to call!

2. Parameter Descriptions

"properties": {
    "q": {
        "type": "string",
        "description": "Search term (e.g., 'red shoes size 42')"
    }
}

3. Use Enums for Options

"status": {
    "type": "string",
    "enum": ["pending", "processing", "completed"]
}

Prevents invalid inputs.

4. Min/Max for Numbers

"max_results": {
    "type": "integer",
    "minimum": 1,
    "maximum": 100
}

Anti-Patterns

Too Many Tools (>20): LLM gets confused ❌ Tools Too Generic: "do_anything" ❌ No Error Handling: Crashes if missing field ❌ Infinite Loops: A→B→C→A→...

Production Tool Use

class ProductionToolUse:
    def __init__(self, max_tool_calls: int = 10):
        self.client = Anthropic()
        self.max_tool_calls = max_tool_calls

    def execute_with_tools(self, prompt: str) -> str:
        messages = [{"role": "user", "content": prompt}]
        tool_call_count = 0

        while tool_call_count < self.max_tool_calls:
            response = self.client.messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1024,
                tools=self.tools,
                messages=messages
            )

            if response.stop_reason == "end_turn":
                for content in response.content:
                    if hasattr(content, "text"):
                        return content.text

            elif response.stop_reason == "tool_use":
                # Execute tools...
                tool_call_count += 1

        return None

Advanced: Multi-Turn Tool Use

Realistic scenario: Agent needs multiple tool calls to complete task.

class MultiTurnToolAgent:
    def __init__(self):
        self.tools = [
            self.search_tool,
            self.fetch_url_tool,
            self.parse_json_tool,
            self.save_results_tool
        ]

    def search_tool(self, query: str) -> str:
        """Search web for query."""
        return f"Found URLs for {query}: url1, url2, url3"

    def fetch_url_tool(self, url: str) -> str:
        """Fetch content from URL."""
        return f"Content from {url}: [mock content]"

    def parse_json_tool(self, text: str) -> dict:
        """Parse JSON from text."""
        return {"parsed": "data"}

    def save_results_tool(self, data: dict, filename: str) -> str:
        """Save results to file."""
        return f"Saved to {filename}"

    def run(self, task: str) -> str:
        """Example: 'Find AI news, parse articles, save results'"""
        messages = [{"role": "user", "content": task}]
        max_turns = 10

        for turn in range(max_turns):
            # Call Claude
            response = self.client.messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1024,
                tools=self._get_tools_schema(),
                messages=messages
            )

            if response.stop_reason == "end_turn":
                # Agent finished
                for block in response.content:
                    if hasattr(block, "text"):
                        return block.text

            # Agent wants to use tools
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    # Execute tool
                    result = self._execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })

            # Feed results back
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

        return "Max turns exceeded"

    def _execute_tool(self, name: str, input: dict) -> str:
        if name == "search":
            return self.search_tool(input["query"])
        elif name == "fetch_url":
            return self.fetch_url_tool(input["url"])
        elif name == "parse_json":
            return self.parse_json_tool(input["text"])
        elif name == "save_results":
            return self.save_results_tool(input["data"], input["filename"])

Tool Use Cost Analysis

Tool calls have minimal overhead. Useful tools save tokens overall:

Scenario: "Get current Bitcoin price"

Without tool (make LLM guess):
  Input: 150 tokens
  Output: 100 tokens
  Total: 250 tokens
  Problem: Answer outdated (trained data cutoff)

With tool (use price API):
  Input: 150 tokens
  Tool schema: 50 tokens
  Output: 50 tokens (just "fetch current price")
  Tool result: 20 tokens (price data)
  Final output: 20 tokens
  Total: 290 tokens
  Problem solved: CURRENT price, not hallucinated

Cost: +40 tokens but 100% accuracy. Always worth it.

Tool Discovery (Agentic Feature)

Let agent choose which tools to use from a large toolkit:

class ToolDiscoveryAgent:
    def __init__(self):
        self.available_tools = {
            "weather": "Get weather for city",
            "stocks": "Get stock price",
            "news": "Search news articles",
            "email": "Send email",
            "slack": "Post to Slack",
            "github": "Query GitHub repos",
            "database": "Query SQL database"
        }

    def solve(self, task: str) -> str:
        # Task: "What's happening with Tesla stock and relevant news?"
        # Agent figures out: needs stocks + news tools

        response = self.client.messages.create(
            model="claude-3-5-sonnet-20241022",
            tools=[self._tool_to_schema(t) for t in self.available_tools.values()],
            messages=[{
                "role": "user",
                "content": f"Use available tools to: {task}"
            }]
        )

        # Agent picks 'stocks' and 'news' automatically
        # You don't tell it which tools to use—it discovers

This is true agency: agent decides what data it needs.

Streaming Tool Use

Return tool results as they arrive (useful for long-running tools):

class StreamingToolAgent:
    def run_with_streaming(self, task: str):
        messages = [{"role": "user", "content": task}]

        with self.client.messages.stream(
            model="claude-3-5-sonnet-20241022",
            tools=self.tools,
            messages=messages
        ) as stream:
            for text in stream.text_stream:
                # Tool decision arrives piece by piece
                print(text, end="", flush=True)

            # After stream, get tool uses
            for tool_use in stream.get_final_message().tool_use():
                result = self.execute_tool(tool_use.name, tool_use.input)
                # Continue conversation with result

Tool Composition Patterns

Sequential Tools

Tool A → Tool B → Tool C (linear)

Get user ID → Fetch user data → Send notification

Branching Tools

Conditional: IF result, use Tool B, ELSE use Tool C

# Claude decides:
# If stock price high: use alert tool
# If stock price low: use buy_recommendation tool

Parallel Tools

Call multiple tools simultaneously

# Claude wants multiple data points at once:
# Get weather + stocks + news (all called in parallel)
results = await asyncio.gather(
    get_weather("Berlin"),
    get_stock_price("TSLA"),
    get_news("Tesla")
)

Tool Use Benchmarks (2026)

Performance of agents with vs without tools:

Task No Tools With Tools Accuracy Speed
"Current Bitcoin price" Outdated guess Real data 100% Same
"Fetch user data" Hallucinated Real data 98% Fast
"Complex calculation" Error-prone Accurate 99% Instant
"Multi-step workflow" Mistakes Systematic 95% 2x slower

Tools don't add latency—they improve accuracy at same cost.

Common Mistakes

1. Too Many Tools (Analysis Paralysis)

# WRONG: 50 tools, Claude confused
tools = [
    get_weather, get_stocks, get_news, get_email,
    send_message, create_file, delete_file, ...
    # (48 more)
]

# RIGHT: 5-10 focused tools
tools = [
    search_web,
    fetch_url,
    parse_json,
    save_file
]

2. Vague Tool Descriptions

# WRONG
"description": "Do something"

# RIGHT
"description": "Search recent news articles. Returns top 10 matches."

3. No Input Validation

# WRONG
def send_email(to, body):
    # What if `to` is not valid email?
    smtp.send(to, body)

# RIGHT
def send_email(to: str, body: str) -> bool:
    if "@" not in to:
        raise ValueError(f"Invalid email: {to}")
    return smtp.send(to, body)

Production Safety

Rate Limiting

from functools import wraps
import time

class RateLimiter:
    def __init__(self, calls_per_second: int = 5):
        self.calls_per_second = calls_per_second
        self.last_call = 0

    def rate_limit(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - self.last_call
            min_interval = 1.0 / self.calls_per_second

            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)

            result = func(*args, **kwargs)
            self.last_call = time.time()
            return result

        return wrapper

limiter = RateLimiter(calls_per_second=3)

@limiter.rate_limit
def call_expensive_api():
    # Will never exceed 3 calls/second
    pass

Cost Tracking

class CostTracker:
    def __init__(self):
        self.tool_costs = {}
        self.total_cost = 0

    def track(self, tool_name: str, cost: float):
        if tool_name not in self.tool_costs:
            self.tool_costs[tool_name] = 0
        self.tool_costs[tool_name] += cost
        self.total_cost += cost

        if self.total_cost > 10:  # Max budget EUR 10
            raise RuntimeError(f"Budget exceeded: {self.total_cost}")

    def report(self):
        for tool, cost in sorted(self.tool_costs.items()):
            print(f"{tool}: €{cost:.4f}")
        print(f"Total: €{self.total_cost:.4f}")

Summary

Tool Use Pattern:

  1. Define → JSON Schema + clear description
  2. Pass to LLM → Via API
  3. LLM decides → Which tool, when, with what input
  4. Execute → Your code runs it safely
  5. Return result → To LLM for next decision
  6. Loop → Until task complete
  7. Track costs → Monitor spending
  8. Rate limit → Protect services

Best Practices:

  • ✓ Clear descriptions (most important)
  • ✓ Enums for options, min/max for numbers
  • ✓ Error handling in every tool
  • ✓ Max 15-20 tools (more = confusion)
  • ✓ Tool call limit (safety)
  • ✓ Cost tracking (budget)
  • ✓ Rate limiting (protection)

Resources