liminfo

Claude API Tool Use Practical Guide

Build an AI agent from scratch that calls external functions and integrates results using Claude API's Tool Use feature. Covers JSON Schema-based tool definitions, message loops, parallel invocation, error handling, and production patterns with practical examples.

Claude API Tool UseClaude function callingAnthropic APIAI agent buildingtool_use tool_resultJSON Schema tool definitionparallel tool callingClaude Opus Sonnet Haiku

Problem

An AI chatbot needs to query real-time data (weather, stock prices, schedules, etc.) or interact with external systems (databases, APIs, file systems), but LLMs cannot access information beyond their training data and cannot directly call external APIs. Stuffing all data into the prompt hits context limits and raises cost concerns, while parsing formatted output is unreliable. With Claude API's Tool Use feature, the model can request function calls in a structured manner, and developers can build a safe agent loop that executes the functions and returns results. Model pricing (input/output per MTok): Opus 4.6 ($5/$25), Sonnet 4.5 ($3/$15), Haiku 4.5 ($1/$5) - choosing the right model for cost efficiency is critical in agent use cases.

Required Tools

Anthropic Python SDK

Official Python client for the Claude API. Install with pip install anthropic. Requires Python 3.8+.

Anthropic TypeScript SDK

Official TypeScript/Node.js client for the Claude API. Install with npm install @anthropic-ai/sdk. Requires Node.js 18+.

JSON Schema

Standard schema for defining tool input parameters. Uses type, properties, and required fields to guide Claude in generating correct arguments.

Python / TypeScript

Languages for implementing the agent loop and tool execution logic. Examples are provided in both languages.

Solution Steps

1

Prerequisites and Understanding Tool Use Concepts

Tool Use is a feature that allows Claude to call external tools (functions) to answer user questions. Prerequisites: 1. Anthropic API key - Obtain from console.anthropic.com (usage-based billing) 2. Python 3.8+ or Node.js 18+ installed 3. SDK installed (see code below) Overall Tool Use Flow (4 steps): 1. The developer includes a list of available tools (name, description, parameter schema) in the API request 2. Claude analyzes the user's question, selects the necessary tools, and requests invocation via tool_use blocks 3. The developer executes the actual function and returns the result to Claude as a tool_result message 4. Claude synthesizes the tool execution results and generates the final response Key concepts: - Claude does NOT "execute tools directly." It only "requests" tool calls - actual execution and security controls are handled by the developer's code. - If stop_reason is "tool_use," it is not the final answer yet -> you must execute the tool and send the result back - If stop_reason is "end_turn," the final answer is complete Cost considerations (agents make multiple API calls): - Simple tool routing: Haiku 4.5 ($1/$5) - cheapest - Complex reasoning + tool use: Sonnet 4.5 ($3/$15) - recommended - Highest accuracy needed: Opus 4.6 ($5/$25) - most expensive You will know it is working when: After SDK installation, a simple API call returns a response If you get an "AuthenticationError": Check that your API key is correct. The environment variable ANTHROPIC_API_KEY must be set.

# ============================================
# Python SDK installation and initialization
# ============================================
pip install anthropic

# Set API key as environment variable (never hardcode in source!)
# Linux/Mac:
export ANTHROPIC_API_KEY="sk-ant-api03-..."

# Windows PowerShell:
$env:ANTHROPIC_API_KEY="sk-ant-api03-..."

# Python client initialization and test
import anthropic

client = anthropic.Anthropic()  # Automatically uses ANTHROPIC_API_KEY env var

# Connection test
message = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=100,
    messages=[{"role": "user", "content": "Hello!"}]
)
print(message.content[0].text)  # If you see a response, setup is complete

# ============================================
# TypeScript SDK installation and initialization
# ============================================
# npm install @anthropic-ai/sdk
#
# import Anthropic from "@anthropic-ai/sdk";
# const client = new Anthropic();  // Automatically uses ANTHROPIC_API_KEY env var
2

Tool Definition - Writing Function Specs with JSON Schema

