Skip to main content
Chapter 4 Tool Use and Function Calling

Multi-Step Tool Chains

22 min read Lesson 15 / 28

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.