Lesson 2.1: Tool calling and function schemas
Learn how an agent uses a tool: the schema the model reads, the structured request it writes, how the harness validates it, and what invalid calls cost.
By the end of this lesson you will be able to explain step by step how a language model uses a tool, read and write a function schema, validate a tool request before running it, say what happens when the request is wrong, and estimate how much invalid requests add to the length and cost of a run. You will take 1,000 tool requests and sort the failures into unknown tools, missing arguments and wrong types, and you will see that a 6 per cent rate of invalid requests stretches a six-step task to 6.4 steps on average. You should have read Lessons 1.2 and 1.4, which describe the agent loop and the parts of an agent, in particular the idea that the model only writes text and the harness acts. A little Python helps for the code example, though the main text needs none. The mathematics is a ratio and a division.
Module 1 said that an agent uses tools, and that the model never runs them itself. This lesson explains how that works in practice, because the details decide whether an agent is dependable. Everything the model can do in the world, every search, lookup, calculation and payment, passes through one narrow gate: a structured request that the model writes and the harness checks. Learn that gate well and many mysterious agent failures become easy to understand.
The idea: a model that can ask
A language model produces text. Taken alone, it cannot look up today's exchange rate or read a file; it can only write about them. Tool calling, also called function calling, is a convention that closes this gap. It has four parts, and they repeat at every step of an agent loop.
1. The harness describes the tools. Along with the instructions and the conversation, it tells the model what tools exist, what each does and what arguments each takes.
2. The model decides whether to use one. If it needs information or an action, instead of ordinary prose it writes a structured request: the name of a tool and the values of its arguments.
3. The harness runs the tool. It reads the request, checks it, runs the matching function in ordinary code and captures the result.
4. The result goes back to the model. The harness adds it to the conversation, and the model continues, either with another request or with a final answer.
The model never executes anything. It produces a request, a few lines of structured text. This is a safety property as much as a technical one: between the model's intention and any real effect there is always a program that can inspect, refuse, limit or log the request.
Here is what the exchange looks like for a loan lookup. The format varies between providers and frameworks, but the shape is stable.
Who · What is saidHarness to model · Tools available: getloan(loannumber), which "returns the status, due date and fee for one loan"
Customer · "Why was I charged a fee on loan 4471?"
Model · A request: call getloan with loannumber equal to 4471
Harness · Runs the function; the database returns status active, due date 2 October, fee 500
Harness to model · The result of the call, as text
Model · "The fee was charged because the payment is recorded as arriving after the due date of 2 October."
Notice that a model reply can contain more than one request. Some models can ask for several independent tools at once, for instance the loan and the customer's payment history in the same turn. The harness can run them at the same time, which saves a step.
What the model reads: the function schema
How does the model know which tools exist? The harness gives it a schema, a structured description of each tool. A schema has three kinds of information.
Name. A short, specific identifier such as getloan.
Description. A plain-language account of what the tool does and when to use it.
Parameters. The arguments, each with a name, a type (text, number, true or false, a choice from a list), a short explanation and whether it is required.
The most common format for the parameters is JSON Schema, a widely used way to describe the shape of data. Here is a schema for a lookup tool, written as the structure you would hand to a model.
name: getloan
description: Returns the status, due date, outstanding amount and any fee for one personal loan.
Use when you have a loan number. Do not use it to search by customer name.
parameters:
loannumber (text, required) The 4 to 8 digit loan number, for example 4471.
includehistory (true or false, optional, default false) Also return the last 12 payments.
Four points about this small example repay attention.
The description is for the model. The model uses it to decide whether this tool fits the situation. "Returns the status, due date, outstanding amount and any fee" tells it what it will get. "Use when you have a loan number. Do not use it to search by customer name" tells it when to call and when not to. A schema with a lazy description such as "gets loan" leads to wrong calls.
Types are guard rails. A parameter declared as text, a number, or one of a fixed list of choices limits what the model can send. A status argument limited to the choices open, closed or overdue cannot be filled with the word "pending". The tighter the type, the fewer mistakes.
Required and optional matter. If a required argument is missing, the harness must not guess. It should refuse the call and tell the model what is missing.
Defaults reduce choices. includehistory defaults to false, so the model only sets it when it needs the history. Defaults mean less to decide, and less to get wrong.
The schema is also text that the model reads at every step, so it uses part of the context window, as Lesson 1.4 showed. Careful, short descriptions give the model what it needs without wasting space.
The request the model writes
When the model decides to use a tool, it writes something like this:
tool: getloan
arguments: { "loannumber": "4471", "includehistory": true }
It is ordinary structured text, usually in the JSON format. Because a model generates text one piece at a time, it can make the same kinds of mistakes in a request that it makes in prose: a misspelled tool name, a missing argument, a number written as a word, an argument it has invented. Models are trained to be good at this, and are usually reliable, but "usually" is not enough in a loop that may make hundreds of calls a day. This is why the harness checks every request before acting.
Validating a request
A request can fail in several ways, and each should produce a clear, different response.
Problem · Example · What the harness should doUnknown tool · getloans instead of getloan · Refuse. Tell the model the tool does not exist and list the available names
Missing required argument · No loannumber · Refuse. Say which argument is missing
Wrong type · loannumber is a number, not text, or the word "four" · Refuse. Say what type is expected
Value outside allowed choices · status: "pending" when the choices are open, closed, overdue · Refuse. List the allowed choices
Value out of range or format · loannumber of 3 digits · Refuse. State the rule, such as "4 to 8 digits"
Extra argument · customername supplied but not defined · Refuse, or ignore with a note. Never silently pass it on
Not allowed here · Refund above the limit, or a tool restricted to a person · Refuse, or route to an approval step
The harness returns the refusal to the model as the result of the call, in plain words. A good error message is a form of instruction: "Error: loannumber must be 4 to 8 digits; you sent 447. Check the number and try again." The model can then fix the call on the next step. A bad message such as "Error 422" teaches it nothing. Lesson 2.3 returns to this.
Predict first: a team measures its agent over 1,000 tool requests and finds that 60 are invalid. The agent needs six tool calls to finish an average task. Roughly how many calls will it make on average once the invalid ones are counted: still 6, about 6.4, or about 9? Decide before reading on.
About 6.4. Each attempt is invalid with probability 0.06 and, if it is, the step is wasted and must be repeated. The expected number of attempts per successful call is 1 ÷ (1 − 0.06) = 1.064, so six successful calls take about 6 ÷ 0.94 = 6.38 attempts. The overhead looks small, only 6 per cent, but each invalid call is a full model call with a growing context, and an agent whose invalid rate is 20 per cent needs 7.5 attempts for six calls. Invalid requests also lower reliability, since a model that confuses itself over format can then confuse itself about substance.
The practical lessons are plain. Validate first, in code, before anything runs. Return errors the model can act on. Measure the invalid rate, because a rising rate is an early warning that a schema is unclear or a model has changed. And design schemas that make mistakes hard: tight types, enumerated choices and sensible defaults.
The result the model reads
After the harness runs the tool, it sends the result back as text. Results can be small, such as a confirmation, or large, such as a record with fifty fields. What the model gets back becomes part of its context at every later step, so the design of results matters as much as the design of requests. The next lesson covers this in detail. For now, remember three rules:
1. The model can only use what it is shown. If a field is not in the result, the model cannot see it, and may make it up.
2. Results should say what they are. A bare "4471" is unclear. "loannumber: 4471, status: active" is not.
3. Failures are results too. A tool that finds nothing should say "no loan found with number 4471", not return an empty string. Silence invites invention.
Calling several tools
Many tasks need more than one tool, and models can either ask for tools one at a time or several at once.
Sequential calls happen when the second needs the first's output. To check a payment you must first have the loan number, then the payments for it.
Parallel calls happen when the requests are independent. Looking up a loan and a customer profile can be asked in the same turn, and the harness can run them together. If each takes two seconds, running them in parallel takes two seconds, not four.
Parallel calls shorten a run and reduce the number of model turns, which cuts cost as well, because each turn re-reads the whole history. They also complicate error handling: one of the two may fail while the other succeeds. A good harness returns each result with the identifier of its request so that the model can match them.
A complete dispatcher in code
The harness part of tool calling is small enough to show in full. This example defines two tools with schemas, validates a request against its schema, runs it and returns either the result or a helpful error. It uses only the standard library.
python
TOOLS = {
"getloan": {
"description": "Returns status, due date and fee for one loan. Use when you have a loan number.",
"params": {
"loannumber": {"type": str, "required": True, "patternlen": (4, 8)},
"includehistory": {"type": bool, "required": False, "default": False},
},
"run": lambda loannumber, includehistory=False: {
"loannumber": loannumber, "status": "active", "due": "2026-10-02", "fee": 500,
"payments": [{"date": "2026-10-01", "amount": 12000}] if includehistory else "not requested",
},
},
"listopenloans": {
"description": "Lists loan numbers by status. status must be one of: open, closed, overdue.",
"params": {"status": {"type": str, "required": True, "choices": ["open", "closed", "overdue"]}},
"run": lambda status: {"status": status, "loans": ["4471", "5120"]},
},
}
def validate(name, args):
"""Return None if the request is acceptable, otherwise an error message the model can act on."""
if name not in TOOLS:
return f"Error: no tool named '{name}'. Available tools: {', '.join(TOOLS)}."
spec = TOOLS[name]["params"]
for key in args:
if key not in spec:
return f"Error: '{key}' is not an argument of {name}. Allowed: {', '.join(spec)}."
for key, rule in spec.items():
if key not in args:
if rule["required"]:
return f"Error: {name} needs the argument '{key}'."
continue
value = args[key]
if not isinstance(value, rule["type"]):
return f"Error: '{key}' must be {rule['type'].name}, you sent {type(value).name}."
if "choices" in rule and value not in rule["choices"]:
return f"Error: '{key}' must be one of {rule['choices']}, you sent '{value}'."
if "patternlen" in rule and not (rule["patternlen"][0] <= len(value) <= rule["patternlen"][1]):
lo, hi = rule["patternlen"]
return f"Error: '{key}' must be {lo} to {hi} characters, you sent {len(value)}."
return None
def dispatch(name, args):
problem = validate(name, args)
if problem:
return {"ok": False, "message": problem} # the model reads this and corrects itself
run = TOOLS[name]["run"]
return {"ok": True, "result": run(args)}
print(dispatch("getloan", {"loannumber": "4471", "includehistory": True}))
print(dispatch("getloan", {"loannumber": "447"}))
print(dispatch("getloans", {"loannumber": "4471"}))
print(dispatch("listopenloans", {"status": "pending"}))
Notice what the harness does and does not do. It never lets a malformed request reach the real function. It never guesses a missing argument. And every refusal names the problem and the fix, because the reader of the message is a model that can use it.
Common mistakes
Running requests without validating them. A wrong type or an invented argument reaches your real systems. Check first.
Silent guessing. Filling in a missing argument or correcting a name without telling the model hides the problem and teaches it nothing.
Lazy descriptions. "Gets loan" does not tell the model when to call the tool or what comes back.
Loose types. Free text where a fixed list of choices would do invites invalid values.
Opaque errors. "Error 422" is useless to a model. Say what is wrong and how to fix it.
Empty results for failures. Return a clear "not found" message, or the model may invent an answer.
Ignoring the invalid rate. Track it. A rising rate signals unclear schemas or a changed model.
Believing the request is the action. A model writing a request has done nothing until the harness runs it.
Key takeaways
Tool calling is a convention: the harness describes the tools, the model writes a structured request, the harness validates and runs it, and the result goes back to the model.
The model never runs anything. Between its request and any effect there is always a program that can check, refuse or log it.
A schema has a name, a description and typed parameters. The description tells the model when to use the tool, and tight types and defaults make mistakes harder.
Validate every request: unknown tools, missing or extra arguments, wrong types, disallowed values. Return errors in plain words that tell the model how to fix the call.
Invalid requests cost whole model calls. With a 6 per cent invalid rate, a six-call task averages 6.4 attempts, and with 20 per cent it needs 7.5.
Results are part of the context at every later step. Make them clear, and make failures explicit.
Independent calls can run in parallel to save time and turns.
Check yourself
1. In tool calling, who runs the function: the model or the harness?
The harness. The model writes a structured request, and the harness validates it, runs the function and returns the result.
2. Of 500 requests, 25 are invalid. A task needs 8 successful calls. About how many attempts will it take on average?
The invalid rate is 5 per cent. Expected attempts: 8 ÷ 0.95 = 8.4.
3. Why is a parameter limited to a fixed list of choices better than free text?
The model cannot send a value outside the list, so a whole class of invalid requests disappears.
4. A tool is asked for a loan that does not exist and returns an empty string. What is the risk, and what should it return instead?
The model may invent an answer to fill the gap. It should return a clear message such as "no loan found with number 4471".
5. Two independent lookups each take 3 seconds. How long do they take sequentially and in parallel?
Sequentially, 6 seconds. In parallel, about 3 seconds.
Try this
Set aside sixty minutes.
1. Choose three tools your agent from the Lesson 1.4 exercise would need. Write a schema for each: name, description with "use when" and "returns", and typed parameters with required or optional marked.
2. For each parameter, ask whether the type can be tighter. Replace free text with a list of choices wherever possible.
3. Write five bad requests for one tool: an unknown name, a missing argument, a wrong type, a value outside the choices and an extra argument. Write the error message each should produce.
4. Run the dispatcher from this lesson. Add your tool to it and confirm your five bad requests are refused with your messages.
5. Estimate your agent's invalid rate if it were 5 per cent, 10 per cent and 20 per cent. Use the simulator to see the extra attempts and cost.
6. For each tool, decide what the result should contain, and what the "nothing found" result should say.
7. Mark which of your tools could run in parallel.
A good result is a set of schemas with tight types, a validator that refuses every bad request with a useful message, and a clear idea of what a result and a failure look like. The next lesson goes deeper into the design of the tools themselves, because a well-built tool does half the work of a good agent.
Tags: Agentic AI Tool calling Function calling JSON schema AI agents