AnyLearn
All lessons
AIadvanced

Tool Use Patterns: Schema Design, Structured Output, and Validation Loops

A deep dive into designing tool schemas that LLMs actually call correctly — covering parameter naming, description quality, structured output via response schemas, output parsing, and error message design that drives self-correction.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 14

Why Tool Schema Design Is a First-Class Problem

Every model that supports tool use (Anthropic's Claude, OpenAI's GPT-4o, Google's Gemini) parses your JSON schema at inference time and uses it to decide which tool to call, when to call it, and what arguments to supply. A poorly designed schema doesn't crash immediately — it fails silently: the model calls the wrong tool, omits required arguments, hallucinates enum values, or passes a string where you wanted an integer.

The practical consequence is that schema design is a core engineering discipline, not an afterthought. The description fields, parameter names, required vs. optional split, and enum constraints all function as a prompt the model reads. Treat them with the same care you'd give a system prompt.

This lesson covers the full lifecycle: schema design principles, structured output via response_format, output parsing, and validation loops that teach the model to self-correct.

Full lesson text

All 14 steps on one page — for reading, reference, and search.

Show

1. Why Tool Schema Design Is a First-Class Problem

Every model that supports tool use (Anthropic's Claude, OpenAI's GPT-4o, Google's Gemini) parses your JSON schema at inference time and uses it to decide which tool to call, when to call it, and what arguments to supply. A poorly designed schema doesn't crash immediately — it fails silently: the model calls the wrong tool, omits required arguments, hallucinates enum values, or passes a string where you wanted an integer.

The practical consequence is that schema design is a core engineering discipline, not an afterthought. The description fields, parameter names, required vs. optional split, and enum constraints all function as a prompt the model reads. Treat them with the same care you'd give a system prompt.

This lesson covers the full lifecycle: schema design principles, structured output via response_format, output parsing, and validation loops that teach the model to self-correct.

2. The Anatomy of a Tool Definition

A tool definition has three moving parts: name, description, and parameters (a JSON Schema object). Each part does different work.

{
  "name": "search_orders",
  "description": "Look up orders for a customer by their email address. Use this when the user asks about order status, past purchases, or shipping. Do NOT use for product availability queries.",
  "parameters": {
    "type": "object",
    "properties": {
      "customer_email": {
        "type": "string",
        "description": "The customer's email address, exactly as provided by the user."
      },
      "status_filter": {
        "type": "string",
        "enum": ["all", "pending", "shipped", "delivered", "cancelled"],
        "description": "Filter results to a specific order status. Use 'all' when the user hasn't specified."
      }
    },
    "required": ["customer_email"]
  }
}
  • name — The model sees this as the primary signal for routing. Use snake_case verbs: get_, search_, create_, update_.
  • description — Explains intent AND scope. Include negative examples (Do NOT use for...) to prevent misrouting.
  • parameters.description — Tells the model how to populate each argument from the conversation.

3. Description Quality: The Biggest Lever You Have

The single biggest predictor of correct tool calls is description quality. Models use description text the same way they use system prompt instructions — it directly steers their reasoning.

What makes a good description:

  • State the purpose, not just the syntax: "Returns the closing price for a stock ticker on a given date" beats "Gets stock data".
  • Include when to call and when not to call: models faced with two similar tools (e.g., search_products vs. lookup_inventory) will misroute without explicit disambiguation.
  • For string parameters, describe the format: "ISO-8601 date string, e.g. '2024-03-15'".
  • For numeric parameters, include units: "Duration in seconds, not milliseconds".

Common mistakes:

  • Leaving description blank on parameters (forces the model to guess from the name alone).
  • Describing the implementation rather than the contract: "calls /api/v2/orders endpoint" tells the model nothing useful.
  • Using the same wording for two tools that handle adjacent domains.

Heads up: Description text counts against your context window. Keep each field tight — 1-2 sentences. Don't write essays.

4. Parameter Names as Semantic Signal

The model tokenizes your parameter names and reasons about them. A name like q tells the model almost nothing; search_query tells it everything. Prefer verbose, domain-accurate names over brevity.

Good vs. bad parameter names:

AmbiguousClear
qsearch_query
idorder_id
tscreated_at_utc
nmax_results
typedocument_type

Naming conventions also matter for array items. If a parameter is tags: string[], the model infers each element is a tag string. If you name it data: string[], it may pack arbitrary content into it.

For nested objects, flatten where possible. The model handles flat schemas more reliably than deeply nested ones. Instead of:

"address": { "street": "...", "city": "...", "zip": "..." }

prefer:

"street_address": "...",
"city": "...",
"postal_code": "..."

Flattening reduces the chance the model omits an entire nested branch.

5. Tool Call Lifecycle

flowchart TD
  A["User message"] --> B["Model reasons over tools + context"]
  B --> C["Model emits tool_call JSON"]
  C --> D["Host validates schema"]
  D --> E["Schema valid?"]
  E -- "Yes" --> F["Execute tool function"]
  E -- "No" --> G["Return structured error to model"]
  G --> B
  F --> H["Return tool_result to model"]
  H --> I["Model synthesizes final response"]

6. Structured Output via Response Schemas

When you need the model's final reply (not a tool call) to be machine-parseable, you can constrain it with a response schema. OpenAI calls this response_format: { type: 'json_schema', json_schema: { ... } }; Anthropic achieves the same via a JSON-producing tool the model is forced to call.

Anthropic pattern — force a single tool call:

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=[{
        "name": "extract_entities",
        "description": "Extract named entities from the text.",
        "input_schema": {
            "type": "object",
            "properties": {
                "people": {"type": "array", "items": {"type": "string"}},
                "organizations": {"type": "array", "items": {"type": "string"}},
                "locations": {"type": "array", "items": {"type": "string"}}
            },
            "required": ["people", "organizations", "locations"]
        }
    }],
    tool_choice={"type": "tool", "name": "extract_entities"},
    messages=[{"role": "user", "content": "Elon Musk and Sam Altman met in San Francisco."}]
)

entities = response.content[0].input

Setting tool_choice to a specific tool name guarantees the model calls it. The input field of the resulting ToolUseBlock is already a parsed dict — no json.loads needed.

7. Output Parsing Without Brittle Hacks

Even with structured output, production code must handle edge cases. The model might return null where you expected a string, or an integer where you expected an enum. Parse defensively.

from pydantic import BaseModel, ValidationError
from typing import Literal

class OrderQuery(BaseModel):
    customer_email: str
    status_filter: Literal["all", "pending", "shipped", "delivered", "cancelled"] = "all"

def parse_tool_input(raw: dict) -> OrderQuery | None:
    try:
        return OrderQuery.model_validate(raw)
    except ValidationError as e:
        # Log the full error; return None to trigger a retry
        print(f"Tool input invalid: {e.json()}")
        return None

Parsing hierarchy (most to least reliable):

  1. tool_choice forced call → model writes into a schema-validated input dict.
  2. Response schema (json_schema mode) → JSON guaranteed, but field values may still be wrong types.
  3. Markdown extraction (json.loads(text.split('```json')[1].split('```')[0])) → fragile; only use as last resort.

Heads up: Pydantic v2's model_validate raises ValidationError with field-level detail. Feed that detail back to the model — see the next step.

8. Validation Loops and Self-Correction

The most robust pattern for tool-using agents is a validation loop: run the tool call, validate the output, and if validation fails, send a structured error back to the model as a tool_result so it can retry.

def run_with_retry(client, tools, messages, max_retries=3):
    for attempt in range(max_retries):
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )
        block = response.content[0]
        if block.type != "tool_use":
            return None  # Unexpected text response

        parsed = parse_tool_input(block.input)
        if parsed:
            return parsed  # Success

        # Append the failed call + a corrective error result
        messages = messages + [
            {"role": "assistant", "content": response.content},
            {
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": f"Validation failed: 'status_filter' must be one of [all, pending, shipped, delivered, cancelled]. You passed '{block.input.get('status_filter')}'. Please retry.",
                    "is_error": True
                }]
            }
        ]
    return None