To tell Claude which tools are available, define each tool in JSON Schema format. Three required elements of a tool definition: 1. name: Unique name for the tool (alphanumeric, underscores allowed). e.g., "get_weather", "search_database" 2. description: Detailed explanation of what the tool does. This is the key information Claude uses to decide "when to use this tool" 3. input_schema: Parameter definition in JSON Schema format Writing the description is the most important part: - Bad example: "Gets weather" (too short, unclear what input is needed) - Good example: "Retrieves the current temperature, humidity, and weather conditions for a specified city. City names can be provided in English or local language (e.g., 'Seoul', 'Tokyo')." input_schema writing tips: - type: "object" at the top level (required) - properties: type and description for each parameter - required: list of required parameters (array) - enum: specify when allowed values are fixed (e.g., ["celsius", "fahrenheit"]) - description: description for each parameter (guides Claude to generate correct values) You will know it is working when: After the API call, response.stop_reason is "tool_use" and the content contains a ToolUseBlock If Claude does not use a tool: Write the description more specifically. Claude decides whether to use a tool based on the description.

import anthropic

client = anthropic.Anthropic()

# ============================================
# Tool definitions - parameter specs via JSON Schema
# ============================================
tools = [
    {
        "name": "get_weather",
        "description": (
            "Retrieves current weather information (temperature, humidity, conditions) "
            "for a specified city. City names can be provided in English or local language "
            "(e.g., 'Seoul', 'Tokyo', 'New York'). "
            "Use this tool when the user asks about weather, temperature, or climate."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name to check weather for (e.g., 'Seoul', 'Tokyo', 'New York')"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit. Defaults to celsius"
                }
            },
            "required": ["city"]
        }
    },
    {
        "name": "manage_schedule",
        "description": (
            "View (list), add, or delete user schedules. "
            "Supports checking schedules for a specific date, registering new events, "
            "and removing existing events. "
            "Use this tool when the user mentions schedules, appointments, meetings, or events."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "action": {
                    "type": "string",
                    "enum": ["list", "add", "delete"],
                    "description": "Action to perform: list (view), add (create), delete (remove)"
                },
                "date": {
                    "type": "string",
                    "description": "Target date (YYYY-MM-DD format). e.g., '2025-05-15'"
                },
                "title": {
                    "type": "string",
                    "description": "Event title (required when action is 'add')"
                },
                "time": {
                    "type": "string",
                    "description": "Event time (HH:MM format, optional when action is 'add')"
                },
                "schedule_id": {
                    "type": "string",
                    "description": "ID of the schedule to delete (required when action is 'delete')"
                }
            },
            "required": ["action", "date"]
        }
    }
]

# ============================================
# API call test with tools included
# ============================================
response = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the weather like in Seoul?"}
    ]
)

# Analyze the response
print(f"stop_reason: {response.stop_reason}")
# -> "tool_use" (Claude is requesting a tool call)

for block in response.content:
    if hasattr(block, "text"):
        print(f"Text: {block.text}")
    if block.type == "tool_use":
        print(f"Tool call: {block.name}")
        print(f"Parameters: {block.input}")
        print(f"tool_use_id: {block.id}")
# -> Tool call: get_weather
# -> Parameters: {"city": "Seoul"}
# -> tool_use_id: "toolu_01Abc..."
3

Implementing the Agent Loop - Tool Calls and Result Returns

The core of Tool Use is the "agent loop." When Claude requests a tool call (stop_reason == "tool_use"), the developer executes the corresponding function and returns the result as a tool_result message. This process repeats. Agent loop message flow: 1. [user] "Tell me the weather in Seoul" 2. [assistant] TextBlock("Let me check that for you") + ToolUseBlock(get_weather, {city: "Seoul"}) 3. [user] ToolResultBlock(tool_use_id, "{"temp": 22, "condition": "Clear"}") 4. [assistant] "The current temperature in Seoul is 22 degrees with clear skies." (stop_reason: "end_turn") Three rules you must follow when writing tool_result: 1. The tool_use_id must match exactly (use the block.id value) 2. The previous assistant response must be included in messages as-is (omitting it causes an API error) 3. The content should be passed as a string (str) (not a JSON object) When errors occur: - Setting is_error: True lets Claude recognize the error and appropriately inform the user - Do not include sensitive error information (stack traces, internal URLs) Preventing infinite loops: - Always set max_turns (typically 5-15 turns is sufficient) - When the turn limit is exceeded, notify the user and terminate the loop You will know it is working when: Sending "Tell me the weather in Seoul" executes the tool and ultimately returns a natural language response

