Function Calling in Production: Patterns and Pitfalls
Back to all articles
AI Engineering
18 min read11 min read

Function Calling in Production: Patterns and Pitfalls

Turning LLMs into action-takers. Covers function design, error handling, security, validation, and building reliable tool-using AI systems.

Debasish Maji
Debasish Maji
AI Engineering Lead
May 6, 2026
Function CallingToolsAgentsAPIProduction

From Chat to Action

LLMs that only generate text are limited. Function calling transforms them into actors that can do things.

Search databases. Send emails. Update records. Book appointments. Function calling bridges the gap between AI understanding and real-world action. But with great power comes great complexity.

•••

Function Calling Fundamentals

How It Works

StepActionExample
1User request"What is the weather in Tokyo?"
2LLM chooses functionget_weather(city: "Tokyo")
3System executesCall weather API
4Result returned{ temp: 22, condition: "sunny" }
5LLM synthesizes"It is 22C and sunny in Tokyo"
Diagram
sequenceDiagram participant User participant LLM participant System participant API User->>LLM: "Weather in Tokyo?" LLM->>System: tool_call: get_weather(city: "Tokyo") System->>API: GET /weather?city=Tokyo API-->>System: { temp: 22, condition: "sunny" } System-->>LLM: tool_result: { temp: 22, condition: "sunny" } LLM-->>User: "It's 22°C and sunny in Tokyo!"

Function Definition

ComponentPurposeExample
NameIdentifies functionget_weather
DescriptionHelps LLM choose"Get current weather for a city"
ParametersInput schema{ city: string, units?: string }
RequiredMandatory params["city"]

Provider Comparison

ProviderFunction CallingParallel CallsStreaming
OpenAIExcellentYesYes
AnthropicExcellentYesYes
GoogleGoodYesYes
Open sourceVariableLimitedLimited
•••

Function Design

Design Principles

PrincipleDescriptionExample
Single responsibilityOne function, one jobget_user, not get_user_and_orders
Clear namingVerb + nounsend_email, create_ticket
Minimal parametersOnly necessary inputsAvoid optional overload
Predictable outputConsistent structureAlways same shape

Parameter Design

PatternGoodBad
Naminguser_idid, u, userId
TypesExplicitAny, mixed
DefaultsSensibleNone when needed
EnumsConstrainedFree text

Description Writing

ElementPurposeExample
SummaryWhat it does"Sends an email to a user"
ParametersEach param"recipient: Email address to send to"
ReturnsOutput format"Returns { success: boolean, id: string }"
ConstraintsLimitations"Max 10 recipients per call"

Good vs Bad Functions

AspectGoodBad
Scopesearch_products(query, limit)do_everything(action, params)
Paramsbook_flight(from, to, date)book_flight(flight_json)
Output{ price: 100, currency: "USD" }"The price is $100"
Namingget_user_ordersfetchData
•••

Error Handling

Error Categories

CategoryExampleHandling
ValidationInvalid parameterReturn clear error
PermissionUnauthorized actionDeny with reason
Not foundResource missingReturn null or error
ExternalAPI failureRetry or fallback
Rate limitToo many callsBackoff

Error Response Format

FieldPurposeExample
successQuick checkfalse
error_codeProgrammatic"USER_NOT_FOUND"
error_messageHuman readable"User with ID 123 not found"
retryCan retry?false
suggestionsNext steps["Check user ID", "Try search"]

Error Recovery

StrategyWhenImplementation
RetryTransient errorsExponential backoff
FallbackAlternative availableTry backup function
Graceful degradationPartial data okReturn what we have
User escalationCannot recoverAsk user for help

LLM Error Handling

ScenarioResponse Strategy
Invalid paramsAsk LLM to fix and retry
Permission deniedExplain to user why
Not foundLet LLM search alternatively
System errorApologize, suggest retry
•••

Security Considerations

Threat Model

