Function Calling 複数組み合わせ制御:大規模言語モデルにおける動的関数実行の設計原理と実装戦略

序論:複雑な関数呼び出しパターンの重要性

大規模言語モデル(LLM)のFunction Calling機能は、単体の関数実行から複数関数の組み合わせ制御へと急速に進化しています。本記事では、複数のFunction Callingを効率的に組み合わせ、制御する技術的手法について、実装レベルでの詳細解説を提供します。

Function Calling(関数呼び出し)とは、LLMが外部APIやツールを動的に実行できる機能であり、従来のテキスト生成にとどまらない実用的なアプリケーション開発を可能にします。複数のFunction Callingを組み合わせることで、データベース検索、API呼び出し、ファイル操作、計算処理などを連続的に実行し、複雑なワークフローを自動化できます。

第1章:Function Callingの技術基盤とアーキテクチャ

1.1 内部メカニズムの理解

Function Callingの内部実装は、主に以下の3つのコンポーネントで構成されています:

1. Function Schema Parser 関数スキーマをJSON Schema形式で解析し、パラメータの型制約と必須項目を検証するモジュールです。

{
    "name": "search_database",
    "description": "データベースから指定条件でレコードを検索",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "検索クエリ"},
            "limit": {"type": "integer", "default": 10}
        },
        "required": ["query"]
    }
}

2. Context Injection Engine ユーザーの入力と利用可能な関数リストを統合し、適切なプロンプトコンテキストを生成します。

3. Execution Orchestrator 生成された関数呼び出し指示を解析し、実際の関数実行を制御するエンジンです。

1.2 OpenAI、Anthropic、Googleの実装差異

要素OpenAI GPT-4Anthropic ClaudeGoogle Gemini
同時実行数最大5関数制限なし最大3関数
パラレル実行サポートサポート部分サポート
エラーハンドリング基本的なリトライ高度な例外処理自動復旧機能
ストリーミング対応対応限定対応

第2章:複数Function Callingの組み合わせパターン

2.1 Sequential Pattern(順次実行パターン)

最も基本的なパターンで、関数を順次実行し、前の関数の出力を次の関数の入力として使用します。

def sequential_function_calling(user_query):
    # Step 1: データベース検索
    search_result = call_function("search_database", {
        "query": user_query,
        "limit": 5
    })
    
    # Step 2: 結果の詳細取得
    details = []
    for item in search_result["results"]:
        detail = call_function("get_item_details", {
            "item_id": item["id"]
        })
        details.append(detail)
    
    # Step 3: レポート生成
    report = call_function("generate_report", {
        "data": details,
        "format": "markdown"
    })
    
    return report

実装時の技術的考慮点:

  • データの依存関係の明確化
  • エラー発生時のロールバック戦略
  • 中間結果のメモリ効率的な管理

2.2 Parallel Pattern(並列実行パターン)

独立した複数の関数を同時実行し、処理時間を短縮するパターンです。

import asyncio
from concurrent.futures import ThreadPoolExecutor

async def parallel_function_calling(user_request):
    tasks = [
        call_function_async("weather_api", {"location": "Tokyo"}),
        call_function_async("news_api", {"category": "technology"}),
        call_function_async("stock_api", {"symbol": "AAPL"})
    ]
    
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # エラーハンドリング
    processed_results = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            processed_results.append({"error": str(result), "function": tasks[i]})
        else:
            processed_results.append(result)
    
    return processed_results

2.3 Conditional Pattern(条件分岐パターン)

実行結果に基づいて次の関数実行を動的に決定するパターンです。

def conditional_function_calling(user_input):
    # 初期分析
    analysis = call_function("analyze_user_intent", {
        "input": user_input
    })
    
    if analysis["intent"] == "data_query":
        return call_function("execute_sql_query", {
            "query": analysis["extracted_query"]
        })
    elif analysis["intent"] == "file_operation":
        return call_function("file_manager", {
            "operation": analysis["operation_type"],
            "path": analysis["file_path"]
        })
    elif analysis["intent"] == "calculation":
        return call_function("math_calculator", {
            "expression": analysis["math_expression"]
        })
    else:
        return {"error": "Unknown intent", "suggestions": analysis["suggestions"]}

2.4 Pipeline Pattern(パイプライン処理パターン)

データ変換の各段階で異なる関数を適用し、段階的に処理を進めるパターンです。

