ScaledByDesign/Insights
ServicesPricingAboutContact
Book a Call
Scaled By Design

Fractional CTO + execution partner for revenue-critical systems.

Company

  • About
  • Services
  • Contact

Resources

  • Insights
  • Pricing
  • FAQ

Legal

  • Privacy Policy
  • Terms of Service

© 2026 ScaledByDesign. All rights reserved.

contact@scaledbydesign.com

On This Page

The Demo vs. Production GapTool Definition: Be Explicit, Not CleverParameter Validation LayerError Handling: Tell the Model What Went WrongConfirmation for Destructive ActionsTool Call Limits
  1. Insights
  2. AI & Automation
  3. AI Agent Tool Calling Patterns That Actually Work in Production

AI Agent Tool Calling Patterns That Actually Work in Production

May 6, 2026·ScaledByDesign·
aiagentstool-callingfunction-callingproduction

The Demo vs. Production Gap

Every AI agent demo looks the same: ask the agent to book a flight, it calls a tool, returns a confirmation. Clean, simple, impressive. Then you build it for real customers and discover: the agent calls the wrong tool 15% of the time, passes invalid parameters 8% of the time, and when a tool fails, it hallucinates a success message.

Production agents need guardrails that demos don't show. Here's what works.

Tool Definition: Be Explicit, Not Clever

The quality of tool definitions determines whether the agent calls the right tool with the right parameters:

// ✗ Bad tool definition (too vague)
const badTool = {
  name: "search",
  description: "Search for things",
  parameters: {
    query: { type: "string" },
  },
};
 
// ✓ Good tool definition (explicit about everything)
const goodTool = {
  name: "search_orders",
  description: "Search customer orders by order ID, email, or date range. Use this when the customer asks about an existing order, shipment status, or order history. Do NOT use for product searches.",
  parameters: {
    type: "object",
    required: ["search_type"],
    properties: {
      search_type: {
        type: "string",
        enum: ["order_id", "email", "date_range"],
        description: "The type of search to perform",
      },
      order_id: {
        type: "string",
        pattern: "^ORD-[0-9]{5,8}$",
        description: "Order ID in format ORD-XXXXX. Required when search_type is 'order_id'",
      },
      email: {
        type: "string",
        format: "email",
        description: "Customer email. Required when search_type is 'email'",
      },
      date_from: { type: "string", format: "date", description: "Start date (YYYY-MM-DD)" },
      date_to: { type: "string", format: "date", description: "End date (YYYY-MM-DD)" },
    },
  },
};

Key principles: tell the model when to use the tool and when NOT to use it. Use enums to constrain choices. Add format/pattern for validation. Make required fields explicit.

Parameter Validation Layer

Never trust the model's parameter output. Validate everything:

import { z } from "zod";
 
// Define strict schemas for each tool
const toolSchemas = {
  search_orders: z.object({
    search_type: z.enum(["order_id", "email", "date_range"]),
    order_id: z.string().regex(/^ORD-[0-9]{5,8}$/).optional(),
    email: z.string().email().optional(),
    date_from: z.string().date().optional(),
    date_to: z.string().date().optional(),
  }).refine(data => {
    if (data.search_type === "order_id" && !data.order_id) return false;
    if (data.search_type === "email" && !data.email) return false;
    return true;
  }, "Missing required field for search type"),
 
  cancel_order: z.object({
    order_id: z.string().regex(/^ORD-[0-9]{5,8}$/),
    reason: z.string().min(5).max(500),
    refund_type: z.enum(["full", "partial", "store_credit"]),
  }),
};
 
async function executeToolCall(toolName: string, params: unknown) {
  const schema = toolSchemas[toolName];
  if (!schema) throw new ToolError(`Unknown tool: ${toolName}`);
 
  const validated = schema.safeParse(params);
  if (!validated.success) {
    // Return validation error to the model so it can self-correct
    return {
      error: `Invalid parameters: ${validated.error.issues.map(i => i.message).join(", ")}`,
      hint: "Please check the parameter requirements and try again.",
    };
  }
 
  return await tools[toolName](validated.data);
}

Error Handling: Tell the Model What Went Wrong

When a tool call fails, the model needs structured feedback to recover:

