Function calling is the specific mechanism that lets a language model use tools - it is how an agent goes from "generates text" to "takes real actions."

Describing a tool to the model

You describe each available function to the model - its name, what it does, and what parameters it needs - and the model can then respond by asking to call one, with arguments it fills in based on the conversation:

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
            "city": {"type": "string", "description": "City name"}
        }
    }
]

The model requests a call - your code executes it

Critically, the model does not run the function itself - it cannot. It outputs a structured request ("call get_weather with city=Paris"), and your own code is responsible for actually running that function and feeding the result back:

def get_weather(city):
    response = requests.get(f"https://api.weather-example.com/{city}")
    return response.json()

# Pseudocode for the loop:
model_response = model.generate(messages, tools=tools)
if model_response.wants_tool_call:
    result = get_weather(model_response.tool_call.arguments["city"])
    messages.append({"role": "tool", "content": result})
    final_response = model.generate(messages)

Why your code stays in control

Because the model can only request a call - never execute one directly - you decide exactly what functions exist, what they are allowed to do, and can validate every argument before running anything. This is the actual security boundary of an agent system: an agent is only as dangerous as the tools you give it access to.

Good tool design

Give each tool a narrow, clear purpose rather than one giant do-everything function - a model reasons about which tool to use far more reliably when each one has an obvious name and job, the same principle behind writing small, well-named functions in any codebase.