Skip to main content
LLM Engineering intermediate Lesson 4 of 12

LLM Tool Use and Function Calling

Give LLMs the ability to call your functions, query databases, search the web, and take actions in the real world.

Real-World Scenario

A customer support bot needs to: look up order status in the database, check shipping carrier APIs, and issue refunds through the payment system. Without tool use, the LLM can only answer from its training data. With tool use, it becomes an agent that performs real actions in your systems — while you maintain full control over what those actions are.

Defining and Calling Tools

import anthropic
import json

client = anthropic.Anthropic()

# Define tools as JSON schemas
tools = [
    {
        "name": "get_weather",
        "description": "Get current weather for a city. Use when the user asks about weather.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name, e.g. 'London' or 'New York'"
                },
                "units": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature units. Default: celsius"
                }
            },
            "required": ["city"]
        }
    }
]

# Simulated tool implementation
def get_weather(city: str, units: str = "celsius") -> dict:
    """In production: call a real weather API here."""
    mock_data = {
        "London":   {"temp": 14, "condition": "Cloudy", "humidity": 78},
        "New York": {"temp": 22, "condition": "Sunny",  "humidity": 55},
        "Tokyo":    {"temp": 28, "condition": "Humid",  "humidity": 85},
    }
    data = mock_data.get(city, {"temp": 20, "condition": "Unknown", "humidity": 60})
    unit_label = "°C" if units == "celsius" else "°F"
    if units == "fahrenheit":
        data["temp"] = round(data["temp"] * 9/5 + 32)
    return {**data, "city": city, "unit": unit_label}


def run_tool(tool_name: str, tool_input: dict) -> str:
    """Dispatch to the correct function and return JSON result."""
    if tool_name == "get_weather":
        result = get_weather(**tool_input)
    else:
        result = {"error": f"Unknown tool: {tool_name}"}
    return json.dumps(result)


# The agentic loop
def agent(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

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

        # No tool calls — model gave a final text answer
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text

        # Model wants to call tools
        elif response.stop_reason == "tool_use":
            # Add the model's response (with tool_use blocks) to history
            messages.append({"role": "assistant", "content": response.content})

            # Execute all requested tools
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = run_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            # Return tool results to the model
            messages.append({"role": "user", "content": tool_results})

        else:
            break  # unexpected stop reason

    return "No response generated."


# Test it
print(agent("What's the weather like in London and Tokyo right now?"))

Multi-Tool Agent

import anthropic
import json
import sqlite3
from datetime import datetime

client = anthropic.Anthropic()

# ─── Tool definitions ──────────────────────────────────────────────────────

tools = [
    {
        "name": "lookup_order",
        "description": "Look up an order by order ID. Returns order status, items, and total.",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "Order ID like 'ORD-12345'"}
            },
            "required": ["order_id"]
        }
    },
    {
        "name": "lookup_customer",
        "description": "Look up a customer by email. Returns account status and order history summary.",
        "input_schema": {
            "type": "object",
            "properties": {
                "email": {"type": "string", "description": "Customer email address"}
            },
            "required": ["email"]
        }
    },
    {
        "name": "issue_refund",
        "description": "Issue a refund for an order. Only use after confirming the order is eligible.",
        "input_schema": {
            "type": "object",
            "properties": {
                "order_id":  {"type": "string"},
                "amount":    {"type": "number", "description": "Refund amount in USD"},
                "reason":    {"type": "string", "description": "Reason for refund"}
            },
            "required": ["order_id", "amount", "reason"]
        }
    }
]

# ─── Simulated tool implementations ───────────────────────────────────────

MOCK_ORDERS = {
    "ORD-12345": {"status": "delivered", "items": ["Widget Pro"], "total": 149.99,
                   "delivered_date": "2025-06-01", "eligible_for_refund": True},
    "ORD-67890": {"status": "processing", "items": ["Gadget X2"], "total": 89.99,
                   "delivered_date": None, "eligible_for_refund": False},
}

MOCK_CUSTOMERS = {
    "[email protected]": {"name": "Alice Chen", "tier": "Gold", "total_orders": 23,
                            "account_status": "active"},
}