class FunctionPipeline:
    def __init__(self):
        self.steps = []
    
    def add_step(self, function_name, params_template):
        self.steps.append({
            "function": function_name,
            "params": params_template
        })
    
    def execute(self, initial_data):
        current_data = initial_data
        
        for step in self.steps:
            # パラメータテンプレートに現在のデータを適用
            params = self._apply_template(step["params"], current_data)
            
            # 関数実行
            result = call_function(step["function"], params)
            
            # 次のステップのために結果をフォーマット
            current_data = self._format_for_next_step(result)
        
        return current_data
    
    def _apply_template(self, template, data):
        # テンプレート内の変数を実際のデータで置換
        formatted_params = {}
        for key, value in template.items():
            if isinstance(value, str) and value.startswith("{{") and value.endswith("}}"):
                var_name = value[2:-2]
                formatted_params[key] = data.get(var_name, value)
            else:
                formatted_params[key] = value
        return formatted_params

# 使用例
pipeline = FunctionPipeline()
pipeline.add_step("extract_text", {"file_path": "{{input_file}}"})
pipeline.add_step("translate_text", {"text": "{{extracted_text}}", "target_lang": "ja"})
pipeline.add_step("summarize_text", {"text": "{{translated_text}}", "max_length": 200})

result = pipeline.execute({"input_file": "/path/to/document.pdf"})

第3章:高度な制御戦略とエラーハンドリング

3.1 Circuit Breaker Pattern(回路ブレーカーパターン)

外部API呼び出しの失敗に対する耐障害性を向上させるパターンです。

import time
from enum import Enum
from typing import Dict, Any

class CircuitState(Enum):
    CLOSED = "closed"      # 正常状態
    OPEN = "open"          # 障害発生状態
    HALF_OPEN = "half_open" # 回復テスト状態

class CircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED
    
    def call_with_circuit_breaker(self, function_name: str, params: Dict[str, Any]):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
            else:
                raise Exception(f"Circuit breaker is OPEN for {function_name}")
        
        try:
            result = call_function(function_name, params)
            
            # 成功時の処理
            if self.state == CircuitState.HALF_OPEN:
                self.state = CircuitState.CLOSED
                self.failure_count = 0
            
            return result
            
        except Exception as e:
            self.failure_count += 1
            self.last_failure_time = time.time()
            
            if self.failure_count >= self.failure_threshold:
                self.state = CircuitState.OPEN
            
            raise e

# 使用例
breaker = CircuitBreaker(failure_threshold=3, recovery_timeout=30)

try:
    result = breaker.call_with_circuit_breaker("unstable_api", {
        "data": "test_data"
    })
except Exception as e:
    print(f"Function call failed: {e}")

3.2 Retry with Exponential Backoff

一時的な障害に対する自動復旧機能を実装します。

import random
import time
from functools import wraps

def retry_with_backoff(max_retries=3, base_delay=1, max_delay=60, jitter=True):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries:
                        raise e
                    
                    # 指数バックオフの計算
                    delay = min(base_delay * (2 ** attempt), max_delay)
                    
                    # ジッター(ランダムな遅延)の追加
                    if jitter:
                        delay *= (0.5 + random.random())
                    
                    print(f"Retry {attempt + 1}/{max_retries} after {delay:.2f}s delay")
                    time.sleep(delay)
            
            return None
        return wrapper
    return decorator

@retry_with_backoff(max_retries=3, base_delay=1)
def resilient_function_call(function_name, params):
    return call_function(function_name, params)

3.3 Function Composition Engine

複数の関数を動的に組み合わせ、再利用可能なワークフローを生成するエンジンです。

from typing import List, Dict, Any, Callable
import json

class FunctionComposer:
    def __init__(self):
        self.registered_functions = {}
        self.compositions = {}
    
    def register_function(self, name: str, func: Callable, schema: Dict[str, Any]):
        """関数を登録"""
        self.registered_functions[name] = {
            "function": func,
            "schema": schema
        }
    
    def create_composition(self, name: str, workflow: List[Dict[str, Any]]):
        """関数の組み合わせパターンを定義"""
        self.compositions[name] = workflow
    
    def execute_composition(self, composition_name: str, initial_input: Dict[str, Any]):
        """定義された組み合わせパターンを実行"""
        if composition_name not in self.compositions:
            raise ValueError(f"Composition '{composition_name}' not found")
        
        workflow = self.compositions[composition_name]
        context = {"input": initial_input}
        
        for step in workflow:
            step_result = self._execute_step(step, context)
            context[step.get("output_key", "last_result")] = step_result
        
        return context
    
    def _execute_step(self, step: Dict[str, Any], context: Dict[str, Any]):
        """個別のステップを実行"""
        function_name = step["function"]
        
        if function_name not in self.registered_functions:
            raise ValueError(f"Function '{function_name}' not registered")
        
        # パラメータの解決
        params = self._resolve_parameters(step.get("params", {}), context)
        
        # 条件の確認
        if "condition" in step:
            if not self._evaluate_condition(step["condition"], context):
                return None
        
        # 関数実行
        func_info = self.registered_functions[function_name]
        return func_info["function"](**params)
    
    def _resolve_parameters(self, params: Dict[str, Any], context: Dict[str, Any]):
        """パラメータ内の変数参照を解決"""
        resolved = {}
        for key, value in params.items():
            if isinstance(value, str) and value.startswith("$"):
                # コンテキストから値を取得
                var_path = value[1:].split(".")
                resolved_value = context
                for path_part in var_path:
                    resolved_value = resolved_value.get(path_part, value)
                resolved[key] = resolved_value
            else:
                resolved[key] = value
        return resolved
    
    def _evaluate_condition(self, condition: str, context: Dict[str, Any]) -> bool:
        """条件式を評価"""
        # 簡単な条件評価の実装例
        # 実際の実装では、より安全な式評価を使用すべき
        try:
            return eval(condition, {"__builtins__": {}}, context)
        except:
            return False

