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.