Guide
Tool calling for AI agents: designing reliable tools
In tool calling, the model requests a tool by name and arguments, and your application decides whether to run it. Reliable tools have a narrow purpose, clear descriptions and schemas, validated arguments, errors the model can act on and side effects that are safe to repeat.
How tool calling works
A model cannot run code or reach your database. In tool calling, your application tells the model which tools exist, usually as a name, a description and a schema of parameters. When the model decides that one would help, it returns a structured request naming the tool and its arguments. Your code then decides whether to run it, carries it out and sends back the result. The model uses that result to continue or to finish.
Exact formats vary between providers and frameworks, but the important point does not: the model proposes and your application executes. That boundary is where reliability and security are enforced. It also means that a tool's definition is effectively a piece of the prompt, and that its implementation is an API whose caller is imaginative, sometimes careless and occasionally manipulated.
Designing the tool's interface
Choose a clear, narrow purpose
One tool should do one recognisable job. A general manage_order tool with an action parameter asks the model to make two decisions at once and makes permissions harder to separate. get_order_status and cancel_order are easier to choose between, easier to test and can be governed differently. The opposite mistake also exists: exposing every internal endpoint forces the model to assemble long chains of low-level calls. Aim for tools that match a task a user would recognise, and combine steps that always occur together.
Write descriptions for the reader that will use them
The model chooses tools largely from their names and descriptions. A good description says what the tool does, when to use it, when not to use it and what it returns. If two tools could be confused, say how they differ. Consistent naming helps too, particularly once the list grows.
Make parameters hard to get wrong
- Use enumerations for fixed sets of values rather than free text.
- Prefer flat, simple structures. Deep nesting invites errors.
- State formats and units in the schema, such as an ISO 8601 date or an amount in the smallest currency unit.
- Avoid opaque values the model has no way of knowing. It should not be guessing an internal ID. Provide a lookup tool, and have your code supply whatever it already knows.
- Do not ask the model for identity or permissions. The user's ID should come from the authenticated session, never from a model-supplied argument that could be altered.
Here is an illustrative, generic definition; each provider and framework has its own exact format:
{
"name": "search_orders",
"description": "Find the current user's orders by status or date. Use before cancel_order if the order ID is unknown. Returns at most 10 orders, newest first.",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["open", "shipped", "delivered", "cancelled"]
},
"placed_after": {
"type": "string",
"description": "Earliest order date as YYYY-MM-DD"
}
}
}
}
Keep the tool list as short as possible
Each additional tool takes up context and gives the model another way to choose wrongly. If an agent has accumulated too many, consider loading only those relevant to the current step or dividing responsibilities. The guide on single agent vs multi-agent systems covers when dividing is justified, since a confused tool list is not always a reason to add more agents.
Validate before you execute
Treat arguments as untrusted input, including when a structured output feature is used. Check them against the schema, then apply checks that a schema cannot express: does this order belong to this user? Is the amount within limits? Is this state change allowed?
Rejecting a bad call is usually better than repairing it silently. A silent "fix" can turn a wrong request into a wrong action that nobody can see.
A tool that runs generated queries needs particular care, as explained in text-to-SQL agents: risks and safeguards.
Return errors the model can act on
When a tool fails, the error is an instruction for what comes next. An unhandled stack trace wastes context and can reveal internal details, while a vague "failed" tends to produce blind retries. Return a structured result that says what happened and whether trying again could help:
# Illustrative only. A consistent result shape for every tool.
def cancel_order(order_id, session):
order = find_order(order_id, session.user_id)
if order is None:
return {
"ok": False,
"error": "order_not_found",
"retryable": False,
"message": "No order with this ID for the current user. "
"Use search_orders to find the right ID.",
}
if order.status == "cancelled":
return {"ok": True, "order_id": order.id, "status": "cancelled"}
if order.status != "open":
return {
"ok": False,
"error": "not_cancellable",
"retryable": False,
"message": f"An order that is {order.status} cannot be cancelled.",
}
apply_cancellation(order)
return {"ok": True, "order_id": order.id, "status": "cancelled"}
Separate the outcomes a tool can have. Empty is not a failure: "no matching orders" is valid information. Invalid arguments can be corrected by the model. Temporary faults such as a timeout are best retried by your code with backoff before the model ever hears about them. Permanent or policy refusals should clearly discourage another attempt.
Make side effects safe
Agents repeat themselves. A model may call a tool twice, a retry may follow a timeout on a request that actually succeeded, or a run may resume from a checkpoint and redo its last step. For anything that changes state, this needs handling on purpose.
- Use idempotency keys. Create a key per intended action in your code, and have the operation return the original result when it sees the same key again.
- Separate preparing from committing. A tool can draft an email or calculate a refund, and a separate step can send or apply it. This creates a natural place for review.
- Grade tools by risk. Reads, reversible writes and irreversible actions deserve different treatment. Sensitive ones can require human approval before they run, enforced in the execution layer rather than requested in a prompt.
- Grant the least privilege. Credentials belong to the tool layer, scoped to the current user and task, and never appear in the prompt. LLM guardrails and prompt injection defence explains why this matters so much when an agent also reads untrusted content.
Keep results useful and small
Tool results go into the context, where they use up space and influence the next decision. Return what the model needs, not a full database record. Paginate lists, say when results have been cut short, and offer a way to fetch more. An agent told "showing 10 of 240" behaves differently from one that assumes it has seen everything.
Results may also contain text from outside your system, such as emails or web pages. The model cannot reliably separate data from instructions, so assume such text may try to steer it, and keep risky tools away from the same step where you can. What the agent retains from these results afterwards is a separate decision, covered in agent memory: short-term and long-term state.
Control the loop around the tools
Well-designed tools still need limits around them:
- Step limits. Cap the number of model turns and tool calls per run, and handle reaching the limit gracefully.
- Timeouts. Give each tool a timeout, as well as the run as a whole. A hanging call should not hang the agent.
- Loop detection. Repeated identical calls that keep returning the same result are a sign to stop or escalate.
- Parallel call rules. Some models can request several tools at once. Parallel reads are usually fine, but writes that depend on each other need ordering, or parallel calls should be turned off.
- Tracing. Log each call's name, arguments, result, duration and outcome, with sensitive values redacted. Without this, you cannot explain what an agent did.
Testing tools at two levels
Test the tool as ordinary code. Unit tests should cover valid input, invalid input, permission failures, empty results, timeouts and repeated calls. These need no model and run quickly.
Test how the model uses it. Build a set of realistic requests alongside the tool, and the arguments you would expect for each. Include requests that should use no tool, and pairs that could be confused. Check selection and arguments separately, and repeat the runs, because output can vary. Re-run the set whenever you edit a description, add a tool or change a model, as a one-sentence change to a description can alter behaviour elsewhere.
Finally, read real traces. Patterns such as one tool being retried repeatedly or a useful tool never being chosen often point to a description or schema problem, not a model problem. For help designing agents with controlled tools and visible limits, see multi-agent systems development.
Frequently asked questions
Does the model run the tool itself?
No. In standard tool calling the model returns a structured request, and your application decides whether to run it. That is why validation, permissions and approval checks belong in your code.
How many tools should an agent have?
There is no fixed number. Keep the list as short as the job allows, since each tool uses context and adds another way to choose wrongly. If selection suffers, merge tools, load them by step or review the descriptions.
What should a tool return when it fails?
Return a structured result saying what went wrong and whether retrying could help, along with a suggested next step where there is one. Handle temporary faults in code where possible, and keep stack traces out of the model's context.
How do I stop an agent repeating an action?
Make state-changing tools idempotent, typically with a key per intended action, so that a repeat returns the first result. Add step limits and loop detection around the agent as well.