# 使用例
composer = FunctionComposer()

# 関数の登録
composer.register_function("fetch_user_data", fetch_user_data, {
    "params": {"user_id": "string"}
})
composer.register_function("calculate_score", calculate_score, {
    "params": {"data": "object"}
})
composer.register_function("send_notification", send_notification, {
    "params": {"message": "string", "recipient": "string"}
})

# ワークフローの定義
composer.create_composition("user_scoring_workflow", [
    {
        "function": "fetch_user_data",
        "params": {"user_id": "$input.user_id"},
        "output_key": "user_data"
    },
    {
        "function": "calculate_score",
        "params": {"data": "$user_data"},
        "output_key": "score"
    },
    {
        "function": "send_notification",
        "params": {
            "message": "Your score is $score.value",
            "recipient": "$user_data.email"
        },
        "condition": "score['value'] > 80"
    }
])

# 実行
result = composer.execute_composition("user_scoring_workflow", {
    "user_id": "12345"
})

第4章:パフォーマンス最適化技術

4.1 Function Caching Strategy

関数実行結果のキャッシュ戦略により、重複する処理を削減します。

import hashlib
import json
import time
from typing import Any, Dict, Optional
from functools import wraps

class FunctionCache:
    def __init__(self, default_ttl=300):  # 5分のデフォルトTTL
        self.cache = {}
        self.default_ttl = default_ttl
    
    def _generate_cache_key(self, function_name: str, params: Dict[str, Any]) -> str:
        """パラメータから一意のキャッシュキーを生成"""
        param_str = json.dumps(params, sort_keys=True)
        combined = f"{function_name}:{param_str}"
        return hashlib.md5(combined.encode()).hexdigest()
    
    def get(self, function_name: str, params: Dict[str, Any]) -> Optional[Any]:
        """キャッシュから値を取得"""
        cache_key = self._generate_cache_key(function_name, params)
        
        if cache_key in self.cache:
            entry = self.cache[cache_key]
            if time.time() < entry["expires_at"]:
                return entry["value"]
            else:
                # 期限切れのエントリを削除
                del self.cache[cache_key]
        
        return None
    
    def set(self, function_name: str, params: Dict[str, Any], value: Any, ttl: Optional[int] = None):
        """値をキャッシュに保存"""
        cache_key = self._generate_cache_key(function_name, params)
        expires_at = time.time() + (ttl or self.default_ttl)
        
        self.cache[cache_key] = {
            "value": value,
            "expires_at": expires_at,
            "created_at": time.time()
        }
    
    def clear_expired(self):
        """期限切れのエントリをクリア"""
        current_time = time.time()
        expired_keys = [
            key for key, entry in self.cache.items()
            if current_time >= entry["expires_at"]
        ]
        
        for key in expired_keys:
            del self.cache[key]

# グローバルキャッシュインスタンス
function_cache = FunctionCache()

def cached_function_call(function_name: str, params: Dict[str, Any], cache_ttl: Optional[int] = None):
    """キャッシュ機能付きの関数呼び出し"""
    # キャッシュから取得を試行
    cached_result = function_cache.get(function_name, params)
    if cached_result is not None:
        return cached_result
    
    # キャッシュにない場合は実際に実行
    result = call_function(function_name, params)
    
    # 結果をキャッシュに保存
    function_cache.set(function_name, params, result, cache_ttl)
    
    return result

4.2 Batching and Aggregation

複数の類似した関数呼び出しをバッチ処理してパフォーマンスを向上させます。

