We use cookies to enhance your experience on the site
CodeWorlds

Function Calling - AI with Tools

A language model has no idea what today's weather in the Serengeti is like, nor how many lions still roam the park - all it can really do is arrange words. Function calling hands it tools: your own Python functions, which it can reach for whenever a question goes beyond what it knows. Despite the name it has nothing to do with a telephone call center and nothing to do with a type of database - it is the ability of AI to decide to invoke external functions.

The most important part: the model does not execute the function

Before you write a single line of code, get one thing straight - it is the source of every misunderstanding around function calling. The model never runs your function by itself. All it does is say: "I want to call

get_weather
with the argument
Serengeti
". The running is your job. That is exactly where this differs from ordinary calling functions in Python code: the code is still yours, but the decision to reach for it belongs to the model. It is a conversation in turns:

  1. Define tools - the function descriptions the model is allowed to read.
  2. Send a request with tools to the model, together with the user question.
  3. The model returns
    tool_calls
    instead of a normal answer.
  4. Execute the selected function yourself, in Python.
  5. Send the result back to the model, which turns it into a human sentence.

We will walk through those turns one at a time. We start with how to describe a tool, because the model never sees your code - only its description.

1from openai import OpenAI
2import json
3
4client = OpenAI()
5
6tools = [
7    {
8        "type": "function",
9        "function": {
10            "name": "get_weather",
11            "description": "Retrieves weather for a Safari location",
12            "parameters": {
13                "type": "object",
14                "properties": {
15                    "location": {
16                        "type": "string",
17                        "description": "Location name, e.g. 'Serengeti'"
18                    }
19                },
20                "required": ["location"]
21            }
22        }
23    }
24]

Look carefully at what you are reading: in the OpenAI API you define tools as a list of dictionaries describing functions and parameters - not as separate JSON files, not as Python classes, and definitely not as npm modules, which belong to an entirely different ecosystem. The shape resembles JSON, but it lives inside your Python code as an ordinary list, which is why a real Safari assistant would simply carry more entries in it: one for the weather, another for animal data. And this is only the description of a tool, never its behaviour. The

description
fields matter most - the model reads them to decide when to reach for the function and what to put in the argument. The
required
section states which arguments cannot be left out.

Turn two: the model asks for a call

Now we send the question together with the list of tools. Read the response closely, because it is not an answer meant for the user.

1response = client.chat.completions.create(
2    model="gpt-5",
3    messages=[{"role": "user", "content": "What is the weather in Serengeti?"}],
4    tools=tools,
5    tool_choice="auto"
6)
7
8message = response.choices[0].message
9if message.tool_calls:
10    for tool_call in message.tool_calls:
11        function_name = tool_call.function.name
12        arguments = json.loads(tool_call.function.arguments)
13        print(f"Model wants to call: {function_name}({arguments})")

Instead of a finished reply, the model returned

tool_calls
- a list of requests to invoke something. The arguments arrive as JSON text, which is why
json.loads
converts them into a Python dictionary. At this stage nothing has actually happened yet - the model has merely stated what it wants. The setting
tool_choice="auto"
leaves that judgement to it: it may reach for a tool, or answer immediately when the question needs no help at all. Execution is our next step.

Turn three: you are the one who executes

The model knows the function name only as a piece of text. So we need the real implementation, plus a way of getting from that text to the right code.

1def get_weather(location: str) -> dict:
2    # In a real application - a call to a weather API
3    return {"location": location, "temperature": 28, "conditions": "Sunny"}
4
5available_functions = {
6    "get_weather": get_weather
7}

The

available_functions
dictionary is the bridge between the world of the model and your code: the model hands you the string
"get_weather"
, and you use that string to pull out the genuine function. You decide what truly runs - the model can reach nothing you have not placed in this mapping. Notice that this is the natural spot for a safety check, since a name that is missing from the dictionary simply never gets executed.

All the turns together

Now watch the whole conversation: the question, the model request, the execution, handing the result back and the final answer. Follow how the

messages
list grows with every turn - defining a tool and handling the function call made by the model is the entire skill.

1def chat_with_tools(user_message: str):
2    messages = [{"role": "user", "content": user_message}]
3
4    response = client.chat.completions.create(
5        model="gpt-5", messages=messages, tools=tools
6    )
7    message = response.choices[0].message
8
9    if message.tool_calls:
10        messages.append(message)  # store the model request
11
12        for tool_call in message.tool_calls:
13            name = tool_call.function.name
14            args = json.loads(tool_call.function.arguments)
15            result = available_functions[name](**args)  # you execute it
16
17            tool_response = {
18                "role": "tool",
19                "tool_call_id": tool_call.id,
20                "content": json.dumps(result)  # hand the result back
21            }
22            messages.append(tool_response)
23
24        final = client.chat.completions.create(model="gpt-5", messages=messages)
25        return final.choices[0].message.content
26
27    return message.content

What matters here are the two

create
calls: the first, in which the model asks for a tool, and the second - once the result has been added - in which the model composes an answer for a human. Every result travels back into the conversation as a message with the role
"tool"
carrying a
tool_call_id
, and it gets there through
messages.append(tool_response)
. That is how you tell the model "here is the output of exactly the function you asked for". Without that second turn the user would receive raw JSON instead of a sentence.

LangChain - the same idea, less code

Steering the turns by hand gets tedious fast. LangChain packs the entire loop into an agent - you only mark your functions as tools, and it runs the conversation for you.

1from langchain.tools import tool
2from langchain.agents import create_tool_calling_agent, AgentExecutor
3from langchain_openai import ChatOpenAI
4from langchain_core.prompts import ChatPromptTemplate
5
6@tool
7def calculate_distance(from_loc: str, to_loc: str) -> str:
8    """Calculates the distance between Safari locations."""
9    return f"Distance from {from_loc} to {to_loc}: 150 km"
10
11llm = ChatOpenAI(model="gpt-5")
12prompt = ChatPromptTemplate.from_messages([
13    ("system", "You are a helpful Safari assistant."),
14    ("human", "{input}"),
15    ("placeholder", "{agent_scratchpad}")
16])
17
18agent = create_tool_calling_agent(llm, [calculate_distance], prompt)
19executor = AgentExecutor(agent=agent, tools=[calculate_distance], verbose=True)
20executor.invoke({"input": "How far is it from Nairobi to Serengeti?"})

Start with the first line, because everything else depends on it: the decorator arrives through

from langchain.tools import tool
- those four pieces in that exact order. The
@tool
decorator then does what we wrote out by hand earlier: it builds the model-facing description from the docstring, and the argument list from the signature.
AgentExecutor
drives every turn from the previous example on your behalf, round after round, until the model decides it finally has the full picture.

Remember one sentence from this lesson, @name: function calling is not "the AI runs your code", it is the model asking, while you execute and hand the result back. Everything else - OpenAI or LangChain - is only a more comfortable way of writing down that same conversation in turns.

Go to CodeWorlds