How to design tools an LLM can actually use
A large share of agent failures trace back to the tools rather than the model. The model picked a reasonable action from a confusing menu, got back a wall of text, or received an error it couldn't interpret. Fix the tools and the "dumb model" often turns out to be fine.
A tool for an LLM isn't quite the same thing as a function for a programmer. A programmer reads the source and the docs, remembers the last call, and can step through a debugger. A model has a name, a paragraph of description, and whatever text came back last time. Designing for that reader is the whole job.
Anthropic's engineering team has written about the same ideas in their guidance on writing tools for agents, and it's worth reading alongside this. What follows is a working checklist.
Start from the task, not the API
The lazy way to build tools is to wrap every endpoint of an existing API. You end up with list_users, get_user, list_orders, get_order, list_order_items, and thirty more, and then wonder why the agent flounders.
The better way is to start from what someone is trying to accomplish and build tools around those jobs. Say the task is "find out why a customer was charged twice." A human support agent would want to see the customer, the recent charges, and any refunds in one place. A single tool, get_billing_history(customer_email), that returns exactly that is far better than four low-level ones the model has to chain and reconcile.
A useful test: read your tool list aloud. If it sounds like a database schema, rework it. If it sounds like a list of things a teammate could be asked to do, you're close.
Fewer, more meaningful tools
Every tool you add competes for the model's attention and adds another chance to pick wrong. There's no magic number, but past a dozen or so you should have a good reason for each one.
A few ways to shrink a list. Merge tools that are always used together: if every call to find_customer is followed by list_orders, offer one tool that does both. Combine near-duplicates, since search_orders and find_orders will get confused. Use a parameter instead of a sibling tool, because a format argument beats get_report_pdf and get_report_csv. And drop tools the model has never used. If a run log shows a tool untouched across fifty runs, remove it.
Merging has a limit. A tool that does five different things depending on a mode flag is just five tools in a trench coat. The goal is a clean, memorable purpose per tool.
Names and namespacing
Names are read constantly and cost nothing to get right. Prefer verb-noun pairs that say exactly what happens: search_tickets, create_invoice, get_weather_forecast. Avoid cute names, abbreviations, and version suffixes.
When you have many tools across several systems, prefix them: github_search_issues, slack_post_message, jira_create_ticket. The prefix helps the model pick between systems and keeps similar verbs from colliding. It's a cheap way to reduce mistakes, especially once tools come from several sources, as they do when you connect MCP servers.
Write descriptions like instructions
The description does the most work. Treat it as a short prompt addressed to a smart new colleague who has never seen your system.
A strong description covers four things:
- What the tool does, in one plain sentence.
- When to use it, and when not to.
- What it returns, including any limits or truncation.
- Anything surprising, like rate limits, side effects, or inputs that must come from another tool first.
Compare these two.
Weak: "Searches the database."
Better: "Search support tickets by keyword. Use this to find past issues that match a customer's problem. Returns up to 10 tickets with id, title, status, and a two-line summary, newest first. To read a full ticket, call get_ticket with its id. Does not search closed tickets older than one year."
The second version answers every question the model would otherwise have to guess at. It costs a few dozen tokens per call and saves far more in wrong turns.
Make parameters hard to misuse
Schemas are teaching material too. Describe every parameter, with an example for anything format-sensitive, and use enums for fixed choices so the model can't invent a value. Prefer human-friendly identifiers where you can: a model handles customer_email better than a 36-character UUID it has to copy exactly. If you must use IDs, make sure a search tool returns them, so it never has to invent one. Give sensible defaults and say what they are, and keep inputs flat, since deep nesting invites malformed calls.
Where you can, validate inputs on your side and reject bad ones with a message that explains the fix. The tool use explainer has more on schemas.
Return what the model needs, and no more
Output design is where most token waste hides. A raw API response with fifty fields, IDs, timestamps, and nested metadata is noise. The model has to dig through it, and every token costs money and dilutes attention.
Start by returning only the useful fields: names and statuses beat internal IDs, unless an ID is needed for the next call. Format for reading, since short labeled lines or compact tables are easier for a model than deeply nested JSON. Paginate and cap, returning the first ten results with a note like "showing 10 of 143, refine your query or request the next page," because unbounded results are how a single call fills a whole context window. A detail parameter with brief and full values lets the model start cheap and expand only when it needs to. And if you cut something off, say so in the output, because silent truncation causes confident wrong answers.
A quick way to check yourself: paste a real tool result into a chat and ask, "Could someone answer the question from just this?" If you can't skim it in ten seconds, neither can the model.
Errors that teach
An error message is the model's only feedback about what to change. Compare:
"Error 422""Invalid date range: 'end' (2026-01-05) is before 'start' (2026-03-01). Dates must be YYYY-MM-DD and end must not be earlier than start."
The second lets the model fix the call and retry successfully. The first sends it into a loop of guesses. Good errors say what was wrong, show the offending value, and state the expected form. Include an example of a valid input when the format isn't obvious.
Also distinguish "nothing found" from "something broke." An empty search result and a database timeout call for different reactions, so report them differently.
Safety belongs in the tool
Anything dangerous should be constrained by the tool itself, not by a polite request in the prompt. A prompt says "please don't delete production data." A tool that only has read access on production physically can't.
Prefer narrow tools to general ones: get_invoice(id) is safer than run_sql(query). If you need a broad tool, put guardrails around it, such as read-only credentials, row limits, query timeouts, and an allow-list of tables. Separate reads from writes, and gate the writes with confirmation steps, dry-run modes, or human approval. Make write tools idempotent or attach an idempotency key, because models retry. Validate paths, URLs, and identifiers, as the file tool in the first-agent tutorial does with its sandbox check, and log every call with its inputs, outputs, and what triggered it.
For the wider picture, least privilege for AI agents goes into permissions, sandboxes, and approval flows.
A worked example
Say you're building a calendar assistant. A first attempt might expose the raw calendar API:
| Tool | Problem |
|---|---|
list_calendars |
Fine, but rarely needed |
list_events(calendar_id, time_min, time_max) |
Needs IDs and ISO timestamps the model must construct |
get_event(event_id) |
Extra hop for details |
create_event(...) |
Twelve parameters, half of them optional |
update_event(...) |
Overlaps with create; easy to clobber fields |
delete_event(event_id) |
Destructive, no confirmation |
The redesigned set, built around what people actually ask for:
| Tool | Why it's better |
|---|---|
find_free_time(duration_minutes, within_days, attendees) |
Answers the most common question in one call |
search_events(query, date_range) |
Human-friendly inputs, compact results with ids |
schedule_meeting(title, start, duration, attendees) |
Few required fields, clear defaults |
reschedule_meeting(id, new_start) |
One purpose, no accidental overwrites |
cancel_meeting(id, reason) |
Requires confirmation before anything is deleted |
Six low-level tools became five task-shaped ones, and the dangerous one gained a gate. The model has fewer choices and each choice is clearer.
Search first, then fetch
A pattern that shows up in almost every well-behaved toolset is a pair: a cheap search tool that returns short summaries with identifiers, and a fetch tool that retrieves the full item by identifier. The model searches, scans the summaries, and pulls details only for the one or two results that matter.
This keeps context small. It also mirrors how people work: skim a list, open the interesting one. The alternative, a single tool that returns everything for every match, tends to flood the window and forces the model to sift through material it has no use for.
The same idea applies to files, tickets, documents, and database rows. Give the model a way to look before it reads, and a way to read only what it chose.
Changing tools over time
Tools evolve, and a change that looks harmless to you can confuse a model that has been using the old behavior. Some guidelines for evolving them:
- Don't rename a tool casually. If prompts, examples, or saved transcripts refer to the old name, they'll break in odd ways.
- Add optional parameters rather than changing required ones. Old calls keep working.
- Re-run your test set after any edit, including edits to descriptions. A reworded sentence can shift which tool gets picked.
- Keep a changelog for your tool definitions, even a simple one in the repository. When behavior drifts, you'll want to know what changed and when.
Treat the tool set as a public interface with one very literal consumer, and version it accordingly.
Test tools like you'd test a product
You won't get the design right on the first try, so build a loop for improving it.
- Write 15 to 20 realistic requests, including some ambiguous ones and a few the tools shouldn't be able to handle.
- Run them and save the transcripts.
- Read the transcripts. Look for wrong tool choices, malformed arguments, repeated calls, and outputs the model ignored.
- Change one thing (a description, a parameter name, a truncation rule) and run the same set again.
- Track simple numbers: success rate, average steps, tokens per task.
Notice how often the fix is a wording change. Most of the improvement here comes from text, not code. When the set of test requests grows into something you run on every change, you've started building a proper evaluation, which how to evaluate an AI agent picks up.
The short checklist
- Built around tasks, not endpoints
- As few tools as the job allows
- Verb-noun names, prefixed when there are many
- Descriptions that say what, when, returns, and gotchas
- Flat, well-described inputs with enums and examples
- Compact, readable, capped outputs
- Errors that explain the fix
- Safety enforced in code
- A test set you rerun after every change
Get these right and a mid-sized model will often beat a larger one working with a messy toolbox. That's a cheaper way to improve an agent than most of the alternatives.