def lookup_order(order_id: str) -> dict:
    return MOCK_ORDERS.get(order_id, {"error": f"Order {order_id} not found"})

def lookup_customer(email: str) -> dict:
    return MOCK_CUSTOMERS.get(email, {"error": f"Customer {email} not found"})

def issue_refund(order_id: str, amount: float, reason: str) -> dict:
    order = MOCK_ORDERS.get(order_id)
    if not order:
        return {"success": False, "error": "Order not found"}
    if not order["eligible_for_refund"]:
        return {"success": False, "error": "Order not eligible for refund"}
    return {
        "success": True,
        "refund_id": f"REF-{hash(order_id) % 100000:05d}",
        "amount": amount,
        "processed_at": datetime.now().isoformat(),
    }

TOOL_MAP = {
    "lookup_order":    lookup_order,
    "lookup_customer": lookup_customer,
    "issue_refund":    issue_refund,
}


# ─── Generic agentic loop ─────────────────────────────────────────────────

def customer_support_agent(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

    system = """You are a customer support agent for an e-commerce company.
You have tools to look up orders, customers, and process refunds.
Before issuing a refund, always look up the order first to verify eligibility.
Be helpful, concise, and professional."""

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            system=system,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if hasattr(b, "text"))

        messages.append({"role": "assistant", "content": response.content})
        tool_results = []

        for block in response.content:
            if block.type == "tool_use":
                func = TOOL_MAP.get(block.name, lambda **kw: {"error": "Unknown tool"})
                result = func(**block.input)
                print(f"  [tool] {block.name}({block.input}) → {result}")
                tool_results.append({
                    "type": "tool_use_id" and "tool_result",
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                })

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


# Test
print(customer_support_agent(
    "Hi, I'm [email protected]. I'd like a refund for my order ORD-12345 "
    "because the product didn't match the description."
))

Parallel Tool Calls

import anthropic
import json
import concurrent.futures

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_stock_price",
        "description": "Get current stock price for a ticker symbol.",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string", "description": "Stock ticker, e.g. AAPL"}
            },
            "required": ["ticker"]
        }
    },
    {
        "name": "get_company_news",
        "description": "Get recent news headlines for a company.",
        "input_schema": {
            "type": "object",
            "properties": {
                "company": {"type": "string"}
            },
            "required": ["company"]
        }
    }
]

# Mock implementations
def get_stock_price(ticker: str) -> dict:
    prices = {"AAPL": 185.20, "GOOGL": 172.50, "MSFT": 415.30}
    return {"ticker": ticker, "price": prices.get(ticker, 100.0), "currency": "USD"}

def get_company_news(company: str) -> dict:
    return {"company": company, "headlines": [f"{company} reports strong Q2 earnings",
                                               f"{company} announces new product line"]}

TOOL_MAP = {"get_stock_price": get_stock_price, "get_company_news": get_company_news}

def run_tools_parallel(tool_use_blocks) -> list[dict]:
    """Execute multiple tool calls concurrently."""
    def execute(block):
        result = TOOL_MAP[block.name](**block.input)
        return {"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}

    with concurrent.futures.ThreadPoolExecutor() as pool:
        return list(pool.map(execute, tool_use_blocks))


response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[{
        "role": "user",
        "content": "Compare AAPL and MSFT — get current prices and recent news for both."
    }]
)

if response.stop_reason == "tool_use":
    tool_blocks = [b for b in response.content if b.type == "tool_use"]
    print(f"Model requested {len(tool_blocks)} tool calls — running in parallel")
    results = run_tools_parallel(tool_blocks)
    print(f"All tools completed: {[r['tool_use_id'] for r in results]}")

Frequently Asked Questions

How does tool use work mechanically?
You define tools as JSON schemas describing function names, parameters, and types. When the LLM determines a tool should be called, it returns a structured tool_use block instead of text. Your code executes the function, returns the result, and the LLM incorporates the result into its final response. The LLM never executes code directly — it only requests calls.
Can the LLM call multiple tools in one turn?
Yes. Claude can call multiple tools in parallel within a single response when they don't depend on each other. Your code runs all requested tool calls concurrently, returns all results, and Claude synthesizes them into one response.