Three retries are usually enough. If the model still fails after three attempts, the schema or the prompt has a deeper problem.

9. Error Message Design That Drives Self-Correction

How you phrase validation errors determines whether the model can fix itself. Vague errors produce vague corrections.

Ineffective error:

"Invalid input."

Effective error:

"'duration_seconds' must be a positive integer. You passed '5m' (a string). Convert the user's time expression to seconds before calling this tool."

Design errors with three components:

  1. What was wrong — identify the field and the rule that failed.
  2. What you received — echo back the bad value so the model knows what to correct.
  3. How to fix it — give explicit corrective instruction, not a generic hint.

This matters because the model treats your error message as a new prompt. Treat it like one: write it to unambiguously produce the corrected call.

Enum mismatches are the most common failure. If the model passes "in_transit" but the enum only includes "shipped", your error should say: "'in_transit' is not a valid status. Use 'shipped' for orders that have left the warehouse." That one sentence closes the conceptual gap.

10. Required vs. Optional: The Minimalism Rule

Mark a parameter required only when the tool genuinely cannot run without it. Every required field is a commitment the model must fulfill — more required fields means more opportunities for the model to stall or hallucinate values.

Principle: prefer sensible defaults over required fields.

{
  "page_size": {
    "type": "integer",
    "description": "Number of results per page. Defaults to 20 on the server if omitted.",
    "minimum": 1,
    "maximum": 100
  }
}

