Agentic AI · Module 3 of 41 · 11 concepts · about 19 min

Tool Use and Function Calling

What a tool is, how function calling really works on the wire, how to write schemas a model can use, how tools are chosen, chained, run in parallel, retried when they fail, and restricted by permission.

At work: Most of the engineering in a real agent. A team that gets tools right can use a cheaper model; a team that gets them wrong cannot be rescued by an expensive one.

What tools are and why agents need them

What it is

A tool is a function in your program that the model may ask to have run. A language model alone has three gaps that tools fill:

  • It does not know your data: tools look it up (orders, files, the web).
  • It cannot calculate or run code reliably: tools do it exactly (a calculator, a Python sandbox).
  • It cannot change anything: tools act (send an email, issue a refund, book a slot).

Why it matters

Tools turn a writer into a worker.

  • Read tools replace guessing with facts, which is the largest single cut in made-up answers.
  • Write tools are what make the system useful, and also what make it dangerous.
  • Each tool is ordinary code you control, so it is the natural place for every rule.

In the real world: A finance assistant that added invoice lines itself was wrong on one long invoice in forty; routing every sum through a calculate tool brought that to zero.

Visual example

Tool catalogue

Read tools are safe to retry. Write tools need the most care.

Common kinds of tool and what each is for
toolkindexample callmain risk
Web searchReadsearch('RBI repo rate today')Untrusted text comes back
Database queryReadget_order(order_id=88213)Seeing another user's data
File retrievalReadread_file('policy.md')Huge results fill the context
CalculatorComputecalculate('1499 * 0.18')None
Python executionComputerun_python(code)Code escapes its sandbox
External APIRead or writecreate_ticket(title, body)Rate limits and partial failure
EmailWritesend_email(to, subject, body)Cannot be unsent
CalendarWritebook_slot(day, time)Double booking on retry

Letting the model do arithmetic or date maths in its head because 'it is usually right'. Usually is not good enough for a refund amount. Give it a calculator tool or compute the value in code.

Practise it: Design an AI Customer Support Agent

Function calling, step by step

What it is

Function calling (also called tool calling) is the agreed format in which a model asks for a tool. One round trip has four steps:

  • You send the conversation plus a list of tool definitions.
  • The model replies with a tool request: the tool's name, an id, and the inputs as JSON. The reply's stop reason says it stopped to use a tool.
  • Your code runs the function with those inputs.
  • You send back a tool result carrying the same id, and the model continues.

Why it matters

The model never touches your systems. It only produces structured text.

  • That is a safety property: between step 2 and step 3 your code can validate, refuse, ask a person, or log.
  • It is also why agents work with any system: if you can write a function for it, the model can use it.

In the real world: A developer new to agents is surprised to find no 'agent API': the provider only ever returns text and tool requests, and the loop that runs tools is thirty lines of their own code.

Visual example

Function calling

The model writes a request. Your code does everything else.

One tool call, start to finish

  1. Send definitionsConversation + tool schemas
  2. Model requestsget_order {order_id: 88213}
  3. Your code runs itValidate, permit, call
  4. Return the resultSame id; call the model again
import anthropic

client = anthropic.Anthropic()          # reads the API key from the environment

tools = [{
    "name": "get_order",
    "description": "Look up one order by its id. Use this before making any "
                   "statement about an order's contents, status or lateness.",
    "input_schema": {
        "type": "object",
        "properties": {"order_id": {"type": "integer", "description": "The order number"}},
        "required": ["order_id"],
        "additionalProperties": False,
    },
}]

messages = [{"role": "user", "content": "Was order 88213 late?"}]
reply = client.messages.create(
    model="claude-opus-5-5", max_tokens=16000, tools=tools, messages=messages,
)

# reply.stop_reason == "tool_use"
# reply.content contains: {"type": "tool_use", "id": "toolu_01", "name": "get_order",
#                          "input": {"order_id": 88213}}

call = next(block for block in reply.content if block.type == "tool_use")
result = get_order(**call.input)        # YOUR code runs the function

messages.append({"role": "assistant", "content": reply.content})
messages.append({"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": call.id, "content": str(result)},
]})
# Call the model again with the longer messages list to get its answer.

Dropping the model's own tool request from the history and sending only the result. The result's id must match a request the model can see, or the API rejects the call. Always append the whole reply, then the result.

Tool schemas

What it is

A tool schema describes a tool's inputs in JSON Schema, a standard way to say what shape a piece of JSON must have. A good one states:

  • each input's name and type (string, integer, boolean, list);
  • which inputs are required;
  • the allowed values where there are only a few (an enum);
  • a description of each input in plain words, with an example and the format;
  • that no extra fields are allowed.

Why it matters

