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.
| Step | Action | Example |
|---|
| 1 | User request | "What is the weather in Tokyo?" |
| 2 | LLM chooses function | get_weather(city: "Tokyo") |
| 3 | System executes | Call weather API |
| 4 | Result returned | { temp: 22, condition: "sunny" } |
| 5 | LLM 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!"
| Component | Purpose | Example |
|---|
| Name | Identifies function | get_weather |
| Description | Helps LLM choose | "Get current weather for a city" |
| Parameters | Input schema | { city: string, units?: string } |
| Required | Mandatory params | ["city"] |
| Provider | Function Calling | Parallel Calls | Streaming |
|---|
| OpenAI | Excellent | Yes | Yes |
| Anthropic | Excellent | Yes | Yes |
| Google | Good | Yes | Yes |
| Open source | Variable | Limited | Limited |
| Principle | Description | Example |
|---|
| Single responsibility | One function, one job | get_user, not get_user_and_orders |
| Clear naming | Verb + noun | send_email, create_ticket |
| Minimal parameters | Only necessary inputs | Avoid optional overload |
| Predictable output | Consistent structure | Always same shape |
| Pattern | Good | Bad |
|---|
| Naming | user_id | id, u, userId |
| Types | Explicit | Any, mixed |
| Defaults | Sensible | None when needed |
| Enums | Constrained | Free text |
| Element | Purpose | Example |
|---|
| Summary | What it does | "Sends an email to a user" |
| Parameters | Each param | "recipient: Email address to send to" |
| Returns | Output format | "Returns { success: boolean, id: string }" |
| Constraints | Limitations | "Max 10 recipients per call" |
| Aspect | Good | Bad |
|---|
| Scope | search_products(query, limit) | do_everything(action, params) |
| Params | book_flight(from, to, date) | book_flight(flight_json) |
| Output | { price: 100, currency: "USD" } | "The price is $100" |
| Naming | get_user_orders | fetchData |
| Category | Example | Handling |
|---|
| Validation | Invalid parameter | Return clear error |
| Permission | Unauthorized action | Deny with reason |
| Not found | Resource missing | Return null or error |
| External | API failure | Retry or fallback |
| Rate limit | Too many calls | Backoff |
| Field | Purpose | Example |
|---|
| success | Quick check | false |
| error_code | Programmatic | "USER_NOT_FOUND" |
| error_message | Human readable | "User with ID 123 not found" |
| retry | Can retry? | false |
| suggestions | Next steps | ["Check user ID", "Try search"] |
| Strategy | When | Implementation |
|---|
| Retry | Transient errors | Exponential backoff |
| Fallback | Alternative available | Try backup function |
| Graceful degradation | Partial data ok | Return what we have |
| User escalation | Cannot recover | Ask user for help |
| Scenario | Response Strategy |
|---|
| Invalid params | Ask LLM to fix and retry |
| Permission denied | Explain to user why |
| Not found | Let LLM search alternatively |
| System error | Apologize, suggest retry |
| Threat | Description | Mitigation |
|---|
| Injection | Malicious params | Input validation |
| Privilege escalation | Unauthorized actions | Permission checks |
| Data exfiltration | Leak sensitive data | Output filtering |
| Resource abuse | Excessive calls | Rate limiting |
| Validation | Purpose | Implementation |
|---|
| Type checking | Correct types | Schema validation |
| Range checking | Valid values | Min/max constraints |
| Format checking | Valid format | Regex patterns |
| Sanitization | Remove dangerous | Escape, encode |
| Level | Description | Example |
|---|
| Public | Anyone can call | get_public_info |
| Authenticated | Logged in users | get_my_profile |
| Authorized | Specific permissions | delete_user |
| Admin | Admin only | modify_system |
| Function Type | Risk | Mitigation |
|---|
| Delete operations | Data loss | Soft delete, confirm |
| Financial | Money loss | Double confirm, limits |
| External calls | Side effects | Sandbox, review |
| Data access | Privacy | Scope limiting |
| Layer | What | Tool |
|---|
| Input | Parameter types | Zod, JSON Schema |
| Output | Return format | Zod, TypeScript |
| Runtime | Values | Custom validators |
| Step | Action | Failure Response |
|---|
| 1 | Schema validation | Return type error |
| 2 | Business rules | Return rule violation |
| 3 | Permission check | Return permission error |
| 4 | Resource validation | Return not found |
| 5 | Execute | Return 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 },
};
}
}
| Validation | Pattern | Example |
|---|
| Email | Regex + DNS | user@domain.com |
| URL | URL parse + whitelist | https://allowed.com |
| ID | Format + exists | UUID, database lookup |
| Date | Parse + range | Future dates only |
| Amount | Number + limits | 0 < amount < 10000 |
| Scenario | Strategy | Example |
|---|
| Independent data | Parallel | Get weather + get news |
| Dependent data | Sequential | Get user, then orders |
| Aggregation | Parallel + merge | Multiple searches |
| Approach | Implementation | Trade-off |
|---|
| Promise.all | All or nothing | Fails if any fails |
| Promise.allSettled | All complete | Must handle failures |
| Batching | Grouped calls | Complexity |
| Pattern | Use Case | Example |
|---|
| Chain | A depends on B | Get user -> get orders |
| Conditional | Based on result | If user exists -> update |
| Loop | Iterate results | For each order -> get details |
| Error Type | Retry? | Strategy |
|---|
| Validation | No | Fix input |
| Not found | No | Alternative approach |
| Rate limit | Yes | Exponential backoff |
| Timeout | Yes | Immediate retry |
| Server error | Yes | Backoff with limit |
| State | Behavior | Transition |
|---|
| Closed | Normal operation | Opens on failures |
| Open | Fail fast | Half-open after timeout |
| Half-open | Test requests | Close or reopen |
| Operation | Timeout | Fallback |
|---|
| Fast lookup | 2s | Cache |
| External API | 10s | Error message |
| Complex operation | 30s | Async with callback |
| Metric | Description | Target |
|---|
| Call rate | Calls per minute | Baseline |
| Success rate | Successful / total | Over 95% |
| Latency | Time per call | Under 1s |
| Error rate | Errors / total | Under 5% |
| Dimension | Purpose | Insight |
|---|
| By function | Usage patterns | Popular functions |
| By user | User behavior | Power users |
| By outcome | Success patterns | Reliability |
| By time | Trends | Peak usage |
| Alert | Condition | Severity |
|---|
| High error rate | Over 10% | High |
| Slow calls | p95 over 5s | Medium |
| Unusual volume | Over 3x normal | Medium |
| Security event | Blocked call | High |
| Pattern | Description | Use Case |
|---|
| Pipeline | Output feeds input | Data transformation |
| Orchestration | Coordinate multiple | Complex workflows |
| Saga | Compensating actions | Transactions |
| Approach | Description | Trade-off |
|---|
| Static registry | Fixed functions | Simple, limited |
| Dynamic loading | Load at runtime | Flexible, complex |
| User-defined | User creates functions | Powerful, risky |
| Step | LLM Action | System Action |
|---|
| 1 | Identify goal | Provide functions |
| 2 | Plan steps | Validate plan |
| 3 | Execute step | Run function |
| 4 | Evaluate result | Return result |
| 5 | Continue or finish | Loop or complete |
- 1Design functions for LLMs - Clear names, good descriptions, minimal parameters help the model choose correctly.
- 2Validate everything - Never trust LLM-generated parameters. Validate types, ranges, and permissions.
- 3Handle errors gracefully - Return structured errors that help the LLM recover or explain to users.
- 4Security is non-negotiable - Function calling is an attack surface. Validate, authorize, and monitor.
- 5Monitor function usage - Track success rates, latency, and error patterns to improve reliability.
- 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.