Skip to content

function_calling_reliability

LLM function calls fail in predictable ways

Hallucinated parameters, missing tools, type mismatches, unguarded destructive operations. Here are the six failure modes, how to prevent each one, and what the retries are quietly costing you.

the_cost_of_retries

Every function-calling error triggers a retry. A single retry is one more LLM turn, doubling the cost for that request.

Base cost / call$0.0500
Avg retries (bad schema)1.5
With retries$0.1250

Fix the schema, eliminate the retries, save ~30% per call. On 100K calls/mo, that’s roughly $1,500/month.

six_failure_modes

Hallucinated Parameters

mode 1

LLM invokes a function with parameters that don't match the schema (wrong type, missing required field, invented parameter).

root_cause

Unclear parameter descriptions, too many optional params, or the schema itself is ambiguous. Model falls back to guessing.

Avg retries
Cost multiple
Typical timeout5000ms
prevention
  • Describe parameters in plain English. Good: 'user_id: integer ID from the users table'. Bad: 'id: unique identifier'.
  • Provide enums for categorical params. Instead of description, let the schema define allowed values.
  • Use examples in the schema. Add 'example: 12345' so the model sees a concrete value.
  • Mark required fields explicitly. Only truly optional params should be optional.
  • Limit param count. If a function has 10+ parameters, split it into 2-3 focused functions.
detection

Parse LLM function-call output before execution. Validate params against schema. Log mismatches.

✕ bad
{
  "name": "create_user",
  "parameters": {
    "type": "object",
    "properties": {
      "email": { "type": "string" },
      "id": { "type": "integer" },
      "metadata": { "type": "object" }
    }
  }
}
✓ good
{
  "name": "create_user",
  "parameters": {
    "type": "object",
    "properties": {
      "email": {
        "type": "string",
        "description": "User email address (e.g., alice@example.com). Must be valid email format."
      },
      "role": {
        "type": "string",
        "enum": ["admin", "user", "guest"],
        "description": "User role (required). One of: admin, user, guest."
      },
      "metadata": {
        "type": "object",
        "description": "Optional metadata (e.g., signup_source, referrer). Omit if not needed."
      }
    },
    "required": ["email", "role"]
  }
}

Requests Non-Existent Tool

mode 2

LLM tries to call a function that isn't available in the schema (typo, wrong name, or tool was removed).

root_cause

Tool definitions changed but the model's context wasn't updated. Or the model is hallucinating a tool.

Avg retries
Cost multiple
Typical timeout10000ms
prevention
  • Version your tool schema. If you remove or rename tools, re-prompt the model with the new list.
  • Use unique, clear tool names. 'search_documents' not 'search'. 'fetch_user_by_id' not 'get_user'.
  • Provide tool examples. Include 1-2 example function calls in your prompt: 'Example: search_documents(query="LLM pricing")'.
  • List all tools upfront. Put the schema at the top of your prompt so the model doesn't invent tools.
detection

When the LLM requests a tool not in your schema, log it. Aggregate these to catch hallucinated tools early.

✕ bad
Available tools: search, get, post, delete
✓ good
Available tools:
1. search_documents(query: string, limit: int=10) -> List[Document]
2. fetch_user_profile(user_id: int) -> UserProfile
3. create_ticket(title: string, description: string, priority: string) -> TicketId

Example calls:
- search_documents(query="LLM cost optimization")
- fetch_user_profile(user_id=42)
- create_ticket(title="Bug: search fails on special chars", description="...", priority="high")

Wrong Parameter Type

mode 3

LLM calls a function with the right name and parameters, but parameter types don't match (string instead of integer, object instead of array).

root_cause

Parameter type descriptions are vague or the schema allows coercion (e.g., accepting both int and string for a count).

Avg retries
Cost multiple1.5×
Typical timeout3000ms
prevention
  • Explicit type declarations. Use JSON schema types strictly: 'type': 'integer', not 'number'.
  • No loose unions. Don't use {'type': ['string', 'integer']} unless truly intentional.
  • Numeric bounds. For integers, set 'minimum' and 'maximum': {type: 'integer', minimum: 1, maximum: 100}.
  • Array vs object. If you need a list, declare 'type': 'array' with 'items'. Don't rely on the model to infer.