The model reads the schema to learn how to call the tool, and your code uses it to reject bad calls.

  • Tight types prevent whole classes of mistake: an enum of three reasons cannot be misspelt.
  • Descriptions are instructions. 'Date in YYYY-MM-DD' removes a guess.
  • Many providers offer a strict mode that guarantees the inputs match the schema exactly.

In the real world: A booking agent kept sending dates as '5th March'; adding 'ISO date, for example 2026-03-05' to one description ended a week of parsing errors.

refund_tool = {
    "name": "refund_delivery_fee",
    "description": (
        "Refund the delivery fee of ONE order to the customer's wallet. "
        "Use only after get_order shows the order was more than 30 minutes late. "
        "Do not use for item refunds; use refund_item for those."
    ),
    "strict": True,                      # inputs are guaranteed to match the schema
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "integer", "description": "From get_order, e.g. 88213"},
            "reason": {
                "type": "string",
                "enum": ["late_delivery", "rider_no_show", "goodwill"],
                "description": "Why the fee is being refunded",
            },
        },
        "required": ["order_id", "reason"],
        "additionalProperties": False,
    },
}

A free-text input where a fixed list would do. reason: string invites 'Late', 'late delivery' and 'customer upset'. An enum gives you three clean values you can count and enforce.

Tool descriptions and tool selection

What it is

Tool selection is the model choosing which tool to request. It chooses by reading the descriptions, so a description should say:

  • what the tool does, in one sentence;
  • when to use it;
  • when not to, and which tool to use instead;
  • what it returns.

Why it matters

A wrong choice is the most common tool failure, and it is nearly always a description problem.

  • Two tools whose descriptions overlap will be confused.
  • A tool with no 'when to use' is used too often or never.
  • Rewriting descriptions is the cheapest improvement available: no code, no bigger model.

In the real world: An agent kept calling search_orders when it already had an order id; adding 'if you already have the order id, use get_order instead' fixed it in one line.

Visual example

Descriptions

The good version answers what, when, when not, and what comes back.

The same tool, described badly and well
weak descriptionstrong description
WhatGets ordersReturns one order: items, total, delivery time, status
When(missing)Use before any claim about an order
When not(missing)To list several orders, use get_recent_orders
Returns(missing)A short summary, or 'not found'

Describing the tool for a programmer ('wraps GET /v2/orders'). The model needs to know what the tool is for, in the language of the task, not how it is implemented.

Practise it: Design a Tool-Calling Agent Over 500 APIs

Tool routing and dynamic tool selection

What it is

When there are many tools, showing them all on every turn fails. Tool routing narrows the list first:

  • Static routing: a first step classifies the request ('billing', 'delivery') and loads only that group's tools.
  • Tool retrieval: tool descriptions are indexed like documents, and the few most relevant to the request are fetched by search.
  • Dynamic selection: the visible set changes during the task, for example write tools appear only after the customer is verified.

Why it matters

Every tool definition is text the model reads on every turn.

  • Beyond a few dozen tools, selection accuracy drops and cost rises.
  • Showing fewer, more relevant tools makes the right choice easier and each call cheaper.
  • Changing the set by stage is also a safety control: a tool that is not offered cannot be misused.

In the real world: A platform with five hundred internal APIs gives its agent a search over tool descriptions; each task sees about eight tools, and accuracy matches a hand-picked set.

Visual example

Tool routing

The model picks from a short list that was picked for it.

Choosing which tools the model sees

  1. Request“Where is my refund?”
  2. RouteClassifier picks the billing group
  3. Short listFour tools, not four hundred
  4. Model selectsget_refund_status

Routing so narrowly that the agent cannot finish. If a billing question turns out to need the delivery tools, there must be a way to widen the set, or a hand-over.

Practise it: Design a Tool-Calling Agent Over 500 APIs

Tool execution and tool responses

What it is

Tool execution is your code running the function. A robust executor does five things in order:

  • look up the tool by name, and reject unknown names;
  • validate the inputs against the schema;
  • check permission for this user and this action;
  • run the function with a time limit;
  • shape the response: short, plain, and only the fields that matter.

Why it matters

The response is the model's next observation, so its shape decides the next step.

  • A short, clear result leads to a good decision; a long raw one hides the important line.
  • Everything returned is paid for again on every later turn.
  • Returning ids and names the model can use in the next call makes chaining work.

In the real world: An agent's order tool returned the full order record, 3,000 tokens of internal fields; cutting it to eight lines made every later turn cheaper and removed two kinds of wrong answer.

import json

TOOLS = {"get_order": get_order, "refund_delivery_fee": refund_delivery_fee}

