Function Calling and Tool Use in LLMs Explained

Function calling lets a language model ask your code to look something up or take an action. This guide explains the round trip and how to define tools, then puts ten customer requests to a small local model with three tools, and shows the four ways it went wrong and the safeguards that caught them.

GloryTecks Team

Training & Placement Faculty

9 January 2026 12 min read

Last updated: 3 October 2026

Share:

A language model on its own can only produce text. It cannot check an order, read a database or send an email. Function calling, also called tool use, is how it gets those abilities without being given them directly. The model writes a request to call one of your functions. Your code decides whether to run it, runs it, and hands the result back.

That division is the whole design. The model chooses; your code acts. Most of the reliability and almost all of the safety of tool use comes from taking the second half seriously.

The round trip

OpenAI's documentation describes the flow in five steps: send a request with the tools the model may call, receive a tool call, execute the code in your application, send the result back, and receive the final reply or further calls (OpenAI: function calling). Other providers work the same way. Claude signals a tool request with a tool_use stop reason, and your code executes the call and returns the result (Anthropic: tool use). A local model served by Ollama returns tool_calls in its message, and the result goes back as a message with the tool role (Ollama: tool calling).

A tool is described by three things:

PartPurpose
NameWhat the model writes to ask for it
DescriptionHow the model decides when to use it
ParametersA JSON schema: argument names, types, allowed values

The model never sees your code, only these descriptions. It chooses a tool the way a new colleague would choose from a list of internal services: by reading what the list says.

Ten requests, three tools

The script gives a small local model three tools for an invented furniture shop: look up an order, check stock, and start a return. It sends ten customer messages and compares each tool call with the one a careful human would make. Every call is checked before anything runs, and the return tool does nothing until the customer confirms.

python
import json
import re
import urllib.request

URL = "http://localhost:11434/api/chat"
MODEL = "llama3.2"
OPTIONS = {"temperature": 0, "seed": 42}

# A stub shop, invented for this example
ORDERS = {"A1001": "shipped, arriving 6 October", "A1002": "being packed", "A1003": "delivered on 30 September"}
STOCK = {"desk lamp": 0, "office chair": 14, "monitor arm": 3}


def get_order_status(order_id):
    return ORDERS.get(order_id, "no such order")


def check_stock(product):
    return f"{STOCK[product]} in stock"


def create_return(order_id, reason):
    # an action with consequences: the application asks the customer before doing it
    return "not done yet: ask the customer to confirm the return"


TOOLS = [
    {"type": "function", "function": {
        "name": "get_order_status", "description": "Look up the delivery status of an order",
        "parameters": {"type": "object", "required": ["order_id"], "properties": {
            "order_id": {"type": "string", "description": "Order number, a letter A followed by four digits, e.g. A1234"}}}}},
    {"type": "function", "function": {
        "name": "check_stock", "description": "Check how many units of a product are in stock",
        "parameters": {"type": "object", "required": ["product"], "properties": {
            "product": {"type": "string", "enum": list(STOCK)}}}}},
    {"type": "function", "function": {
        "name": "create_return", "description": "Start a return for an order",
        "parameters": {"type": "object", "required": ["order_id", "reason"], "properties": {
            "order_id": {"type": "string", "description": "Order number, e.g. A1234"},
            "reason": {"type": "string", "enum": ["damaged", "wrong item", "no longer needed"]}}}}},
]
FUNCTIONS = {"get_order_status": get_order_status, "check_stock": check_stock, "create_return": create_return}
SYSTEM = ("You are the support assistant of an online furniture shop. Use a tool when it helps. "
          "If you need a detail the customer has not given, ask for it.")


