Appel d'outils

Function Calling est le mécanisme central qui permet aux LLM d'utiliser des outils externes.

Function Calling permet au modèle de décider quand appeler des outils, quel outil appeler et quels paramètres transmettre.

Pourquoi Function Calling est-il nécessaire ?

Imaginez qu'un LLM est un consultant intelligent mais démuni. Il sait beaucoup de choses, mais ne peut pas :

  • Obtenir des informations en temps réel (comme la météo la plus récente, les cours boursiers)
  • Effectuer des calculs (comme des opérations mathématiques complexes).
  • Piloter des systèmes externes (comme envoyer des e-mails, lire/écrire des fichiers)

Function Calling donne des mains au LLM, lui permettant de dépasser ses propres limites et d'exécuter de vraies tâches.

Le principe de fonctionnement de Function Calling.

Le flux de base de Function Calling est le suivant :

Concept clé

  • Définition de l'outilDécrire la fonction, les paramètres et la valeur de retour d'un outil.
  • Sélection de l'outil: le LLM sélectionne l'outil approprié en fonction de la question de l'utilisateur.
  • Extraction des paramètres: : le LLM extrait les paramètres requis par l'outil à partir de la question.
  • Traitement des résultatsIntégrer les résultats d'exécution de l'outil dans la réponse finale

Explication de l'exemple

Supposons que nous ayons un outil de requête météo. Lorsqu'un utilisateur demandeQuel temps fait-il à Pékin aujourd'hui ?time:

  1. Le LLM identifie qu'il doit appeler l'outil de requête météo
  2. Extraire les paramètres de la question:city="北京", date="今天"
  3. Appeler l'API météo pour récupérer les données
  4. Intègre les données météo dans une réponse conviviale et la renvoie à l'utilisateur

Rédaction de la définition et de la description de l'outil

Pour permettre au LLM d'utiliser correctement l'outil, celui-ci doit d'abord être clairement défini. Une bonne définition d'outil doit être comme un manuel d'instructions clair qui permet au LLM de comprendre:

  • Que fait cet outil ?
  • Quand doit-il être utilisé ?
  • Quels paramètres sont nécessaires ?
  • Quel est le format des paramètres ?

La structure des définitions d'outils.

Une définition complète d'outil comprend généralement les parties suivantes:

Exemple

# Exemple de structure de définition d'outil
weather_tool = {
    "name": "get_weather",  # Nom de l'outil
    "description": "Obtenir les informations météorologiques pour la ville spécifiée",  # Description de l'outil
    "parameters": {  # Définition des paramètres
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": Nom de la ville, par exemple 'Beijing', 'Shanghai'
            },
            "date": {
                "type": "string",
                "description": Date, format 'YYYY-MM-DD', ou 'aujourd'hui', 'demain',
                "enum": ["Aujourd'hui", "Demain", "Après-demain"]
            }
        },
        "required": ["city"]  # Paramètres requis
    }
}

Rédigez des descriptions d'outils de haute qualité.

1. Rendez la description claire et précise

  • Mauvaise description.: query weather
  • Bonne description: Obtenez les informations météorologiques pour une ville spécifiée à une date spécifique, y compris la température, l'humidité, la vitesse du vent et les conditions météorologiques (ensoleillé, pluvieux, nuageux, etc.)

2. Rendez les descriptions des paramètres détaillées

Mauvaise description de paramètre:

"city": {"type": "string"}

Bonne description de paramètre :

"city": {
    "type": "string",
    "description": "完整的城市名称,如'北京市'、'上海市'。不要使用简称或拼音。"
}

3. Utilisez des énumérations pour contraindre les options

Pour des options limitées, utilisez des énumérations pour aider le LLM à comprendre :

"unit": {
    "type": "string",
    "description": "温度单位",
    "enum": ["celsius", "fahrenheit"],
    "default": "celsius"
}

4. Fournissez des exemples de valeurs

Fournissez des exemples dans la description pour aider le LLM à comprendre le format :

"date": {
    "type": "string",
    "description": "日期,格式应为'YYYY-MM-DD',例如'2024-06-15'"
}

