Right, so you want to get some actual work done. You’re tired of just having a witty chat with a language model and getting back a blob of text you have to parse with regex like some kind of digital archaeologist. You want it to, I don’t know, check the weather, query a database, or send an email. That’s where function calling comes in. Don’t let the name fool you; it’s less about the AI actually running your code and more about it being a spectacularly good structured data extraction and reasoning tool. You describe your functions (or “tools”) to the model, and when it decides one is needed, it returns a perfectly formatted JSON object for you to execute. It’s the handoff between the brilliant but disembodied brain and your grunt-work code.

The Core Concept: It’s Just Structured Data

Think of it this way: you’re not really “calling a function” in the traditional programming sense. The OpenAI API doesn’t execute your send_email() function on their servers. Instead, you’re defining a schema for a function call—its name, description, and parameters. The model’s job is to understand the user’s request and, if it aligns with one of your described tools, generate the arguments for that function call as a JSON object. You then take that JSON, run your actual function with it, and send the result back to the model to summarize for the user. It’s a dance, and you’re leading.

Defining Your Tools: The Devil’s in the Details

Here’s the kicker: the quality of your function definitions makes or breaks this entire process. The model is shockingly good at this, but it’s not a mind reader. Your tool definitions are its instructions.

Let’s define a simple, slightly absurd tool to get a user’s astrology sign. Why? Because it’s a great example of structured parameters and the model’s reasoning.

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_astrology_sign",
            "description": "Get the user's astrology sign based on their birth month and day. Useful for when the user asks about their horoscope, sign, or for astrological advice.",
            "parameters": {
                "type": "object",
                "properties": {
                    "month": {
                        "type": "integer",
                        "description": "The numeric birth month (1-12).",
                        "minimum": 1,
                        "maximum": 12
                    },
                    "day": {
                        "type": "integer",
                        "description": "The numeric birth day (1-31).",
                        "minimum": 1,
                        "maximum": 31
                    }
                },
                "required": ["month", "day"]
            }
        }
    }
]

Notice a few key things:

  • The description is crucial. The model uses this to decide when to call the function. “What’s my sign?” will trigger it. “What’s a good zodiac-themed cocktail?” probably won’t.
  • The parameter descriptions are equally important. They tell the model what each field means.
  • We’re using integer types with minimum and maximum constraints. The model will do its best to respect these, but it’s on you to validate the values it returns before you blindly use them. Never trust parsed input, even from a genius AI.

The Execution Loop: The Dance in Code

Now, let’s see the full dance. We’ll simulate a user asking about their sign.

import json
from openai import OpenAI

client = OpenAI()

# Step 1: Send the user message with our tool definition
response = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=[{"role": "user", "content": "My birthday is March 15th, what's my sign?"}],
    tools=tools,
    tool_choice="auto",  # Let the model decide if a tool is needed
)

# Step 2: Check if the model wants to call a function
response_message = response.choices[0].message
tool_calls = response_message.tool_calls

if tool_calls:
    # Step 3: It does! Let's parse the arguments.
    function_name = tool_calls[0].function.name
    function_args = json.loads(tool_calls[0].function.arguments)

    print(f"Model wants to call: {function_name}")
    print(f"With arguments: {function_args}")

    # Step 4: Here, you'd call your actual function.
    # Let's pretend we have a function called get_astrology_sign(month, day)
    # available_functions = {"get_astrology_sign": get_astrology_sign}
    # function_to_call = available_functions[function_name]
    # function_response = function_to_call(**function_args)

    # For this example, we'll just simulate a response.
    simulated_function_response = "Pisces"

    # Step 5: Send the result back to the model for summarization.
    second_response = client.chat.completions.create(
        model="gpt-4-turbo",
        messages=[
            {"role": "user", "content": "My birthday is March 15th, what's my sign?"},
            response_message,  # The original response that called the tool
            {
                "role": "tool",
                "tool_call_id": tool_calls[0].id,
                "content": simulated_function_response,
            },
        ],
    )
    print(second_response.choices[0].message.content)

This would output something like the model wanting to call get_astrology_sign with {'month': 3, 'day': 15}, and the final response would be, “Your birthday on March 15th means your sign is Pisces!”

Pitfalls and Power-Ups

  • Bad Descriptions Cause Chaos: A vague description is the fastest way to get the model to call the wrong tool or not call one at all. Be specific about the tool’s purpose.
  • Validation is Non-Negotiable: The model is great, but it can hallucinate parameters or get creative with formats. Always validate the JSON arguments against your schema in your code before execution. That "day": 35 you didn’t validate will break your function.
  • tool_choice is Your Lever: Use "auto" to let the model decide. Use "none" to force it not to use any tools for this turn. Or, use {"type": "function", "function": {"name": "get_astrology_sign"}} to force it to call a specific tool. This is incredibly useful for guiding the conversation.
  • It’s Not Just for Functions: This pattern is perfect for any action that requires structured data: calling an API, querying a database, or even just getting the user’s input in a specific, parsable format. It’s the end of “Please output in JSON format” prompts. You just define the schema and let the model comply.