Appearance
12.10.3 — Designing Tools
The first version of Cartwise's refund agent did not have the four clean tools of 12.10.2. It had eleven, generated straight from the internal order service's API: search_orders, find_customer_orders, lookup_order, get_order_details, get_order_items, get_shipment, list_refunds, and so on, plus one called run_sql "for anything else". Reviewing a week of traces, the team found the agent:
- calling
search_orderswhen it meantget_order_details, because nothing in the descriptions said how they differed; - passing the customer's email as
order_id, getting back400 Bad Request, and trying again with the same email three times; - reading a 300-line JSON order record, of which it needed four fields, on every step;
- using
run_sqlto check a refund rule, and writing a query that ignored partial refunds.
None of these were the model's "fault" in a useful sense. The model can know the world only through what its tools tell it: their names, descriptions, argument schemas, results and error messages. Those are the whole of its interface to your systems. The 2024 SWE-agent paper from Princeton, which built one of the first strong coding agents, gave this interface a name, the agent-computer interface, and measured what it was worth. With the same model, GPT-4 Turbo, an agent given only an ordinary command-line shell fixed 11% of a standard set of real software bugs; the same agent with tools redesigned for a model to use fixed 18%, about 64% more, without any change to the model. (One of the authors, Shunyu Yao, also led the ReAct paper of 12.10.1.) Anthropic's engineering guidance makes the same point: put as much care into the tools an agent uses as you would into a user interface for people.
This page is that care, one part of a tool at a time.
1. Descriptions: say when, not only what
The model decides whether to call a tool from its description. A description that says only what the tool does leaves the model to guess when, and guessing is where wrong calls come from. A good description answers four questions: what it does, when to use it, when not to use it, and what it returns.
python
tool("get_order_details", "Gets order details.",
order_id={"type": "string"})python
tool("get_order",
"Look up one order belonging to the current customer. Returns the items, "
"amount paid, amount already refunded, payment method, delivery date and "
"product category. Call this before making any decision about an order, "
"including eligibility and refunds. Do not use it to search; if the "
"customer has not given an order number, ask them for it.",
order_id={"type": "string",
"description": "Cartwise order number, digits only, e.g. '4471'"})The clear version tells the model the tool covers only this customer's orders, lists exactly what comes back (so the model knows it need not look elsewhere for the delivery date), says it must come first, and closes off the commonest misuse by saying what to do instead. The argument's own description gives the format with an example, which stops the model passing "#4471" or "ORD-4471".
Two habits help. Write descriptions for a capable new colleague who has never seen your systems, because that is roughly who reads them. And when two tools could plausibly answer the same request, either merge them or make each description say explicitly when to use the other. Cartwise merged search_orders, find_customer_orders, lookup_order and get_order_details into the single get_order above and a separate list_recent_orders for when the customer gives no number.
2. Arguments: make wrong values impossible
Every argument is a chance for the model to get something wrong, so design arguments so that as few wrong values as possible can even be expressed.
Few arguments. Each one the model must fill is one more decision. If the value is known to your code, such as the customer's identity, the current date or the store's currency, your code supplies it and the model never sees it.
Enums for fixed choices. reason with four allowed values cannot be filled with an invented fifth reason that your refund logic does not handle.
Units and formats in the name or description. amount_rupees cannot be confused with paise or dollars the way amount can. Dates as YYYY-MM-DD with an example.
Identifiers, not descriptions. order_id is better than "the order the customer means", because an ID is either right or wrong and can be checked.
Strict schemas. With strict set, as in 12.8.3, the API guarantees the arguments match the schema, so a missing or mistyped argument never reaches your code.
A useful test for a tool's arguments: could a careful person, reading only the schema, fill them correctly for a real request? If they would have to ask a question, the model will guess instead.
3. Results: return what the model needs, in words it can use
Everything a tool returns goes into the model's context. There it costs tokens on this step and every later step, because the whole history is re-sent each time (12.10.2). It also competes for the model's attention with everything else. So results should be as small as they can be while still containing what the next decision needs.
json
{"order": {"id": "b7e2c1f0-5a8d-4c33-9f1e-2d6a7b8c9e01", "legacy_ref": "4471",
"created_at": 1760083200, "status_code": 7, "fulfilment": {"warehouse_id": "...",
"carrier_events": [ ... 40 events ... ]}, "lines": [{"sku": "EAR-2291-BLK",
"tax_breakdown": { ... }, ...}], "payment": { ... }, ... }}json
{"order": "4471", "items": ["Wireless earphones, black"], "paid_rupees": 2499,
"refunded_rupees": 0, "payment": "credit card", "category": "electronics",
"delivered_on": "2026-10-13", "status": "delivered"}The shaped version drops what the model does not need (warehouse, tax lines, carrier events), translates codes into words (status_code: 7 means nothing to a model; "delivered" does), converts the timestamp into a readable date, and uses the order number the customer knows instead of an internal identifier. Each of those changes removes a chance for the model to misread something, as well as saving tokens.
When results can be large, such as search results, a list of orders, or a log file, limit them and say so: return the first 10 with "showing 10 of 143; narrow the search with a date range to see others". Silent truncation is dangerous, because the model will assume it has everything. Some tools offer a detail argument with values like "summary" and "full", so the model can ask for more only when it needs it.
4. Errors: write them as instructions
When a tool fails, the error message is the model's only clue about what to do next. An HTTP status code tells it nothing.
| Error the model sees | What it does next |
|---|---|
400 Bad Request | retries the same call |
Invalid order_id | guesses another format |
No order "priya@example.com". Order numbers are digits, like 4471. Use list_recent_orders if the customer did not give one. | calls the right tool |
The third message works because it says what was wrong, what a right value looks like, and what to do instead. That is the pattern for every tool error: treat the error message as a short instruction to the model, because that is exactly what it is.
Two related points. Errors should be marked as errors, with is_error on the result, so the model knows the call failed rather than returned something odd. And when an error has nothing to do with the model, say so plainly and suggest what to do instead. A timeout from the payment system is a good example. The tool should return something like "the payment system is not responding; tell the customer a colleague will complete the refund". Without that, the model keeps retrying something it cannot fix.
5. How many tools, and how big each one is
Too many tools make every choice harder: the model must read every definition on every step, and more tools means more pairs that overlap. A practical guide is to keep an agent's tool set to the smallest that covers its job, often under twenty, and when an application genuinely needs hundreds, to let the model search for relevant tools rather than load them all. Anthropic's API offers a tool search tool for exactly this: the model looks up tools by description and only the relevant definitions are added to its context.
The more important question is the size of each tool. Generated-from-the-API tools mirror the API: one tool per endpoint. But the model thinks in tasks, not endpoints. To arrange a replacement, the eleven-tool version needed get_order_items, check_stock, create_order, link_orders and notify_customer, in the right order with the right IDs passed between them: five chances to fail, as the arithmetic of 12.10.1 predicts. The redesigned agent has one tool, send_replacement(order_id, item), whose code does all five steps reliably. Put the predictable sequence inside the tool and leave only the decision to the model.
The opposite extreme is a single all-powerful tool: run_sql(query) or bash(command). Coding agents genuinely need a shell, because their job is open-ended work inside a project. But a broad tool hides what the agent is doing behind a string of text. Your code cannot easily tell a harmless SELECT from a DELETE, cannot show a person a clear "approve this refund?" prompt, and cannot allow one action while blocking another. Anthropic's agent-design guidance puts it this way: start with broad tools for breadth, and promote an action to its own dedicated tool when you need to gate it, show it, audit it or run it in parallel. Anything that moves money, sends messages or deletes data qualifies on all four counts.
6. Power: give each tool the least it needs
A tool's power is the worst thing it can do if the model calls it wrongly or is tricked into calling it. Prompt injection, covered in Chapter 12.14.1, means you must assume that sooner or later someone will try to make your agent misuse every tool it has. So design each tool assuming the arguments are hostile.
Compare the dangerous and safe versions of the same capability:
python
def run_query(run, sql: str):
return db.execute(sql) # the model writes the query: SELECT, UPDATE, DELETE, anythingpython
def list_refunds(run, order_id: str):
order = orders.get(order_id)
if order is None or order.customer_id != run.customer_id:
raise ToolError(f"No order {order_id} for this customer.")
return json.dumps(refunds.for_order(order_id)) # fixed, read-only query; model supplies one IDIn the safe version the model chooses which order, not what the query does. The query is fixed and read-only, the ownership check limits it to this customer, and the worst a manipulated model can achieve is reading the refunds on the customer's own order.
The same thinking applies to every capability:
- Fetching web pages. A tool that fetches any URL can be pointed by an attacker at addresses inside your network that were never meant to be reachable from outside. The best-known case: cloud servers can request a special internal address to obtain their own access credentials, so a fetch tool that can reach it may hand those credentials to whoever wrote the prompt. This attack is called server-side request forgery. The defence is an allowlist of the domains the tool may fetch, and blocking internal addresses entirely.
- Sending messages.
send_email(to, subject, body)can email anyone anything.reply_to_customer(body)can only reply to the customer of this run. - Paying.
make_payment(account, amount)versuspropose_refund(order_id, amount_rupees, reason)with limits checked in code, as in 12.10.2. - Running code. If an agent must run code it wrote, run it in an isolated, disposable environment with no network and no secrets, the sandbox of 12.10.8.
The tools also run with credentials, and those should be narrow too: the refund agent's database login can read orders and write refund proposals, and nothing else, so even a bug in a tool cannot do more.
7. Side effects: safe to repeat, easy to check, possible to undo
Tools that change things need three more properties.
Safe to repeat. Models sometimes call the same tool twice, and harnesses retry after failures. A tool that creates, pays or sends must use an idempotency key, as issue_refund did, so the second call does nothing new.
Easy to check before it happens. Split consequential actions into propose and carry out. The model proposes (propose_refund returns a summary), something checks (code rules, a person, or both), and only then does it happen. A proposal can also be shown to the customer: "I can refund ₹2,499 to your credit card; shall I go ahead?"
Possible to undo where you can. Prefer actions with an undo: a cancellation that can be reversed within an hour, a draft rather than a sent email, a held payment rather than a completed one. When an action cannot be undone, it is the first candidate for approval.
8. Watching how the model uses your tools
Tools are never right the first time. The way to improve them is to read what the agent actually does with them, in traces like the one in 12.10.2, looking for specific patterns:
| Pattern in the traces | Usual cause |
|---|---|
| wrong tool for the job | overlapping descriptions |
| same failing call repeated | error message does not say what to do |
| malformed arguments | missing format or example |
| tool never called when it should be | description does not say when |
| huge input token counts | results too large |
| long chains of small calls | tools too fine-grained |
Each fix is a change to a description, a schema, a result or an error message, and each is checked against the evaluation set of 12.10.9 to confirm it helped. A useful shortcut is to show the model a set of failing traces along with the tool definitions and ask it what was confusing; models are good at explaining which description misled them.
What the interviewer will push on
"How do you design a tool for an LLM?" They want the specific ingredients, not just "make it clear":
- A description that says when to use the tool, and when not to.
- Few arguments, each typed, with enums and formats.
- Strict schemas.
- Small, readable results.
- Errors written as instructions for what to do next.
- The least power the tool needs.
The tell is talking about results and errors, which most candidates forget.
"Why not expose your whole API as tools?" Endpoint-shaped tools force the model to chain many small calls, multiplying failure chances, and overlapping endpoints confuse its choices. Build task-shaped tools that do the predictable sequence in code.
"Is it OK to give an agent a SQL tool?" For an analyst's own read-only replica in a sandbox, perhaps. In a customer-facing agent, no: use parameterised, read-only tools where the model supplies values, not query logic. The candidate who says "yes, with a good prompt" has failed this question.
"What happens when a tool returns 10,000 rows?" Limit the result, say it was limited and how to narrow it, and consider a summary mode. Silent truncation makes the model believe it saw everything.
"How do you know your tools are working?" Read traces for the patterns in section 8, fix descriptions, schemas and errors, and confirm each change against an agent evaluation set.
Something to volunteer: the SWE-agent result. Changing only the interface, the tools and their outputs, with the same model, raised the share of real bugs fixed from 11% to 18%. It is the strongest evidence that tool design is engineering work with measurable payoff, not polish.
Recall
- The model knows your systems only through its tools: names, descriptions, schemas, results, errors. This is the agent-computer interface, and improving it alone, with the same model, measurably raises how many tasks an agent completes.
- Descriptions say what the tool does, when to use it, when not to, and what it returns. Merge overlapping tools or make each say when to use the other.
- Arguments: few, typed, enums for fixed choices, units and formats in names, IDs not descriptions, strict schemas; values your code knows are never asked of the model.
- Results: small, readable, codes translated into words; large results limited with a note saying so. Errors: marked as errors and written as instructions: what was wrong, what is right, what to do instead.
- Task-shaped tools beat endpoint-shaped ones; promote an action to its own tool when it must be gated, shown, audited or run in parallel.
- Least privilege: fixed queries with model-supplied values, allowlists for fetching, narrow messaging and payment tools, narrow credentials. Side effects: idempotent, proposed before carried out, undoable where possible.
Self-test: Rewrite the error 400 Bad Request for a model that passed an email as an order number. Why is send_replacement(order_id, item) more reliable than five separate tools? What is the worst thing a manipulated model can do with list_refunds as written in section 6?
Next: 12.10.4 The Model Context Protocol — Cartwise's tools are written inside its own agent; the next page shows the open standard that lets any agent use tools written once by anyone, how it works underneath, and how a coding assistant uses it to drive a real browser.