Tool Use

Design tools an agent can use well: the right size, inputs that are hard to misuse, short results, errors that say what to do next, and safe retries.

14 minLesson 2 of 6
On this page
  1. The model only sees the text
  2. Errors that say what to do next
  3. The tool in code
  4. Short results, with a way to get more
  5. Size the tools like the task
  6. Read tools and write tools
  7. Try it yourself
  8. Common mistakes
  9. Check yourself
  10. What to practise next

A customer asks a support agent where order 1O42 is. The customer typed a capital O instead of a zero. The agent calls getOrder("1O42") and gets back Error: invalid input. It tries again with the same ID. Same error. It tries a third time, then tells the customer the order system is down.

The model was not the main problem. The tool said something was wrong but not what, so the model had nothing to fix. Change that one error message and the same model will usually get it right on the second try.

The model only sees the text

Tools and MCP showed the mechanics: the app describes each tool, the model writes a request, the app runs it and puts the result back in the context. For an agent, that means a tool is three pieces of text the model reads:

  1. The description: the name, what it does, when to use it.
  2. The inputs: what it takes, and in what form.
  3. The output: the result, or the error.

The model cannot read your source code or ask a colleague. If those three pieces are unclear, it guesses. Designing a tool for an agent is mostly writing those three pieces for a reader who is smart, literal and in a hurry.

Errors that say what to do next

Here is the order lookup twice, with the same model and the same typo. Only the error message changes.

Same typo, two error messagesUse the arrows or ← → keys
  1. version A: vague error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    attempts
    1
    solved
    no

    1The customer wrote "1O42" with a letter O. The agent copies it into the tool call, which is a reasonable thing to do.

  2. version A: vague error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    toolError: invalid input
    attempts
    1
    solved
    no

    2The tool says only invalid input. The model cannot tell whether the ID is wrong, the system is broken, or it formatted the call badly.

  3. version A: vague error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    toolError: invalid input
    modelgetOrder("1O42")
    toolError: invalid input
    modelgetOrder("1O42")
    toolError: invalid input
    modelSorry, the order system seems to be down.
    attempts
    3
    solved
    no

    3With nothing to go on, it retries the same call, gets the same error, and gives up with a wrong explanation. Three calls, no answer.

  4. version B: helpful error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    tool"1O42" is not an order ID. IDs are 4 digits. Did you mean "1042"?
    attempts
    1
    solved
    no

    4Version B: same model, same typo. This time the tool explains what an ID looks like and offers a likely fix.

  5. version B: helpful error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    tool"1O42" is not an order ID. IDs are 4 digits. Did you mean "1042"?
    modelgetOrder("1042")
    toolorder 1042: shipped, 3 items
    attempts
    2
    solved
    yes

    5The model now has something to act on. It retries with 1042 and gets the order.

  6. version B: helpful error
    whotext
    userWhere is order 1O42?
    modelgetOrder("1O42")
    tool"1O42" is not an order ID. IDs are 4 digits. Did you mean "1042"?
    modelgetOrder("1042")
    toolorder 1042: shipped, 3 items
    modelOrder 1042 has shipped (3 items).
    attempts
    2
    solved
    yes

    6It answers the customer and mentions the corrected ID, so they can spot it if the guess was wrong. Two calls instead of three, and a right answer instead of a false outage.

1 / 6

A good error message answers three questions: what was wrong, what a correct input looks like, and what to try next. Write it as you would for a new colleague, not as a code for a log file.

The tool in code

Here is version B. It checks the input before looking anything up, and every failure returns a sentence the model can act on. The data is fake so you can run it.

const orders = { "1042": { status: "shipped", items: 3 } };
 
function getOrder(orderId) {
  if (!/^\d{4}$/.test(orderId)) {
    const guess = orderId.replace(/[oO]/g, "0");
    const hint = /^\d{4}$/.test(guess) ? ` Did you mean "${guess}"?` : "";
    return { error: `"${orderId}" is not an order ID. IDs are 4 digits.${hint}` };
  }
  const order = orders[orderId];
  if (!order) {
    return { error: `No order "${orderId}". Use findOrders(email) to list a customer's orders.` };
  }
  return { orderId, ...order };
}
 
console.log(getOrder("1O42")); // error, suggests "1042"
console.log(getOrder("1042")); // { orderId: '1042', status: 'shipped', items: 3 }
import re
 
orders = {"1042": {"status": "shipped", "items": 3}}
 