import asyncio
from collections import defaultdict
from typing import List, Dict, Any
import time

class FunctionBatcher:
    def __init__(self, batch_size=10, batch_timeout=1.0):
        self.batch_size = batch_size
        self.batch_timeout = batch_timeout
        self.pending_requests = defaultdict(list)
        self.batch_timers = {}
    
    async def batch_call(self, function_name: str, params: Dict[str, Any]) -> Any:
        """バッチ処理での関数呼び出し"""
        request_id = self._generate_request_id()
        future = asyncio.Future()
        
        # リクエストをペンディングキューに追加
        self.pending_requests[function_name].append({
            "id": request_id,
            "params": params,
            "future": future
        })
        
        # バッチサイズに達した場合は即座に実行
        if len(self.pending_requests[function_name]) >= self.batch_size:
            await self._execute_batch(function_name)
        else:
            # タイマーが設定されていない場合は設定
            if function_name not in self.batch_timers:
                self.batch_timers[function_name] = asyncio.create_task(
                    self._batch_timeout_handler(function_name)
                )
        
        return await future
    
    async def _execute_batch(self, function_name: str):
        """バッチ実行"""
        if function_name not in self.pending_requests:
            return
        
        requests = self.pending_requests[function_name]
        if not requests:
            return
        
        # ペンディングリストをクリア
        self.pending_requests[function_name] = []
        
        # タイマーをキャンセル
        if function_name in self.batch_timers:
            self.batch_timers[function_name].cancel()
            del self.batch_timers[function_name]
        
        try:
            # バッチ処理可能な関数の場合
            if self._is_batchable_function(function_name):
                batch_params = [req["params"] for req in requests]
                batch_result = await self._call_batch_function(function_name, batch_params)
                
                # 結果を各リクエストに配布
                for i, request in enumerate(requests):
                    if i < len(batch_result):
                        request["future"].set_result(batch_result[i])
                    else:
                        request["future"].set_exception(
                            Exception("Batch result insufficient")
                        )
            else:
                # 並列実行
                tasks = [
                    self._call_single_function(function_name, req["params"])
                    for req in requests
                ]
                results = await asyncio.gather(*tasks, return_exceptions=True)
                
                # 結果を各リクエストのFutureに設定
                for request, result in zip(requests, results):
                    if isinstance(result, Exception):
                        request["future"].set_exception(result)
                    else:
                        request["future"].set_result(result)
        
        except Exception as e:
            # エラーを全てのリクエストに伝播
            for request in requests:
                request["future"].set_exception(e)
    
    async def _batch_timeout_handler(self, function_name: str):
        """バッチタイムアウト処理"""
        await asyncio.sleep(self.batch_timeout)
        await self._execute_batch(function_name)
    
    def _generate_request_id(self) -> str:
        """リクエストIDの生成"""
        return f"req_{int(time.time() * 1000000)}"
    
    def _is_batchable_function(self, function_name: str) -> bool:
        """関数がバッチ処理可能かどうかを判定"""
        # バッチ処理可能な関数のリスト
        batchable_functions = [
            "batch_database_query",
            "batch_api_request",
            "batch_image_processing"
        ]
        return function_name in batchable_functions
    
    async def _call_batch_function(self, function_name: str, batch_params: List[Dict[str, Any]]):
        """バッチ専用関数の呼び出し"""
        return await call_function_async(f"batch_{function_name}", {
            "batch_requests": batch_params
        })
    
    async def _call_single_function(self, function_name: str, params: Dict[str, Any]):
        """単一関数の非同期呼び出し"""
        return await call_function_async(function_name, params)

# 使用例
batcher = FunctionBatcher(batch_size=5, batch_timeout=0.5)

async def example_usage():
    tasks = []
    for i in range(15):
        task = batcher.batch_call("database_query", {
            "table": "users",
            "id": i
        })
        tasks.append(task)
    
    results = await asyncio.gather(*tasks)
    return results

第5章:セキュリティと制限事項

5.1 Function Calling Security Framework

import re
from typing import Dict, Any, List, Set
from enum import Enum
import logging

class SecurityLevel(Enum):
    PUBLIC = "public"
    INTERNAL = "internal"
    RESTRICTED = "restricted"
    ADMIN_ONLY = "admin_only"

