A tool that works when you call it can still fail when a model calls it. Good tool design is API design for a reader that takes every word literally.
Estimated Reading Time : 8m
The reader is different
When you design an API for other engineers, you can count on them reading the docs once, building a mental model, and filling gaps with common sense.
A model reads your tool definitions fresh on every request. It has nothing but the name, the description and the schema. It won’t open your source code, and it won’t ask a colleague. If something isn’t written down, it doesn’t exist.
That changes what “well designed” means.
Say when, not just what
Compare these two descriptions:
Get the status of a deployment.
Get the rollout status of a Kubernetes Deployment: desired, updated and
available replicas, plus any failing conditions. Call this before scaling
or restarting a deployment, and whenever the user asks if a rollout finished.
The first describes the tool. The second tells the model when to reach for it. Recent models are fairly conservative about calling tools, and explicit trigger conditions make a measurable difference in how reliably they get called.
The same goes for names. get_deployment_status beats status. search_runbooks beats search. Once you have more than a few tools, vague names start to collide.
Fewer, higher-level tools
It’s tempting to wrap your CLI one command at a time: kubectl_get, kubectl_describe, kubectl_logs, kubectl_scale. Now the model has to plan a sequence of low-level calls, and every step is a chance to go wrong.
Design around tasks instead:
diagnose_deployment(namespace, name)
-> status, recent events, last 50 log lines from failing pods
One call returns everything needed to answer “why is this broken?” The model spends its effort reasoning about the answer, not orchestrating the lookup.
A smaller tool set also helps the model choose. Every tool you add is one more option to weigh on every turn.
Make the schema do the work
Every constraint you put in the schema is one the model doesn’t have to infer:
- Use
enumfor fixed sets of values - Mark only truly required fields as
required - Put a description on every property, with an example where the format matters
- Prefer specific types (
integerwith a range) over free-form strings
With the Anthropic API, you can go further and set strict on the tool. Strict tools guarantee the model’s input validates against your schema, so you never receive a missing field or a wrong type. Strict schemas need additionalProperties: false and an explicit required list:
scaleTool := anthropic.ToolParam{
Name: "scale_deployment",
Description: anthropic.String(
"Set the replica count of a Kubernetes Deployment. " +
"Only call this after get_deployment_status, and only when the user asked to scale."),
Strict: anthropic.Bool(true),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"namespace": map[string]any{"type": "string"},
"name": map[string]any{"type": "string"},
"replicas": map[string]any{
"type": "integer",
"description": "Desired replica count, between 0 and 20",
},
},
Required: []string{"namespace", "name", "replicas"},
ExtraFields: map[string]any{"additionalProperties": false},
},
}
Strict mode guarantees the shape of the input. It doesn’t make the values sensible. You still check that replicas is within your limits before acting on it.
Return what the model needs, and no more
Tool results go straight into the model’s context window. Every token you return is a token the model has to read, and you pay for it on every following turn.
- Trim. Return the five fields that matter, not the full 300-line object.
- Summarize. “143 pods Running, 2 in CrashLoopBackOff: worker-5c2a, worker-8d1e” beats the full pod list.
- Paginate. If a result can be large, cap it and say how to get more:
"showing 50 of 1,204 lines; call again with offset=50". - Prefer stable, readable formats. Plain text or compact JSON both work. Consistency matters more than the choice.
Errors that teach
An error result is an instruction for the model’s next attempt. Write it that way.
error: not found
deployment "api" not found in namespace "default".
Deployments in "default": api-gateway, billing, worker.
The second message lets the model fix its own mistake in one step. Return errors as tool results with is_error set, rather than failing the request, so the model gets that chance at all.
Be explicit about side effects
The model can’t see the difference between a read and a write unless you tell it. For anything with side effects:
- Say so in the description: “This restarts every pod in the deployment.”
- Offer a
dry_runparameter that reports what would happen - Make operations idempotent where you can. Models retry, and so do networks.
- Gate destructive operations behind a human approval step in your loop
Don’t count on forcing a tool call
Older APIs let you force the model to call a specific tool with tool_choice. On the newest Claude models, including Claude Opus 5.5, forcing a call with tool_choice set to any or a specific tool returns a 400 error.
The replacement is straightforward:
- Leave
tool_choiceonauto - Say in the prompt which tool you expect: “Use get_deployment_status to answer.”
- Set
stricton the tool so the arguments are schema-valid when the call happens - Check that a call was made, and retry if it wasn’t
If you were only forcing a tool call to get JSON back, use structured outputs instead. That’s what they’re for.
Design for parallel calls
Models can request several tools in one turn, for example checking three deployments at once. That only works if your tools are independent. If tool_b silently depends on state that tool_a sets, parallel calls will race.
Keep tools stateless where you can. Where you can’t, make the dependency explicit in the description (“requires a session_id from open_session”).
A checklist
Before I ship a tool now, I check:
- Would the description alone tell a new teammate when to use it?
- Is the name specific enough to never be confused with another tool?
- Does the schema use enums, required fields and descriptions?
- Is the result small and focused?
- Does every error say how to fix it?
- Are side effects stated, and destructive ones gated?
Wrapping up
The model is a literal reader with no context beyond what you give it. Tell it when to use each tool, give it fewer and higher-level tools, let the schema enforce what it can, keep results small, and write errors that teach. Most “the model is bad at using my tool” problems turn out to be tool design problems.