If page_size has a server-side default, don't require it. The model will omit it when the user doesn't specify a preference — which is correct behavior.

Use JSON Schema constraints to reduce hallucination surface:

  • "minimum" / "maximum" on integers prevents the model from passing -1 or 99999.
  • "minLength" / "maxLength" on strings helps with IDs.
  • "pattern" with a regex (e.g., "^[A-Z]{2}-\\d{6}$") tightly constrains order number formats — models respect these.
  • "enum" is the strongest constraint; use it whenever the valid set is small and closed.

Avoid "type": "object" with no further schema — it's an open invitation for the model to pack arbitrary structure in.

11. Multi-Tool Routing and Tool Choice Modes

When you expose multiple tools, the model must choose. Both Anthropic and OpenAI offer a tool_choice parameter with three modes:

ModeAnthropicOpenAIBehavior
Auto{"type": "auto"}"auto"Model decides whether to call a tool
Forced any{"type": "any"}"required"Must call at least one tool
Forced specific{"type": "tool", "name": "..."}{"type": "function", "function": {"name": "..."}}Must call this exact tool
None{"type": "none"}"none"Must not call any tool

For deterministic workflows — classification, extraction, structured output generation — always force a specific tool. Reserve auto for conversational agents where you genuinely want the model to decide.

When running with auto and more than ~8 tools, routing accuracy drops measurably. Group related tools behind a single dispatcher tool, or split your agent into focused sub-agents, each with a small, coherent toolset. The model is most reliable when each tool is clearly distinguishable from every other tool in the same call.

12. Parallel Tool Calls and Ordering Guarantees

Modern models can emit multiple tool calls in a single response turn. Anthropic's Claude may return content blocks like [ToolUseBlock, ToolUseBlock] for independent lookups.

for block in response.content:
    if block.type == "tool_use":
        # Execute each tool; they may run concurrently
        results[block.id] = execute_tool(block.name, block.input)

# Return all results in one user message
tool_results = [
    {"type": "tool_result", "tool_use_id": tid, "content": str(res)}
    for tid, res in results.items()
]
messages.append({"role": "user", "content": tool_results})

Two gotchas:

  1. Order doesn't imply dependency. Two parallel calls may fetch data the model will combine — don't assume call A must finish before call B.
  2. You must return ALL tool results before the model can continue. Anthropic's API will return a 400 if you send the next turn without including tool_result blocks for every tool_use block in the prior assistant turn. Track by tool_use_id.

For operations with side effects (writes, emails, payments), disable parallel calls by setting tool_choice to force one specific tool per turn, or architect your tools so that order-sensitive operations are gated behind a sequencing check.

13. Schema Quality vs. Call Accuracy

flowchart LR
  A["Blank descriptions"] --> B["Low routing accuracy"]
  C["Named parameters + descriptions"] --> D["Medium routing accuracy"]
  E["Descriptions + enums + examples + negative scope"] --> F["High routing accuracy"]
  G["Validation loop + corrective errors"] --> H["Near-deterministic output"]

