Multi-Step Tool Chains
Complex tasks require Claude to call multiple tools in sequence, using the result of one to inform the next. Implementing a robust agentic loop handles this automatically.
The Complete Agent Loop
import anthropic
import json
from typing import Any
client = anthropic.Anthropic()
TOOL_MAP = {}
def tool(func):
"""Decorator to register a function as a tool."""
TOOL_MAP[func.__name__] = func
return func
@tool
def search_web(query: str, num_results: int = 3) -> str:
# Mock implementation
return json.dumps([
{"title": f"Result for {query}", "url": "https://example.com", "snippet": "..."}
for _ in range(num_results)
])
@tool
def get_page_content(url: str) -> str:
# Mock implementation
return f"Content from {url}: Lorem ipsum dolor sit amet..."
@tool
def summarize_findings(findings: list[str], max_words: int = 200) -> str:
# Mock implementation — in real code this could be another LLM call
return f"Summary of {len(findings)} findings: " + " ".join(findings)[:max_words]
tools_definition = [
{
"name": "search_web",
"description": "Search the web for current information on a topic.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"num_results": {"type": "integer", "minimum": 1, "maximum": 10},
},
"required": ["query"],
},
},
{
"name": "get_page_content",
"description": "Fetch and read the full text content of a web page URL.",
"input_schema": {
"type": "object",
"properties": {"url": {"type": "string", "description": "Full URL to fetch"}},
"required": ["url"],
},
},
]
def run_research_agent(query: str, max_turns: int = 10) -> str:
messages = [{"role": "user", "content": query}]
turns = 0
while turns < max_turns:
turns += 1
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
tools=tools_definition,
messages=messages,
)
# Done — return final answer
if response.stop_reason == "end_turn":
text_blocks = [b.text for b in response.content if b.type == "text"]
return "\n".join(text_blocks)
# Process tool calls
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
handler = TOOL_MAP.get(block.name)
if handler:
result = handler(**block.input)
else:
result = json.dumps({"error": f"Unknown tool: {block.name}"})
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
return "Max turns reached without a final answer."
answer = run_research_agent(
"Research the main differences between React Server Components and traditional SSR."
)
print(answer)
Limiting Tool Calls
Prevent runaway agents with turn limits and budget checks:
def run_agent_with_budget(query: str, max_turns: int = 5, max_tokens_budget: int = 50_000) -> str:
total_tokens = 0
# ... same loop, but track tokens and abort if budget exceeded
pass
Multi-step chains are where agents become genuinely powerful — each tool call narrows the problem space.