June 10, 2026Ā·8 minĀ·By

Function Calling with LLMs: A Complete Implementation Guide

function callingLLMOpenAItool usePython

Function calling lets LLMs return structured data and trigger actions instead of just generating text. It's the foundation of every serious LLM application.

How Function Calling Works

You define a schema, the model returns a structured JSON object matching that schema, and your code executes the actual function. The model never runs code -- it just signals intent.

OpenAI Function Calling

from openai import OpenAI
import json

client = OpenAI()

functions = [
    {
        "name": "get_weather",
        "description": "Get current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "City and country, e.g. 'Vancouver, CA'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location"]
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}],
    tools=[{"type": "function", "function": f} for f in functions],
    tool_choice="auto"
)

msg = response.choices[0].message
if msg.tool_calls:
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_weather(**args)
        print(f"Tool: {call.function.name}, Result: {result}")

Claude Tool Use

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "get_weather",
        "description": "Get current weather for a location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location"]
        }
    }
]

response = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)

for block in response.content:
    if block.type == "tool_use":
        result = get_weather(**block.input)
        # Return result in next message turn

Parallel Function Calls

Both APIs support calling multiple functions simultaneously:

# Handle all tool calls before responding
tool_results = []
for call in response.choices[0].message.tool_calls:
    args = json.loads(call.function.arguments)
    result = dispatch_tool(call.function.name, args)
    tool_results.append({
        "tool_call_id": call.id,
        "role": "tool",
        "content": json.dumps(result)
    })

follow_up = client.chat.completions.create(
    model="gpt-4o",
    messages=[original_message, response.choices[0].message, *tool_results]
)

Writing Good Function Descriptions

The description is what the model reads to decide when to call your function. Bad descriptions lead to wrong calls.

Bad:

{"name": "db_query", "description": "Query the database"}

Good:

{
  "name": "search_products",
  "description": "Search the product catalog by name, category, or price range. Use this when the user asks about available products, wants to find something specific, or asks what you carry. Returns up to 10 matching products with prices and inventory status."
}

Error Handling

Functions fail. Handle it gracefully:

def safe_tool_call(name: str, args: dict) -> str:
    try:
        result = dispatch_tool(name, args)
        return json.dumps(result)
    except ValueError as e:
        return json.dumps({"error": f"Invalid input: {e}"})
    except TimeoutError:
        return json.dumps({"error": "Tool timed out. Try a simpler query."})
    except Exception as e:
        return json.dumps({"error": f"Tool failed: {type(e).__name__}"})

Always return something to the model -- never raise an exception that cuts the conversation short.

Structured Outputs Alternative

For extraction (not action), consider structured outputs:

from pydantic import BaseModel

class ProductInfo(BaseModel):
    name: str
    price: float
    in_stock: bool

response = client.beta.chat.completions.parse(
    model="gpt-4o",
    messages=messages,
    response_format=ProductInfo
)

product = response.choices[0].message.parsed
print(product.name, product.price)

Function calling is for triggering actions. Structured outputs are for extracting information. Use the right tool for the job.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Do I need a GPU to run this in production?+

For hosted APIs (OpenAI, Anthropic, Google) — no. You call an HTTPS endpoint. GPUs only matter if you're self-hosting models, which is overkill for most production use cases.

How do I keep LLM costs under control?+

Cache identical prompts aggressively, use the smallest model that meets your quality bar, and set hard token limits per request. A response cache alone can cut costs 40–60% on typical workloads.

What's the difference between fine-tuning and RAG?+

Fine-tuning bakes knowledge into model weights — expensive, slow to update. RAG retrieves context at query time — cheap to update, easier to debug. Use RAG for most production use cases and fine-tune only when you need a very specific tone or format.

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation