Chapter 52 — Tools: giving the model hands
In one sentence
A tool is a normal function you expose to the model with a name, a description, and a JSON schema for its arguments — and the description is a prompt, so write it like one.
What a tool definition looks like
{
"name": "get_order_status",
"description": "Look up the current status of a customer order by its ID. Returns status, items, and estimated delivery date. Use this whenever a customer asks about an order.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "The order ID, e.g. 'ORD-12345'. Must start with ORD-."
}
},
"required": ["order_id"]
}
}
The model sees only this. It never sees your implementation. So the description and the schema are the entire interface — and they're written in natural language, which means they are prompts (Chapter 44) and deserve the same care.
How to write tools the model will actually use correctly
| Rule | Why | Bad | Good |
|---|---|---|---|
| Say when to use it | The model must decide between tools | "Gets order data" | "Use this whenever a customer asks about the status or contents of an order." |
| Describe the return value | It plans the next step from this | (nothing) | "Returns status, items, and estimated delivery date." |
| Constrain the arguments | Fewer invalid calls | {"id": "string"} |
{"order_id": {"type":"string", "pattern":"^ORD-[0-9]+$"}} |
| Use enums for closed sets | Eliminates a whole class of error | "status": "string" |
"status": {"enum": ["pending","shipped","delivered"]} |
| Few tools, clearly distinct | 30 similar tools confuse it | 12 search variants | one search with a type enum |
| Return errors as data | It can recover | raise a 500 | {"error": "Order not found. Check the ID format."} |
| Keep results small | Context and cost (Chapter 45) | dump 5,000 rows | top 10 rows + a total count |
A useful test: hand your tool descriptions to a colleague with no context. If they can't tell which tool to use for a given task, neither can the model.
The tool categories worth having
READ-ONLY (safe, allow freely)
search documents, look up a record, get the time, read a file,
run a database SELECT, fetch a URL
COMPUTE (safe if sandboxed)
run code, do maths, transform data
WRITE / IRREVERSIBLE (gate these -- Chapter 60)
send an email, issue a refund, delete a file, deploy,
modify a database, post to an API, spend money
Draw this line on day one. Every guardrail decision follows from which side of it a tool sits on. Read-only tools can be called freely; write tools need validation, limits, logging, and usually human approval.
The two mistakes everyone makes
1. Giving the model a tool that is too powerful.
BAD: run_sql(query) <-- the model can do anything,
including DROP TABLE
GOOD: get_order(order_id) <-- one job, validated input
get_customer_orders(customer_id, limit=10)
search_orders(status, date_from, date_to)
A narrow tool is safer, easier for the model to use correctly, and easier to test. "Give it SQL access" is the single most common serious mistake in agent projects.
2. Trusting the arguments.
The model generates the arguments. Those arguments are untrusted input — they may be influenced by anything in the context, including a customer's message (Chapter 59). Validate them in your code exactly as you would validate a web form:
def get_order_status(order_id: str, *, caller_customer_id: str):
# 1. VALIDATE the shape
if not re.fullmatch(r"ORD-\d{4,10}", order_id):
return {"error": "Invalid order ID format."}
# 2. AUTHORISE -- does this caller own this order?
order = db.get_order(order_id)
if order is None or order.customer_id != caller_customer_id:
return {"error": "Order not found."} # don't leak existence
# 3. Return the MINIMUM needed
return {"status": order.status, "eta": order.eta, "items": order.items[:20]}
Note caller_customer_id — it comes from your session, not from the model.
Identity must never be a model-supplied argument.
Practice (30 min)
A. Write a tool schema for "search the help centre", with a query string and an optional category enum. Show it to a colleague and ask when they'd use it.
B. Take the loop from Chapter 51 and add a read-only tool that queries a small JSON file of fake orders. Ask the agent questions that require it.
C. Break it on purpose:
- Make the description vague ("gets stuff"). Does the model still use it correctly?
- Add a second, near-identical tool. Does it pick the right one?
- Ask about an order that doesn't exist. Does your error message help it respond sensibly to the user?
D. Now write a refund_order tool — but don't wire it up yet. Just write
down: what could go wrong if the model called it with the wrong arguments? Keep
that list for Chapter 60.
Common confusions
- "Should I use MCP or plain function calling?" Function calling for tools that live in your codebase. MCP (Part 13) when you want tools that are reusable across applications, or built by someone else.
- "How many tools is too many?" Quality degrades as the list grows — beyond roughly 15–20 similar tools, consolidate or use a router (Chapter 50) to select a subset first.
- "Can the model call tools in parallel?" Most modern models can request several at once. Run independent ones concurrently; it's a large latency win.
Check yourself
- What does the model actually see of a tool?
- Why is the tool description a prompt?
- Why is
run_sql(query)a dangerous tool? - Why must caller identity never come from the model?
Further reading
- Article (excellent): "Writing effective tools for AI agents" — Anthropic Engineering. Practical, with before/after examples.
- Docs: the function-calling / tool-use guide from whichever provider you use. Read one end to end.
- Docs: JSON Schema (json-schema.org) — you'll write a lot of it.