import anthropic
import json

client = anthropic.Anthropic()

# ============================================
# 1. Define actual tool execution functions
# ============================================
def get_weather(city: str, unit: str = "celsius") -> dict:
    """In practice, this would call a weather API like OpenWeatherMap."""
    # Example data (replace with actual API call in real implementation)
    weather_data = {
        "Seoul": {"temp": 22, "humidity": 55, "condition": "Clear", "wind": "3m/s"},
        "Tokyo": {"temp": 26, "humidity": 70, "condition": "Cloudy", "wind": "5m/s"},
        "New York": {"temp": 18, "humidity": 45, "condition": "Partly cloudy", "wind": "8m/s"},
    }
    data = weather_data.get(city, {"temp": 20, "humidity": 50, "condition": "No data", "wind": "N/A"})
    if unit == "fahrenheit":
        data["temp"] = round(data["temp"] * 9 / 5 + 32, 1)
    data["city"] = city
    data["unit"] = unit
    return data

def manage_schedule(action: str, date: str, **kwargs) -> dict:
    """Schedule management (in practice, connects to a DB or Google Calendar API)"""
    if action == "list":
        return {
            "date": date,
            "schedules": [
                {"id": "1", "title": "Team Meeting", "time": "10:00"},
                {"id": "2", "title": "Lunch Appointment", "time": "12:30"},
                {"id": "3", "title": "Code Review", "time": "15:00"},
            ]
        }
    elif action == "add":
        return {
            "status": "success",
            "message": f"Event '{kwargs.get('title', '')}' has been added on {date} {kwargs.get('time', '')}.",
            "id": "4"
        }
    elif action == "delete":
        return {
            "status": "success",
            "message": f"Schedule ID {kwargs.get('schedule_id', '')} has been deleted."
        }
    return {"status": "error", "message": "Unknown action."}

# ============================================
# 2. Tool name -> function mapping (dispatcher)
# ============================================
TOOL_FUNCTIONS = {
    "get_weather": get_weather,
    "manage_schedule": manage_schedule,
}

# ============================================
# 3. Agent loop (core code)
# ============================================
def run_agent(user_message: str, tools: list, max_turns: int = 10) -> str:
    """Agent loop that repeats tool calls until Claude gives a final answer"""
    messages = [{"role": "user", "content": user_message}]

    for turn in range(max_turns):
        # Call Claude API
        response = client.messages.create(
            model="claude-sonnet-4-5-20250514",
            max_tokens=4096,
            system="You are an assistant that helps with weather checks and schedule management. Respond naturally in English.",
            tools=tools,
            messages=messages,
        )

        # [Important] Add Claude's response to message history
        messages.append({"role": "assistant", "content": response.content})

        # If stop_reason is "end_turn," the final answer is complete
        if response.stop_reason == "end_turn":
            return "\n".join(
                block.text for block in response.content
                if hasattr(block, "text")
            )

        # If stop_reason is "tool_use," execute the tools
        if response.stop_reason == "tool_use":
            tool_results = []

            for block in response.content:
                if block.type != "tool_use":
                    continue

                func = TOOL_FUNCTIONS.get(block.name)

                if func is None:
                    # Unknown tool: notify Claude with is_error=True
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,  # Must match!
                        "content": json.dumps({
                            "error": f"Tool '{block.name}' not found."
                        }),
                        "is_error": True,
                    })
                    continue

                try:
                    # Execute the tool
                    result = func(**block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result, ensure_ascii=False),
                    })
                except Exception as e:
                    # Execution error: is_error=True + error message
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps({
                            "error": f"{type(e).__name__}: {str(e)}"
                        }),
                        "is_error": True,
                    })

            # [Important] Add tool_results as a user message
            messages.append({"role": "user", "content": tool_results})

    return "Maximum conversation turns exceeded. Please try a more specific question."