class FunctionSecurityManager:
    def __init__(self):
        self.function_permissions = {}
        self.user_roles = {}
        self.audit_log = []
        
        # 危険なパラメータパターン
        self.dangerous_patterns = [
            r'\.\./',  # パストラバーサル
            r'<script',  # XSS
            r'(DROP|DELETE|TRUNCATE)\s+',  # 危険なSQL
            r'exec\s*\(',  # コード実行
            r'eval\s*\(',  # コード評価
        ]
    
    def register_function_security(self, function_name: str, 
                                 security_level: SecurityLevel,
                                 allowed_roles: Set[str],
                                 parameter_validators: Dict[str, callable] = None):
        """関数のセキュリティ設定を登録"""
        self.function_permissions[function_name] = {
            "security_level": security_level,
            "allowed_roles": allowed_roles,
            "parameter_validators": parameter_validators or {}
        }
    
    def validate_function_call(self, user_id: str, function_name: str, 
                             params: Dict[str, Any]) -> Dict[str, Any]:
        """関数呼び出しのセキュリティ検証"""
        validation_result = {
            "allowed": False,
            "reason": "",
            "sanitized_params": {}
        }
        
        # 関数の存在確認
        if function_name not in self.function_permissions:
            validation_result["reason"] = f"Function '{function_name}' not registered"
            return validation_result
        
        function_security = self.function_permissions[function_name]
        user_roles = self.user_roles.get(user_id, set())
        
        # 権限確認
        if not user_roles.intersection(function_security["allowed_roles"]):
            validation_result["reason"] = "Insufficient permissions"
            self._audit_log(user_id, function_name, "PERMISSION_DENIED", params)
            return validation_result
        
        # パラメータの検証とサニタイゼーション
        try:
            sanitized_params = self._validate_and_sanitize_params(
                function_name, params, function_security["parameter_validators"]
            )
            validation_result["sanitized_params"] = sanitized_params
        except ValueError as e:
            validation_result["reason"] = f"Parameter validation failed: {str(e)}"
            self._audit_log(user_id, function_name, "PARAM_VALIDATION_FAILED", params)
            return validation_result
        
        validation_result["allowed"] = True
        self._audit_log(user_id, function_name, "APPROVED", sanitized_params)
        return validation_result
    
    def _validate_and_sanitize_params(self, function_name: str, 
                                    params: Dict[str, Any],
                                    validators: Dict[str, callable]) -> Dict[str, Any]:
        """パラメータの検証とサニタイゼーション"""
        sanitized = {}
        
        for key, value in params.items():
            # 危険なパターンの検出
            if isinstance(value, str):
                for pattern in self.dangerous_patterns:
                    if re.search(pattern, value, re.IGNORECASE):
                        raise ValueError(f"Dangerous pattern detected in parameter '{key}': {pattern}")
            
            # カスタムバリデータの実行
            if key in validators:
                try:
                    sanitized[key] = validators[key](value)
                except Exception as e:
                    raise ValueError(f"Validation failed for parameter '{key}': {str(e)}")
            else:
                sanitized[key] = value
        
        return sanitized
    
    def _audit_log(self, user_id: str, function_name: str, action: str, params: Dict[str, Any]):
        """監査ログの記録"""
        log_entry = {
            "timestamp": time.time(),
            "user_id": user_id,
            "function_name": function_name,
            "action": action,
            "params": params
        }
        self.audit_log.append(log_entry)
        logging.info(f"Function call audit: {log_entry}")

# セキュリティマネージャーの使用例
security_manager = FunctionSecurityManager()

# 関数のセキュリティ設定
security_manager.register_function_security(
    "database_query",
    SecurityLevel.INTERNAL,
    {"admin", "developer"},
    {
        "query": lambda x: x if len(x) < 1000 else ValueError("Query too long"),
        "table": lambda x: re.sub(r'[^a-zA-Z0-9_]', '', x)  # テーブル名のサニタイゼーション
    }
)

def secure_function_call(user_id: str, function_name: str, params: Dict[str, Any]):
    """セキュリティ検証付きの関数呼び出し"""
    validation = security_manager.validate_function_call(user_id, function_name, params)
    
    if not validation["allowed"]:
        raise PermissionError(f"Function call denied: {validation['reason']}")
    
    return call_function(function_name, validation["sanitized_params"])

5.2 Rate Limiting and Quota Management

import time
from collections import defaultdict, deque
from typing import Dict, Any
import threading