async function safeToolExecution(toolName: string, params: unknown) {
  try {
    const result = await executeToolCall(toolName, params);
    return { status: "success", data: result };
  } catch (error) {
    if (error instanceof NotFoundError) {
      return {
        status: "not_found",
        message: `No ${toolName.replace("_", " ")} found with those parameters.`,
        suggestion: "Ask the customer to verify the information and try again.",
      };
    }
    if (error instanceof PermissionError) {
      return {
        status: "unauthorized",
        message: "This action requires customer verification.",
        suggestion: "Ask the customer to verify their identity before proceeding.",
      };
    }
    if (error instanceof RateLimitError) {
      return {
        status: "rate_limited",
        message: "Too many requests. Please wait a moment.",
        suggestion: "Inform the customer there's a brief delay.",
      };
    }
    // Unknown errors — don't expose internals
    return {
      status: "error",
      message: "Unable to complete this action right now.",
      suggestion: "Apologize and offer to escalate to a human agent.",
    };
  }
}

Confirmation for Destructive Actions

Never let the agent execute destructive actions without confirmation:

const DESTRUCTIVE_TOOLS = ["cancel_order", "process_refund", "delete_account"];
 
async function handleToolCall(toolName: string, params: unknown, conversationState: State) {
  if (DESTRUCTIVE_TOOLS.includes(toolName)) {
    if (!conversationState.pendingConfirmation) {
      // Don't execute — ask for confirmation first
      conversationState.pendingConfirmation = { toolName, params };
      return {
        status: "confirmation_required",
        message: `Please confirm: ${describeAction(toolName, params)}`,
        action: describeAction(toolName, params),
      };
    }
    // User confirmed — execute
    conversationState.pendingConfirmation = null;
  }
 
  return await safeToolExecution(toolName, params);
}

Tool Call Limits

Prevent agents from getting stuck in loops:

const MAX_TOOL_CALLS_PER_TURN = 5;
const MAX_RETRIES_PER_TOOL = 2;
 
async function agentLoop(messages: Message[]) {
  let toolCallCount = 0;
  const retryTracker = new Map<string, number>();
 
  while (toolCallCount < MAX_TOOL_CALLS_PER_TURN) {
    const response = await llm.chat({ messages, tools });
    
    if (!response.tool_calls?.length) break; // Agent is done
 
    for (const call of response.tool_calls) {
      const retries = retryTracker.get(call.id) || 0;
      if (retries >= MAX_RETRIES_PER_TOOL) {
        messages.push({
          role: "tool",
          content: JSON.stringify({
            error: "Maximum retries exceeded. Please respond without this tool.",
          }),
        });
        continue;
      }
 
      const result = await safeToolExecution(call.function.name, call.function.arguments);
      messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
      
      if (result.status === "error") retryTracker.set(call.id, retries + 1);
      toolCallCount++;
    }
  }
 
  if (toolCallCount >= MAX_TOOL_CALLS_PER_TURN) {
    messages.push({
      role: "system",
      content: "Tool call limit reached. Provide your best response with available information.",
    });
  }
 
  return await llm.chat({ messages }); // Final response without tools
}

Production AI agents aren't about making the model smarter. They're about making the system around the model robust enough to handle every way the model can fail — and it will fail in ways you never predicted. Build the guardrails before you build the features.

Previous
Microservices Communication Patterns — Sync, Async, and When to Use Each
Insights
AI Agent Tool Calling Patterns That Actually Work in ProductionRAG Pipeline Optimization — From 8s to 400msLLM Cost Optimization — How We Cut a Client's AI Bill by 73%AI Hallucination Detection in Production — What Actually WorksWe Built an AI Code Review Bot — Here's What It Actually Catches (And What It Misses)Prompt Engineering Is Dead — Context Engineering Is What MattersYour AI Agent Isn't Working Because You Skipped the GuardrailsRAG vs Fine-Tuning: When to Use What in ProductionHow to Cut Your LLM Costs by 70% Without Losing QualityThe AI Implementation Playbook for Non-Technical FoundersWhy Most AI Chatbots Fail (And What Production-Grade Looks Like)Building AI Agents That Know When to Hand Off to HumansVibe Coding Is Destroying Your CodebaseAI Won't Fix Your Broken Data Pipeline

Ready to Ship?

Let's talk about your engineering challenges and how we can help.

Book a Call