Skip to content

Function Calling: Structured Tool Invocation

Overview

Function Calling is the standardized mechanism for agents to invoke tools through an LLM. Instead of generating text that describes what tool to use, the LLM directly outputs structured function calls.

This is the 2025 standard that makes agents practical.


The Evolution

Before Function Calling (2022-2023)

Agent: "I should search for information about AI trends.
        I'll use a search tool."

Human has to parse text and extract:
- Tool name: "search"
- Arguments: {"query": "AI trends"}

❌ Fragile: Text parsing breaks easily
❌ Expensive: Extra reasoning to generate text

With Function Calling (2024-2026)

Agent thinking → LLM → Structured output:
{
  "type": "function_call",
  "function": "search",
  "arguments": {"query": "AI trends"}
}

✅ Reliable: Machine-readable format
✅ Fast: Direct to execution
✅ Flexible: One LLM call can make multiple calls

How Function Calling Works

3-Step Process

Step 1: Define Tools

tools = [
    {
        "name": "search",
        "description": "Search the web for information",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search term"}
            },
            "required": ["query"]
        }
    }
]

Step 2: Send to LLM

response = client.messages.create(
    model="claude-3-5-sonnet",
    messages=[
        {"role": "user", "content": "What are latest AI trends?"}
    ],
    tools=tools
)

Step 3: LLM Returns Structured Call

# LLM response:
{
    "type": "tool_use",
    "name": "search",
    "input": {"query": "latest AI trends 2026"}
}


Implementation: OpenAI Style

class OpenAIFunctionCalling:
    """OpenAI's function_call parameter"""

    def __init__(self):
        self.client = OpenAI()
        self.tools = [
            {
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get weather for a location",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "location": {"type": "string"},
                            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                        },
                        "required": ["location"]
                    }
                }
            }
        ]

    def call_with_functions(self, user_message: str):
        """Call LLM with function definitions"""

        messages = [{"role": "user", "content": user_message}]

        response = self.client.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=self.tools,
            tool_choice="auto"  # Let model decide if/when to use tools
        )

        # Check if model wants to call a tool
        if response.choices[0].message.tool_calls:
            for tool_call in response.choices[0].message.tool_calls:
                tool_name = tool_call.function.name
                tool_args = json.loads(tool_call.function.arguments)

                # Execute the tool
                result = self.execute_tool(tool_name, tool_args)

                # Send result back to model
                messages.append({"role": "assistant", "content": response.choices[0].message})
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": json.dumps(result)
                })

                # Get final response
                final_response = self.client.chat.completions.create(
                    model="gpt-4",
                    messages=messages,
                    tools=self.tools
                )

                return final_response.choices[0].message.content

        return response.choices[0].message.content

    def execute_tool(self, name: str, args: dict):
        """Execute requested tool"""

        if name == "get_weather":
            location = args["location"]
            unit = args.get("unit", "celsius")
            return {"location": location, "temp": 22, "unit": unit}

Implementation: Claude (Anthropic) Style

class ClaudeFunctionCalling:
    """Claude's tool_use content block"""

    def __init__(self):
        self.client = Anthropic()
        self.tools = [
            {
                "name": "get_weather",
                "description": "Get current weather for a location",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "location": {"type": "string", "description": "City name"},
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                    },
                    "required": ["location"]
                }
            }
        ]

    def call_with_tools(self, user_message: str) -> str:
        """Call Claude with tool definitions"""

        messages = [{"role": "user", "content": user_message}]

        while True:
            response = self.client.messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1024,
                tools=self.tools,
                messages=messages
            )

            # Check if Claude wants to use a tool
            has_tool_use = any(
                block.type == "tool_use" 
                for block in response.content
            )

            if not has_tool_use:
                # Claude gave final answer
                return next(
                    block.text for block in response.content 
                    if hasattr(block, "text")
                )

            # Process tool uses
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input

                    # Execute tool
                    result = self.execute_tool(tool_name, tool_input)

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result)
                    })

            # Add assistant response and tool results to messages
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

    def execute_tool(self, name: str, input_data: dict):
        """Execute the requested tool"""

        if name == "get_weather":
            location = input_data["location"]
            unit = input_data.get("unit", "celsius")

            # Call weather API or return mock
            return {
                "location": location,
                "temperature": 22,
                "condition": "Sunny",
                "unit": unit
            }

# Usage
agent = ClaudeFunctionCalling()
response = agent.call_with_tools("What's the weather in Paris?")
print(response)

Function Calling Patterns

Pattern 1: Single Tool Call

User: "What's 5 + 3?"
LLM: "I'll use the calculator tool"
→ function_call: calculator(5 + 3)
→ Result: 8
→ Response: "5 + 3 = 8"

Pattern 2: Multiple Tool Calls (Parallel)

User: "Compare weather in Paris, London, and Tokyo"
LLM: "I'll get weather for all three cities"
→ function_call: get_weather("Paris")
→ function_call: get_weather("London")
→ function_call: get_weather("Tokyo")
→ All execute in parallel
→ Response: Comparison table

Pattern 3: Sequential Tool Calls

