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.
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 1LLM invokes a function with parameters that don't match the schema (wrong type, missing required field, invented parameter).
Unclear parameter descriptions, too many optional params, or the schema itself is ambiguous. Model falls back to guessing.
- 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.
Parse LLM function-call output before execution. Validate params against schema. Log mismatches.
{
"name": "create_user",
"parameters": {
"type": "object",
"properties": {
"email": { "type": "string" },
"id": { "type": "integer" },
"metadata": { "type": "object" }
}
}
}{
"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 2LLM tries to call a function that isn't available in the schema (typo, wrong name, or tool was removed).
Tool definitions changed but the model's context wasn't updated. Or the model is hallucinating a tool.
- 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.
When the LLM requests a tool not in your schema, log it. Aggregate these to catch hallucinated tools early.
Available tools: search, get, post, delete
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 3LLM calls a function with the right name and parameters, but parameter types don't match (string instead of integer, object instead of array).
Parameter type descriptions are vague or the schema allows coercion (e.g., accepting both int and string for a count).
- 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.
Type validation at function invocation. Reject or coerce strict type mismatches.
{
"name": "create_order",
"parameters": {
"quantity": { "description": "quantity of items" },
"tags": { "description": "labels (can be string or list)" }
}
}{
"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 4LLM invokes a function but omits a required parameter.
Parameter is marked required but has a weak description, or the model didn't understand it's mandatory.
- 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.
Validate required params before function execution. Return a clear error message if missing.
{
"name": "charge_card",
"parameters": {
"user_id": { "type": "integer" },
"amount_cents": { "type": "integer" },
"card_token": { "type": "string" }
}
}{
"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 5LLM can call delete, update, or cancel operations without confirmation or rate limiting. Can cause data loss or financial impact.
Tool definitions don't enforce guardrails (no confirmation prompt, no rate limits, no dry-run mode).
- 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.
Static analysis: flag any 'delete', 'remove', 'cancel' operations without a 'dry_run' param or confirmation step.
{
"name": "cancel_subscription",
"parameters": {
"subscription_id": { "type": "integer" }
}
}{
"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 6LLM can pass raw SQL or shell commands directly to a backend. Model can inject malicious or incorrect commands.
Tool allows 'query' or 'command' params without validation, or no parameterized query support.
- 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.
Static analysis: reject any tool with a 'query', 'command', or 'code' parameter that accepts arbitrary strings.
{
"name": "run_query",
"parameters": {
"sql": {
"type": "string",
"description": "SQL query to execute"
}
}
}{
"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.
Prefer proof first? See how this plays out in real case studies →