The Three Parts of a Tool Definition
A model cannot call your order API. It has no code and no network access — it only emits tokens. Function calling works by handing the model a menu: a name, a description, and a JSON Schema of parameters, from which it produces a structured tool_calls block that your code executes. Every word of that menu is read by the model, so the definition is the whole contract.
TOOLS = [{
"type": "function",
"function": {
"name": "search_orders",
"description": (
"Search a customer's order history. Use when the user names an order "
"number, asks about a recent purchase, or wants delivery status. "
"Returns up to 20 orders. Not for inventory levels or product pricing."
),
"parameters": {
"type": "object",
"properties": {
"customer_id": {"type": "string",
"description": "Customer ID from the session, e.g. c_88213"},
"query": {"type": "string",
"description": "Order number or product name fragment"},
"status": {"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"],
"description": "Fulfillment status filter; omit for all"},
},
"required": ["customer_id", "query"],
"additionalProperties": False,
},
},
}]
Send it with a conversation and the model answers with a call rather than prose:
resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=TOOLS)
msg = resp.choices[0].message
# msg.tool_calls[0].function.name -> "search_orders"
# msg.tool_calls[0].function.arguments -> '{"customer_id": "c_88213", "query": "88213"}'
Note that arguments arrives as a string, not a dict — json.loads it before you dispatch, and treat the result as untrusted input.
| Part | Read by | Failure when wrong |
|---|---|---|
name |
the model, during selection | mis-selection between similar tools |
description |
the model, during selection | tool never called, or called for the wrong task |
parameters |
the model (fills it), your code (validates it) | malformed arguments and wasted retry rounds |
The description earns the most attention per character: it is where you state when to use the tool and when not to, which is what keeps a near-neighbor tool from stealing the call.
Parameter Schemas the Model Can Satisfy
Tool parameters use the same JSON Schema vocabulary as structured outputs — type, properties, required, enum, additionalProperties — so the design rules from Schema-Constrained Generation transfer directly. What changes is the audience: here the schema shapes arguments the model invents, and the model writes them with far less context than it had when it wrote your last structured reply.
Four decisions decide how often argument generation succeeds:
- Required versus optional. Every optional property is an open decision the model can get wrong. Mark required what your handler cannot run without (
customer_id,query), and let the rest default in your code. - Enums for closed sets.
"status": "completed"is not in your database vocabulary and will not match anything.["pending", "shipped", "delivered", "cancelled"]makes the wrong strings unsamplable in strict mode. - A description per property. These are read mid-generation.
"customer_id"with no description invites88213(the order number) instead ofc_88213; the example in the description removes the ambiguity. - One stable shape. Deeply nested parameter objects and
anyOfalternatives multiply failure points the same way they do in output schemas.
Model-generated arguments are untrusted input, exactly like a request from a browser:
import json
from jsonschema import validate, ValidationError
def parse_args(name: str, raw: str) -> dict:
args = json.loads(raw) # the model emitted a string
validate(instance=args, schema=SCHEMAS[name]) # raises ValidationError on drift
return args
model arguments: '{"customer_id": "c_88213", "status": "completed"}'
ValidationError: 'query' is a required property
Catch that error and return it as structured feedback for the next round rather than raising — the loop in Running Multi-Step Tool Flows needs a payload it can put back in the transcript so the model can add the missing field itself.
Quiz
1. What role does a tool description play in a function-calling request?
2. What vocabulary do tool parameter schemas use?
3. call.function.arguments arrives as '{"customer_id": "c_88213"}'. Correct handling?
4. Selection accuracy drops once 30+ tools sit behind one request. Which change helps most?
Flashcards
Question
Who reads a tool name and description, and when?
Click to reveal answer
Answer
The model, during selection. Write them as instructions including exclusions - not as human-facing documentation.
Question
How do tool parameter schemas relate to structured-output schemas?
Click to reveal answer
Answer
Identical JSON Schema vocabulary - types, enums, required, additionalProperties - but they shape invented arguments rather than replies.
Question
What exactly arrives in function.arguments?
Click to reveal answer
Answer
A JSON-encoded string, not a dict. json.loads it, validate against the schema, and treat the result as untrusted input.
Question
What degrades as the tool list grows, and what is the fix?
Click to reveal answer
Answer
Selection accuracy. Prune the list per request, merge near-duplicates into one parameterized tool, and log chosen versus available alternatives.
Revision Notes
Key Takeaways
- 1. The model reads names and descriptions during selection, so they must state triggers and exclusions
- 2. Parameters share the JSON Schema vocabulary with structured outputs; arguments arrive as a JSON string
- 3. Required fields and enums are the two cheapest reliability wins in a parameter schema
- 4. Selection accuracy falls as the menu grows; prune per request and merge near-duplicates
Interview Tips
- • Show a real tool definition and explain each part: what the model reads and what your code enforces
- • Explain how you validate model-generated arguments - untrusted input, same class as a browser request
- • Give the scale at which tool menus start failing and what you did about it
- • Describe when a schema-bound JSON response beats a tool call, and why
Cheat Sheet
Tool Definition Basics — Cheat Sheet
Anatomy of one tool
{"type": "function",
"function": {
"name": "search_orders", # verb_noun, lowercase, unambiguous
"description": "When to use it ... and when NOT to",
"parameters": {
"type": "object",
"properties": {"customer_id": {"type": "string",
"description": "e.g. c_88213"},
"status": {"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"]}},
"required": ["customer_id"],
"additionalProperties": False}}}
Who reads what
| Part | Reader | Wrong → |
|---|---|---|
| name | model (selection) | picks the neighbor tool |
| description | model (selection) | never called / called wrongly |
| parameters | model (fills), your code (validates) | malformed arguments, retry rounds |
Argument rules
function.argumentsarrives as a string →json.loads, then validate against the schema- Treat generated arguments as untrusted input, like any HTTP request
- Required = what the handler cannot run without; everything else defaults in code
- Enums for every closed set; a description with an example on every ID-like field
- One flat shape; deep nesting and
anyOfcost reliability the same way output schemas do
Menu size
- 3–5 focused tools: reliable. 30+ in one list: the model guesses from names.
- Merge near-duplicates into one parameterized tool; prune the list per request
- Log chosen tool + available alternatives to catch systematic mis-selection
Not every structured result needs a tool: two short fields with no external data → schema-bound JSON output instead.
Continue Learning
Before You Start
- No prerequisites listed.