def execute(call, user) -> dict:
    """Run one tool request and return a tool_result block."""
    def result(text: str, error: bool = False) -> dict:
        return {"type": "tool_result", "tool_use_id": call.id,
                "content": text, "is_error": error}

    fn = TOOLS.get(call.name)
    if fn is None:                                   # the model invented a tool
        return result(f"No tool named {call.name}. Available: {sorted(TOOLS)}", True)

    problem = validate(call.name, call.input)        # check against the JSON schema
    if problem:
        return result(f"Invalid input: {problem}", True)

    if not allowed(user, call.name, call.input):     # permission, decided by code
        return result("Not permitted for this customer. Use hand_over.", True)

    try:
        value = fn(**call.input, user=user)          # identity comes from the session
    except TimeoutError:
        return result("The orders service timed out. You may try once more.", True)

    return result(json.dumps(value)[:4000])          # short, bounded observation

Taking the customer id from the model's inputs. The model can be talked into passing someone else's id. Take identity from the logged-in session and pass it to the tool yourself.

Tool errors and retries

What it is

Tools fail. What you do depends on the kind of failure:

  • The model's mistake (bad input, unknown tool): return a clear error message as the tool result. The model reads it and corrects itself.
  • A passing fault (timeout, rate limit): your code retries, waiting a little longer each time (backoff), a limited number of times.
  • A lasting fault (service down, permission denied): do not retry. Tell the model plainly so it can try another route or hand over.

Why it matters

An error message is an observation like any other, and a good one is a small lesson.

  • 'Invalid input: order_id must be an integer, got "88213A"' gets fixed on the next turn.
  • A bare 'Error' or an empty string gets the same call repeated.
  • Raising an exception that crashes the loop throws away the whole task for a problem the model could have worked around.

In the real world: A payments outage made a refund call time out after succeeding; the agents with idempotency keys retried safely, and the one without refunded forty customers twice.

Visual example

Errors

Only passing faults are retried by code.

What to do with each kind of tool failure
failureexamplewho handles itaction
Bad inputorder_id is not a numberThe modelReturn a clear error; it corrects the call
Unknown toolcancel_subscription does not existThe modelReturn the list of real tools
TimeoutOrders service slowYour codeRetry with backoff, at most 3 times
Rate limitToo many requestsYour codeWait as told, then retry
Not permittedAnother customer's orderNobody retriesReturn refusal; hand over
Service downPayments unavailableNobody retriesTell the model; queue or hand over
import time

def call_with_retry(fn, *, attempts: int = 3, base_delay: float = 0.5, **kwargs):
    """Retry passing faults only. Waits 0.5s, 1s, 2s between tries."""
    for attempt in range(attempts):
        try:
            return fn(**kwargs)
        except (TimeoutError, RateLimited):
            if attempt == attempts - 1:
                raise
            time.sleep(base_delay * 2 ** attempt)
        # ValidationError and PermissionDenied are NOT caught: retrying cannot fix them.

Retrying a write tool blindly. If 'issue refund' timed out, the money may already have moved. A retry then pays twice. Write tools need an idempotency key: a unique label sent with the request so the service performs it once however many times it arrives. Module 28 returns to this.

Tool chaining: sequential and parallel execution

What it is

Tool chaining is using several tools for one task. There are two ways to run them:

  • Sequential: one after another, because each needs the previous result. Get the order, then check the policy for that order's problem, then refund it.
  • Parallel: several at once, because they are independent. Fetch the order, the customer's history and the policy in the same turn.

Most models can request several tools in one reply; your code runs them together and returns all the results in one message.

Why it matters

The choice sets both speed and correctness.

  • Three independent look-ups run in parallel take as long as the slowest one, not the sum.
  • Fewer turns also means the growing history is resent fewer times, which cuts cost.
  • But two actions that depend on each other, run in parallel, race. Never run a refund alongside the look-up that justifies it.

In the real world: A support agent that fetched order, policy and history in one turn instead of three cut its median reply time from nine seconds to four.

from concurrent.futures import ThreadPoolExecutor

READ_ONLY = {"get_order", "get_policy", "get_customer_history"}

def run_calls(calls, user) -> list[dict]:
    """Read tools run together; anything that writes runs one at a time."""
    if all(call.name in READ_ONLY for call in calls):
        with ThreadPoolExecutor() as pool:
            return list(pool.map(lambda c: execute(c, user), calls))
    return [execute(call, user) for call in calls]

# All results go back in ONE user message, one tool_result per request.

Returning parallel results in separate messages. The model then learns that parallel calls are awkward and stops making them. Put every result of one turn in a single message.

Tool permissions

What it is

Tool permissions decide which tools an agent may use, on whose behalf, and within what limits. Four layers:

  • Which tools exist for this agent: the smallest set that does the job.
  • Whose data a tool may touch: bound to the logged-in user, never to an input the model supplies.
  • How much: caps inside the tool, such as a maximum refund.
  • Whether a person must approve: required for costly or irreversible actions.