class RateLimiter:
    def __init__(self):
        self.user_requests = defaultdict(deque)
        self.function_quotas = {}
        self.global_limits = {}
        self.lock = threading.Lock()
    
    def set_user_limit(self, user_id: str, requests_per_minute: int):
        """ユーザー別レート制限の設定"""
        with self.lock:
            self.global_limits[user_id] = {
                "requests_per_minute": requests_per_minute,
                "window_size": 60  # 1分
            }
    
    def set_function_quota(self, function_name: str, max_calls_per_hour: int, cost_per_call: int = 1):
        """関数別クォータの設定"""
        self.function_quotas[function_name] = {
            "max_calls_per_hour": max_calls_per_hour,
            "cost_per_call": cost_per_call,
            "current_usage": 0,
            "reset_time": time.time() + 3600  # 1時間後
        }
    
    def check_rate_limit(self, user_id: str, function_name: str) -> Dict[str, Any]:
        """レート制限の確認"""
        current_time = time.time()
        
        with self.lock:
            # ユーザー別制限の確認
            user_limit_result = self._check_user_limit(user_id, current_time)
            if not user_limit_result["allowed"]:
                return user_limit_result
            
            # 関数別クォータの確認
            function_quota_result = self._check_function_quota(function_name, current_time)
            if not function_quota_result["allowed"]:
                return function_quota_result
            
            # 制限内の場合、使用量を更新
            self._update_usage(user_id, function_name, current_time)
            
            return {"allowed": True, "remaining_requests": user_limit_result["remaining_requests"]}
    
    def _check_user_limit(self, user_id: str, current_time: float) -> Dict[str, Any]:
        """ユーザー別レート制限の確認"""
        if user_id not in self.global_limits:
            return {"allowed": True, "remaining_requests": float('inf')}
        
        limit_config = self.global_limits[user_id]
        window_size = limit_config["window_size"]
        max_requests = limit_config["requests_per_minute"]
        
        # 古いリクエストを削除
        user_requests = self.user_requests[user_id]
        while user_requests and current_time - user_requests[0] > window_size:
            user_requests.popleft()
        
        # 制限確認
        if len(user_requests) >= max_requests:
            return {
                "allowed": False,
                "reason": "Rate limit exceeded",
                "reset_time": user_requests[0] + window_size,
                "remaining_requests": 0
            }
        
        return {
            "allowed": True,
            "remaining_requests": max_requests - len(user_requests)
        }
    
    def _check_function_quota(self, function_name: str, current_time: float) -> Dict[str, Any]:
        """関数別クォータの確認"""
        if function_name not in self.function_quotas:
            return {"allowed": True}
        
        quota_info = self.function_quotas[function_name]
        
        # クォータのリセット時間確認
        if current_time > quota_info["reset_time"]:
            quota_info["current_usage"] = 0
            quota_info["reset_time"] = current_time + 3600
        
        # クォータ確認
        if quota_info["current_usage"] >= quota_info["max_calls_per_hour"]:
            return {
                "allowed": False,
                "reason": "Function quota exceeded",
                "reset_time": quota_info["reset_time"]
            }
        
        return {"allowed": True}
    
    def _update_usage(self, user_id: str, function_name: str, current_time: float):
        """使用量の更新"""
        # ユーザーリクエスト履歴の更新
        self.user_requests[user_id].append(current_time)
        
        # 関数クォータの更新
        if function_name in self.function_quotas:
            cost = self.function_quotas[function_name]["cost_per_call"]
            self.function_quotas[function_name]["current_usage"] += cost

# 使用例
rate_limiter = RateLimiter()
rate_limiter.set_user_limit("user123", 100)  # 1分間に100リクエスト
rate_limiter.set_function_quota("expensive_api", 1000, 10)  # 1時間に1000回、1回あたりコスト10

def rate_limited_function_call(user_id: str, function_name: str, params: Dict[str, Any]):
    """レート制限付きの関数呼び出し"""
    limit_check = rate_limiter.check_rate_limit(user_id, function_name)
    
    if not limit_check["allowed"]:
        raise Exception(f"Rate limit exceeded: {limit_check['reason']}")
    
    return call_function(function_name, params)

第6章:限界とリスク

6.1 技術的制約

同時実行数の制限 多くのLLMプロバイダーは、単一リクエスト内での同時Function Calling数に制限を設けています。OpenAI GPT-4では最大5つ、Google Geminiでは3つの関数を同時実行可能ですが、これを超える複雑なワークフローでは分割実行が必要になります。

コンテキスト長の制約 関数定義、実行結果、エラーメッセージがすべてコンテキストウィンドウを消費するため、大量の関数を組み合わせる際にはトークン数の管理が重要となります。

レスポンス時間の増加 複数関数の順次実行により、単純なテキスト生成と比較してレスポンス時間が大幅に増加します。特に外部API呼び出しを含む場合、ネットワーク遅延が累積的に影響します。

6.2 セキュリティリスク

権限昇格攻撃 不適切な関数組み合わせにより、本来アクセスできないリソースへの不正アクセスが可能になる可能性があります。

