Tools and Function Calling: Giving a Model Real Capabilities

Learn how to define tools that AI models can actually use — and why the schema and description you write matters far more than the function itself.

The Moment I Realized I Was Doing It Wrong

When I first started wiring up tools to AI models, I made a very classic beginner mistake. I spent hours perfecting the Python function — clean code, good error handling, nice return values — and then wrote something like "description": "gets weather" in the schema. Then I wondered why the model kept calling the wrong tool or passing garbage parameters.

Here's the thing nobody tells you upfront: the model never sees your code. It only sees the schema. The description you write is the only thing the model has to understand what a tool does, when to use it, and how to use it correctly. Once that clicked for me, everything changed.

What Function Calling Actually Is

Let's back up. By default, an AI model like Claude or GPT-4 generates text. That's it. It can't look up your database, call an API, or check what time it is. It's a brilliant guesser that works entirely from what's in its training data and the current context window.

Function calling (sometimes called "tool use") changes that. You give the model a list of tools it's allowed to call, described in a structured schema. When the model thinks a tool is appropriate, it outputs a structured call — basically a JSON object saying "call this function with these arguments." Your code then actually runs the function and feeds the result back to the model, which uses it to finish responding.

So the flow looks like this:

flow
# Simplified tool call lifecycle
1. You send: message + list of available tools (schemas)
2. Model responds: tool_call { name: "get_weather", args: { city: "London" } }
3. Your code runs: get_weather(city="London") → { temp: 14, condition: "cloudy" }
4. You send: tool result back to the model
5. Model responds: "It's 14°C and cloudy in London right now."

The model is the decision-maker. You're the executor. And the schema is the instruction manual the model reads to decide when and how to call each tool.

Anatomy of a Tool Schema

Here's a minimal tool definition in the format Claude uses (the Anthropic API). This is what actually gets sent to the model:

python
# A well-defined tool schema
tool = {
"name": "get_weather",
"description": "Retrieves current weather conditions for a given city.
Use this when the user asks about current weather, temperature,
or conditions in a specific location. Do NOT use for historical
weather or forecasts beyond today.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'London' or 'New York'. Do not include country codes."
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default to celsius unless user specifies otherwise."
}
},
"required": ["city"]
}
}

Notice how much text is in that description. It's not just "gets weather" — it explains when to use it, when NOT to use it, and how to format the inputs. Every word there is guidance the model will actually follow.

Why Descriptions Matter More Than You Think

The model is making a reasoning decision every time it decides to call a tool. It's essentially asking itself: "Does what the user wants match what this tool does?" Your description is the only input to that reasoning.

This means vague descriptions cause real problems:

comparison
# BAD: Vague, model will guess wrong
"description": "searches for things"

# BAD: Technically accurate but useless for reasoning
"description": "calls the search API"

# GOOD: Tells the model exactly when and how to use it
"description": "Searches a product catalog by keyword or SKU.
Use when the user wants to find products, check availability,
or look up pricing. Returns up to 10 results sorted by relevance.
Not suitable for order history or customer account lookups."

Good descriptions answer three questions: What does this tool do? When should you use it? When should you NOT use it? That last part is especially important when you have multiple tools that might seem similar.

Treat Descriptions Like Prompts

Writing a tool description is exactly like writing a prompt. The model reads it and uses it to reason. Spend as much time on descriptions as you do on your system prompt — it pays off.

Parameter Descriptions Are Just as Important

It's easy to write a decent top-level description and then get lazy with the individual parameters. Don't. Each parameter description tells the model how to extract the right value from the user's message.

Compare these two:

json
// BAD: Model will guess at format
"start_date": {
"type": "string",
"description": "The start date"
}

// GOOD: Model knows exactly what format to use
"start_date": {
"type": "string",
"description": "The start date in ISO 8601 format (YYYY-MM-DD).
If the user says 'next Monday', calculate the actual date.
If no date is mentioned, use today's date."
}

That extra sentence — "If the user says 'next Monday', calculate the actual date" — is the difference between a tool that works and one that constantly needs fallback handling. You're encoding your business logic into the description itself.

Handling Multiple Tools Without Chaos

Things get interesting when you give the model several tools. Now it has to pick the right one, or decide to use multiple in sequence. This is where name clarity and description contrast really matter.

If you have two tools called search_products and search_orders, the names already do some work. But your descriptions should explicitly say when NOT to use each one:

python
# Tool 1
"description": "Search the product catalog for items available for purchase.
Use for product discovery, pricing, and availability.
Do NOT use for order history or tracking — use search_orders for that."

# Tool 2
"description": "Search a customer's order history by order ID or date range.
Use for tracking, returns, and past purchase questions.
Do NOT use for browsing available products — use search_products for that."

Cross-referencing like this helps the model build a mental map of which tool belongs to which job. I've found that when models pick the wrong tool, it's almost always because the descriptions were too similar or didn't draw a clear enough boundary.

Keep Your Tool List Lean

Don't pass every tool you have on every request. Give the model only the tools relevant to the current task. More tools = more cognitive load = more wrong choices. Load context-appropriate tools dynamically if you can.

Required vs Optional Parameters — Use Them Intentionally

The required array in your schema is more than validation — it's a signal to the model about what it absolutely needs before calling. If a parameter is in required, the model will try to extract it from context or ask the user for it before making the call.

So be deliberate. If your function can run without a parameter (maybe it has a sensible default), make it optional and describe the default behavior. If you make everything required, the model will halt and ask for information it could have inferred — which makes for a frustrating user experience.

json
// Only "query" is required — model won't ask for limit
{
"properties": {
"query": {
"type": "string",
"description": "The search term to look up"
},
"limit": {
"type": "integer",
"description": "Max results to return. Defaults to 5 if not specified."
}
},
"required": ["query"]
}

What to Return (And How to Format It)

Your schema defines inputs, but the output matters too. The model reads your tool result and uses it to continue its response. If you return a wall of raw JSON with 40 fields, the model will try to process all of it — which wastes context and sometimes causes it to fixate on irrelevant data.

Return only what the model needs. If you're fetching a product, don't return the entire database row — return the fields that are relevant to what the user asked. Think of your tool result as another input to the model, not just a callback.

Return Errors Descriptively

When your tool fails, return a clear error message in the result — don't throw an exception that breaks the flow. Something like { "error": "City not found. Ask the user to check the spelling." } gives the model what it needs to recover gracefully.

The Real Skill Is Schema Design, Not Code

Here's my honest take after building a bunch of these: getting the underlying function to work is usually the easy part. Writing a schema that reliably guides the model to call it correctly at the right time with the right arguments — that's where the actual work happens.

Think of it like writing an API contract for a very capable but very literal contractor. They'll do exactly what the contract says. If the contract is vague, they'll make their best guess — which may not be what you wanted. The more precise and complete your schema descriptions, the more reliably the model will behave like it read your mind.

Start simple: one tool, a really thorough description, descriptive parameter fields, and clear required/optional separation. Test it by asking the model things that should and shouldn't trigger the tool. Iterate on the descriptions, not the code. You'll be surprised how much behavior you can shape without touching a single line of your function logic.

Keep going

More tutorials in this category, or explore the full field guide.

More Claude Code Tutorials Official Docs ↗