User: "Analyze stock price trend for AAPL"
LLM: Step 1 - Get historical data
→ function_call: get_stock_data("AAPL")
LLM: Step 2 - Calculate trend
→ function_call: calculate_trend(data)
LLM: Step 3 - Return analysis

Function Calling Best Practices

1. Clear Tool Descriptions

# ❌ Bad: Vague
{"name": "search", "description": "Search tool"}

# ✅ Good: Specific
{
    "name": "search",
    "description": "Search the web for current information. "
                   "Use for finding recent news, data, or facts. "
                   "Do NOT use for personal knowledge.",
    "parameters": {...}
}

2. Constrain Parameters

# ❌ Bad: Unlimited
{
    "name": "search",
    "parameters": {
        "query": {"type": "string"}  # Could be 10K chars
    }
}

# ✅ Good: Bounded
{
    "name": "search",
    "parameters": {
        "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Search query (1-500 chars)"
        }
    }
}

3. Provide Examples

# ✅ Good: Show usage
{
    "name": "search",
    "description": "Search for information...",
    "examples": [
        {"query": "latest AI trends", "expected_results": "Recent AI news"},
        {"query": "Python asyncio", "expected_results": "Python async docs"}
    ],
    "parameters": {...}
}

Function Calling Warnings ⚠️

Warning 1: Hallucinated Tools

# ❌ LLM invents tools that don't exist
LLM: "I'll call translate_to_klingon()"
# Tool doesn't exist!

# ✅ Validate before executing
if tool_name not in available_tools:
    return error_response("Unknown tool")

Key Lesson: Always validate tool names exist.

Warning 2: Invalid Arguments

# ❌ LLM calls with wrong arguments
LLM: search(query=123)  # Should be string!

# ✅ Validate arguments
def validate_arguments(tool_name, arguments):
    schema = tools[tool_name]["parameters"]
    try:
        validate(arguments, schema)
    except ValidationError:
        return False
    return True

Key Lesson: Validate arguments against schema before execution.

Warning 3: Infinite Loops

# ❌ Tool calls tool that calls original tool
search()  calls parse()  calls search()
# Infinite loop!

# ✅ Track call depth
call_depth = 0
MAX_DEPTH = 10

def execute_tool(name, args):
    global call_depth
    if call_depth >= MAX_DEPTH:
        raise ToolLimitError("Max depth exceeded")
    call_depth += 1

Key Lesson: Limit call depth to prevent infinite loops.


Real-World Example: E-Commerce Agent

class ECommerceAgent:
    def __init__(self):
        self.client = Anthropic()
        self.tools = [
            {
                "name": "search_products",
                "description": "Search product catalog",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"},
                        "category": {"type": "string"},
                        "max_price": {"type": "number"}
                    },
                    "required": ["query"]
                }
            },
            {
                "name": "get_product_details",
                "description": "Get detailed product info",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "product_id": {"type": "string"}
                    },
                    "required": ["product_id"]
                }
            },
            {
                "name": "check_inventory",
                "description": "Check product availability",
                "input_schema": {
                    "type": "object",
                    "properties": {
                        "product_id": {"type": "string"},
                        "size": {"type": "string"},
                        "color": {"type": "string"}
                    },
                    "required": ["product_id"]
                }
            }
        ]

    def help_customer(self, customer_query: str) -> str:
        """Help customer find products"""

        messages = [{"role": "user", "content": customer_query}]

        while True:
            response = self.client.messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1024,
                tools=self.tools,
                messages=messages
            )

            # Check for tool use
            tool_use_blocks = [
                block for block in response.content
                if block.type == "tool_use"
            ]

            if not tool_use_blocks:
                # Final response
                return next(
                    block.text for block in response.content
                    if hasattr(block, "text")
                )

            # Execute tools
            tool_results = []
            for tool_use in tool_use_blocks:
                result = self.execute_tool(tool_use.name, tool_use.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tool_use.id,
                    "content": json.dumps(result)
                })

            # Continue conversation
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})

    def execute_tool(self, tool_name: str, tool_input: dict):
        """Execute tool and return result"""

        if tool_name == "search_products":
            # Search in database
            return self.search_db(tool_input)

        elif tool_name == "get_product_details":
            # Get full product info
            return self.get_details(tool_input["product_id"])

        elif tool_name == "check_inventory":
            # Check inventory
            return self.check_inv(tool_input)

# Usage
agent = ECommerceAgent()
response = agent.help_customer("I need a blue winter coat, size M, under $200")
print(response)

Comparison: Provider APIs

Provider Mechanism Status Key Docs
OpenAI tool_use parameter Stable Function Calling API
Claude tool_use block Stable Tool Use Guide
Gemini function_calling mode Stable Function Calling Docs
Llama Server-side tool calling Beta LlamaIndex Docs

Key Takeaways

  1. Function calling is standard - All major LLMs support it
  2. Machine-readable, not text-parsing - Reliable and fast
  3. Can make multiple calls - Sequential or parallel
  4. Requires validation - Always check tools exist and arguments are valid
  5. Enable agentic behavior - Agents need tools to affect world
  6. Loop until done - LLM may call tools multiple times

Next Steps


Last Updated: August 9, 2026