# ============================================
# 4. Execution test
# ============================================
# Single tool call
result = run_agent("Tell me the weather in Seoul", tools)
print(result)
# -> "The current temperature in Seoul is 22 degrees with clear skies. Humidity is 55%..."

# Multiple tool calls (Claude decides automatically)
result = run_agent("Tell me the weather in Seoul and also check today's schedule", tools)
print(result)
# -> Claude calls both get_weather and manage_schedule
4

Parallel Tool Call Handling and Error Management

Claude can return multiple tool_use blocks simultaneously in a single response. For example, if you ask "Compare the weather in Seoul and Tokyo," it will call get_weather twice in parallel. Rules for handling parallel calls: 1. You must return a tool_result for every tool_use block 2. Each tool_result's tool_use_id must correspond exactly to the matching tool_use's id 3. If one tool fails, the remaining results should still be returned normally 4. All tool_results should be sent as an array in a single user message Error handling strategies (required for production): 1. Tool execution errors: - Mark with is_error: true - Claude will inform the user, e.g., "Sorry, I was unable to retrieve the weather information" 2. API-level errors: - 429 Rate Limit: Wait and retry (exponential backoff) - 500 Server Error: Retry (up to 3 times) - 401 Auth Error: API key needs checking -> do not retry 3. Security errors: - Do not pass sensitive error information (stack traces, internal DB addresses) to Claude - Only include user-facing messages in the content 4. Timeouts: - Set an execution time limit for each tool (typically 30 seconds) - When a timeout occurs, return is_error: true + "Timed out" message You will know it is working when: Sending "Compare the weather in Seoul and Tokyo" returns a response comparing both cities' weather

import anthropic
import json
import asyncio
from concurrent.futures import ThreadPoolExecutor

client = anthropic.Anthropic()

# ============================================
# Parallel tool execution (asyncio + ThreadPoolExecutor)
# ============================================
async def execute_tools_parallel(
    tool_blocks: list,
    tool_functions: dict,
    timeout_seconds: float = 30.0,
) -> list:
    """Execute multiple tools in parallel and collect results."""
    loop = asyncio.get_event_loop()
    executor = ThreadPoolExecutor(max_workers=5)

    async def run_single_tool(block):
        func = tool_functions.get(block.name)

        # 1. Unknown tool
        if not func:
            return {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps({
                    "error": f"Tool '{block.name}' not found."
                }),
                "is_error": True,
            }

        try:
            # 2. Timeout + thread pool execution
            result = await asyncio.wait_for(
                loop.run_in_executor(
                    executor, lambda: func(**block.input)
                ),
                timeout=timeout_seconds,
            )
            return {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(result, ensure_ascii=False),
            }

        # 3. Timeout
        except asyncio.TimeoutError:
            return {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps({
                    "error": f"Tool execution exceeded {timeout_seconds} seconds."
                }),
                "is_error": True,
            }

        # 4. Other errors (filter sensitive information)
        except Exception as e:
            return {
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps({
                    "error": f"Error during tool execution: {type(e).__name__}"
                    # str(e) may contain sensitive info, so only pass the type
                }),
                "is_error": True,
            }

    # Execute all tools in parallel
    tasks = [
        run_single_tool(b)
        for b in tool_blocks
        if b.type == "tool_use"
    ]
    results = await asyncio.gather(*tasks)
    return list(results)

# ============================================
# Production agent with API error handling
# ============================================
import time

