Skip to the lesson
FixTheRobot Explore Robotics / LLM / LM11 Building LLM Applications / LM11-T4

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.

  • Lesson 4 of 6
  • Module 11: Building LLM Applications
  • Theory
  • Difficulty: Advanced
  • About 9 minutes to read
  • Prerequisite: LM11-T3

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.

Tool calling with the application in controlA vertical flow of five stages with the model on the left and the application on the right. The model proposes a call as structured text. The application parses and validates it against the schema, checks permissions and risk, asks for human confirmation for dangerous tools, executes only then, and returns the result as data to the model for the next step. Rejected calls return an error code.Modelproposes a callJSON Parse + validateschema, types, ranges Policyallow-list, permissions Confirmif dangerous Executethe application acts Result as datatool output rejected: error code goes back to the model The model proposes. The application validates, authorises and acts.Nothing the model writes is executed directly.
The path of a tool call. Validation, policy and confirmation sit between the model and the effect, and the tool result returns to the model as data.

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

  1. Parse,Is it JSON at all? If not, reject with a code; do not guess what was meant.
  2. Shape and tool,Does it have a string tool name and an object of arguments? Is the tool on the allow-list?
  3. 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.
  4. 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)?
  5. Confirmation,For dangerous or irreversible tools, require an explicit human approval that the model cannot supply.
  6. Execute and log,Run it with a timeout, record the call, the arguments and the result.

Risk classes

Classify tools by what they can do, not by their names
ClassExamplesPolicy
Readget battery level, read a map, search documentsallow; still validate arguments; mind data exposure
Act, reversible and boundedmove within the workspace at limited speed, set a lightallow after validation and range limits
Act, hard to reverse or dangerousopen a door, disable a safety function, send money or messages, delete datarequire 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

a call is executed only if every conjunct holds
valid⁡(call)=parse⁡(call)∧tool∈T∧args∈Atool∧policy⁡(state, call)\operatorname{valid}(\text{call})=\operatorname{parse}(\text{call})\wedge \text{tool}\in T\wedge \text{args}\in A_{\text{tool}}\wedge \operatorname{policy}\text{(state, call)}
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

Design questions for a tool
QuestionWhy 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

  1. 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.
  2. Be closed by default and total on bad input: unknown tools and fields are errors, and the validator returns a code for every string.
  3. Classify tools by risk; do not offer tools whose worst case you cannot accept.
  4. Return tool results to the model as delimited data, bound multi-step loops, prefer idempotent tools and add timeouts.
  5. 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.

Practice this lesson

Validate Structured Outputs and Tool Calls

Parse model output into a schema, reject invalid or unsafe calls and execute only validated actions.

Next lesson

LM11-T5 — Application Evaluation, Latency, Cost and Reliability

Evaluate whole applications, not just models, and budget latency and cost.

Nothing below is required to finish this lesson.