Tool Calling: How an LLM Actually Uses a Tool

The phrase "the model called the function" is convenient and wrong, and the misunderstanding it creates is the reason so many tool-using systems have security holes in the same place. A language model cannot execute anything. It produces text. Everything that happens after that is your program's decision — which is inconvenient framing right up until you realise it is also where all your control lives.

What actually happens

You describe your tools to the model as name, description and parameter schema. The model, when it judges one relevant, emits a structured request naming the tool and its arguments.

Your code receives that request, decides whether to honour it, executes it if so, and sends the result back as another message. The model never touches your systems — it only ever asks.

The loop, step by step

Five stages, and the boundary between the model's world and yours sits between stages two and three.

1. You send: messages + tool definitions │ 2. Model responds: "call get_weather{city:'Leeds'}" │ ════════════╪════════════ // the boundary. Model stops here.3. YOUR CODE: validate → authorise → execute │ 4. You send back: the tool result, as a message │ 5. Model responds: prose using the result, or another tool call (loop to 3)

Notice that the model's turn ends at step two. It has no way to wait for a result, no ability to retry, no view of what happened. It emitted a request and stopped generating. Everything at step three happens while the model is not running at all.

💡 The model is writing a request, not making one

A useful reframing: the model is filling in a form. It has read the form's labels — your tool descriptions — and produced values it believes fit. It has never seen the office the form goes to.

This is why tool descriptions matter so much and why the model's confidence tells you nothing about whether the call is appropriate. It is describing what it thinks should happen, from a menu you wrote.

What the model actually sees

Your entire tool surface, from the model's perspective, is this:

{ "name": "get_weather", "description": "Current conditions for a city. Returns temperature in Celsius and a text summary. Use for current weather only, not forecasts.", "input_schema": { "type": "object", "properties": { "city": { "type": "string", "description": "City name, e.g. 'Leeds'" }, "units": { "enum": ["c", "f"], "default": "c" } }, "required": ["city"] } }

That is all. Not your implementation, not your endpoint, not your credentials. The description is the entire basis on which the model decides whether this tool is the right one — which makes it a prompt, not documentation.

The consequences are practical. A description saying "gets weather" produces a tool called for forecasts, historical data and climate questions it cannot answer. The version above rules those out explicitly, and that sentence does more for reliability than any amount of prompt engineering elsewhere. This is the substance of designing tool schemas an LLM can use.

Why the arguments are always well-formed

Tool calls do not arrive as prose the model hopes you can parse. The same machinery behind structured outputs applies: during generation, the sampler is restricted to tokens that keep the output valid against your schema.

// At each step, only schema-valid tokens are permitted {"city": // "units" also allowed here {"city": "Leeds", // string required — numbers blocked {"city": "Leeds", "units": "c" // only "c" or "f" possible // Malformed JSON is not unlikely. It is unreachable.

So you will not get invalid JSON, and you will not get a field of the wrong type. This is worth knowing precisely because of what it does not guarantee.

🚨 Valid is not authorised

The schema promises shape. It says nothing about whether the values are ones this user may act on.

// Every one of these is schema-valid {"user_id": "someone-elses-id"} // not their record {"path": "../../etc/passwd"} // a string, as required {"limit": 10000000} // an integer, as required {"amount": -500} // a number, as required

Authorisation, bounds and path checks belong in your handler, exactly as they would for a request from a browser. Treat every tool call as untrusted input from the internet — because if the conversation contains anything a third party wrote, that is what it is.

Sending results back

The result goes into the conversation as a message tied to the call's ID. Two things about it are worth deliberate thought.

Errors should be returned, not thrown. A model that receives an error message can adapt — correct the city name, narrow the range, try a different tool. A model that receives nothing because your code raised an exception cannot do anything at all.

// Useless: the model learns nothing it can act on "Error" // Useful: states the problem and the way forward "No city named 'Leedz'. Did you mean 'Leeds'? Provide a city name from the supported list." // Useful: a constraint the model can respect next time "Range too large: 400 days requested, maximum is 90. Narrow the range and retry."

Results consume context. Everything you return stays in the conversation for every subsequent turn. Returning a full API response because it was easier than filtering it will fill the context window with fields the model never uses, and on a multi-step task those accumulate quickly. Return what the task needs.