def run_agent_production(
    user_message: str,
    tools: list,
    max_turns: int = 10,
    max_api_retries: int = 3,
) -> str:
    """Production-grade agent (error handling + parallel execution + retries)"""
    messages = [{"role": "user", "content": user_message}]

    for turn in range(max_turns):
        # API call (with retry logic)
        response = None
        for retry in range(max_api_retries):
            try:
                response = client.messages.create(
                    model="claude-sonnet-4-5-20250514",
                    max_tokens=4096,
                    system="You are an assistant that helps with weather checks and schedule management. Respond naturally in English.",
                    tools=tools,
                    messages=messages,
                )
                break  # Exit loop on success

            except anthropic.RateLimitError:
                # 429: Wait and retry
                wait_time = 2 ** retry  # 1s, 2s, 4s (exponential backoff)
                print(f"Rate limit exceeded. Waiting {wait_time} seconds before retrying...")
                time.sleep(wait_time)

            except anthropic.APIStatusError as e:
                if e.status_code >= 500:
                    # 5xx: Server error, retry
                    print(f"Server error ({e.status_code}). Retry {retry + 1}/{max_api_retries}")
                    time.sleep(1)
                else:
                    # 4xx (401, 403, etc.): Cannot retry
                    return f"API error: {e.status_code}. Please check your API key and settings."

            except anthropic.APIConnectionError:
                print(f"Network connection failed. Retry {retry + 1}/{max_api_retries}")
                time.sleep(2)

        if response is None:
            return "API call failed. Please check your network connection and API key."

        # Process response
        messages.append({"role": "assistant", "content": response.content})

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

        if response.stop_reason == "tool_use":
            tool_blocks = [b for b in response.content if b.type == "tool_use"]
            # Parallel execution
            tool_results = asyncio.run(
                execute_tools_parallel(tool_blocks, TOOL_FUNCTIONS)
            )
            messages.append({"role": "user", "content": tool_results})

    return "Maximum conversation turns exceeded."

# ============================================
# Execution test: a case where parallel calls occur
# ============================================
result = run_agent_production(
    "Compare the weather in Seoul and Tokyo, and also check tomorrow's schedule",
    tools,
)
print(result)
# Claude calls get_weather("Seoul"), get_weather("Tokyo"),
# manage_schedule("list", "2025-05-16") - three tools in parallel
5

Complete Agent Example in TypeScript

Here is a complete agent example in TypeScript that integrates everything covered so far. Using the Anthropic TypeScript SDK, it implements an agent that handles both weather queries and schedule management simultaneously. Differences between Python and TypeScript: - Type safety: TypeScript's type system allows safer handling of tool inputs and outputs - Anthropic.Tool[]: Apply types to tool definitions - Anthropic.TextBlock, Anthropic.ToolUseBlock: Filter response blocks by type - async/await: Asynchronous handling is natural in TypeScript Practical considerations: - If tool execution results are too large, summarize before passing (to save token costs) - e.g., DB query result of 1000 rows -> pass only the top 10 - Clearly define the agent's role and constraints via the system prompt - Validate user input and sanitize tool inputs (prevent SQL injection, etc.) You will know it is working when: The TypeScript agent calls multiple tools and returns a final answer

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

// ============================================
// 1. Tool definitions (with TypeScript types)
// ============================================
const tools: Anthropic.Tool[] = [
  {
    name: "get_weather",
    description:
      "Retrieves current weather information (temperature, humidity, conditions) " +
      "for a specified city. City names can be in English or local language.",
    input_schema: {
      type: "object" as const,
      properties: {
        city: {
          type: "string",
          description: "City name (e.g., 'Seoul', 'Tokyo')",
        },
        unit: {
          type: "string",
          enum: ["celsius", "fahrenheit"],
          description: "Temperature unit (default: celsius)",
        },
      },
      required: ["city"],
    },
  },
  {
    name: "manage_schedule",
    description:
      "View (list), add, or delete user schedules.",
    input_schema: {
      type: "object" as const,
      properties: {
        action: {
          type: "string",
          enum: ["list", "add", "delete"],
          description: "Action to perform",
        },
        date: {
          type: "string",
          description: "Date (YYYY-MM-DD)",
        },
        title: {
          type: "string",
          description: "Event title (for add)",
        },
        time: {
          type: "string",
          description: "Time (HH:MM, for add)",
        },
        schedule_id: {
          type: "string",
          description: "Schedule ID (for delete)",
        },
      },
      required: ["action", "date"],
    },
  },
];