14. Putting It Together: A Production Checklist

Before shipping a tool-using agent to production, run through this checklist:

Schema:

  • Every parameter has a non-empty description with format hints.
  • Enums cover all valid values; invalid inputs are excluded by pattern or minimum/maximum.
  • required list contains only truly mandatory fields.
  • Nested objects are flattened where possible.
  • Similar tools have explicit disambiguation in their description.

Tool choice:

  • Deterministic workflows use tool_choice: specific_tool.
  • Agent workflows cap the toolset at ≤8 tools per invocation.

Parsing and validation:

  • All tool inputs are validated via Pydantic or equivalent before execution.
  • Validation errors include field name, bad value, and corrective instruction.
  • Retry loop is capped (2-3 attempts) with fallback behavior.

Parallel calls:

  • All tool_use block IDs are echoed back in tool_result before the next turn.
  • Side-effecting tools are sequenced, not parallelized.

Schema design is the highest-leverage investment you can make in a tool-using system. A well-designed schema reduces prompt engineering overhead, improves reliability without re-training, and makes validation loops converge in fewer iterations.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. A model is calling the wrong tool between `search_products` and `lookup_inventory`. What is the most effective single fix?
    • Add more required parameters to each tool
    • Include explicit 'Do NOT use for...' scope statements in each tool's description
    • Rename the tools to have no overlapping words
    • Force tool_choice to 'any' so the model must pick one
  2. On Anthropic's API, what happens if you send the next conversation turn without returning tool_result blocks for every tool_use block in the prior assistant turn?
    • The model silently ignores the missing results and continues
    • The API returns a 400 error
    • The model re-calls the same tools automatically
    • The response is truncated at max_tokens
  3. Which tool_choice mode guarantees the model calls one specific named tool and no other?
    • auto
    • any / required
    • The tool name specified directly in tool_choice
    • none
  4. A model keeps passing '5m' (a string) for a parameter that expects an integer number of seconds. Which error message will best drive self-correction?
    • 'Invalid input for duration_seconds.'
    • 'duration_seconds must be an integer.'
    • 'duration_seconds must be a positive integer. You passed 5m (a string). Convert the time expression to seconds before calling this tool.'
    • 'Please check the parameter types in the schema.'
  5. You have 12 tools in a single agent invocation with tool_choice set to auto. What is the recommended fix for declining routing accuracy?
    • Increase max_tokens so the model has more room to reason
    • Remove all parameter descriptions to reduce schema size
    • Group related tools into a dispatcher or split into focused sub-agents with smaller toolsets
    • Switch all tools to required parameters

Related lessons

AI
beginner

Loop engineering basics: the agent control loop

How LLM agents actually run: the iterative prompt-action-observation loop, the ReAct shape, the smallest tool-calling loop in twelve lines, why you always stack three termination layers, what the model sees on iteration N, and when a single prompt is the better answer.

8 steps·~12 min
AI
advanced

Loop engineering: verification, orchestration, and anti-patterns

Make loops trustworthy. Adversarial verification panels, sub-agent orchestration, loop-until-dry for unbounded discovery, loop-until-budget for paid depth, multi-modal sweeps with diverse prompts, eval-driven cap selection, and the five anti-patterns — silent caps, infinite plans, drift, premature termination, thrashing — that ship to prod more than they should.

9 steps·~14 min
AI
intermediate

Loop engineering in production: state, errors, and budgets

Once an agent loop runs past a few iterations the failure surface shifts: state bloats, tool calls fail in three distinct ways, retries multiply if you stack them wrong, and unbounded budgets rack up four-figure bills overnight. The state, errors, and budgets you need to make the loop survive contact with reality.

8 steps·~12 min
AI
intermediate

LLM Observability with OpenTelemetry: GenAI Semantic Conventions

Master the OTel GenAI semantic conventions — gen_ai.* attributes, span structure for prompts/completions/tools, sampling strategies, and cost attribution — and understand why standardizing across LangSmith, Phoenix, Datadog, and Grafana matters for production AI systems.

13 steps·~20 min·audio