LLM · Theory · LM11
LM11-T4 — Tool Calling and Schema Validation
The model asks; the application decides. A tool call is just text until your code has parsed it, validated it, checked it against policy and chosen to run it.
Language models cannot read a sensor or move a robot. Applications give them that reach through tools: functions the application exposes and the model may request by emitting a structured call. This turns text generation into a source of actions, and with it the central rule of this lesson: whatever the model writes is an untrusted request, not a command. The lesson shows how tool definitions are expressed, how calls are validated and authorised, how dangerous actions are gated, and how multi-step tool use is bounded. Lab LM11-L2 has you write the validator and the safe executor.
Learning objectives
After this lesson, you should be able to:
- Describe how a model requests a tool call and how the application responds.
- Write a schema for a tool and validate a call against it, including the boolean and range pitfalls.
- Separate read, act and dangerous tools and apply allow-lists and confirmation.
- Bound a multi-step tool loop and log every call.
The idea: the model proposes, the application disposes
In a tool-using application the prompt includes a list of available tools: a name, a description and a schema for each argument. The model replies either with ordinary text or with a structured request such as {"tool": "move_to", "args": {"x": 3, "y": 4, "speed": 0.5}}. Models are trained or fine-tuned (Module 7) to produce such calls reliably, and some serving stacks constrain decoding so the syntax is always valid. But even a perfectly formatted call is only a proposal.
Why validation is not optional
Models produce wrong calls in many ways: invalid JSON, a tool that does not exist, arguments of the wrong type, values out of range, extra or missing fields, or a valid call that is simply a bad idea. Some of those are accidents, some come from text the model read (a web page, an email, a retrieved document) that told it what to do (Lesson LM11-T6). In all cases the application is the only party that knows what is allowed, so it must check.
Layers of checking
- Parse,Is it JSON at all? If not, reject with a code; do not guess what was meant.
- Shape and tool,Does it have a string tool name and an object of arguments? Is the tool on the allow-list?
- Arguments,Exactly the declared fields, correct types, values within ranges. In Python remember that a boolean is an integer: reject it where a number is required.
- Policy,Is this caller allowed to use this tool now? Is the target inside the allowed area, the speed under the limit, the state compatible (not stopped, not mid-task)?
- Confirmation,For dangerous or irreversible tools, require an explicit human approval that the model cannot supply.
- Execute and log,Run it with a timeout, record the call, the arguments and the result.
Risk classes
| Class | Examples | Policy |
|---|---|---|
| Read | get battery level, read a map, search documents | allow; still validate arguments; mind data exposure |
| Act, reversible and bounded | move within the workspace at limited speed, set a light | allow after validation and range limits |
| Act, hard to reverse or dangerous | open a door, disable a safety function, send money or messages, delete data | require human confirmation; or do not offer the tool at all |
Results come back as data
The tool’s output goes into the next prompt so the model can continue. It is untrusted too: a web search result, a file’s content or a sensor name can contain instructions. Insert tool results inside delimiters with the same “this is data” instruction as retrieved passages.
Loops need limits
Multi-step use (the model calls a tool, reads the result, calls another) is a loop. Bound it: a maximum number of steps, a total time and cost budget, no repetition of the same failing call more than twice, and a safe stop when the bound is hit. Reliability compounds: if each step is valid with probability p, five steps succeed with probability p5 (Lesson LM11-T5).
The one rule
The mathematics: schemas as predicates, and the cost of an unchecked call
A tool is a function f with an argument schema A, the set of argument dictionaries the tool accepts. For move_to in the example, A = { (x, y, s) : x, y ∈ [0, 10], s ∈ [0, 1] }, numbers only. Validation is the predicate
- T
- the set of allowed tool names (the allow-list).
- Atool
- the declared argument set: field names, types and ranges.
- policy(state, call)
- a rule that may depend on the current state: robot position, whether an emergency stop was pressed, who is asking.
Two properties make this robust. First, closed by default: anything not explicitly allowed is rejected (an unknown field is an error, not ignored). Second, total on bad input: the validator returns a code for every string, including arbitrary text; it never raises an exception that could leave the system in an undefined state.
Expected loss from unchecked calls
If a fraction q of calls is invalid or harmful and the average damage of one such call is D, the expected loss per call without checks is q · D. Checks with catch rate c reduce it to (1 − c) · q · D. For q = 0.02 per thousand calls and a damage that is large (a crash, a leak), even a small q dominates; this is why the check must be total for the dangerous class rather than “usually right”. Confirmation and removal of dangerous tools drive D down, validation drives q down; both matter.
Idempotency and timeouts
Retries are normal: the network drops, the model repeats a call. An idempotent tool gives the same final state when called twice with the same arguments (move_to to a target is; “move 1 m forward” is not). Prefer idempotent designs, attach a request identifier to non-idempotent calls so duplicates can be detected, and give every execution a timeout.
A worked example: a dispatcher
The code defines three tools of increasing risk, a validator that returns a code for every input, and a dispatcher that rejects, asks for confirmation, or executes. Seven inputs are run, including malformed JSON, an unknown tool, a boolean where a number is required and a dangerous action. Only three calls reach the robot.
import json
TOOLS = {
"get_battery": {"args": {}, "risk": "read"},
"move_to": {"args": {"x": (0, 10), "y": (0, 10), "speed": (0, 1)}, "risk": "act"},
"open_door": {"args": {"door": None}, "risk": "dangerous"},
}
def validate(raw):
try:
call = json.loads(raw)
except ValueError:
return None, "bad_json"
if not isinstance(call, dict) or not isinstance(call.get("tool"), str) or not isinstance(call.get("args"), dict):
return None, "bad_shape"
spec = TOOLS.get(call["tool"])
if spec is None:
return None, "unknown_tool"
if set(call["args"]) != set(spec["args"]):
return None, "wrong_arguments"
for name, limits in spec["args"].items():
v = call["args"][name]
if limits is None:
if not isinstance(v, str):
return None, "bad_type"
else:
if isinstance(v, bool) or not isinstance(v, (int, float)):
return None, "bad_type"
if not limits[0] <= v <= limits[1]:
return None, "out_of_range"
return call, None
def dispatch(raw, confirmed=False, log=None):
call, err = validate(raw)
if err:
return {"status": "rejected", "reason": err}
risk = TOOLS[call["tool"]]["risk"]
if risk == "dangerous" and not confirmed:
return {"status": "needs_confirmation", "tool": call["tool"]}
if log is not None:
log.append(call)
return {"status": "executed", "tool": call["tool"], "args": call["args"]}
log = []
tests = [
'{"tool": "get_battery", "args": {}}',
'{"tool": "move_to", "args": {"x": 3, "y": 4, "speed": 0.5}}',
'{"tool": "move_to", "args": {"x": 3, "y": 40, "speed": 0.5}}',
'{"tool": "move_to", "args": {"x": true, "y": 4, "speed": 0.5}}',
'{"tool": "format_disk", "args": {}}',
'{"tool": "open_door", "args": {"door": "north"}}',
'move to 3 4 please',
]
for t in tests:
print(dispatch(t, log=log))
# {'status': 'executed', 'tool': 'get_battery', 'args': {}}
# {'status': 'executed', 'tool': 'move_to', 'args': {'x': 3, 'y': 4, 'speed': 0.5}}
# {'status': 'rejected', 'reason': 'out_of_range'}
# {'status': 'rejected', 'reason': 'bad_type'}
# {'status': 'rejected', 'reason': 'unknown_tool'}
# {'status': 'needs_confirmation', 'tool': 'open_door'}
# {'status': 'rejected', 'reason': 'bad_json'}
print(dispatch(tests[5], confirmed=True, log=log)) # {'status': 'executed', 'tool': 'open_door', 'args': {'door': 'north'}}
print(len(log)) # 3 (only three calls ever reached the robot)
Three details are worth studying. The boolean true for x is rejected before the range check, because isinstance(True, int) is true in Python. The dangerous tool does not execute until confirmed=True is supplied by the caller, and the confirmation is a parameter of the application, not of the model’s call. And the final count shows that the log contains only the calls that were really executed, which is the audit trail you want.
A checklist for each new tool
| Question | Why it matters |
|---|---|
| What is the worst thing this tool can do with valid arguments? | Determines the risk class and whether to offer the tool at all |
| What are the exact types and ranges of the arguments? | Defines the schema; unlimited numbers are a hazard |
| Is it idempotent? If not, how are duplicates detected? | Retries must not repeat effects |
| Who may call it, and when (state)? | Policy depends on caller and state |
| What does it return, and could the result contain instructions? | Results are untrusted data |
| How is each call logged? | You need an audit trail to debug and to investigate incidents |
Engineering implications and limitations
Write the schema once and use it twice. Generate the tool description for the prompt and the validator from the same definition so they cannot drift apart.
Reject, report, retry — never repair. Return a precise error code to the model, allow a bounded retry, and fall back safely. Guessing what a malformed call meant is how unsafe actions happen.
Keep the policy outside the model’s reach. The model must not be able to change limits, allow-lists or confirmation flags through the text it writes or through tool arguments.
Minimise the tool surface. Offer only the tools the task needs, with the narrowest arguments that work. A tool that does not exist cannot be abused.
Test with hostile and sloppy inputs. Build a regression set of invalid and malicious calls (truncated JSON, wrong types, huge numbers, extra fields, injection text in arguments) and require 100% rejection.
Log and replay. Store the prompt, the raw model output, the validation result and the execution result. Replaying a failure is the fastest way to fix it.
For robots. Put limits where the model cannot reach them: speed, workspace, force, rate of commands, and a hardware emergency stop. The language model should select among safe, bounded behaviours; it should not generate motor commands. The Physical AI and VLA tracks on this site treat the control side.
Common misconceptions
“If the model emits valid JSON the call is safe.”
Valid syntax says nothing about permission, range or intent.
“The prompt tells the model which tools are allowed, so no allow-list is needed.”
The model can emit any tool name; the allow-list must be enforced in code.
“Clamping out-of-range values is friendlier than rejecting.”
It executes something the model did not ask for; for unsafe domains reject and report.
“Confirmation can be part of the model’s call.”
A model can set any flag; confirmation must come from a human through a channel the model cannot write to.
“Tool outputs are trustworthy because our code produced them.”
They often contain external content and must be treated as data.
Engineering takeaways
- A tool call is a proposal. Parse, validate (shape, tool, arguments, types, ranges), check policy against the current state, require confirmation for dangerous tools, then execute and log.
- Be closed by default and total on bad input: unknown tools and fields are errors, and the validator returns a code for every string.
- Classify tools by risk; do not offer tools whose worst case you cannot accept.
- Return tool results to the model as delimited data, bound multi-step loops, prefer idempotent tools and add timeouts.
- Test the guard with hostile and sloppy inputs and keep the policy outside the model’s reach.
References and further reading
The papers describe how models can be trained or prompted to call tools and the risks of doing so; the dispatcher in this lesson is an original illustration and its printed results come from running the code shown.
- Schick, T. et al. (2023). Toolformer: Language models can teach themselves to use tools. arXiv:2302.04761.
- Yao, S. et al. (2023). ReAct: Synergizing reasoning and acting in language models. arXiv:2210.03629.
- Greshake, K. et al. (2023). Not what you have signed up for: Compromising real-world LLM-integrated applications with indirect prompt injection. arXiv:2302.12173.
Knowledge check
Three conceptual questions. Write an answer, then reveal the explanation. Your answers stay in your browser.
Question 1
A model returns {"tool": "move_to", "args": {"x": 3, "y": 4, "speed": 5}} for a tool with speed in [0, 1]. What should the application do?
Explanation
Reject it with an out-of-range code (not clamp it to 1), tell the model the reason for a bounded retry, and fall back to a safe default if no valid call arrives.
Question 2
Why is a boolean a problem when validating a numeric field in Python, and how do you handle it?
Explanation
In Python bool is a subclass of int, so isinstance(True, int) is true and true would pass a naive numeric check (and compare as 1). Reject booleans explicitly before accepting numbers.
Question 3
A tool unlocks a door. Where should the confirmation for it come from, and why not from the model’s own output?
Explanation
From a human through an application channel that the model cannot write to. Anything in the model’s output can be produced by the model, including by text injected from a document, so it cannot serve as authorisation.
What’s next
Next: evaluating a whole application: quality, latency, cost and reliability.
Related content
Nothing below is required to finish this lesson.
- BeforeLM11-T3: Retrieval-Augmented Generation
Chunk documents, retrieve relevant passages and ground answers in them.
- LabLM11-L2: Validate Structured Outputs and Tool Calls
Parse model output into a schema, reject invalid or unsafe calls and execute only validated actions.
Get notified when the next lesson and new labs are released.
One email per new lesson or lab.