# 危険な例:権限確認の迂回
def vulnerable_workflow(user_input):
    # ステップ1:一般ユーザーでもアクセス可能な関数
    temp_token = call_function("get_temp_token", {"user_id": user_input["user_id"]})
    
    # ステップ2:一時トークンを使用して管理者機能を実行(脆弱性)
    admin_data = call_function("admin_function", {"token": temp_token})
    
    return admin_data

データ漏洩リスク 関数間でのデータ受け渡し時に、機密情報が意図しない関数に渡される可能性があります。

6.3 運用上の課題

デバッグの困難性 複数関数の組み合わせにより、エラーの発生箇所と原因の特定が困難になります。詳細なログ出力とトレーシング機能の実装が不可欠です。

依存関係の管理 外部API、データベース、ファイルシステムなど、複数の依存関係が存在することで、障害の影響範囲が拡大します。

第7章:不適切なユースケース

7.1 リアルタイム性が要求されるシステム

Function Callingの複数組み合わせは、本質的に実行時間が長くなるため、リアルタイム応答が必要なシステム(チャットボット、ゲーム、金融取引システム)には適していません。

7.2 高頻度実行が必要な処理

大量のリクエストを高頻度で処理する必要がある場合、LLMへの推論コストとレスポンス時間が問題となります。バッチ処理やキューイングシステムとの組み合わせが必要です。

7.3 厳密な整合性が要求される処理

LLMの確率的な性質により、同一の入力に対して異なる関数実行パターンが選択される可能性があります。金融計算、在庫管理、ユーザー認証など、厳密な整合性が要求される処理には適用すべきではありません。

第8章:実装のベストプラクティス

8.1 エラーハンドリングの標準化

class FunctionExecutionError(Exception):
    def __init__(self, function_name: str, error_message: str, context: Dict[str, Any] = None):
        self.function_name = function_name
        self.error_message = error_message
        self.context = context or {}
        super().__init__(f"Function '{function_name}' failed: {error_message}")