ThreatDescriptionMitigation
InjectionMalicious paramsInput validation
Privilege escalationUnauthorized actionsPermission checks
Data exfiltrationLeak sensitive dataOutput filtering
Resource abuseExcessive callsRate limiting

Input Validation

ValidationPurposeImplementation
Type checkingCorrect typesSchema validation
Range checkingValid valuesMin/max constraints
Format checkingValid formatRegex patterns
SanitizationRemove dangerousEscape, encode

Permission Model

LevelDescriptionExample
PublicAnyone can callget_public_info
AuthenticatedLogged in usersget_my_profile
AuthorizedSpecific permissionsdelete_user
AdminAdmin onlymodify_system

Dangerous Functions

Function TypeRiskMitigation
Delete operationsData lossSoft delete, confirm
FinancialMoney lossDouble confirm, limits
External callsSide effectsSandbox, review
Data accessPrivacyScope limiting
•••

Validation Strategies

Schema Validation

LayerWhatTool
InputParameter typesZod, JSON Schema
OutputReturn formatZod, TypeScript
RuntimeValuesCustom validators

Validation Pipeline

StepActionFailure Response
1Schema validationReturn type error
2Business rulesReturn rule violation
3Permission checkReturn permission error
4Resource validationReturn not found
5ExecuteReturn result or error
Typescript
import { z } from 'zod';
import OpenAI from 'openai';

// Define function schemas with Zod
const functions = {
  search_products: {
    schema: z.object({
      query: z.string().min(1).max(200),
      category: z.enum(['electronics', 'clothing', 'home', 'all']).default('all'),
      limit: z.number().min(1).max(50).default(10),
      price_max: z.number().positive().optional(),
    }),
    description: 'Search the product catalog',
    handler: async (params, context) => {
      // Permission check
      if (!context.user.hasPermission('products:read')) {
        return { error: 'PERMISSION_DENIED', message: 'Cannot access products' };
      }
      
      const results = await db.products.search({
        query: params.query,
        category: params.category === 'all' ? undefined : params.category,
        limit: params.limit,
        priceMax: params.price_max,
      });
      
      return { products: results, count: results.length };
    },
  },
  
  create_order: {
    schema: z.object({
      product_id: z.string().uuid(),
      quantity: z.number().int().min(1).max(100),
      shipping_address: z.object({
        street: z.string(),
        city: z.string(),
        country: z.string(),
        postal_code: z.string(),
      }),
    }),
    description: 'Create a new order',
    handler: async (params, context) => {
      // Check product exists
      const product = await db.products.findById(params.product_id);
      if (!product) {
        return { error: 'NOT_FOUND', message: 'Product not found' };
      }
      
      // Check inventory
      if (product.stock < params.quantity) {
        return { error: 'INSUFFICIENT_STOCK', message: `Only ${product.stock} available` };
      }
      
      // Create order
      const order = await db.orders.create({
        userId: context.user.id,
        productId: params.product_id,
        quantity: params.quantity,
        shippingAddress: params.shipping_address,
        total: product.price * params.quantity,
      });
      
      return { order_id: order.id, total: order.total, status: 'created' };
    },
  },
};

// Convert Zod schemas to OpenAI function format
function zodToOpenAI(name: string, schema: z.ZodObject<any>, description: string) {
  return {
    type: 'function' as const,
    function: {
      name,
      description,
      parameters: zodToJsonSchema(schema),
    },
  };
}

// Execute with validation
async function executeFunction(
  name: string,
  args: unknown,
  context: { user: User }
): Promise<{ success: boolean; result?: any; error?: any }> {
  const fn = functions[name as keyof typeof functions];
  
  if (!fn) {
    return { success: false, error: { code: 'UNKNOWN_FUNCTION', message: `Unknown: ${name}` } };
  }
  
  // Validate with Zod
  const validation = fn.schema.safeParse(args);
  if (!validation.success) {
    return {
      success: false,
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid parameters',
        details: validation.error.issues,
      },
    };
  }
  
  try {
    const result = await fn.handler(validation.data, context);
    
    if (result.error) {
      return { success: false, error: result };
    }
    
    return { success: true, result };
  } catch (error) {
    return {
      success: false,
      error: { code: 'EXECUTION_ERROR', message: error.message },
    };
  }
}