detection

Type validation at function invocation. Reject or coerce strict type mismatches.

✕ bad
{
  "name": "create_order",
  "parameters": {
    "quantity": { "description": "quantity of items" },
    "tags": { "description": "labels (can be string or list)" }
  }
}
✓ good
{
  "name": "create_order",
  "parameters": {
    "quantity": {
      "type": "integer",
      "minimum": 1,
      "maximum": 1000,
      "description": "Number of items (integer between 1 and 1000)"
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "maxItems": 10,
      "description": "List of tags (array of strings, max 10 items)"
    }
  }
}

Missing Required Parameter

mode 4

LLM invokes a function but omits a required parameter.

root_cause

Parameter is marked required but has a weak description, or the model didn't understand it's mandatory.

Avg retries
Cost multiple1.5×
Typical timeout3000ms
prevention
  • Emphasize required fields. Mark them in the schema ('required': []) and repeat in the description: 'user_id (REQUIRED)'.
  • Provide defaults strategically. If a param is truly optional, make it optional in the schema and provide a sensible default.
  • Use examples with all required params. Show a full, valid call in your prompt.
  • Limit required params. If a function requires 5+ params, split it or make some optional with defaults.
detection

Validate required params before function execution. Return a clear error message if missing.

✕ bad
{
  "name": "charge_card",
  "parameters": {
    "user_id": { "type": "integer" },
    "amount_cents": { "type": "integer" },
    "card_token": { "type": "string" }
  }
}
✓ good
{
  "name": "charge_card",
  "parameters": {
    "type": "object",
    "properties": {
      "user_id": {
        "type": "integer",
        "description": "User ID (REQUIRED). Example: 42"
      },
      "amount_cents": {
        "type": "integer",
        "minimum": 1,
        "description": "Amount in cents (REQUIRED). Example: 5000 for $50.00"
      },
      "card_token": {
        "type": "string",
        "description": "Tokenized card from payment processor (REQUIRED). Example: tok_visa_4242"
      }
    },
    "required": ["user_id", "amount_cents", "card_token"]
  }
}

Unguarded Destructive Tool

mode 5

LLM can call delete, update, or cancel operations without confirmation or rate limiting. Can cause data loss or financial impact.

root_cause

Tool definitions don't enforce guardrails (no confirmation prompt, no rate limits, no dry-run mode).

Avg retries
Cost multiple
Typical timeout0ms
prevention
  • Require confirmation for destructive ops. Implement a two-phase pattern: 'preview what will be deleted' → 'confirm deletion'.
  • Add dry-run mode. Let the model call delete(user_id=123, dry_run=true) first to see what would happen.
  • Implement rate limiting. Track destructive calls per model per day. Reject if threshold exceeded.
  • Use separate tool names. 'delete_user_draft' vs 'delete_user_permanent'. Make the destructiveness explicit.
  • Log and audit. Every destructive call should log: who (model ID), what (params), when, and result.
detection

Static analysis: flag any 'delete', 'remove', 'cancel' operations without a 'dry_run' param or confirmation step.

✕ bad
{
  "name": "cancel_subscription",
  "parameters": {
    "subscription_id": { "type": "integer" }
  }
}
✓ good
{
  "name": "cancel_subscription",
  "parameters": {
    "type": "object",
    "properties": {
      "subscription_id": {
        "type": "integer",
        "description": "Subscription ID to cancel (REQUIRED)"
      },
      "dry_run": {
        "type": "boolean",
        "default": true,
        "description": "If true, preview what will be canceled without actually canceling. Default: true (safe). Set to false only after confirming with the user."
      },
      "refund_type": {
        "type": "string",
        "enum": ["full", "prorated", "none"],
        "default": "prorated",
        "description": "Refund type (optional, default: prorated)"
      }
    },
    "required": ["subscription_id"]
  }
}