Why it matters

An agent acts with whatever authority its tools have.

  • If a tool can read any customer's orders, so can anyone who can persuade the model.
  • The principle is least privilege: give the least access that lets the task be done.
  • Permissions enforced in code hold even when the model is confused or manipulated.

In the real world: A company's internal agent ran with an admin database role; an employee asked it for 'everyone's salary bands' and got them. The fix was tools that query as the employee, not as the agent.

Visual example

Permissions

Reads are free. Writes are capped. The riskiest needs a person.

Permissions for Kirana Kart's tools
toolkindscopelimitapproval
get_orderReadOwn orders onlyNoneNo
get_policyReadPublic policiesNoneNo
refund_delivery_feeWriteOwn orders onlyOnce per orderNo
refund_itemWriteOwn orders onlyUp to ₹500Above ₹500
cancel_orderWriteOwn orders onlyBefore dispatchNo
close_accountWriteNot offeredHuman only

Giving the agent one powerful service account 'to keep things simple'. Every user of the agent then has that account's power. Tools should act with the requesting user's rights.

Practise it: Design a Tool-Calling Agent Over 500 APIs

Interview questions: tool use and function calling

What it is

The questions interviewers ask about tool use and function calling, from the first screening call to the senior round. Cover the answer, say yours aloud, then compare.

Why it matters

Reading an idea and explaining it are different skills.

  • Each answer below is the two or three sentences an interviewer is listening for.
  • The reasoning behind each one is in the cards above. If an answer surprises you, go back to that card.

In the real world: A senior interview hands the candidate a tool definition and asks what is wrong with it; the expected answers are a vague description, a free-text field that should be an enum, and a customer id the model supplies.

Q: What are tools?

Functions in your code that the model can ask to have run, each described by a name, a description and an input schema. They let the model read live data, compute exactly and take actions.

Q: How does function calling work?

You send tool definitions with the conversation. The model replies with a structured request: tool name, id, JSON inputs. Your code runs the function and sends a result with the same id. The model then continues. The model never executes anything.

Q: What makes a good tool?

Shaped around a task, not an API endpoint; one clear purpose; a description saying when to use it and when not; narrow typed inputs with enums; short results; error messages that say how to fix the call.

Q: How do you handle a tool error?

Return it to the model as a result marked as an error, with a message it can act on. Retry passing faults in code with backoff. Never retry permission failures. Make writes idempotent so a retry cannot repeat the effect.

Q: What if the model calls a tool that does not exist?

Do not crash. Return an error result naming the real tools. Strict schemas and a short tool list make it rare; it is counted as a metric and used to improve descriptions.

Q: When do you run tools in parallel?

When they are independent reads. It cuts latency to the slowest call and reduces turns. Writes, or calls that depend on each other, run in sequence.

Q: How do you support five hundred tools?

Do not show them all. Route by category or retrieve the few relevant tool definitions by search, keep a small always-on core, and measure selection accuracy.

Q: Where do permissions live?

In the tool and the executor, not the prompt. Identity comes from the session, limits are checked in code against real data, and risky actions wait for approval.

Saying the model 'calls the API'. It asks; your code calls. Interviewers listen for that distinction because every safety control depends on it.

Practise it: Design an AI Customer Support Agent, Design an Autonomous Research Agent

Exercise: implement a tool registry and executor

What it is

A short build that uses this module's ideas. Write it yourself before looking anything up; the checks at the end tell you whether it works.

Why it matters

An idea you have only read fades in a week.

  • Building it once, even in fifty lines, is what makes it yours.
  • Interviewers can tell the difference between someone who has read about a loop and someone who has debugged one.

In the real world: A team writes exactly this layer before choosing any framework, and keeps it when they later adopt one, because it is where their permissions and limits live.

Your task. Write a small tool layer in Python with no agent framework: register tools, validate inputs, run them, and return well-shaped results and errors.

Steps

  1. Write three plain functions over an in-memory dictionary of fake orders: get_order(order_id), calculate(expression) and refund_delivery_fee(order_id, reason).
  2. Write a JSON Schema for each, with descriptions, required fields, an enum for reason and additionalProperties: false.
  3. Write execute(name, inputs, user) that rejects unknown tools, validates inputs (the jsonschema package does this), checks the order belongs to user, and returns {ok, content}.
  4. Make refund_delivery_fee refuse a second refund for the same order.
  5. Write six tests that call execute directly: a good call, an unknown tool, a missing field, a wrong type, another user's order, and a repeated refund.

You are done when

  • All six tests pass without any model being called.
  • Every error message says what was wrong and what to do instead.
  • No tool takes the user's identity from its inputs.

Stretch. Add run_calls that executes read-only calls in parallel and writes in sequence, and time three look-ups both ways.