Complete Tool Use Guide: Single Tool, Parallel Calls, Tool Chain Design Patterns and Token Overhead Calculation
Chapter 18: Tool Use Architecture Overview: Definition, Invocation, and Result Injection
18.1 The Core Design Philosophy of Tool Use
Tool Use is the most important capability extension mechanism in the Claude API. It allows the model to recognize when external capabilities are needed, construct structured function call requests, wait for results to return, and then integrate those results into a final answer.
Compared to simple prompt embedding, the essential difference of Tool Use lies in a structured contract: the input and output formats of tools are strictly defined. Claude doesn't guess results from text — it calls real external systems under explicit Schema constraints.
Tool Use solves three categories of core problems:
- Real-time information: Claude's training data has a cutoff date; search tools provide access to current information
- Calculation precision: Complex math and data processing are handled by code execution tools, avoiding errors from direct LLM computation
- System integration: Database queries and API call tools connect Claude to existing enterprise systems
18.2 The Complete Tool Use Interaction Flow
The key to understanding Tool Use is mastering its multi-turn interaction pattern. A complete tool call involves the following steps:
User message → Claude analysis → tool_use block (call request)
↓
Developer executes tool → obtains result
↓
tool_result block (result injection) → Claude generates final answer
At the API level, this process is represented by the following message sequence:
# Round 1: Send user request with tool definitions
messages = [
{"role": "user", "content": "Query the account balance for user ID 12345"}
]
response1 = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=[...],
messages=messages
)
# response1.stop_reason == "tool_use"
# response1.content contains a tool_use block
# Round 2: Inject tool result
messages.append({"role": "assistant", "content": response1.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": response1.content[0].id,
"content": "Account balance: $2,341.87"
}]
})
response2 = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=[...],
messages=messages
)
# response2.stop_reason == "end_turn"
# Contains the final text answer
18.3 Complete JSON Schema for Tool Definitions
Basic Tool Structure
Each tool is defined by the following fields:
{
"name": "get_account_balance",
"description": "Query the account balance for a specified user. Returns the current balance and last updated timestamp.",
"input_schema": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "Unique user identifier as a numeric string"
},
"currency": {
"type": "string",
"enum": ["USD", "EUR", "GBP"],
"description": "Currency unit for the returned balance, defaults to USD",
"default": "USD"
}
},
"required": ["user_id"]
}
}
Multi-Tool Definition Example
Production systems typically need multiple tools defined:
import anthropic
import json
tools = [
{
"name": "search_database",
"description": "Search records in the enterprise database. Supports fuzzy matching and field filtering.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search keywords"
},
"table": {
"type": "string",
"enum": ["users", "orders", "products", "invoices"],
"description": "The database table to search"
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return",
"minimum": 1,
"maximum": 100,
"default": 10
},
"filters": {
"type": "object",
"description": "Field filter conditions: keys are field names, values are filter values",
"additionalProperties": {"type": "string"}
}
},
"required": ["query", "table"]
}
},
{
"name": "send_email",
"description": "Send an email. Used to notify users or deliver reports.",
"input_schema": {
"type": "object",
"properties": {
"to": {
"type": "array",
"items": {"type": "string"},
"description": "List of recipient email addresses"
},
"subject": {
"type": "string",
"description": "Email subject line"
},
"body": {
"type": "string",
"description": "Email body, supports HTML"
},
"cc": {
"type": "array",
"items": {"type": "string"},
"description": "CC address list (optional)"
}
},
"required": ["to", "subject", "body"]
}
},
{
"name": "execute_calculation",
"description": "Perform precise mathematical calculations. Always use this tool for complex numerical calculations instead of computing directly.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression using Python syntax. E.g., '2 ** 32' or 'sum([1,2,3,4,5])'"
}
},
"required": ["expression"]
}
}
]
18.4 The Complete Structure of tool_use Blocks
When Claude decides to call a tool, a tool_use type block appears in the response content array:
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Search for all orders with status pending"}]
)
print(f"Stop reason: {response.stop_reason}") # "tool_use"
for block in response.content:
print(f"Block type: {block.type}")
if block.type == "tool_use":
print(f"Tool name: {block.name}")
print(f"Tool use ID: {block.id}")
print(f"Input: {json.dumps(block.input, indent=2)}")
A typical tool_use block in JSON:
{
"type": "tool_use",
"id": "toolu_01XFDUDYJgAf9n7mP7YT7V5H",
"name": "search_database",
"input": {
"query": "pending",
"table": "orders",
"filters": {
"status": "pending"
},
"limit": 50
}
}
18.5 The Injection Format for tool_result Blocks
After executing a tool, inject the result into the message history in tool_result format:
Successful Result
tool_result_success = {
"type": "tool_result",
"tool_use_id": "toolu_01XFDUDYJgAf9n7mP7YT7V5H",
"content": json.dumps({
"results": [
{"order_id": "ORD-001", "user_id": "U123", "amount": 299.00, "status": "pending"},
{"order_id": "ORD-002", "user_id": "U456", "amount": 1599.00, "status": "pending"}
],
"total_count": 2,
"query_time_ms": 45
})
}
Error Result
tool_result_error = {
"type": "tool_result",
"tool_use_id": "toolu_01XFDUDYJgAf9n7mP7YT7V5H",
"content": "Query failed: database connection timed out (retried 3 times)",
"is_error": True
}
Image Result
Some tools (like screenshot tools) can return image content:
import base64
with open("screenshot.png", "rb") as f:
image_data = base64.standard_b64encode(f.read()).decode("utf-8")
tool_result_image = {
"type": "tool_result",
"tool_use_id": "toolu_01XFDUDYJgAf9n7mP7YT7V5H",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": image_data
}
},
{
"type": "text",
"text": "Screenshot captured, resolution 1920x1080"
}
]
}
18.6 Complete End-to-End Tool Call Implementation
Tool Execution Engine
import anthropic
import json
from typing import Any, Dict, List, Callable
class ToolExecutor:
"""Tool execution engine"""
def __init__(self):
self.tools: Dict[str, Callable] = {}
self.tool_definitions: List[dict] = []
def register(self, tool_definition: dict, func: Callable):
"""Register a tool"""
name = tool_definition["name"]
self.tools[name] = func
self.tool_definitions.append(tool_definition)
return self
def execute(self, tool_name: str, tool_input: dict) -> Any:
"""Execute a tool call"""
if tool_name not in self.tools:
raise ValueError(f"Unknown tool: {tool_name}")
return self.tools[tool_name](**tool_input)
def build_result_block(self, tool_use_id: str, result: Any,
is_error: bool = False) -> dict:
"""Build a tool_result block"""
if isinstance(result, str):
content = result
else:
content = json.dumps(result, default=str)
block = {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": content
}
if is_error:
block["is_error"] = True
return block
class ToolUseAgent:
"""Agent with tool call support"""
def __init__(self, executor: ToolExecutor, model: str = "claude-opus-4-5"):
self.client = anthropic.Anthropic()
self.executor = executor
self.model = model
self.max_iterations = 10
def run(self, user_message: str, system: str = "") -> str:
"""Run the tool call loop"""
messages = [{"role": "user", "content": user_message}]
create_kwargs = {
"model": self.model,
"max_tokens": 4096,
"tools": self.executor.tool_definitions,
"messages": messages
}
if system:
create_kwargs["system"] = system
for iteration in range(self.max_iterations):
response = self.client.messages.create(**create_kwargs)
if response.stop_reason == "end_turn":
return ' '.join(
b.text for b in response.content if b.type == "text"
)
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
print(f"[Tool call] {block.name}({json.dumps(block.input)})")
try:
result = self.executor.execute(block.name, block.input)
result_block = self.executor.build_result_block(block.id, result)
print(f"[Tool result] Success")
except Exception as e:
result_block = self.executor.build_result_block(
block.id, str(e), is_error=True
)
print(f"[Tool result] Error: {e}")
tool_results.append(result_block)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
create_kwargs["messages"] = messages
else:
break
return "Maximum iterations reached; task may be incomplete."
18.7 The tool_choice Parameter: Controlling Tool Selection Behavior
The tool_choice parameter precisely controls whether Claude uses tools and which tool it uses:
# Mode 1: auto (default) — Claude decides whether to use tools
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto"},
messages=messages
)
# Mode 2: any — must use at least one tool
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "any"},
messages=messages
)
# Mode 3: tool — force a specific tool
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={
"type": "tool",
"name": "search_database"
},
messages=messages
)
# Mode 4: none — disallow tool use (text output only)
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "none"},
messages=messages
)
Usage Scenarios
| tool_choice mode | Suitable scenario |
|---|---|
auto |
General purpose; let Claude judge whether tools are needed |
any |
Ensure Claude queries fresh data rather than relying on memory |
tool + name |
Data extraction tasks; force structured output |
none |
All information collected; Claude needs only to summarize |
18.8 Error Handling and Retry Strategies
Tool Execution Error Handling
class RobustToolExecutor(ToolExecutor):
"""Tool executor with retry and error handling"""
def __init__(self, max_retries: int = 3, retry_delay: float = 1.0):
super().__init__()
self.max_retries = max_retries
self.retry_delay = retry_delay
def execute(self, tool_name: str, tool_input: dict) -> Any:
import time
last_error = None
for attempt in range(self.max_retries):
try:
return super().execute(tool_name, tool_input)
except Exception as e:
last_error = e
if attempt < self.max_retries - 1:
print(f"Tool {tool_name} failed (attempt {attempt+1}), "
f"retrying in {self.retry_delay}s...")
time.sleep(self.retry_delay)
raise RuntimeError(
f"Tool {tool_name} failed after {self.max_retries} attempts: {last_error}"
)
Claude's Behavior When Handling Tool Errors
When is_error: True is set, Claude will:
- Understand that the tool call failed
- Potentially retry with different parameters
- Inform the user why the task cannot be completed
- Offer alternative suggestions where applicable
18.9 Token Accounting and Performance Optimization
Tokens Consumed by Tool Definitions
Tool definitions are sent as part of the system prompt and consume input tokens:
def estimate_tool_tokens(tools: list) -> int:
"""Roughly estimate tokens consumed by tool definitions"""
tool_text = json.dumps(tools)
return len(tool_text) // 4 # Rough estimate: ~4 chars/token for English
print(f"Tool definitions estimated to consume ~{estimate_tool_tokens(tools)} tokens")
Optimization Strategies
- Concise descriptions: Keep each tool description under 100 words, avoid redundancy
- On-demand tool loading: Dynamically select a subset of tools based on conversation context
- Structured output via tool: Using
tool_choice: toolfor data extraction is more reliable than asking Claude to freely output JSON - Trim tool results: Avoid returning oversized JSON — return only the fields Claude needs
def filter_tool_result(raw_result: dict, needed_fields: list) -> dict:
"""Filter tool results to retain only necessary fields"""
return {k: v for k, v in raw_result.items() if k in needed_fields}
Summary
The core of the Tool Use architecture is a strict contract: the tool's Schema defines the input format, the tool_use block is Claude's call request, the tool_result block is the developer's result injection, and the entire loop is driven by stop_reason.
With this foundational architecture in hand, the next chapter dives deep into how to design high-quality tool Schemas that enable Claude to make more precise tool selections in complex scenarios.