// ============================================
// 2. Tool execution functions (type-safe)
// ============================================
type ToolInput = Record<string, string>;

function executeTool(name: string, input: ToolInput): string {
  switch (name) {
    case "get_weather":
      // In real implementation, call a weather API
      return JSON.stringify({
        city: input.city,
        temp: 23,
        humidity: 60,
        condition: "Clear",
        wind: "4m/s",
        unit: input.unit || "celsius",
      });

    case "manage_schedule":
      if (input.action === "list") {
        return JSON.stringify({
          date: input.date,
          schedules: [
            { id: "1", title: "Team Standup", time: "09:30" },
            { id: "2", title: "Code Review", time: "14:00" },
            { id: "3", title: "1:1 Meeting", time: "16:00" },
          ],
        });
      }
      if (input.action === "add") {
        return JSON.stringify({
          status: "success",
          message: `Added '${input.title}' on ${input.date} ${input.time || ""}`,
          id: "4",
        });
      }
      if (input.action === "delete") {
        return JSON.stringify({
          status: "success",
          message: `Schedule ${input.schedule_id} deleted`,
        });
      }
      return JSON.stringify({ error: "Unknown action" });

    default:
      return JSON.stringify({ error: `Unknown tool: ${name}` });
  }
}

// ============================================
// 3. Agent main loop
// ============================================
async function runAgent(userMessage: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  const MAX_TURNS = 10;

  for (let turn = 0; turn < MAX_TURNS; turn++) {
    const response = await client.messages.create({
      model: "claude-sonnet-4-5-20250514",
      max_tokens: 4096,
      system:
        "You are an assistant that helps with weather checks and schedule management. " +
        "Always respond naturally in English. " +
        "Summarize tool results in natural sentences rather than listing raw data.",
      tools,
      messages,
    });

    // Add assistant response to history
    messages.push({ role: "assistant", content: response.content });

    // If final answer, extract text and return
    if (response.stop_reason === "end_turn") {
      return response.content
        .filter((b): b is Anthropic.TextBlock => b.type === "text")
        .map((b) => b.text)
        .join("\n");
    }

    // Handle tool calls
    if (response.stop_reason === "tool_use") {
      const toolResults: Anthropic.ToolResultBlockParam[] =
        response.content
          .filter(
            (b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
          )
          .map((block) => {
            try {
              const result = executeTool(
                block.name,
                block.input as ToolInput
              );
              return {
                type: "tool_result" as const,
                tool_use_id: block.id,
                content: result,
              };
            } catch (e) {
              return {
                type: "tool_result" as const,
                tool_use_id: block.id,
                content: JSON.stringify({
                  error:
                    e instanceof Error
                      ? e.message
                      : "Unknown execution error",
                }),
                is_error: true,
              };
            }
          });

      messages.push({ role: "user", content: toolResults });
    }
  }

  return "Maximum conversation turns exceeded.";
}

// ============================================
// 4. Execution (multi-step tool call scenario)
// ============================================
(async () => {
  const answer = await runAgent(
    "Tell me the weather in Seoul, check today's schedule, " +
    "and if the weather is nice, add a walk at 3 PM."
  );
  console.log(answer);
  // Claude processes in 3 steps:
  // 1. get_weather("Seoul") + manage_schedule("list", "today")
  // 2. Check weather -> if nice, manage_schedule("add", "today", "Walk", "15:00")
  // 3. Final summary response
})();
6

Operations/Expansion Tips: Production Patterns and Advanced Techniques

Key patterns and expansion strategies for operating Tool Use in a production environment. 1. Managing tool result size: If a tool returns a large amount of data, token costs spike. - DB query results: Return only the top N items + display total count - API responses: Extract and pass only the necessary fields - File contents: Pass only a summary or excerpt 2. Tool input validation (security): Do not trust the inputs Claude generates. - SQL injection prevention: Use parameter binding - Path traversal prevention: Whitelist file paths - Permission checks: Verify per-user access rights 3. Forcing tool selection: - tool_choice: "auto" (default, Claude decides) - tool_choice: {"type": "tool", "name": "get_weather"} (force a specific tool call) - tool_choice: "any" (must use at least one tool) 4. Streaming with Tool Use: Tool Use can be used in streaming mode. Detect the tool_use type in content_block_start events and incrementally collect parameters via input_json_delta. 5. Cost optimization: - Tool definitions themselves count as input tokens -> exclude unused tools - Use Haiku for simple tool routing, Sonnet for complex reasoning - Cache cacheable tool results to prevent duplicate calls 6. Reference documentation: - Official docs: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview - SDK reference: https://github.com/anthropics/anthropic-sdk-python

import anthropic
import json

client = anthropic.Anthropic()

# ============================================
# [Pattern 1] Managing tool result size
# ============================================
def search_database(query: str, limit: int = 10) -> dict:
    """If DB search results are large, summarize and return"""
    # Execute actual DB query
    all_results = [{"id": i, "name": f"Item {i}"} for i in range(100)]

    return {
        "total_count": len(all_results),
        "returned_count": min(limit, len(all_results)),
        "results": all_results[:limit],
        "message": f"Returning top {limit} of {len(all_results)} total results."
    }

# ============================================
# [Pattern 2] Tool input validation (security)
# ============================================
import re

def safe_file_read(filepath: str) -> dict:
    """Validate file path before reading"""
    # Allowed path list
    ALLOWED_DIRS = ["/data/reports/", "/data/exports/"]

    # Prevent path traversal attacks
    normalized = filepath.replace("\\", "/")
    if ".." in normalized:
        return {"error": "Invalid file path."}

    if not any(normalized.startswith(d) for d in ALLOWED_DIRS):
        return {"error": "Access to this path is not allowed."}

    # Actual file read (omitted)
    return {"content": "File contents...", "size": "1.2KB"}

# ============================================
# [Pattern 3] Controlling tool selection with tool_choice
# ============================================

# Default: Claude automatically decides whether to use tools
response_auto = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "auto"},  # Default
    messages=[{"role": "user", "content": "Hello!"}],
)
# -> Claude responds directly without tools (not weather/schedule related)