Raw SQL/Command Passthrough

mode 6

LLM can pass raw SQL or shell commands directly to a backend. Model can inject malicious or incorrect commands.

root_cause

Tool allows 'query' or 'command' params without validation, or no parameterized query support.

Avg retries
Cost multiple
Typical timeout0ms
prevention
  • Never accept raw SQL. Use parameterized queries: 'query_users(email_pattern: string)' not 'sql: string'.
  • Allowlist operations. Provide fixed tool names for each operation: 'get_user_by_id', 'list_users_by_email', etc.
  • Validate before execution. If you must accept user input, validate it against a schema before executing.
  • Separate read and write. Provide 'query_*' tools (read-only) and 'mutation_*' tools (write, require confirmation).
  • Use ORMs or stored procedures. Don't expose raw SQL generation to the model.
detection

Static analysis: reject any tool with a 'query', 'command', or 'code' parameter that accepts arbitrary strings.

✕ bad
{
  "name": "run_query",
  "parameters": {
    "sql": {
      "type": "string",
      "description": "SQL query to execute"
    }
  }
}
✓ good
{
  "name": "search_users",
  "parameters": {
    "type": "object",
    "properties": {
      "email_pattern": {
        "type": "string",
        "description": "Email substring to search (e.g., 'alice' will match 'alice@example.com'). Case-insensitive."
      },
      "created_after": {
        "type": "string",
        "format": "date-time",
        "description": "Optional: only users created after this date (ISO 8601, e.g., 2026-01-01T00:00:00Z)"
      }
    },
    "required": ["email_pattern"]
  }
}

testing_strategy

Testing strategy

1. Unit tests (schema)

  • Schema validation: ensure all params have descriptions, required fields are marked, types are strict.
  • Parameter parsing: mock LLM responses with various malformed params. Verify your parser rejects/retries correctly.
  • Tool execution: test each tool with valid + invalid params. Measure retry count + latency.

2. Integration tests (agent)

  • End-to-end agent flow: agent receives a task, calls tools, handles errors, retries. Measure success rate + cost.
  • Failure injection: simulate tool failures (timeout, 500 error, malformed response) and verify agent recovery.
  • Concurrent calls: if agent makes parallel tool calls, test race conditions and state consistency.

3. Empirical tests (real data)

  • Run 100+ real queries on your agent. Track: success rate, cost (USD), retry count, latency per call.
  • Aggregate failures by type (hallucinated params, missing tool, etc.). Use /mcp-audit or similar to identify patterns.
  • A/B test schema changes: does a clearer description reduce retries? Measure before/after on the same 100 queries.

success_metrics

What good looks like

Function-call success rate. Target 95%+. Below 90% means a major schema issue — redo the prevention steps.

Retry count. Target 0.1 retries per call (10% of calls retry once). Above 0.5 means the schema needs work.

Cost impact. Retries typically add 10–50% to agent cost. Measure before and after schema fixes.

End-to-end agent success. If the agent task is “book a meeting,” measure task completion rate. Retries are fine if the task still succeeds.

when_to_escalate

Red flags

  • Success rate below 85%
  • The same error repeating (a pattern)
  • User data corruption
  • Cost spike from retries above 50%

Action

  • Audit the schema with the MCP Tool Auditor
  • Add example calls to the prompt
  • Implement a dry-run for destructive operations
  • Downgrade to a cheaper model if cost is the driver

related

Broader MCP tool failures: the MCP Tool Auditor covers ambiguous tools, weak descriptions, missing parameters, unguarded operations, and raw passthrough.

Cost modeling: Agentic RAG cost — tool-calling errors cascade into multi-turn loops, multiplying cost.

Foundations: the tool-calling glossary term.

Want your tool schemas audited for reliability?

The MCP auditor flags the failure modes; a review tells you which ones are actually firing in production. Book a call, or leave your email and I'll reach out.

Book a call

No spam. You'll get a reply from me.

Prefer proof first? See how this plays out in real case studies →