Building with AILesson 25 of 80

Contents

Building with AI

Lesson 25 of 80. Sign in to keep your place.

Module 3 · Structure, tools and the loop

Designing tools a model can use correctly

9 min read

Your tool definitions are a user interface

The model chooses tools by reading their names, descriptions and parameter schemas. That text is the entire interface. When a model calls the wrong tool, or passes a plausible but wrong argument, the first suspect is not the model: it is the description you wrote in ninety seconds and never revised.

A useful discipline: read your tool list as though you were a competent new employee on their first day, with no other context. If you could not tell which of two tools to use, neither can the model.

Granularity: task-shaped, not endpoint-shaped

The most common mistake is exposing your REST API one endpoint at a time.

Text
get_customer, get_orders, get_order_items, get_shipments,
get_shipment_events, get_refunds, get_refund_status

Seven tools and five sequential calls to answer “where is my order?”. Every hop is a chance to pass a wrong id, and latency is the sum of all of them.

Text
order_status(order_id) -> {state, carrier, last_scan, eta, refund_state}

One tool, one call, and the shape of the answer is decided by you rather than assembled by the model. Design tools around the questions your users ask, not around your database tables. A tool that does the join is a tool the model cannot get wrong.

The opposite failure is also real: one do_anything(action, params) tool with a free-form params object. Now the model is writing an API call from memory and you have no schema to validate against. Both extremes lose; the middle is a handful of task-shaped tools with tight schemas.

Fewer tools than you think

Accuracy in tool selection degrades as the list grows. Somewhere between ten and twenty tools, models start confusing similar ones, and the confusion is not random: it favours the tool whose description best matches surface wording rather than intent.

Three ways to stay under the limit:

  • Scope by context. A billing conversation does not need the shipping tools. Pass the subset the router chose.
  • Merge near-duplicates. search_docs and search_faqs become search(source=...).
  • Hide the plumbing. Anything the model should not decide (pagination, retries, authentication) belongs inside the tool, not as a parameter.

Writing the description

A good description has four parts and fits in five lines:

Python
{
  "name": "order_status",
  "description": (
    "Current status of one order, including carrier and estimated delivery. "
    "Use when the customer asks where an order is or whether it shipped. "
    "Do not use for returns or refunds; use refund_status for those. "
    "Returns state one of: placed, packed, shipped, delivered, cancelled."),
  "input_schema": {
    "type": "object",
    "properties": {"order_id": {"type": "string",
      "description": "Format ORD-12345678. Never invent one; ask the customer if unknown."}},
    "required": ["order_id"]}
}

What it does, when to use it, when not to use it and what to use instead, and what comes back. The negative clause is the one that stops the model reaching for this tool when a neighbouring one is correct, and it is the one people omit.

The parameter description carries its own rule: Never invent one; ask the customer if unknown. Without it, a model with no order id will produce ORD-00000000, because the schema demanded a string and it had none. Hallucinated identifiers are almost always a missing instruction at the parameter level.

Results are prompt text too

Whatever your tool returns goes straight back into the context, so it obeys the same economics as everything else.

Returning a 4,000-token raw API response for a question needing five fields costs money on this turn and every subsequent turn of the conversation. Project the result down to what is needed. Truncate arrays with an explicit marker ("...and 47 more"), so the model knows there was more rather than believing it has seen everything.

And errors deserve care, because an error message is an instruction:

JSON
{"error": "not_found",
 "message": "No order with that id. Ask the customer to confirm it, or use find_orders_by_email."}

A bare 500 Internal Server Error leaves the model to invent a recovery, and what it invents is usually to try the same call again. A message that names the next step turns a dead end into a recoverable step.

The security line

One rule from the tool-use lesson bears repeating here because tool design is where it is violated: never put an identity in the arguments.

Python
# wrong: the model decides whose data to read
def get_orders(user_id): ...

# right: identity comes from the session your server holds
def get_orders():
    return db.orders_for(session.user_id)

Any parameter that names a person, an account or a tenant is a parameter an attacker can influence through text in the conversation. Take it out of the schema entirely. The model should be choosing what to do, never who to do it as.

Every lesson here is open. Your progress is recorded once you pass the Python, from Zero, for AI exam.

Check your understanding

An assistant sometimes calls a lookup tool with an invented identifier like ORD-00000000 when the customer has not supplied one. Which fix addresses the cause?

Pick the one you would defend. Your answer stays private.

Next lesson · 8 minWhat an agent actually is, and why the demos fail

Stuck on something?Ask Sayali, your AI tutor

How to cite: Addaly (2026) ‘Designing tools a model can use correctly’, in Building with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-with-ai/designing-a-tool-surface (Accessed: 13 October 2026).