# Force a specific tool call
response_forced = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "tool", "name": "get_weather"},
    messages=[{"role": "user", "content": "Good morning!"}],
)
# -> Forces get_weather call (regardless of the question)

# Must use at least one tool
response_any = client.messages.create(
    model="claude-sonnet-4-5-20250514",
    max_tokens=1024,
    tools=tools,
    tool_choice={"type": "any"},
    messages=[{"role": "user", "content": "What should I do today?"}],
)
# -> Must call either weather or schedule tool

# ============================================
# [Pattern 4] Tool result caching (cost savings)
# ============================================
from functools import lru_cache
from datetime import datetime

@lru_cache(maxsize=100)
def get_weather_cached(city: str, date: str) -> str:
    """Cache same city+date combinations to prevent duplicate API calls"""
    # Actual weather API call (incurs cost)
    result = {"city": city, "temp": 22, "condition": "Clear"}
    return json.dumps(result, ensure_ascii=False)

# Usage: Even if the same city is queried multiple times on the same day, the API is called only once
today = datetime.now().strftime("%Y-%m-%d")
get_weather_cached("Seoul", today)  # API call
get_weather_cached("Seoul", today)  # Returned from cache (no API call)

# ============================================
# [Pattern 5] Logging and monitoring
# ============================================
import logging

logger = logging.getLogger("agent")

