Lesson 17 of 20

Lesson 16: Agentic Tool Use Patterns

What Is Tool Use?

Tool use (also called function calling) is the mechanism by which you give Claude the ability to take actions in the world — calling APIs, running code, querying databases, reading files. Instead of just generating text, Claude can request that a tool be run, receive the result, and continue its reasoning.

This is the foundation of agentic AI. Without tool use, Claude is a brilliant advisor. With tool use, Claude is an executor.


How the Tool Call Loop Works

The loop has four steps that repeat until Claude has a final answer:

  1. You define tools — You tell Claude what tools exist, what they do, and what inputs they accept
  2. Claude calls a tool — Claude generates a tool_use content block with the tool name and arguments
  3. You run the tool — Your code executes the actual function and gets the result
  4. Claude receives the result — You return the result as a tool_result message, and Claude continues
User message → Claude thinks → Claude calls tool_A
→ Your code runs tool_A → Returns result
→ Claude continues → Claude calls tool_B
→ Your code runs tool_B → Returns result
→ Claude produces final answer

Tool Definition Structure

Define tools using JSON Schema to describe their inputs:

tools = [
    {
        "name": "search_codebase",
        "description": "Search for files or code patterns in the repository. "
                       "Use this when you need to find where something is defined or used.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search term or pattern to look for"
                },
                "file_extension": {
                    "type": "string",
                    "description": "Optional file extension filter, e.g. '.py' or '.ts'",
                },
                "case_sensitive": {
                    "type": "boolean",
                    "description": "Whether to match case. Defaults to false.",
                    "default": False
                }
            },
            "required": ["query"]
        }
    }
]

A Complete Tool Use Example

import anthropic
import subprocess

client = anthropic.Anthropic()

def run_bash_command(command: str) -> str:
    """The actual tool implementation."""
    result = subprocess.run(
        command, shell=True, capture_output=True, text=True, timeout=30
    )
    return result.stdout or result.stderr

def agent_loop(user_message: str):
    tools = [{
        "name": "run_command",
        "description": "Run a bash command and return the output.",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string", "description": "The bash command to run"}
            },
            "required": ["command"]
        }
    }]
    
    messages = [{"role": "user", "content": user_message}]
    
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )
        
        # No tool call — we have our final answer
        if response.stop_reason == "end_turn":
            return response.content[0].text
        
        # Claude wants to call a tool
        if response.stop_reason == "tool_use":
            # Add Claude's response to message history
            messages.append({"role": "assistant", "content": response.content})
            
            # Process each tool call
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = run_bash_command(block.input["command"])
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            
            # Return results to Claude
            messages.append({"role": "user", "content": tool_results})

Parallel Tool Calls

Claude can request multiple tool calls in a single response when they're independent of each other. This is a significant performance optimization — instead of sequential round trips, multiple operations happen in one pass.

# Claude might return multiple tool_use blocks at once:
# block 1: search_codebase(query="database connection")
# block 2: search_codebase(query="connection pool")
# block 3: read_file(path="README.md")

# Run all three in parallel:
import concurrent.futures

tool_calls = [b for b in response.content if b.type == "tool_use"]
with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = {
        executor.submit(dispatch_tool, block): block
        for block in tool_calls
    }
    results = {futures[f].id: f.result() for f in concurrent.futures.as_completed(futures)}

Tool Design Principles

Write descriptions for Claude, not for humans. The description is what Claude reads to decide when to use a tool. Be explicit about when the tool is appropriate and when it is not.

# Weak description
"description": "Get weather data"

# Strong description
"description": "Get current weather conditions and 24-hour forecast for a city. "
               "Use this when the user asks about current weather or today's forecast. "
               "Do NOT use this for historical weather data."

Type everything. Use JSON Schema types and enums to constrain inputs. This prevents Claude from passing invalid arguments.

Return structured errors. If a tool fails, return an error message that Claude can reason about:

try:
    result = run_query(sql)
except DatabaseError as e:
    return f"ERROR: {e}. The query failed. Check syntax and table names."

Safety: Confirmation for Destructive Actions

Never let Claude automatically execute destructive or irreversible actions. Build in a confirmation step for anything that deletes data, sends external messages, or makes payments.

DESTRUCTIVE_TOOLS = {"delete_file", "send_email", "charge_card", "drop_table"}

def dispatch_tool(tool_name: str, tool_input: dict) -> str:
    if tool_name in DESTRUCTIVE_TOOLS:
        confirmation = input(f"Claude wants to run {tool_name}({tool_input}). Allow? [y/N]: ")
        if confirmation.lower() != 'y':
            return "Action cancelled by user."
    
    return TOOL_REGISTRY[tool_name](**tool_input)

When to Hand Back to a Human

Agentic loops should not run forever or through ambiguity. Design your agent to pause and ask when:

  • A required input is missing or ambiguous
  • A destructive action is about to be taken
  • The task has gone more steps than expected
  • An error has occurred more than once

Include this in your system prompt:

If you are uncertain about what the user wants, ask one clarifying question 
before proceeding. Do not assume. Do not proceed with destructive actions 
without explicit confirmation.

Key Takeaways

  • Tool use enables Claude to take real-world actions, not just generate text
  • The tool call loop: define → Claude calls → you run → return result → repeat
  • Claude can make parallel tool calls — process them concurrently for speed
  • Write tool descriptions for Claude, not for humans — be explicit about when to use each tool
  • Always require confirmation for destructive or irreversible operations
  • Build in human checkpoints for ambiguous situations or unexpected errors