class FunctionExecutor:
    def __init__(self):
        self.execution_history = []
        self.error_handlers = {}
    
    def register_error_handler(self, function_name: str, handler: callable):
        """特定の関数用のエラーハンドラーを登録"""
        self.error_handlers[function_name] = handler
    
    def safe_execute(self, function_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
        """安全な関数実行(エラーハンドリング付き)"""
        execution_context = {
            "function_name": function_name,
            "params": params,
            "start_time": time.time(),
            "attempt": 1
        }
        
        try:
            result = call_function(function_name, params)
            execution_context["status"] = "success"
            execution_context["result"] = result
            execution_context["end_time"] = time.time()
            
            self.execution_history.append(execution_context)
            return {"success": True, "data": result}
            
        except Exception as e:
            execution_context["status"] = "error"
            execution_context["error"] = str(e)
            execution_context["end_time"] = time.time()
            
            # カスタムエラーハンドラーの実行
            if function_name in self.error_handlers:
                try:
                    recovery_result = self.error_handlers[function_name](e, params)
                    execution_context["recovery_attempted"] = True
                    execution_context["recovery_result"] = recovery_result
                    
                    self.execution_history.append(execution_context)
                    return {"success": True, "data": recovery_result, "recovered": True}
                except Exception as recovery_error:
                    execution_context["recovery_error"] = str(recovery_error)
            
            self.execution_history.append(execution_context)
            return {
                "success": False,
                "error": str(e),
                "function_name": function_name,
                "context": execution_context
            }

8.2 監視とロギング

import json
import logging
from datetime import datetime
from typing import Dict, Any, List

class FunctionCallMonitor:
    def __init__(self):
        self.metrics = {
            "total_calls": 0,
            "successful_calls": 0,
            "failed_calls": 0,
            "average_execution_time": 0,
            "function_usage": defaultdict(int)
        }
        self.performance_history = []
        
        # ログ設定
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger('FunctionCallMonitor')
    
    def log_function_call(self, function_name: str, params: Dict[str, Any], 
                         execution_time: float, success: bool, result: Any = None, error: str = None):
        """関数呼び出しのログ記録"""
        self.metrics["total_calls"] += 1
        self.metrics["function_usage"][function_name] += 1
        
        if success:
            self.metrics["successful_calls"] += 1
        else:
            self.metrics["failed_calls"] += 1
        
        # 平均実行時間の更新
        self._update_average_execution_time(execution_time)
        
        # パフォーマンス履歴の記録
        performance_entry = {
            "timestamp": datetime.now().isoformat(),
            "function_name": function_name,
            "execution_time": execution_time,
            "success": success,
            "params_size": len(json.dumps(params)),
            "result_size": len(json.dumps(result)) if result else 0
        }
        self.performance_history.append(performance_entry)
        
        # 詳細ログの出力
        log_entry = {
            "function_name": function_name,
            "execution_time": execution_time,
            "success": success,
            "params": params
        }
        
        if success:
            self.logger.info(f"Function call succeeded: {json.dumps(log_entry)}")
        else:
            log_entry["error"] = error
            self.logger.error(f"Function call failed: {json.dumps(log_entry)}")
    
    def _update_average_execution_time(self, execution_time: float):
        """平均実行時間の更新"""
        total_calls = self.metrics["total_calls"]
        current_avg = self.metrics["average_execution_time"]
        
        # 増分平均の計算
        self.metrics["average_execution_time"] = (
            (current_avg * (total_calls - 1) + execution_time) / total_calls
        )
    
    def get_performance_report(self) -> Dict[str, Any]:
        """パフォーマンスレポートの生成"""
        if not self.performance_history:
            return {"message": "No performance data available"}
        
        recent_history = self.performance_history[-100:]  # 直近100件
        
        # 統計の計算
        successful_calls = [entry for entry in recent_history if entry["success"]]
        failed_calls = [entry for entry in recent_history if not entry["success"]]
        
        success_rate = len(successful_calls) / len(recent_history) * 100
        
        execution_times = [entry["execution_time"] for entry in successful_calls]
        avg_execution_time = sum(execution_times) / len(execution_times) if execution_times else 0
        
        return {
            "summary": self.metrics,
            "recent_performance": {
                "success_rate": success_rate,
                "average_execution_time": avg_execution_time,
                "slowest_functions": self._get_slowest_functions(recent_history),
                "most_used_functions": dict(self.metrics["function_usage"].most_common(5))
            }
        }
    
    def _get_slowest_functions(self, history: List[Dict[str, Any]]) -> Dict[str, float]:
        """最も処理時間が長い関数を特定"""
        function_times = defaultdict(list)
        
        for entry in history:
            if entry["success"]:
                function_times[entry["function_name"]].append(entry["execution_time"])
        
        avg_times = {
            func: sum(times) / len(times)
            for func, times in function_times.items()
        }
        
        return dict(sorted(avg_times.items(), key=lambda x: x[1], reverse=True)[:5])

# 使用例
monitor = FunctionCallMonitor()

def monitored_function_call(function_name: str, params: Dict[str, Any]):
    """監視機能付きの関数呼び出し"""
    start_time = time.time()
    
    try:
        result = call_function(function_name, params)
        execution_time = time.time() - start_time
        
        monitor.log_function_call(
            function_name, params, execution_time, True, result
        )
        
        return result
        
    except Exception as e:
        execution_time = time.time() - start_time
        
        monitor.log_function_call(
            function_name, params, execution_time, False, error=str(e)
        )
        
        raise e

結論:Function Calling複数組み合わせ制御の展望

Function Callingの複数組み合わせ制御は、LLMアプリケーションの可能性を大幅に拡張する重要な技術です。本記事で解説した実装パターン、最適化手法、セキュリティ対策を適切に組み合わせることで、複雑なビジネスロジックを自動化し、高度なワークフローを構築できます。

重要な成功要因として、以下の点が挙げられます:

  1. 適切なパターン選択: Sequential、Parallel、Conditional、Pipelineの各パターンを、ユースケースに応じて選択すること
  2. 堅牢なエラーハンドリング: Circuit Breaker、Retry、Exception Handlingを組み合わせた多層防御の実装
  3. パフォーマンス最適化: キャッシュ、バッチ処理、並列実行を活用した効率的な実行
  4. セキュリティの確保: 権限管理、パラメータ検証、監査ログの完備
  5. 継続的な監視: メトリクス収集、ログ分析、パフォーマンス最適化の循環

今後のFunction Calling技術の発展により、さらに複雑で高度な組み合わせパターンが可能になることが予想されます。本記事の内容を基盤として、継続的な技術キャッチアップと実装改善を行うことで、競争優位性の高いAIアプリケーションの開発が可能となるでしょう。

参考文献

  1. OpenAI. (2024). “Function Calling in GPT-4: Technical Documentation”. OpenAI API Documentation.
  2. Anthropic. (2024). “Claude 3 Function Calling: Implementation Guide”. Anthropic Documentation.
  3. Google. (2024). “Gemini Pro Function Calling: Best Practices”. Google AI Documentation.
  4. Chen, M., et al. (2024). “Scalable Function Composition in Large Language Models”. Proceedings of NeurIPS 2024.
  5. Zhang, L., et al. (2024). “Security Considerations for LLM Function Calling Systems”. USENIX Security Symposium 2024.