⚠️ Tool results are untrusted content

Whatever your tool returns enters the model's context with the same standing as your own instructions. If it came from a web page, a database record a user wrote, an email or a file, it can carry instructions aimed at the model.

This is prompt injection arriving through the tool channel, and it is the main reason a read tool plus an act tool is a more dangerous combination than either alone. The read tool supplies the payload; the act tool executes the consequence.

Several calls at once

Models can emit multiple tool calls in a single turn when the calls do not depend on one another. Running them concurrently is a straightforward latency win.

// Independent — run together [ get_weather{"city":"Leeds"}, get_weather{"city":"Bath"} ] // 2 calls, 1 round trip // Dependent — the model must see the first result find_user{"email":"a@b.com"} ↓ returns id 4471 get_orders{"user_id":4471} // a second round trip

You cannot make the second case parallel by asking nicely — the dependency is real. What you can do is provide a tool that does both, if the pairing is common. Collapsing a frequent two-step sequence into one well-named tool removes a round trip and a chance for the model to go wrong in between.

The failure modes you will meet

SymptomUsual causeFix
Tool never gets calledDescription too vagueState when to use it
Wrong tool chosenOverlapping descriptionsSay what each is not for
Invented argument valuesMissing from conversationMark required; validate
Calls the same tool repeatedlyResult not answering itImprove the result content
Answers without callingModel thinks it knowsInstruct it to always verify
Stops mid-taskNo loop, or hit a capCheck your loop logic

Nearly every row resolves to the same underlying advice: the model's behaviour is determined by what you told it about the tools and what you sent back. Both are text you control. When a tool-using system behaves oddly, read your descriptions and your results before reaching for a bigger model.

One structural fix outweighs the rest — keep the tool count small. Every additional tool is another opportunity to select the wrong one, and the degradation is not linear. Twelve well-differentiated tools beat forty overlapping ones, and if you genuinely need forty, route to a subset first rather than presenting them all.

When not to use tool calling

If your code already knows which function to run, do not ask a model to choose. Tool calling buys you dynamic selection, and you pay for it in latency, tokens and unpredictability.

  • Fixed sequence? Write the sequence. That is a workflow, not an agent.
  • One tool, always called? Call it, then send the result as context.
  • Extracting fields from text? That is structured output, not a tool call.
  • Deterministic transformation? Ordinary code, no model involved.

Designing tool schemas?

Format and validate JSON schemas in your browser — nothing is uploaded to a server.

Open JSON Formatter →

Summary

  • The model never executes anything. It emits a request; your code decides.
  • Its turn ends at the request. Everything after happens while it is not running.
  • Descriptions are prompts, not docs — they are the entire basis for tool selection.
  • Constrained decoding guarantees shape, not safety. Validate and authorise in your handler.
  • Return errors as results so the model can adapt instead of stalling.
  • Tool results are untrusted content and enter context with full standing.
  • Independent calls can run in parallel; dependent ones genuinely cannot.
  • Fewer, sharper tools beat a long menu.

Frequently Asked Questions

How does an LLM call a tool?

It does not, directly. The model emits structured output naming a tool and its arguments, your code parses that, decides whether to run it, executes it, and passes the result back into the conversation as a new message. The model only ever produces text and receives text — every actual execution happens in your program.

Does the model have access to my API or database?

No. The model sees only your tool definitions — names, descriptions and parameter schemas — and whatever results you choose to send back. It has no network access, no credentials and no idea what your function does internally. It is describing a request, not making one.

Why does the model sometimes invent tool arguments?

Because generating arguments is generation, not lookup. If a required parameter is not present in the conversation the model may produce a plausible-looking value rather than asking for it. Marking parameters required in the schema, describing them precisely, and validating in code before executing are the defences.

What is the difference between tool calling and structured outputs?

Mechanically they are close relatives — both constrain the model to emit JSON matching a schema. The difference is intent: structured outputs shape the final answer to your user, while tool calling produces an intermediate request that your code acts on, with the result fed back so the model can continue.

Should I validate tool arguments if the schema already constrains them?

Yes, always. Schema constraint guarantees the shape is valid, not that the values are safe or sensible. A well-formed request can still name another user's record, ask for a million rows, or specify a path outside the allowed directory. Type checking is not authorisation.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.