Exemple de définition d'utilitaire

1. Outil de calculatrice

calculator_tool = {
    "name": "calculate",
    "description": "执行数学计算,支持加减乘除、幂运算等基本运算",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "数学表达式,如'2 + 3 * 4'、'sqrt(16)'、'sin(30)'"
            }
        },
        "required": ["expression"]
    }
}

2. Outil de recherche

search_tool = {
    "name": "search_web",
    "description": "在互联网上搜索最新信息",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "搜索关键词,尽量具体明确"
            },
            "num_results": {
                "type": "integer",
                "description": "返回结果数量,默认为5",
                "default": 5,
                "minimum": 1,
                "maximum": 10
            }
        },
        "required": ["query"]
    }
}

3. Outil d'opération sur les fichiers

file_tool = {
    "name": "read_file",
    "description": "读取指定文件的内容",
    "parameters": {
        "type": "object",
        "properties": {
            "file_path": {
                "type": "string",
                "description": "文件的完整路径,如'/home/user/document.txt'"
            },
            "encoding": {
                "type": "string",
                "description": "文件编码,默认为'utf-8'",
                "default": "utf-8"
            }
        },
        "required": ["file_path"]
    }
}

Bonnes pratiques pour la définition d'outils.

  • Nom concis et clair:Utilisez des verbes au début, tels queget_、calculate_、search_
  • Description complète et détaillée: Décrivez l'objectif de l'outil, les scénarios applicables et les limitations
  • Validation suffisante des paramètres: Définissez les types de paramètres, les plages et les exigences de format
  • Fournir une valeur par défaut.: Fournissez des valeurs par défaut raisonnables pour les paramètres optionnels
  • Prendre en compte les cas d'erreur: Décrivez les erreurs et limitations possibles dans la description

Validation de la définition d'outil

Après avoir défini un outil, des tests de validation doivent être effectués :

def validate_tool_definition(tool_def):
    """验证工具定义是否完整"""
    required_fields = ["name", "description", "parameters"]

    for field in required_fields:
        if field not in tool_def:
            return False, f"缺少必要字段: {field}"

    # 检查参数结构
    if "properties" not in tool_def["parameters"]:
        return False, "参数定义缺少properties字段"

    return True, "工具定义完整"

# 测试验证
is_valid, message = validate_tool_definition(weather_tool)
print(f"验证结果: {is_valid}, 消息: {message}")

Extraction et validation des paramètres.

Après que le LLM a extrait les paramètres de la question de l'utilisateur, ces paramètres doivent être validés et traités pour garantir que l'outil puisse s'exécuter correctement.

Processus d'extraction des paramètres

Lorsque le LLM décide d'appeler un outil, il analyse l'entrée de l'utilisateur et extrait les paramètres requis par l'outil :

用户输入: "查询北京明天的天气温度"
工具: get_weather
提取参数: {"city": "北京", "date": "明天"}

Défis de l'extraction des paramètres

L'extraction des paramètres peut rencontrer les problèmes suivants :

  • Informations manquantes: L'utilisateur n'a pas fourni toutes les informations nécessaires
  • Incompatibilité de format: Le format fourni par l'utilisateur ne correspond pas aux exigences de l'outil
  • Résolution d'ambiguïté: La même information peut avoir plusieurs interprétations
  • Dépendance au contexte: Les paramètres doivent être compris dans le contexte de l'historique de la conversation

Méthode de validation des paramètres

1. Validation de type

Assurez-vous que les types de paramètres répondent aux exigences :

def validate_parameters(params, tool_def):
    """验证参数类型"""
    errors = []

    for param_name, param_def in tool_def["parameters"]["properties"].items():
        if param_name in params:
            param_value = params[param_name]
            expected_type = param_def.get("type")

            # 类型检查
            if expected_type == "string" and not isinstance(param_value, str):
                errors.append(f"参数'{param_name}'应为字符串类型")
            elif expected_type == "integer" and not isinstance(param_value, int):
                errors.append(f"参数'{param_name}'应为整数类型")
            elif expected_type == "number" and not isinstance(param_value, (int, float)):
                errors.append(f"参数'{param_name}'应为数字类型")
            elif expected_type == "boolean" and not isinstance(param_value, bool):
                errors.append(f"参数'{param_name}'应为布尔类型")

    return errors