def run_agent_with_logging(user_message: str, tools: list) -> str:
    """Agent that logs all tool calls"""
    messages = [{"role": "user", "content": user_message}]
    total_input_tokens = 0
    total_output_tokens = 0

    for turn in range(10):
        response = client.messages.create(
            model="claude-sonnet-4-5-20250514",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )

        # Track token usage
        total_input_tokens += response.usage.input_tokens
        total_output_tokens += response.usage.output_tokens

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

        if response.stop_reason == "end_turn":
            logger.info(
                f"Complete | Turns: {turn + 1} | "
                f"Input tokens: {total_input_tokens} | "
                f"Output tokens: {total_output_tokens} | "
                f"Est. cost: $" + f"{(total_input_tokens * 3 + total_output_tokens * 15) / 1_000_000:.4f}"
            )
            return "\n".join(
                b.text for b in response.content if hasattr(b, "text")
            )

        if response.stop_reason == "tool_use":
            for b in response.content:
                if b.type == "tool_use":
                    logger.info(f"Tool call: {b.name}({json.dumps(b.input)})")

            # Execute tools and return results (abbreviated)
            tool_results = []
            for b in response.content:
                if b.type == "tool_use":
                    func = TOOL_FUNCTIONS.get(b.name)
                    if func:
                        result = func(**b.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": b.id,
                            "content": json.dumps(result, ensure_ascii=False),
                        })

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

    return "Maximum turns exceeded"

Core Code

Core pattern of the Claude Tool Use agent loop: (1) define tools with JSON Schema, (2) include tools parameter in API call, (3) if stop_reason is "tool_use" execute the function, (4) return tool_result with tool_use_id matched exactly, (5) repeat until "end_turn".

import anthropic, json

client = anthropic.Anthropic()

# 1. Tool definition (JSON Schema)
tools = [{
    "name": "get_weather",
    "description": "Retrieves the current weather for a city",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name"}
        },
        "required": ["city"]
    }
}]

# 2. Agent loop (core pattern)
messages = [{"role": "user", "content": "What's the weather in Seoul?"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-4-5-20250514",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )

    # [Required] Add assistant response to history
    messages.append({"role": "assistant", "content": response.content})

    # If final answer, exit
    if response.stop_reason == "end_turn":
        print([b.text for b in response.content if hasattr(b, "text")])
        break

    # tool_use -> execute function -> return tool_result
    if response.stop_reason == "tool_use":
        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = execute_tool(block.name, block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,  # Must match!
                    "content": json.dumps(output, ensure_ascii=False),
                })
        messages.append({"role": "user", "content": results})

Common Mistakes

Incorrect or missing tool_use_id matching in tool_result, causing API errors

The id value from Claude's returned tool_use block must be matched exactly to the tool_use_id in the tool_result. When parallel calls return multiple tool_use blocks, each one must have a tool_result with the correct id. Use block.id directly in your for loop.

Sending only tool_result without including the assistant response in messages, causing context errors

Claude's tool_use response (the entire response.content) must be added to messages via messages.append({"role": "assistant", "content": response.content}) first, and then the tool_result should be added as a user message. If this order is reversed or omitted, a 400 error occurs.

No maximum turn limit on the agent loop, causing infinite loops and cost spikes

Always set a max_turns limit (typically 5-15 turns is sufficient). Use for turn in range(max_turns) instead of while True, and return a notification message to the user when the turn limit is exceeded. In production, also set a token usage limit.

Writing tool descriptions that are too short or vague, causing Claude to select the wrong tool

Short descriptions like "Gets weather" make it hard for Claude to determine when to use the tool. Describe specifically in 2-3 sentences what inputs the tool accepts, what results it returns, and in what situations it should be used. Also write descriptions for every parameter without exception.

Passing tool execution results to Claude without validation or filtering, causing cost spikes or security issues

Extract only the necessary parts from large data sets (e.g., thousands of DB query rows) before passing them. Also filter out sensitive information (passwords, API keys, internal error stack traces) from results. Add logic to summarize results when they exceed a certain size limit.

Related liminfo Services