def chat(messages):
    body = {"model": MODEL, "messages": messages, "tools": TOOLS, "stream": False, "options": OPTIONS}
    req = urllib.request.Request(URL, json.dumps(body).encode(), {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=900) as r:
        return json.load(r)["message"]


def validate(name, args):
    # the model's arguments are untrusted input: check them before running anything
    spec = next(t["function"]["parameters"] for t in TOOLS if t["function"]["name"] == name)
    for key in spec["required"]:
        if key not in args:
            return f"missing {key}"
    for key, value in args.items():
        rule = spec["properties"].get(key)
        if rule is None:
            return f"unknown argument {key}"
        if "enum" in rule and value not in rule["enum"]:
            return f"{key} must be one of {rule['enum']}"
        if key == "order_id" and not re.fullmatch(r"A\d{4}", str(value)):
            return "order_id must look like A1234"
    return None


def show(call):
    f = call["function"]
    return f"{f['name']}(" + ", ".join(f"{k}={v!r}" for k, v in f["arguments"].items()) + ")"


REQUESTS = [
    ("Where is my order A1001?", ["get_order_status(order_id='A1001')"]),
    ("Has order A1003 arrived yet?", ["get_order_status(order_id='A1003')"]),
    ("Do you have any office chairs in stock?", ["check_stock(product='office chair')"]),
    ("Is the monitor arm available?", ["check_stock(product='monitor arm')"]),
    ("The lamp from order A1002 arrived broken. I want to send it back.", ["create_return(order_id='A1002', reason='damaged')"]),
    ("What are your opening hours?", []),
    ("Thanks, that's all!", []),
    ("Where is my order? I can't find the number.", []),
    ("Check order A1002 and tell me if desk lamps are in stock.",
     ["check_stock(product='desk lamp')", "get_order_status(order_id='A1002')"]),
    ("Where is order 1001?", ["get_order_status(order_id='1001')"]),
]

matched = 0
transcripts = {}
for i, (text, expected) in enumerate(REQUESTS, 1):
    messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": text}]
    reply = chat(messages)
    calls = reply.get("tool_calls") or []
    shown = sorted(show(c) for c in calls)
    ok = shown == sorted(expected)
    matched += ok
    print(f"{i:>2}. {text}")
    print(f"    expected: {', '.join(expected) or 'no tool'}")
    print(f"    model:    {', '.join(shown) or 'no tool'}  -> {'as expected' if ok else 'DIFFERENT'}")
    # run the calls that pass validation, return results or errors, and let the model reply
    messages.append(reply)
    for c in calls:
        name, args = c["function"]["name"], c["function"]["arguments"]
        problem = validate(name, args) if name in FUNCTIONS else "unknown tool"
        result = f"error: {problem}" if problem else FUNCTIONS[name](**args)
        print(f"    tool result: {result}")
        messages.append({"role": "tool", "tool_name": name, "content": result})
    final = chat(messages)["content"] if calls else reply["content"]
    transcripts[i] = final

print(f"\nmatched the expected calls: {matched} of {len(REQUESTS)}")
for i in (1, 5, 8, 10):
    print(f"\nfinal reply to request {i}:\n{transcripts[i]}")
text
 1. Where is my order A1001?
    expected: get_order_status(order_id='A1001')
    model:    get_order_status(order_id='A1001')  -> as expected
    tool result: shipped, arriving 6 October
 2. Has order A1003 arrived yet?
    expected: get_order_status(order_id='A1003')
    model:    get_order_status(order_id='A1003')  -> as expected
    tool result: delivered on 30 September
 3. Do you have any office chairs in stock?
    expected: check_stock(product='office chair')
    model:    check_stock(product='office chair')  -> as expected
    tool result: 14 in stock
 4. Is the monitor arm available?
    expected: check_stock(product='monitor arm')
    model:    check_stock(product='monitor arm')  -> as expected
    tool result: 3 in stock
 5. The lamp from order A1002 arrived broken. I want to send it back.
    expected: create_return(order_id='A1002', reason='damaged')
    model:    create_return(order_id='A1002', reason='damaged')  -> as expected
    tool result: not done yet: ask the customer to confirm the return
 6. What are your opening hours?
    expected: no tool
    model:    get_order_status(order_id='N/A')  -> DIFFERENT
    tool result: error: order_id must look like A1234
 7. Thanks, that's all!
    expected: no tool
    model:    no tool  -> as expected
 8. Where is my order? I can't find the number.
    expected: no tool
    model:    get_order_status(order_id='A1234')  -> DIFFERENT
    tool result: no such order
 9. Check order A1002 and tell me if desk lamps are in stock.
    expected: check_stock(product='desk lamp'), get_order_status(order_id='A1002')
    model:    check_stock(product='desk lamp')  -> DIFFERENT
    tool result: 0 in stock
10. Where is order 1001?
    expected: get_order_status(order_id='1001')
    model:    get_order_status(order_id='A1001')  -> DIFFERENT
    tool result: shipped, arriving 6 October

matched the expected calls: 6 of 10

final reply to request 1:
Thank you for providing the order ID A1001. According to our system, your order has been shipped and is expected to arrive on or around October 6th. If you have any further questions or concerns about your order, please don't hesitate to ask. Is there anything else I can assist you with?

final reply to request 5:
I've called the tool to generate a response for your situation.

To proceed with returning the lamp, I need to confirm some details with you. The tool suggests that we start by acknowledging receipt of the damaged item and requesting your confirmation on the return process.

Can you please confirm if the lamp arrived broken as you mentioned? And would you like us to provide a pre-paid return shipping label or would you prefer to arrange for your own shipping?

final reply to request 8:
It looks like we couldn't find any information on an order with that specific ID (A1234). Can you please provide me with your order number or any other details about your order, such as the date you placed it or the products you ordered? I'll do my best to help you track down the status of your order.

final reply to request 10:
Order #1001 has been shipped and is expected to arrive on October 6th. You can track the status of your order by visiting our website and logging into your account. If you have any further questions or concerns, please don't hesitate to contact us.

What went right, and what went wrong

Six of ten requests produced exactly the expected calls, including the return, with the right order number and reason. The four misses are worth more than the successes.

  • It invented an argument. Asked about an order without a number (request 8), the model called the lookup with A1234. That is the example written in the tool's description. The value had the right format, so the validator let it through, and only the lookup failing stopped it. The final reply then did the right thing and asked for the number.
  • It guessed and presented the guess as fact. Asked about "order 1001" (request 10), it silently looked up A1001, a real order, and told the customer that order 1001 had shipped. It also added advice about tracking on a website, which no tool had provided.
  • It called a tool it did not need. Asked about opening hours (request 6), it tried the order lookup with N/A. The validator rejected it.
  • It missed a call. Request 9 needed two lookups, an order and a stock check. The model made only the stock check.

Two of these were caught by code that checked the arguments. One was stopped by the data itself. One, the guessed order number, reached the customer. A larger model makes fewer of these mistakes, but the categories are the same, and they are the ones to design against.

Making tool use reliable

Validate every argument in code. Treat the model's arguments as untrusted input. Check types and formats, then check them against real data: does the order exist, and does it belong to this customer?

Write descriptions without plausible example values. The invented A1234 came straight from the description. Describe the format in words, or use an example that cannot be real.

Use enums wherever the values are known. The stock and reason fields could only take listed values, and the model never got them wrong. OpenAI's guidance makes the same point: use enums and structure to prevent invalid states.

Do not make the model supply what you already know. If the logged-in customer's order is known, pass it from your code. OpenAI's guidance says the same, and keeps the number of tools available at the start of a turn small, suggesting fewer than 20.

Return errors as results. The validation messages went back to the model as tool results. That gives it a chance to recover or ask the user, instead of the application failing.

Expect zero, one or several calls. A reply can contain any number of tool calls, and code that handles only the first will drop work.

Strict mode helps, but only with the format

Hosted APIs can enforce the schema for you. OpenAI's strict mode makes calls reliably follow the function's schema, and the company recommends always enabling it. Claude's strict: true does the same. With it, malformed arguments disappear.

Two of the four failures above would have passed any schema. A1234 and A1001 were both well-formed order numbers, so the format was right and the meaning was wrong. Only checks against your own data catch that.

Actions need a person's confirmation

Reading data is one thing. Changing it is another. A model can be wrong, confused by an ambiguous message, or manipulated by text hidden in content it reads. OWASP calls the resulting risk excessive agency, with three roots: too much functionality, too many permissions and too much autonomy, such as high-impact actions taken without the user's confirmation (OWASP: excessive agency).

The return tool in the script shows the pattern: it records the intent and replies "not done yet: ask the customer to confirm". Refunds, deletions, emails and payments should work the same way. See guardrails for LLMs.

From one call to many

This article covers single rounds of tool use. When the model calls tools repeatedly, reading each result and deciding the next step, it becomes an agent. See AI agents explained. To expose the same tools to many applications through one standard interface, see MCP explained.

Common mistakes

  • Running whatever the model asks for without validation.
  • Examples in descriptions that look like real values.
  • Free-text parameters where a list of allowed values would do.
  • Too many tools with overlapping descriptions.
  • Letting the model take irreversible actions without confirmation.
  • Testing only the happy path. Include requests that need no tool, need two, or lack a detail.

What is function calling in LLMs?

A feature that lets a language model request a call to a function you have described, with arguments it fills in. Your application runs the function and returns the result, which the model uses in its reply. The model itself never executes code.

Is function calling the same as tool use?

Yes. OpenAI and others say function calling, Anthropic says tool use, and Ollama says tool calling. They all describe the same round trip between the model and your code.

Does strict mode stop wrong tool calls?

It guarantees the arguments match the schema, so malformed calls disappear. It cannot tell whether a well-formed value is the right one, such as a real but wrong order number. You still need checks against your own data.

Can small local models do function calling?

Many can, through runners such as Ollama. In the test above, a 3-billion-parameter model made the expected calls for six of ten requests. Expect more mistakes than with large hosted models, and validate accordingly.

Working with the OpenAI API and building assistants with tool use are part of the Generative AI course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Generative AI?

Join GloryTecks’ expert-led Agentic AI training in Hyderabad with real-time projects and 100% placement support.

Explore course