Advanced Function Calling
Function calling unlocks AI agents that don't just respond with text but take real actions in your application. This guide explores advanced patterns, error handling strategies, and complex orchestration techniques for building production-ready function calling systems.
Overview
What You'll Learn
This guide covers advanced function calling patterns beyond basic tool usage:
- Multi-turn Conversations - Chain multiple function calls together
- Parallel Execution - Execute multiple tools simultaneously
- Error Handling - Gracefully handle failures and retry strategies
- Conditional Logic - Dynamic tool selection based on context
- State Management - Maintain state across function calls
- Agent Orchestration - Build complex multi-step workflows
- Security Patterns - Safely execute untrusted function calls
- Performance Optimization - Cache, batch, and optimize tool execution
Prerequisites
This guide assumes you understand:
- Basic function calling and tools
- FunctionBuilder API
- PHP closures and callables
- Laravel's service container and facades
Multi-Turn Function Calling
Most real-world tasks require multiple function calls. The LLM calls functions, processes results, and decides what to do next.
Basic Multi-Turn Pattern
use Mindwave\Mindwave\Facades\LLM;
use Mindwave\Mindwave\LLM\FunctionCalling\FunctionBuilder;
use Mindwave\Mindwave\LLM\FunctionCalling\FunctionCall;
$functions = FunctionBuilder::make()
->addFunction(
name: 'get_user',
description: 'Retrieves user by email',
closure: fn(string $email) => User::where('email', $email)->first()?->toJson() ?? 'User not found'
)
->addFunction(
name: 'get_orders',
description: 'Retrieves orders for a user ID',
closure: fn(int $userId) => Order::where('user_id', $userId)->get()->toJson()
)
->addFunction(
name: 'calculate_total',
description: 'Calculates total amount from orders JSON',
closure: function(string $ordersJson) {
$orders = json_decode($ordersJson, true);
return array_sum(array_column($orders, 'total'));
}
);
// Multi-turn execution
$messages = [
['role' => 'user', 'content' => 'What is the total order value for user john@example.com?']
];
$maxTurns = 10;
$turn = 0;
while ($turn < $maxTurns) {
$response = LLM::driver('openai')->functionCall(
json_encode($messages),
$functions
);
// Check if we got a final text response
if (is_string($response)) {
echo "Final answer: {$response}";
break;
}
// Execute the function
if ($response instanceof FunctionCall) {
Log::info("Turn {$turn}: Calling {$response->name}", $response->arguments);
// Execute based on function name
$result = match($response->name) {
'get_user' => (fn() => User::where('email', $response->arguments['email'])->first()?->toJson() ?? 'User not found')(),
'get_orders' => (fn() => Order::where('user_id', $response->arguments['userId'])->get()->toJson())(),
'calculate_total' => (fn() => json_decode($response->arguments['ordersJson'], true) |> array_sum(array_column($_, 'total')))(),
default => json_encode(['error' => 'Unknown function'])
};
// Add function call and result to conversation history
$messages[] = [
'role' => 'assistant',
'function_call' => [
'name' => $response->name,
'arguments' => $response->rawArguments
]
];
$messages[] = [
'role' => 'function',
'name' => $response->name,
'content' => $result
];
}
$turn++;
}
if ($turn >= $maxTurns) {
Log::warning('Max turns reached without completion');
}How it works:
sequenceDiagram
participant User
participant LLM
participant get_user
participant get_orders
participant calculate_total
User->>LLM: What is the total order value<br/>for john@example.com?
Note over LLM: Turn 1: Need user data
LLM->>get_user: get_user('john@example.com')
get_user-->>LLM: {id: 123, name: "John", ...}
Note over LLM: Turn 2: Now get orders
LLM->>get_orders: get_orders(userId: 123)
get_orders-->>LLM: [{order1}, {order2}, ...]
Note over LLM: Turn 3: Calculate total
LLM->>calculate_total: calculate_total(ordersJson)
calculate_total-->>LLM: 1247.50
Note over LLM: Turn 4: Format response
LLM-->>User: John has spent $1,247.50<br/>across 3 ordersProcess:
- User asks a complex question requiring multiple steps
- LLM calls
get_user('john@example.com')→ Gets user ID - LLM calls
get_orders(userId: 123)→ Gets order data - LLM calls
calculate_total(ordersJson)→ Computes total - LLM returns final formatted answer to user
Conversation State Management
Track conversation state for complex interactions:
class ConversationManager
{
protected array $messages = [];
protected array $executedFunctions = [];
protected int $totalCost = 0;
public function __construct(
protected FunctionBuilder $functions,
protected string $driver = 'openai'
) {}
public function execute(string $userMessage, int $maxTurns = 10): string
{
$this->messages[] = ['role' => 'user', 'content' => $userMessage];
for ($turn = 0; $turn < $maxTurns; $turn++) {
$response = LLM::driver($this->driver)
->functionCall(json_encode($this->messages), $this->functions);
// Track costs (if tracing enabled)
$this->totalCost += $this->estimateTokenCost($response);
if (is_string($response)) {
return $response; // Final answer
}
if ($response instanceof FunctionCall) {
$result = $this->executeFunction($response);
$this->executedFunctions[] = [
'name' => $response->name,
'arguments' => $response->arguments,
'result' => $result,
'turn' => $turn,
];
$this->messages[] = [
'role' => 'assistant',
'function_call' => [
'name' => $response->name,
'arguments' => $response->rawArguments,
],
];
$this->messages[] = [
'role' => 'function',
'name' => $response->name,
'content' => $result,
];
}
}
throw new \RuntimeException('Maximum turns exceeded without completion');
}
protected function executeFunction(FunctionCall $call): string
{
// Execute function through function builder
$functions = $this->functions->build();
foreach ($functions as $func) {
if ($func['name'] === $call->name) {
return ($func['function'])(...array_values($call->arguments));
}
}
return json_encode(['error' => 'Function not found']);
}
public function getExecutionTrace(): array
{
return [
'turns' => count($this->executedFunctions),
'functions_called' => array_column($this->executedFunctions, 'name'),
'estimated_cost' => $this->totalCost,
'full_trace' => $this->executedFunctions,
];
}
protected function estimateTokenCost($response): int
{
// Implement token cost estimation
return 0; // Placeholder
}
}
// Usage
$manager = new ConversationManager($functions);
$answer = $manager->execute('What are the top 3 orders for john@example.com?');
echo $answer;
print_r($manager->getExecutionTrace());Parallel Function Execution
Some LLMs can request multiple function calls simultaneously. Handle them efficiently:
Detecting Parallel Calls
$response = LLM::driver('openai')->functionCall($prompt, $functions);
// OpenAI may return multiple tool calls
if (is_array($response)) {
// Parallel execution requested
$results = [];
foreach ($response as $call) {
if ($call instanceof FunctionCall) {
$results[] = $this->executeFunction($call);
}
}
// Return all results to LLM
return $results;
}Concurrent Execution with Laravel
Execute multiple functions concurrently for better performance:
use Illuminate\Support\Facades\ParallelTesting;
use Illuminate\Support\Arr;
class ParallelFunctionExecutor
{
public function executeConcurrent(array $functionCalls): array
{
// Group by execution speed (fast vs slow)
$fast = [];
$slow = [];
foreach ($functionCalls as $call) {
if ($this->isFastFunction($call->name)) {
$fast[] = $call;
} else {
$slow[] = $call;
}
}
// Execute fast functions synchronously
$fastResults = array_map(fn($call) => $this->execute($call), $fast);
// Execute slow functions concurrently using queues
$slowResults = $this->executeConcurrentlyWithQueues($slow);
return array_merge($fastResults, $slowResults);
}
protected function executeConcurrentlyWithQueues(array $calls): array
{
if (empty($calls)) {
return [];
}
$jobIds = [];
foreach ($calls as $call) {
$jobId = Str::uuid()->toString();
$jobIds[] = $jobId;
ExecuteFunctionJob::dispatch($call, $jobId);
}
// Poll for results (or use Redis pub/sub)
return $this->waitForResults($jobIds);
}
protected function isFastFunction(string $name): bool
{
// Define which functions are fast (in-memory operations)
return in_array($name, ['calculate_total', 'format_date', 'validate_input']);
}
}Promise-Based Parallel Execution
For truly parallel execution within a single request:
use React\Promise\Promise;
use React\Promise\PromiseInterface;
class PromiseBasedExecutor
{
public function executeParallel(array $functionCalls): array
{
$promises = [];
foreach ($functionCalls as $call) {
$promises[] = $this->executeAsync($call);
}
// Wait for all promises to resolve
return \React\Promise\all($promises)->then(function ($results) {
return $results;
})->wait();
}
protected function executeAsync(FunctionCall $call): PromiseInterface
{
return new Promise(function ($resolve, $reject) use ($call) {
try {
$result = $this->execute($call);
$resolve($result);
} catch (\Exception $e) {
$reject($e);
}
});
}
}Error Handling and Recovery
Production systems need robust error handling for function calls.
Graceful Error Handling
class ResilientFunctionExecutor
{
public function execute(FunctionCall $call): string
{
try {
// Validate arguments before execution
$this->validateArguments($call);
// Execute with timeout
$result = $this->executeWithTimeout($call, timeout: 30);
// Validate result format
$this->validateResult($result);
return $result;
} catch (ValidationException $e) {
// Return validation error to LLM
return json_encode([
'error' => 'validation_failed',
'message' => $e->getMessage(),
'hint' => 'Please check the parameter format and try again'
]);
} catch (TimeoutException $e) {
// Function took too long
return json_encode([
'error' => 'timeout',
'message' => 'Function execution exceeded time limit',
'hint' => 'Try breaking the request into smaller parts'
]);
} catch (\Exception $e) {
// Log unexpected errors
Log::error('Function execution failed', [
'function' => $call->name,
'arguments' => $call->arguments,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return json_encode([
'error' => 'execution_failed',
'message' => 'An unexpected error occurred',
'function' => $call->name,
]);
}
}
protected function validateArguments(FunctionCall $call): void
{
// Implement argument validation based on function schema
$schema = $this->getFunctionSchema($call->name);
foreach ($schema['required'] ?? [] as $param) {
if (!isset($call->arguments[$param])) {
throw new ValidationException("Missing required parameter: {$param}");
}
}
}
protected function executeWithTimeout(FunctionCall $call, int $timeout): string
{
// Use pcntl or process timeout for long-running operations
$start = time();
$result = $this->doExecute($call);
if (time() - $start > $timeout) {
throw new TimeoutException('Execution time exceeded');
}
return $result;
}
protected function validateResult(string $result): void
{
// Ensure result is valid JSON
$decoded = json_decode($result, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new ValidationException('Result is not valid JSON');
}
}
}Retry Strategies
Implement intelligent retry logic for transient failures:
class RetryableFunctionExecutor
{
protected int $maxRetries = 3;
protected int $baseDelay = 1000; // milliseconds
public function executeWithRetry(FunctionCall $call): string
{
$attempt = 0;
$lastException = null;
while ($attempt < $this->maxRetries) {
try {
return $this->execute($call);
} catch (TransientException $e) {
// Retry on transient errors (network, rate limits, etc.)
$lastException = $e;
$attempt++;
if ($attempt < $this->maxRetries) {
$delay = $this->calculateBackoff($attempt);
Log::warning("Function call failed, retrying in {$delay}ms", [
'function' => $call->name,
'attempt' => $attempt,
'error' => $e->getMessage(),
]);
usleep($delay * 1000);
}
} catch (\Exception $e) {
// Don't retry on permanent errors
throw $e;
}
}
// All retries exhausted
throw new \RuntimeException(
"Function execution failed after {$this->maxRetries} attempts",
0,
$lastException
);
}
protected function calculateBackoff(int $attempt): int
{
// Exponential backoff with jitter
$exponential = $this->baseDelay * (2 ** ($attempt - 1));
$jitter = rand(0, (int)($exponential * 0.1));
return $exponential + $jitter;
}
protected function isTransientError(\Exception $e): bool
{
// Determine if error is retryable
return $e instanceof NetworkException
|| $e instanceof RateLimitException
|| $e instanceof TimeoutException
|| str_contains($e->getMessage(), 'temporary');
}
}Circuit Breaker Pattern
Prevent cascading failures when a function repeatedly fails:
stateDiagram-v2
[*] --> Closed
Closed --> Closed: Success<br/>(reset failure count)
Closed --> Open: Failures ≥ threshold<br/>(5 failures)
Open --> HalfOpen: After recovery timeout<br/>(60 seconds)
Open --> Open: Request blocked
HalfOpen --> Closed: Success<br/>(service recovered)
HalfOpen --> Open: Failure<br/>(still broken)
note right of Closed
Normal operation
All requests pass through
end note
note right of Open
Circuit tripped
All requests blocked
Returns fallback response
end note
note right of HalfOpen
Testing recovery
Limited requests allowed
end noteImplementation:
use Illuminate\Support\Facades\Cache;
class CircuitBreaker
{
protected int $failureThreshold = 5;
protected int $recoveryTimeout = 60; // seconds
public function execute(string $functionName, callable $callable): mixed
{
$key = "circuit_breaker:{$functionName}";
// Check if circuit is open
if (Cache::has("{$key}:open")) {
throw new CircuitBreakerOpenException(
"Circuit breaker is open for function: {$functionName}"
);
}
try {
$result = $callable();
// Reset failure count on success
Cache::forget("{$key}:failures");
return $result;
} catch (\Exception $e) {
// Increment failure count
$failures = Cache::increment("{$key}:failures", 1, now()->addMinutes(5));
if ($failures >= $this->failureThreshold) {
// Open circuit
Cache::put("{$key}:open", true, $this->recoveryTimeout);
Log::error("Circuit breaker opened for function: {$functionName}", [
'failures' => $failures,
'recovery_timeout' => $this->recoveryTimeout,
]);
}
throw $e;
}
}
}
// Usage
$breaker = new CircuitBreaker();
try {
$result = $breaker->execute('external_api_call', function() use ($call) {
return $this->executeFunction($call);
});
} catch (CircuitBreakerOpenException $e) {
// Provide fallback response
return json_encode([
'error' => 'service_unavailable',
'message' => 'This function is temporarily unavailable',
'retry_after' => 60,
]);
}Conditional Logic and Dynamic Tool Selection
Build agents that adapt tool usage based on context and state.
Context-Aware Function Selection
class ContextAwareFunctionBuilder
{
public function buildForUser(User $user): FunctionBuilder
{
$functions = FunctionBuilder::make();
// Base functions available to all users
$functions->addFunction(
name: 'get_profile',
description: 'Get user profile information',
closure: fn() => $user->toJson()
);
// Admin-only functions
if ($user->isAdmin()) {
$functions->addFunction(
name: 'delete_user',
description: 'Delete a user account',
closure: fn(int $userId) => User::destroy($userId)
);
$functions->addFunction(
name: 'view_all_orders',
description: 'View all orders in the system',
closure: fn() => Order::all()->toJson()
);
}
// Premium user functions
if ($user->isPremium()) {
$functions->addFunction(
name: 'export_data',
description: 'Export user data in various formats',
closure: fn(string $format) => $this->exportData($user, $format)
);
}
// Rate-limited functions
if ($this->checkRateLimit($user)) {
$functions->addFunction(
name: 'generate_report',
description: 'Generate detailed analytics report',
closure: fn(array $params) => $this->generateReport($user, $params)
);
}
return $functions;
}
}State-Based Tool Availability
class StatefulAgent
{
protected array $state = [];
protected array $availableTools = [];
public function __construct(protected FunctionBuilder $baseFunctions)
{
$this->availableTools = $this->baseFunctions->build();
}
public function execute(string $message): string
{
// Update available tools based on current state
$this->updateAvailableTools();
$functions = $this->getCurrentFunctions();
$response = LLM::driver('openai')->functionCall($message, $functions);
if ($response instanceof FunctionCall) {
$result = $this->executeAndUpdateState($response);
return $result;
}
return $response;
}
protected function updateAvailableTools(): void
{
// Example: Only allow checkout after cart has items
if (!empty($this->state['cart'])) {
$this->availableTools[] = [
'name' => 'checkout',
'description' => 'Process order and payment',
'function' => fn() => $this->checkout(),
];
}
// Example: Only allow refund after purchase
if (isset($this->state['order_id'])) {
$this->availableTools[] = [
'name' => 'request_refund',
'description' => 'Request refund for order',
'function' => fn() => $this->requestRefund($this->state['order_id']),
];
}
}
protected function executeAndUpdateState(FunctionCall $call): string
{
$result = $this->execute($call);
// Update state based on function execution
match($call->name) {
'add_to_cart' => $this->state['cart'][] = $call->arguments['product_id'],
'checkout' => $this->state['order_id'] = json_decode($result, true)['order_id'],
'clear_cart' => $this->state['cart'] = [],
default => null,
};
return $result;
}
}Advanced Patterns
Function Call Chaining with Pipelines
Create reusable function call pipelines:
class FunctionPipeline
{
protected array $stages = [];
public function add(string $name, callable $function): self
{
$this->stages[] = compact('name', 'function');
return $this;
}
public function execute($input): mixed
{
$output = $input;
foreach ($this->stages as $stage) {
Log::info("Pipeline stage: {$stage['name']}");
$output = ($stage['function'])($output);
if ($output instanceof PipelineBreak) {
return $output->value;
}
}
return $output;
}
}
// Usage: Data enrichment pipeline
$pipeline = (new FunctionPipeline())
->add('fetch_user', fn($email) => User::where('email', $email)->first())
->add('fetch_orders', fn($user) => Order::where('user_id', $user->id)->get())
->add('calculate_lifetime_value', fn($orders) => $orders->sum('total'))
->add('format_currency', fn($value) => '$' . number_format($value, 2));
$ltv = $pipeline->execute('john@example.com');
// Output: "$1,234.56"Function Call Memoization
Cache expensive function results:
class MemoizedFunctionExecutor
{
public function execute(FunctionCall $call): string
{
$cacheKey = $this->getCacheKey($call);
$ttl = $this->getTtl($call->name);
return Cache::remember($cacheKey, $ttl, function() use ($call) {
return $this->doExecute($call);
});
}
protected function getCacheKey(FunctionCall $call): string
{
return sprintf(
'func:%s:%s',
$call->name,
md5(json_encode($call->arguments))
);
}
protected function getTtl(string $functionName): int
{
// Different TTLs for different functions
return match($functionName) {
'get_weather' => 1800, // 30 minutes
'get_stock_price' => 60, // 1 minute
'get_user' => 300, // 5 minutes
default => 600, // 10 minutes
};
}
}Hierarchical Function Calling
Organize functions into hierarchies for complex domains:
class HierarchicalFunctionExecutor
{
protected array $namespaces = [];
public function registerNamespace(string $namespace, array $functions): void
{
$this->namespaces[$namespace] = $functions;
}
public function execute(string $qualifiedName, array $arguments): string
{
[$namespace, $function] = explode('.', $qualifiedName, 2);
if (!isset($this->namespaces[$namespace][$function])) {
throw new \RuntimeException("Function not found: {$qualifiedName}");
}
return ($this->namespaces[$namespace][$function])(...$arguments);
}
}
// Usage
$executor = new HierarchicalFunctionExecutor();
$executor->registerNamespace('user', [
'get' => fn($id) => User::find($id)->toJson(),
'create' => fn($data) => User::create($data)->toJson(),
'delete' => fn($id) => User::destroy($id),
]);
$executor->registerNamespace('order', [
'get' => fn($id) => Order::find($id)->toJson(),
'create' => fn($data) => Order::create($data)->toJson(),
'cancel' => fn($id) => Order::find($id)->update(['status' => 'cancelled']),
]);
// Call: user.get(123) or order.create({...})
$result = $executor->execute('user.get', [123]);Security Considerations
Secure your function calling implementations against abuse and attacks.
Input Validation and Sanitization
class SecureFunctionExecutor
{
protected array $allowedFunctions = [];
public function execute(FunctionCall $call): string
{
// 1. Whitelist validation
if (!$this->isAllowedFunction($call->name)) {
throw new SecurityException("Function not allowed: {$call->name}");
}
// 2. Sanitize inputs
$sanitized = $this->sanitizeArguments($call->arguments);
// 3. Validate argument types
$this->validateArgumentTypes($call->name, $sanitized);
// 4. Check permissions
$this->checkPermissions($call->name);
// 5. Rate limiting
$this->enforceRateLimit($call->name);
// 6. Execute safely
return $this->executeInSandbox($call->name, $sanitized);
}
protected function sanitizeArguments(array $arguments): array
{
return array_map(function ($value) {
if (is_string($value)) {
// Prevent SQL injection
$value = strip_tags($value);
// Prevent path traversal
$value = str_replace(['../', '..\\'], '', $value);
// Prevent command injection
$value = escapeshellarg($value);
}
return $value;
}, $arguments);
}
protected function enforceRateLimit(string $functionName): void
{
$key = 'rate_limit:' . request()->ip() . ':' . $functionName;
if (RateLimiter::tooManyAttempts($key, 10)) {
throw new RateLimitException('Too many requests for function: ' . $functionName);
}
RateLimiter::hit($key, 60);
}
}Sandboxed Execution
Execute untrusted functions in isolated environments:
class SandboxedExecutor
{
public function execute(FunctionCall $call): string
{
// Execute in separate process with timeout
$process = new Process([
'php',
base_path('scripts/execute_function.php'),
$call->name,
json_encode($call->arguments)
], timeout: 30);
$process->run();
if (!$process->isSuccessful()) {
throw new \RuntimeException('Sandboxed execution failed: ' . $process->getErrorOutput());
}
return $process->getOutput();
}
}Audit Logging
Track all function executions for security and compliance:
class AuditedFunctionExecutor
{
public function execute(FunctionCall $call): string
{
$auditId = Str::uuid();
// Log before execution
Log::info('Function execution started', [
'audit_id' => $auditId,
'function' => $call->name,
'arguments' => $this->sanitizeForLogging($call->arguments),
'user_id' => auth()->id(),
'ip' => request()->ip(),
'timestamp' => now()->toIso8601String(),
]);
try {
$result = $this->doExecute($call);
// Log success
Log::info('Function execution completed', [
'audit_id' => $auditId,
'function' => $call->name,
'success' => true,
]);
return $result;
} catch (\Exception $e) {
// Log failure
Log::error('Function execution failed', [
'audit_id' => $auditId,
'function' => $call->name,
'error' => $e->getMessage(),
'success' => false,
]);
throw $e;
}
}
protected function sanitizeForLogging(array $arguments): array
{
// Remove sensitive data from logs
$sensitive = ['password', 'token', 'api_key', 'secret'];
return array_map(function ($key, $value) use ($sensitive) {
if (in_array($key, $sensitive)) {
return '[REDACTED]';
}
return $value;
}, array_keys($arguments), $arguments);
}
}Performance Optimization
Optimize function calling for production workloads.
Function Result Caching
class CachingFunctionExecutor
{
protected array $cacheConfig = [];
public function setCacheConfig(string $functionName, int $ttl, ?callable $keyGenerator = null): void
{
$this->cacheConfig[$functionName] = compact('ttl', 'keyGenerator');
}
public function execute(FunctionCall $call): string
{
if (!isset($this->cacheConfig[$call->name])) {
return $this->doExecute($call);
}
$config = $this->cacheConfig[$call->name];
$key = $config['keyGenerator']
? ($config['keyGenerator'])($call)
: $this->defaultCacheKey($call);
return Cache::remember($key, $config['ttl'], function() use ($call) {
return $this->doExecute($call);
});
}
protected function defaultCacheKey(FunctionCall $call): string
{
return sprintf(
'func_cache:%s:%s',
$call->name,
md5(json_encode($call->arguments))
);
}
}
// Configuration
$executor = new CachingFunctionExecutor();
$executor->setCacheConfig(
'get_weather',
ttl: 1800,
keyGenerator: fn($call) => "weather:{$call->arguments['city']}"
);
$executor->setCacheConfig('get_stock_price', ttl: 60);Batch Function Execution
Execute multiple independent functions in batches:
class BatchFunctionExecutor
{
public function executeBatch(array $functionCalls): array
{
// Group by function name for potential optimization
$grouped = collect($functionCalls)->groupBy(fn($call) => $call->name);
$results = [];
foreach ($grouped as $functionName => $calls) {
if ($this->supportsBatchExecution($functionName)) {
// Execute all at once
$batchResults = $this->executeBatchOptimized($functionName, $calls);
$results = array_merge($results, $batchResults);
} else {
// Execute individually
foreach ($calls as $call) {
$results[] = $this->execute($call);
}
}
}
return $results;
}
protected function executeBatchOptimized(string $functionName, $calls): array
{
// Example: Batch database queries
if ($functionName === 'get_user') {
$ids = collect($calls)->pluck('arguments.id')->toArray();
$users = User::whereIn('id', $ids)->get()->keyBy('id');
return collect($calls)->map(function($call) use ($users) {
$user = $users->get($call->arguments['id']);
return $user ? $user->toJson() : json_encode(['error' => 'User not found']);
})->toArray();
}
// Fallback to individual execution
return array_map(fn($call) => $this->execute($call), $calls);
}
}Lazy Function Evaluation
Defer expensive function execution until results are needed:
class LazyFunctionCall
{
protected ?string $result = null;
public function __construct(
protected FunctionCall $call,
protected FunctionExecutor $executor
) {}
public function get(): string
{
if ($this->result === null) {
$this->result = $this->executor->execute($this->call);
}
return $this->result;
}
public function isEvaluated(): bool
{
return $this->result !== null;
}
}
// Usage
$lazyCall = new LazyFunctionCall($functionCall, $executor);
// Result not computed yet
if (some_condition()) {
// Only evaluate if needed
$result = $lazyCall->get();
}Best Practices
1. Clear Function Boundaries
Design functions with single, well-defined purposes:
// Bad: Function does too much
$functions->addFunction(
name: 'manage_user',
description: 'Creates, updates, or deletes users',
closure: fn($action, $data) => match($action) {
'create' => User::create($data),
'update' => User::find($data['id'])->update($data),
'delete' => User::destroy($data['id']),
}
);
// Good: Separate functions for each action
$functions->addFunction(
name: 'create_user',
description: 'Creates a new user account',
closure: fn(array $data) => User::create($data)->toJson()
);
$functions->addFunction(
name: 'update_user',
description: 'Updates an existing user account',
closure: fn(int $id, array $data) => User::find($id)->update($data)
);
$functions->addFunction(
name: 'delete_user',
description: 'Deletes a user account',
closure: fn(int $id) => User::destroy($id)
);2. Idempotent Functions
Design functions to be safely retryable:
// Bad: Non-idempotent
$functions->addFunction(
name: 'increment_counter',
closure: fn() => Counter::increment()
);
// Good: Idempotent with unique key
$functions->addFunction(
name: 'increment_counter',
closure: function(string $requestId) {
if (Cache::has("counter_increment:{$requestId}")) {
return Cache::get("counter_increment:{$requestId}");
}
$newValue = Counter::increment();
Cache::put("counter_increment:{$requestId}", $newValue, 3600);
return $newValue;
}
);3. Comprehensive Error Messages
Provide helpful error messages for the LLM:
try {
$user = User::where('email', $email)->firstOrFail();
} catch (ModelNotFoundException $e) {
return json_encode([
'success' => false,
'error' => 'user_not_found',
'message' => "No user found with email: {$email}",
'suggestion' => 'Try checking the email address for typos or use the search_users function to find similar emails',
'available_actions' => ['search_users', 'list_all_users']
]);
}4. Function Documentation
Document functions thoroughly for better LLM understanding:
$functions->addFunction(
name: 'search_products',
description: 'Searches for products in the catalog.
Parameters:
- query: Search term (required)
- category: Filter by category (optional)
- minPrice: Minimum price filter (optional)
- maxPrice: Maximum price filter (optional)
- inStock: Only show in-stock items (optional, default: false)
Returns: JSON array of matching products with id, name, price, and stock.
Examples:
- search_products(query="laptop", category="electronics")
- search_products(query="shirt", minPrice=20, maxPrice=50, inStock=true)',
closure: function(string $query, ?string $category = null, ?float $minPrice = null, ?float $maxPrice = null, bool $inStock = false) {
return Product::search($query, compact('category', 'minPrice', 'maxPrice', 'inStock'));
}
);5. Monitor and Alert
Track function execution metrics:
class MonitoredFunctionExecutor
{
public function execute(FunctionCall $call): string
{
$start = microtime(true);
$success = false;
try {
$result = $this->doExecute($call);
$success = true;
return $result;
} catch (\Exception $e) {
throw $e;
} finally {
$duration = microtime(true) - $start;
// Record metrics
Metrics::record('function.execution.duration', $duration, [
'function' => $call->name,
'success' => $success,
]);
// Alert on slow executions
if ($duration > 5.0) {
Alert::send("Slow function execution: {$call->name} took {$duration}s");
}
// Alert on high failure rates
$failureRate = $this->getRecentFailureRate($call->name);
if ($failureRate > 0.2) {
Alert::send("High failure rate for {$call->name}: {$failureRate}%");
}
}
}
}Common Pitfalls
1. Unbounded Recursion
Prevent infinite loops in multi-turn conversations:
// Bad: No turn limit
while (true) {
$response = LLM::functionCall($prompt, $functions);
if (is_string($response)) break;
// Could loop forever
}
// Good: Bounded with max turns
$maxTurns = 10;
for ($i = 0; $i < $maxTurns; $i++) {
$response = LLM::functionCall($prompt, $functions);
if (is_string($response)) break;
}
if ($i >= $maxTurns) {
Log::warning('Max turns reached without completion');
}2. Missing Error Handling
Always handle function execution failures:
// Bad: Unhandled exceptions
$result = $function($arguments);
// Good: Comprehensive error handling
try {
$result = $function($arguments);
} catch (ValidationException $e) {
return json_encode(['error' => 'validation_failed', 'details' => $e->errors()]);
} catch (\Exception $e) {
Log::error('Function failed', ['error' => $e->getMessage()]);
return json_encode(['error' => 'execution_failed']);
}3. Exposing Sensitive Data
Never return sensitive information in function results:
// Bad: Exposes passwords
fn($email) => User::where('email', $email)->first()->toJson()
// Good: Filters sensitive fields
fn($email) => User::where('email', $email)->first()->only(['id', 'name', 'email'])->toJson()4. Not Validating LLM Inputs
Never trust LLM-generated parameters:
// Bad: Direct usage
$userId = $arguments['user_id'];
User::find($userId)->delete();
// Good: Validation and authorization
$userId = (int) $arguments['user_id'];
if ($userId <= 0) {
throw new ValidationException('Invalid user ID');
}
$user = User::findOrFail($userId);
if (!auth()->user()->can('delete', $user)) {
throw new AuthorizationException('Not authorized to delete this user');
}
$user->delete();5. Ignoring Rate Limits
Implement rate limiting to prevent abuse:
use Illuminate\Support\Facades\RateLimiter;
function executeFunction(FunctionCall $call): string
{
$key = 'func:' . auth()->id() . ':' . $call->name;
if (RateLimiter::tooManyAttempts($key, 10)) {
throw new RateLimitException('Too many function calls');
}
RateLimiter::hit($key, 60);
return $this->doExecute($call);
}