Common Validations

ValidationPatternExample
EmailRegex + DNSuser@domain.com
URLURL parse + whitelisthttps://allowed.com
IDFormat + existsUUID, database lookup
DateParse + rangeFuture dates only
AmountNumber + limits0 < amount < 10000
•••

Parallel and Sequential Calls

When to Parallelize

ScenarioStrategyExample
Independent dataParallelGet weather + get news
Dependent dataSequentialGet user, then orders
AggregationParallel + mergeMultiple searches

Parallel Execution

ApproachImplementationTrade-off
Promise.allAll or nothingFails if any fails
Promise.allSettledAll completeMust handle failures
BatchingGrouped callsComplexity

Sequential Patterns

PatternUse CaseExample
ChainA depends on BGet user -> get orders
ConditionalBased on resultIf user exists -> update
LoopIterate resultsFor each order -> get details
•••

Reliability Patterns

Retry Strategy

Error TypeRetry?Strategy
ValidationNoFix input
Not foundNoAlternative approach
Rate limitYesExponential backoff
TimeoutYesImmediate retry
Server errorYesBackoff with limit

Circuit Breaker

StateBehaviorTransition
ClosedNormal operationOpens on failures
OpenFail fastHalf-open after timeout
Half-openTest requestsClose or reopen

Timeout Strategy

OperationTimeoutFallback
Fast lookup2sCache
External API10sError message
Complex operation30sAsync with callback
•••

Monitoring Function Calls

Key Metrics

MetricDescriptionTarget
Call rateCalls per minuteBaseline
Success rateSuccessful / totalOver 95%
LatencyTime per callUnder 1s
Error rateErrors / totalUnder 5%

Function Analytics

DimensionPurposeInsight
By functionUsage patternsPopular functions
By userUser behaviorPower users
By outcomeSuccess patternsReliability
By timeTrendsPeak usage

Alerting

AlertConditionSeverity
High error rateOver 10%High
Slow callsp95 over 5sMedium
Unusual volumeOver 3x normalMedium
Security eventBlocked callHigh
•••

Advanced Patterns

Function Chaining

PatternDescriptionUse Case
PipelineOutput feeds inputData transformation
OrchestrationCoordinate multipleComplex workflows
SagaCompensating actionsTransactions

Dynamic Functions

ApproachDescriptionTrade-off
Static registryFixed functionsSimple, limited
Dynamic loadingLoad at runtimeFlexible, complex
User-definedUser creates functionsPowerful, risky

Multi-Step Reasoning

StepLLM ActionSystem Action
1Identify goalProvide functions
2Plan stepsValidate plan
3Execute stepRun function
4Evaluate resultReturn result
5Continue or finishLoop or complete
•••

Key Takeaways

  1. 1Design functions for LLMs - Clear names, good descriptions, minimal parameters help the model choose correctly.
  1. 2Validate everything - Never trust LLM-generated parameters. Validate types, ranges, and permissions.
  1. 3Handle errors gracefully - Return structured errors that help the LLM recover or explain to users.
  1. 4Security is non-negotiable - Function calling is an attack surface. Validate, authorize, and monitor.
  1. 5Monitor function usage - Track success rates, latency, and error patterns to improve reliability.
  1. 6Start simple - Begin with a few well-designed functions. Add complexity as you learn usage patterns.

Function calling transforms LLMs from advisors to actors. Build the bridge carefully, with guardrails and observability, and you will unlock powerful capabilities.

Found this helpful?

Share it with others who might benefit

TweetShare

Related articles