Building reliable tools for LLMs
Function calling, agents, and MCP all rest on the same thing: tools an LLM can invoke. But a tool built for a non-deterministic, fallible caller needs different design than one built for code. A deep-dive on what makes a tool reliable for an LLM — naming, scope, validation, error messages it can recover from, and the safety that can't live in the model.
Across this whole arc of writing about LLMs — function calling, agents, MCP — one thing keeps sitting underneath: tools. A function the model can invoke to look something up or do something is the unit that turns a model that can only talk into one that can act, and every higher pattern (agents, multi-agent systems, MCP servers) is ultimately built out of tools. Which raises a question I want to close on, because it’s the foundation everything else stands on: what makes a tool reliable for an LLM to use? The answer isn’t the same as for a tool built for code, because the caller is different — a tool for an LLM is an interface designed for a non-deterministic, fallible, well-meaning-but-imperfect consumer, and that changes how you design it. This is a deep-dive on building tools your model can actually use well.
The caller is the difference
Start with the thing that makes this its own discipline. When you write a function for code to call, the caller is deterministic: it passes exactly the arguments you specified, in the right types, and handles your return value as programmed. When you write a tool for an LLM to call, the caller is a probabilistic model that interprets your tool from its description, decides when to use it, and supplies arguments it extracted from natural language. That caller can misunderstand what the tool does, call it at the wrong time, pass wrong or malformed arguments, and misread your response. Designing for that caller — fallible, non-deterministic, reasoning from your description — is what separates a tool an LLM uses well from one it constantly misuses. Every principle below follows from taking that caller seriously.
Make it understandable: naming and description
Because the model decides whether and how to use a tool from its description, the description is not documentation — it’s the interface, and it’s the highest-leverage thing you control. This is prompt engineering pointed at your tool definitions:
- Name the tool clearly and specifically. A precise, descriptive name (
get_order_status, notlookup) tells the model what the tool is for and when to reach for it. Vague names get reached for vaguely. - Describe what it does, when to use it, and what it returns. The description should make crisp to a model the tool’s purpose, the situations it’s for, and what comes back. A model that misuses a tool is very often a model that was given a poor description of it — fix the description before blaming the model.
- Describe each parameter precisely. What it is, its type, its constraints, what’s required. The model extracts these from natural language, so the clearer you specify them, the more reliably it gets them right.
The single highest-impact improvement you can usually make to an LLM’s tool use is improving the clarity of the tool descriptions. The model is reasoning from words; give it good ones.
Make it well-scoped: narrow and single-purpose
A reliable tool does one well-defined thing. This matters more for an LLM caller than a code caller, because a model choosing among tools picks far more accurately when each tool has a clear, distinct purpose. The design guidance:
- Prefer narrow, single-purpose tools over broad, multi-mode ones. A tool that does one thing is easier for the model to understand, to choose correctly, and to call with the right arguments. A Swiss-army tool with a dozen modes is something the model will use the wrong mode of.
- Avoid overlapping tools. If two tools could plausibly do the same job, the model has to guess which one you meant, and will sometimes guess wrong. Distinct, non-overlapping purposes make the choice unambiguous.
- Keep the argument surface small and clear. Fewer parameters, each with a clear meaning, means fewer chances for the model to supply something wrong. Complexity in the signature is complexity the model has to get right every time.
Narrow, single-purpose, non-overlapping tools are to an LLM what a clean API is to a developer — but more so, because the model can’t read your source to disambiguate. The clarity has to be in the shape of the tool itself.
Make it safe: validate, because the arguments are untrusted
Here is the principle that can’t be compromised, and it’s the one this whole series keeps returning to: the arguments a model supplies are untrusted input, and the tool itself must enforce safety. The model extracted those arguments from natural language; it can get them wrong, and a malicious user can try to manipulate what it sends. So the tool — not the model — is where safety lives:
- Validate every argument before acting on it: types, ranges, formats, permissible values. Treat a model-supplied argument exactly as you’d treat input from any untrusted client, because that’s what it is.
- Enforce authorization in the tool. Whether this request is allowed is the tool’s job to check, not the model’s to be trusted on. The model requesting an action never means the action is permitted; your tool decides.
- Guard destructive operations. A tool that changes or deletes data, invoked off a model’s request, needs real protection — confirmation, scoping, limits — because the model can call it wrongly. Read-only tools can be permissive; consequential ones cannot.
You cannot put the safety in the model and trust it to hold — the model is non-deterministic and manipulable, so the safety has to live in the tool, enforced every call regardless of what the model intended. This is the function-calling control principle, made concrete at the level of building the tool: the model proposes by calling; the tool disposes by validating, authorizing, and guarding.
Make it recoverable: error messages the model can act on
The subtle, often-missed principle: a reliable tool gives errors the model can understand and recover from. Because the model is in a loop and reads your response, an error message is not just a log line — it’s an instruction to the model about what to do next. Design errors for that reader:
- Return clear, actionable error messages. “Invalid order_id: must be a positive integer, got ‘abc’” tells the model exactly what went wrong and how to fix its next attempt. A bare “error” or an opaque stack trace tells it nothing, and it’ll flail.
- Make failures recoverable where possible. If the model called the tool slightly wrong, a good error lets it correct and retry successfully. A good error message turns a failed call into a successful next call — the model reads it, adjusts, and gets it right, which is exactly the loop you want.
- Distinguish “you called this wrong” from “this genuinely failed.” The model should be able to tell a recoverable mistake (fix the arguments) from a real failure (give up or escalate), because the right next action differs.
This is a real shift from designing errors for code, which just needs a status to branch on. The model needs an error it can read and reason from, because its next action depends on understanding what went wrong. A tool whose errors the model can recover from is dramatically more reliable in a loop than one that fails opaquely.
Make it observable
A final, briefer principle, carried over from everything about running these systems: log tool calls and their results. When an agent or a function-calling interaction goes wrong, the tool-call log — what was called, with what arguments, returning what — is how you diagnose it. A tool that’s a black box is one you can’t debug when the model uses it in a way you didn’t expect, which it eventually will. Observability at the tool boundary is part of what makes the whole system maintainable.
Verdict
Tools are the foundation everything else about LLMs is built on — function calling, agents, MCP all reduce to tools a model can invoke — so building reliable ones is the bedrock discipline, and it’s distinct because the caller is distinct: a non-deterministic, fallible model that interprets your tool from its description, decides when to use it, and supplies arguments extracted from natural language. From taking that caller seriously, the principles follow. Make the tool understandable — clear, specific names and descriptions, because the description is the interface the model reasons from and improving it is usually the highest-impact fix. Make it well-scoped — narrow, single-purpose, non-overlapping, with a small argument surface — because a model chooses and calls a focused tool far more accurately. Make it safe — validate every model-supplied argument as untrusted input, enforce authorization in the tool, and guard destructive operations — because safety cannot live in a non-deterministic model and must be enforced by the tool every call. Make it recoverable — clear, actionable error messages the model can read and correct from, turning a failed call into a successful retry — because in a loop the error is an instruction to the model. And make it observable, so you can debug the misuse that will eventually happen. Design tools for the fallible, well-meaning, non-deterministic caller they actually have, and the models, agents, and systems built on top of them become reliable — because everything above only works as well as the tools underneath. That’s where building with LLMs ultimately rests: on tools, built right.