```

2. Validation de plage

Vérifiez si les valeurs des paramètres se trouvent dans la plage autorisée :

def validate_range(params, tool_def):
    """验证参数范围"""
    errors = []

    for param_name, param_def in tool_def["parameters"]["properties"].items():
        if param_name in params:
            param_value = params[param_name]

            # 检查最小值
            if "minimum" in param_def and param_value < param_def["minimum"]:
                errors.append(f"参数'{param_name}'不能小于{param_def['minimum']}")

            # 检查最大值
            if "maximum" in param_def and param_value > param_def["maximum"]:
                errors.append(f"参数'{param_name}'不能大于{param_def['maximum']}")

            # 检查枚举值
            if "enum" in param_def and param_value not in param_def["enum"]:
                errors.append(f"参数'{param_name}'必须是{param_def['enum']}中的一个")

    return errors

3. Validation des paramètres requis

Assurez-vous que tous les paramètres requis ont été fournis :

def validate_required(params, tool_def):
    """验证必填参数"""
    errors = []
    required_params = tool_def["parameters"].get("required", [])

    for param_name in required_params:
        if param_name not in params:
            errors.append(f"缺少必填参数: {param_name}")

    return errors

Système complet de validation des paramètres

Exemple

class ParameterValidator:
    """Validateur de paramètres"""

    def __init__(self, tool_def):
        self.tool_def = tool_def

    def validate(self, params):
        """Effectuer une validation complète des paramètres"""
        all_errors = []

        # Vérifier les paramètres requis
        required_errors = self.validate_required(params)
        all_errors.extend(required_errors)

        # Vérifier les types de paramètres
        type_errors = self.validate_type(params)
        all_errors.extend(type_errors)

        # Vérifier les plages de paramètres
        range_errors = self.validate_range(params)
        all_errors.extend(range_errors)

        # Vérifier les paramètres supplémentaires (paramètres non définis)
        extra_errors = self.validate_extra(params)
        all_errors.extend(extra_errors)

        return len(all_errors) == 0, all_errors

    def validate_required(self, params):
        """Valider les paramètres requis"""
        errors = []
        required_params = self.tool_def["parameters"].get("required", [])

        for param_name in required_params:
            if param_name not in params or params[param_name] is None:
                errors.append(f"Paramètre requis manquant : {param_name}")

        return errors

    def validate_type(self, params):
        """Valider les types de paramètres"""
        errors = []

        for param_name, param_value in params.items():
            if param_name in self.tool_def["parameters"]["properties"]:
                param_def = self.tool_def["parameters"]["properties"][param_name]
                expected_type = param_def.get("type")

                if expected_type == "string" and not isinstance(param_value, str):
                    errors.append(f"Le paramètre '{param_name}' doit être de type chaîne, mais est {type(param_value).__name__}")
                elif expected_type == "integer" and not isinstance(param_value, int):
                    errors.append(f"Le paramètre '{param_name}' doit être de type entier, mais est {type(param_value).__name__}")
                elif expected_type == "number" and not isinstance(param_value, (int, float)):
                    errors.append(f"Le paramètre '{param_name}' doit être de type numérique, mais est {type(param_value).__name__}")
                elif expected_type == "boolean" and not isinstance(param_value, bool):
                    errors.append(f"Le paramètre '{param_name}' doit être de type booléen, mais est {type(param_value).__name__}")

        return errors

    def validate_range(self, params):
        """Valider les plages de paramètres"""
        errors = []

        for param_name, param_value in params.items():
            if param_name in self.tool_def["parameters"]["properties"]:
                param_def = self.tool_def["parameters"]["properties"][param_name]

                # Vérifier la valeur minimale
                if "minimum" in param_def and param_value < param_def["minimum"]:
                    errors.append(f"Le paramètre '{param_name}' ne peut pas être inférieur à {param_def['minimum']}")

                # Vérifier la valeur maximale
                if "maximum" in param_def and param_value > param_def["maximum"]:
                    errors.append(f"Le paramètre '{param_name}' ne peut pas être supérieur à {param_def['maximum']}")

                # Vérifier les valeurs d'énumération
                if "enum" in param_def and param_value not in param_def["enum"]:
                    errors.append(f"Le paramètre '{param_name}' doit être l'un de {param_def['enum']}")

        return errors

    def validate_extra(self, params):
        """Vérifier les paramètres supplémentaires non définis"""
        errors = []
        defined_params = set(self.tool_def["parameters"]["properties"].keys())
        provided_params = set(params.keys())

        extra_params = provided_params - defined_params
        if extra_params:
            errors.append(f"Paramètres non définis fournis : {', '.join(extra_params)}")

        return errors

# Exemple d'utilisation
validator = ParameterValidator(weather_tool)
params = {"city": "Beijing", "date": "tomorrow", "extra": "paramètre qui ne devrait pas exister"}
is_valid, errors = validator.validate(params)

if is_valid:
    print("Validation des paramètres réussie")
else:
    print("Échec de la validation des paramètres :")
    for error in errors:
        print(f" - {error}")

Le flux de travail complet pour l'extraction et la validation des paramètres.

Stratégie de correction des paramètres

Lorsque la validation des paramètres échoue, les stratégies suivantes peuvent être adoptées :

  • Demander à l'utilisateur: Demander directement à l'utilisateur les paramètres manquants ou incorrects
  • Utiliser des valeurs par défaut: Pour les paramètres optionnels, utiliser des valeurs par défaut prédéfinies
  • Inférence intelligente: Déduire des valeurs de paramètres raisonnables en fonction du contexte
  • Conversion de format: Convertir le format fourni par l'utilisateur vers le format requis par l'outil
def fix_parameters(params, errors, tool_def):
    """尝试修正参数错误"""
    fixed_params = params.copy()

    for error in errors:
        if "缺少必填参数" in error:
            param_name = error.split(": ")[1]
            # 尝试从上下文推断或使用默认值
            if param_name == "date":
                fixed_params[param_name] = "今天"  # 使用当天作为默认值

        elif "应为字符串类型" in error:
            param_name = error.split("'")[1]
            # 尝试转换为字符串
            fixed_params[param_name] = str(params[param_name])

    return fixed_params

Gestion des erreurs et mécanisme de réessai.

Dans les applications pratiques, les appels d'outils peuvent échouer. Un mécanisme robuste de gestion des erreurs et de réessai est essentiel pour construire un agent IA fiable.

Erreurs courantes d'appel d'outils

  • Erreurs réseau: Expiration du délai d'appel API, échec de connexion
  • Erreurs de paramètres: Échec de validation des paramètres, format incorrect
  • Erreurs de permission: Clé API invalide, permissions insuffisantes
  • Erreurs de ressources: Service indisponible, limite d'appels atteinte
  • Erreurs logiques: Erreurs de logique interne dans l'outil

Stratégie de gestion des erreurs

1. Gestion des erreurs par niveaux

Adopter différentes stratégies de gestion en fonction du type d'erreur :

class ErrorHandler:
    """错误处理器"""

    def handle(self, error, context):
        """处理错误"""
        error_type = self.classify_error(error)

        if error_type == "network":
            return self.handle_network_error(error, context)
        elif error_type == "parameter":
            return self.handle_parameter_error(error, context)
        elif error_type == "authentication":
            return self.handle_auth_error(error, context)
        elif error_type == "resource":
            return self.handle_resource_error(error, context)
        else:
            return self.handle_unknown_error(error, context)

    def classify_error(self, error):
        """分类错误类型"""
        error_str = str(error).lower()

        if any(word in error_str for word in ["timeout", "connection", "network"]):
            return "network"
        elif any(word in error_str for word in ["parameter", "invalid", "missing"]):
            return "parameter"
        elif any(word in error_str for word in ["auth", "key", "permission"]):
            return "authentication"
        elif any(word in error_str for word in ["limit", "quota", "unavailable"]):
            return "resource"
        else:
            return "unknown"

    def handle_network_error(self, error, context):
        """处理网络错误"""
        return {
            "success": False,
            "error": "网络连接失败,请检查网络后重试",
            "retryable": True,
            "retry_after": 5  # 5秒后重试
        }

    def handle_parameter_error(self, error, context):
        """处理参数错误"""
        return {
            "success": False,
            "error": f"参数错误: {str(error)}",
            "retryable": False,
            "suggestion": "请检查输入参数是否正确"
        }

    # 其他错误处理方法类似...

2. Messages d'erreur conviviaux

Convertir les erreurs techniques en messages que les utilisateurs peuvent comprendre :

def user_friendly_error(error):
    """生成用户友好的错误消息"""
    error_map = {
        "timeout": "请求超时,请稍后重试",
        "connection_error": "网络连接失败,请检查网络",
        "invalid_api_key": "API 密钥无效,请检查配置",
        "rate_limit_exceeded": "调用频率过高,请稍后再试",
        "invalid_parameters": "输入参数不正确,请检查后重试"
    }

    error_str = str(error).lower()
    for key, message in error_map.items():
        if key in error_str:
            return message

    return "系统繁忙,请稍后重试"

Mécanisme de réessai

Pour les erreurs réessayables, une stratégie de réessai intelligente doit être mise en œuvre :

1. Réessai avec repli exponentiel

import time
import random

class ExponentialBackoffRetry:
    """指数退避重试"""

    def __init__(self, max_retries=3, base_delay=1, max_delay=30):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.max_delay = max_delay

    def retry(self, func, *args, **kwargs):
        """执行带重试的函数调用"""
        last_error = None

        for attempt in range(self.max_retries):
            try:
                return func(*args, **kwargs)

            except Exception as e:
                last_error = e

                # 检查是否可重试
                if not self.is_retryable(e):
                    raise

                # 最后一次尝试,不再重试
                if attempt == self.max_retries - 1:
                    break

                # 计算等待时间(指数退避 + 随机抖动)
                delay = min(
                    self.base_delay * (2 ** attempt) + random.uniform(0, 1),
                    self.max_delay
                )

                print(f"第{attempt + 1}次尝试失败,{delay:.1f}秒后重试...")
                time.sleep(delay)

        # 所有重试都失败
        raise last_error

    def is_retryable(self, error):
        """判断错误是否可重试"""
        error_str = str(error).lower()
        retryable_errors = ["timeout", "connection", "busy", "temporarily"]

        for retryable_error in retryable_errors:
            if retryable_error in error_str:
                return True

        return False

# 使用示例
retry = ExponentialBackoffRetry(max_retries=3)

def call_weather_api(city, date):
    """调用天气API(模拟可能失败)"""
    # 模拟API调用
    if random.random() < 0.3:  # 30%概率失败
        raise ConnectionError("API连接超时")
    return {"temperature": 25, "condition": "晴"}

try:
    result = retry.retry(call_weather_api, "北京", "今天")
    print(f"成功获取天气: {result}")
except Exception as e:
    print(f"获取天气失败: {e}")
```