def get_order(order_id):
    if not re.fullmatch(r"\d{4}", order_id):
        guess = re.sub(r"[oO]", "0", order_id)
        hint = f' Did you mean "{guess}"?' if re.fullmatch(r"\d{4}", guess) else ""
        return {"error": f'"{order_id}" is not an order ID. IDs are 4 digits.{hint}'}
    order = orders.get(order_id)
    if order is None:
        return {"error": f'No order "{order_id}". Use find_orders(email) to list a customer\'s orders.'}
    return {"order_id": order_id, **order}
 
print(get_order("1O42"))  # error, suggests "1042"
print(get_order("1042"))  # {'order_id': '1042', 'status': 'shipped', 'items': 3}

The second error is just as deliberate. Besides saying "not found", it names the tool that can find the right ID. Errors are a cheap place to teach the agent how your tools fit together.

Short results, with a way to get more

Every result goes into the context and stays there for the rest of the run. A tool that returns 300 orders in full can take up a large share of the context in one step, and the model has to find the one line that matters in a wall of text. Return the few items and fields that are useful, and say what was left out:

function limitResults(items, max = 5) {
  if (items.length <= max) return { results: items };
  return {
    results: items.slice(0, max),
    note: `Showing ${max} of ${items.length}. Add a status or date range to narrow it.`,
  };
}
 
console.log(limitResults([1, 2, 3, 4, 5, 6, 7, 8]).note);
// Showing 5 of 8. Add a status or date range to narrow it.
def limit_results(items, max_items=5):
    if len(items) <= max_items:
        return {"results": items}
    return {
        "results": items[:max_items],
        "note": f"Showing {max_items} of {len(items)}. Add a status or date range to narrow it.",
    }
 
print(limit_results([1, 2, 3, 4, 5, 6, 7, 8])["note"])
# Showing 5 of 8. Add a status or date range to narrow it.

The note matters. Without it, the model may assume those five orders are all there are.

Size the tools like the task

A tool can be too small or too big.

  • Too small: getCustomerField(id, field) takes one call per field. Looking up a customer's name, email and plan takes three calls, three chances to go wrong and three times the waiting.
  • Too big: runSql(query) can do anything, which means the model has to write correct SQL against a schema it cannot see, and a bad query can change or delete data.
  • About right: findOrders(email, status) matches a step a person would take. It returns what that step needs and can do nothing else.

Then make the inputs hard to get wrong. Use a fixed list of values (status: "packing", "shipped" or "returned") instead of free text. Put units in names: timeoutSeconds, not timeout. Ask for IDs the model has seen in an earlier result rather than names it might misspell.

Read tools and write tools

A read tool, like getOrder, is safe to call twice. A write tool, like refundOrder, is not. Agents do repeat calls: a tool times out, the model cannot tell whether it worked, and it tries again. If that tool sends money, the customer is refunded twice.

Make write tools safe to repeat. Have the tool check whether the action already happened ("order 1042 was already refunded on 3 March"), or accept a request ID and ignore a second request with the same ID. This property is called idempotency: doing it twice has the same effect as doing it once. Which write tools an agent gets at all, and which need a person's approval, is the subject of Guardrails.

Try it yourself

Try it: Redesign a confusing tool

An agent keeps misusing this ticket tool: it passes made-up actions, puts the whole conversation into data, and loops when it sees ERR_22. Redesign it. Say what tools you would offer, what inputs they take, and what one of the error messages should say.

Show a sample answer

One better design splits it into tools that each match one step:

Redesigned tools
createTicket(title, priority)
  priority: one of "low", "normal", "urgent"
  title: one line, under 80 characters
  Returns: { ticketId: "T-5521" }
 
closeTicket(ticketId, reason)
  reason: one of "fixed", "duplicate", "won't fix"
  Safe to repeat: closing a closed ticket returns
  "T-5521 is already closed (fixed)."
 
Example error:
  "T-552 is not a ticket ID. IDs look like T-5521.
   Use findTickets(text) to search by title."

Why it works:

  • Each tool does one thing, so the name says when to use it. There is no free-text action to invent values for.
  • Fixed lists of values for priority and reason rule out whole classes of bad input.
  • The error says what went wrong, what a right ID looks like, and which tool helps. A code like ERR_22 gives the model nothing to change, so it repeats itself.
  • Closing twice is harmless, so a retry after a timeout does no damage.

Common mistakes

Check yourself

Quick check

An agent calls the same failing tool four times in a row with the same input. What is the most likely fix?

Which tool is the best size for an agent that answers questions about customers' orders?

A refundOrder tool times out, and the agent calls it again. What design prevents a double refund?

What to practise next

Take a tool or API you use and write the error message you would want to read if you were the model. Then continue with The Agent Loop, where you write the loop that calls these tools and decide when it should stop.