{"course":{"slug":"building-with-ai","title":"Building with AI","url":"https://addaly.com/learn/building-with-ai","level":"intermediate","track":"build"},"lesson":{"slug":"designing-a-tool-surface","title":"Designing tools a model can use correctly","url":"https://addaly.com/learn/building-with-ai/designing-a-tool-surface","position":25,"of":80,"minutes":9,"answer":"Tool descriptions are the model's only interface, so shape tools around user questions rather than API endpoints, say explicitly when not to use each one.","keyIdea":"Tool descriptions are the model's only interface, so shape tools around user questions rather than API endpoints, say explicitly when not to use each one, and never let identity be a parameter the model can choose."},"body":{"format":"markdown","text":"## Your tool definitions are a user interface\n\nThe 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.\n\nA 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.\n\n## Granularity: task-shaped, not endpoint-shaped\n\nThe most common mistake is exposing your REST API one endpoint at a time.\n\n```text\nget_customer, get_orders, get_order_items, get_shipments,\nget_shipment_events, get_refunds, get_refund_status\n```\n\nSeven 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.\n\n```text\norder_status(order_id) -> {state, carrier, last_scan, eta, refund_state}\n```\n\nOne 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.\n\nThe 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.\n\n## Fewer tools than you think\n\nAccuracy 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.\n\nThree ways to stay under the limit:\n\n- **Scope by context.** A billing conversation does not need the shipping tools. Pass the subset the router chose.\n- **Merge near-duplicates.** `search_docs` and `search_faqs` become `search(source=...)`.\n- **Hide the plumbing.** Anything the model should not decide (pagination, retries, authentication) belongs inside the tool, not as a parameter.\n\n## Writing the description\n\nA good description has four parts and fits in five lines:\n\n```python\n{\n  \"name\": \"order_status\",\n  \"description\": (\n    \"Current status of one order, including carrier and estimated delivery. \"\n    \"Use when the customer asks where an order is or whether it shipped. \"\n    \"Do not use for returns or refunds; use refund_status for those. \"\n    \"Returns state one of: placed, packed, shipped, delivered, cancelled.\"),\n  \"input_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\"order_id\": {\"type\": \"string\",\n      \"description\": \"Format ORD-12345678. Never invent one; ask the customer if unknown.\"}},\n    \"required\": [\"order_id\"]}\n}\n```\n\nWhat 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.\n\nThe 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.\n\n## Results are prompt text too\n\nWhatever your tool returns goes straight back into the context, so it obeys the same economics as everything else.\n\nReturning 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.\n\nAnd errors deserve care, because an error message is an instruction:\n\n```json\n{\"error\": \"not_found\",\n \"message\": \"No order with that id. Ask the customer to confirm it, or use find_orders_by_email.\"}\n```\n\nA 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.\n\n## The security line\n\nOne rule from the tool-use lesson bears repeating here because tool design is where it is violated: **never put an identity in the arguments.**\n\n```python\n# wrong: the model decides whose data to read\ndef get_orders(user_id): ...\n\n# right: identity comes from the session your server holds\ndef get_orders():\n    return db.orders_for(session.user_id)\n```\n\nAny 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*."},"check":{"question":"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?","options":[{"text":"Validate the id server-side and return an error, so the model learns it was wrong.","correct":false,"tempting":"Catches the bad call after it happens and costs a round trip, leaving the model to guess again on the next attempt."},{"text":"Make the parameter optional so the model can omit it.","correct":false,"tempting":"Removes the pressure to invent a value but leaves the tool callable with no target and no instruction about what to do instead."},{"text":"Add to the parameter's description that the id must never be invented and that the customer should be asked if it is unknown.","correct":true},{"text":"Lower the temperature so the model stops generating arbitrary strings.","correct":false,"tempting":"Treats a missing instruction as sampling noise, when the most likely completion for a required string field with no source is still a well-formed placeholder."}]},"prev":{"slug":"tool-use","title":"Tool use: the model never runs anything","url":"https://addaly.com/learn/building-with-ai/tool-use"},"next":{"slug":"what-an-agent-is","title":"What an agent actually is, and why the demos fail","url":"https://addaly.com/learn/building-with-ai/what-an-agent-is"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"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).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Designing tools a model can use correctly","courseTitle":"Building with AI","module":{"index":3,"slug":"structure-tools-and-the-loop","title":"Structure, tools and the loop"},"answer":null,"keyIdea":"Tool descriptions are the model’s only interface, so shape tools around user questions rather than API endpoints, say explicitly when not to use each one, and never let identity be a parameter the model can choose.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"your-tool-definitions-are-a-user-interface","runs":[{"text":"Your tool definitions are a user interface"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"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."}]},{"t":"h2","id":"granularity-task-shaped-not-endpoint-shaped","runs":[{"text":"Granularity: task-shaped, not endpoint-shaped"}]},{"t":"p","runs":[{"text":"The most common mistake is exposing your REST API one endpoint at a time."}]},{"t":"code","lang":"text","label":"Text","text":"get_customer, get_orders, get_order_items, get_shipments,\nget_shipment_events, get_refunds, get_refund_status"},{"t":"p","runs":[{"text":"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."}]},{"t":"code","lang":"text","label":"Text","text":"order_status(order_id) -> {state, carrier, last_scan, eta, refund_state}"},{"t":"p","runs":[{"text":"One tool, one call, and the shape of the answer is decided by you rather than assembled by the model. Design tools around "},{"text":"the questions your users ask","b":true},{"text":", not around your database tables. A tool that does the join is a tool the model cannot get wrong."}]},{"t":"p","runs":[{"text":"The opposite failure is also real: one "},{"text":"do_anything(action, params)","code":true},{"text":" 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."}]},{"t":"h2","id":"fewer-tools-than-you-think","runs":[{"text":"Fewer tools than you think"}]},{"t":"p","runs":[{"text":"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."}]},{"t":"p","runs":[{"text":"Three ways to stay under the limit:"}]},{"t":"ul","items":[[{"text":"Scope by context.","b":true},{"text":" A billing conversation does not need the shipping tools. Pass the subset the router chose."}],[{"text":"Merge near-duplicates.","b":true},{"text":" "},{"text":"search_docs","code":true},{"text":" and "},{"text":"search_faqs","code":true},{"text":" become "},{"text":"search(source=...)","code":true},{"text":"."}],[{"text":"Hide the plumbing.","b":true},{"text":" Anything the model should not decide (pagination, retries, authentication) belongs inside the tool, not as a parameter."}]]},{"t":"h2","id":"writing-the-description","runs":[{"text":"Writing the description"}]},{"t":"p","runs":[{"text":"A good description has four parts and fits in five lines:"}]},{"t":"code","lang":"python","label":"Python","text":"{\n  \"name\": \"order_status\",\n  \"description\": (\n    \"Current status of one order, including carrier and estimated delivery. \"\n    \"Use when the customer asks where an order is or whether it shipped. \"\n    \"Do not use for returns or refunds; use refund_status for those. \"\n    \"Returns state one of: placed, packed, shipped, delivered, cancelled.\"),\n  \"input_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\"order_id\": {\"type\": \"string\",\n      \"description\": \"Format ORD-12345678. Never invent one; ask the customer if unknown.\"}},\n    \"required\": [\"order_id\"]}\n}"},{"t":"p","runs":[{"text":"What it does, when to use it, when "},{"text":"not","i":true},{"text":" 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."}]},{"t":"p","runs":[{"text":"The parameter description carries its own rule: "},{"text":"Never invent one; ask the customer if unknown.","code":true},{"text":" Without it, a model with no order id will produce "},{"text":"ORD-00000000","code":true},{"text":", because the schema demanded a string and it had none. Hallucinated identifiers are almost always a missing instruction at the parameter level."}]},{"t":"h2","id":"results-are-prompt-text-too","runs":[{"text":"Results are prompt text too"}]},{"t":"p","runs":[{"text":"Whatever your tool returns goes straight back into the context, so it obeys the same economics as everything else."}]},{"t":"p","runs":[{"text":"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 ("},{"text":"\"...and 47 more\"","code":true},{"text":"), so the model knows there was more rather than believing it has seen everything."}]},{"t":"p","runs":[{"text":"And errors deserve care, because an error message is an instruction:"}]},{"t":"code","lang":"json","label":"JSON","text":"{\"error\": \"not_found\",\n \"message\": \"No order with that id. Ask the customer to confirm it, or use find_orders_by_email.\"}"},{"t":"p","runs":[{"text":"A bare "},{"text":"500 Internal Server Error","code":true},{"text":" 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."}]},{"t":"h2","id":"the-security-line","runs":[{"text":"The security line"}]},{"t":"p","runs":[{"text":"One rule from the tool-use lesson bears repeating here because tool design is where it is violated: "},{"text":"never put an identity in the arguments.","b":true}]},{"t":"code","lang":"python","label":"Python","text":"# wrong: the model decides whose data to read\ndef get_orders(user_id): ...\n\n# right: identity comes from the session your server holds\ndef get_orders():\n    return db.orders_for(session.user_id)"},{"t":"p","runs":[{"text":"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 "},{"text":"what to do","i":true},{"text":", never "},{"text":"who to do it as","i":true},{"text":"."}]}],"check":{"question":[{"text":"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?"}],"options":[{"text":[{"text":"Validate the id server-side and return an error, so the model learns it was wrong."}],"correct":false,"tempting":[{"text":"Catches the bad call after it happens and costs a round trip, leaving the model to guess again on the next attempt."}]},{"text":[{"text":"Make the parameter optional so the model can omit it."}],"correct":false,"tempting":[{"text":"Removes the pressure to invent a value but leaves the tool callable with no target and no instruction about what to do instead."}]},{"text":[{"text":"Add to the parameter’s description that the id must never be invented and that the customer should be asked if it is unknown."}],"correct":true,"tempting":null},{"text":[{"text":"Lower the temperature so the model stops generating arbitrary strings."}],"correct":false,"tempting":[{"text":"Treats a missing instruction as sampling noise, when the most likely completion for a required string field with no source is still a well-formed placeholder."}]}],"order":[0,2,1,3],"why":null},"prev":{"slug":"tool-use","title":"Tool use: the model never runs anything","minutes":8},"next":{"slug":"what-an-agent-is","title":"What an agent actually is, and why the demos fail","minutes":8}}}