2. Patron de disjoncteur

Prévenir l'avalanche système causée par des échecs consécutifs :

class CircuitBreaker:
    """熔断器"""

    def __init__(self, failure_threshold=5, reset_timeout=60):
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = "closed"  # closed, open, half-open

    def execute(self, func, *args, **kwargs):
        """通过熔断器执行函数"""
        if self.state == "open":
            # 检查是否应该尝试恢复
            if self.should_try_reset():
                self.state = "half-open"
            else:
                raise Exception("熔断器开启,服务暂时不可用")

        try:
            result = func(*args, **kwargs)

            # 成功调用,重置状态
            if self.state == "half-open":
                self.state = "closed"
                self.failure_count = 0

            return result

        except Exception as e:
            self.record_failure()
            raise e

    def record_failure(self):
        """记录失败"""
        self.failure_count += 1
        self.last_failure_time = time.time()

        if self.failure_count >= self.failure_threshold:
            self.state = "open"

    def should_try_reset(self):
        """检查是否应该尝试重置"""
        if self.last_failure_time is None:
            return True

        elapsed = time.time() - self.last_failure_time
        return elapsed > self.reset_timeout

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

def unreliable_service():
    """模拟不可靠的服务"""
    if random.random() < 0.5:
        raise Exception("服务异常")
    return "服务正常"

for i in range(10):
    try:
        result = breaker.execute(unreliable_service)
        print(f"调用{i+1}: {result}")
    except Exception as e:
        print(f"调用{i+1}: {e}")
        time.sleep(1)

Framework complet d'appel d'outils

En combinant tous les composants ci-dessus, nous pouvons construire un framework complet d'appel d'outils :

Exemple

class ToolExecutor:
    """Exécuteur d'outils"""

    def __init__(self):
        self.tools = {}  # Outils enregistrés
        self.validator = ParameterValidator
        self.error_handler = ErrorHandler()
        self.retry = ExponentialBackoffRetry()
        self.breaker = CircuitBreaker()

    def register_tool(self, name, tool_def, func):
        """Enregistrer l'outil"""
        self.tools[name] = {
            "definition": tool_def,
            "function": func
        }

    def execute_tool(self, tool_name, params):
        """Exécuter l'outil"""
        if tool_name not in self.tools:
            return {
                "success": False,
                "error": f"Outil '{tool_name}' non enregistré"
            }

        tool_info = self.tools[tool_name]
        tool_def = tool_info["definition"]
        tool_func = tool_info["function"]

        # Valider les paramètres
        validator = self.validator(tool_def)
        is_valid, errors = validator.validate(params)

        if not is_valid:
            return {
                "success": False,
                "error": "Échec de la validation des paramètres",
                "details": errors
            }

        # Exécuter via le disjoncteur (avec réessai)
        try:
            def execute_with_retry():
                return self.breaker.execute(
                    lambda: self.retry.retry(tool_func, **params)
                )

            result = execute_with_retry()

            return {
                "success": True,
                "result": result,
                "tool": tool_name
            }

        except Exception as e:
            # Gestion des erreurs
            error_response = self.error_handler.handle(e, {
                "tool": tool_name,
                "params": params
            })

            return {
                "success": False,
                "error": error_response["error"],
                "retryable": error_response.get("retryable", False),
                "suggestion": error_response.get("suggestion", "")
            }

    def handle_user_request(self, user_input):
        """Traiter la requête utilisateur (version simplifiée)"""
        # 1. Laisser le LLM sélectionner l'outil et extraire les paramètres (simplifié ici)
        # Dans les applications réelles, le LLM serait appelé ici pour la sélection des outils et l'extraction des paramètres

        # Simuler la sortie du LLM
        if "weather" in user_input:
            tool_name = "get_weather"
            # Extraire simplement la ville (dans les applications réelles, le LLM ferait mieux)
            if "Beijing" in user_input:
                params = {"city": "Beijing", "date": "today"}
            elif "Shanghai" in user_input:
                params = {"city": "Shanghai", "date": "today"}
            else:
                params = {"city": "Beijing", "date": "today"}  # Valeur par défaut
        else:
            return "Désolé, je ne peux pas traiter cette demande"

        # 2. Exécuter l'outil
        result = self.execute_tool(tool_name, params)

        # 3. Générer la réponse finale
        if result["success"]:
            weather = result["result"]
            return f"{params['city']} météo aujourd'hui : {weather['condition']}, température {weather['temperature']}°C"
        else:
            return f"Échec de l'obtention des informations météo : {result['error']}"

# Exemple d'utilisation
executor = ToolExecutor()

# Enregistrer l'outil météo
def mock_weather_api(city, date):
    """API météo simulée"""
    # Simuler un délai d'appel API
    time.sleep(0.1)
    return {"temperature": 22, "condition": "Cloudy"}

executor.register_tool("get_weather", weather_tool, mock_weather_api)

# Traiter la requête de l'utilisateur
response = executor.handle_user_request("Quel temps fait-il à Pékin aujourd'hui ?")
print(response)
Autres extensions