Вызов инструментов

Function Calling — это основной механизм, позволяющий LLM использовать внешние инструменты.

Function Calling позволяет модели решать, когда вызывать инструменты, какой инструмент вызывать и какие параметры передавать.

Зачем нужен Function Calling?

Представьте, что LLM — это умный, но безоружный консультант. Он знает много, но не может:

  • Получать информацию в реальном времени (например, последние данные о погоде, ценах на акции)
  • Выполнять вычисления (например, сложные математические операции).
  • Управлять внешними системами (например, отправлять электронные письма, читать/записывать файлы)

Function Calling даёт LLM руки, позволяя ей преодолевать собственные ограничения и выполнять реальные задачи.

Принцип работы Function Calling.

Базовый процесс Function Calling выглядит следующим образом:

Основная концепция

  • Определение инструментаОпишите функцию, параметры и возвращаемое значение инструмента.
  • Выбор инструмента: LLM выбирает подходящий инструмент на основе вопроса пользователя.
  • Извлечение параметров: : LLM извлекает параметры, требуемые инструментом, из вопроса.
  • Обработка результатовИнтегрировать результаты выполнения инструмента в итоговый ответ

Пояснение на примере

Предположим, у нас есть инструмент запроса погоды. Когда пользователь спрашиваетКакая сегодня погода в Пекине?время:

  1. LLM определяет, что ему нужно вызвать инструмент запроса погоды
  2. Извлеките параметры из вопроса:city="北京", date="今天"
  3. Вызовите API погоды для получения данных
  4. Интегрирует данные о погоде в дружелюбный ответ и возвращает его пользователю

Создание определения и описания инструмента

Чтобы LLM могла правильно использовать инструмент, инструмент сначала должен быть чётко определён. Хорошее определение инструмента должно быть похоже на понятную инструкцию, которая позволяет LLM понять:

  • Что делает этот инструмент?
  • Когда его следует использовать?
  • Какие параметры нужны?
  • Каков формат параметров?

Структура определений инструментов.

Полное определение инструмента обычно включает следующие части:

Пример

# Пример структуры определения инструмента
weather_tool = {
    "name": "get_weather",  # Название инструмента
    "description": "Получить информацию о погоде для указанного города",  # Описание инструмента
    "parameters": {  # Определение параметров
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": Название города, например 'Пекин', 'Шанхай'
            },
            "date": {
                "type": "string",
                "description": Дата, формат 'YYYY-MM-DD', или 'today', 'tomorrow',
                "enum": ["Today", "Tomorrow", "Day after tomorrow"]
            }
        },
        "required": ["city"]  # Обязательные параметры
    }
}

Пишите высококачественные описания инструментов.

1. Сделайте описание чётким и конкретным

  • Плохое описание.: запросить погоду
  • Хорошее описание: Получить информацию о погоде для указанного города на конкретную дату, включая температуру, влажность, скорость ветра и погодные условия (солнечно, дождливо, облачно и т. д.)

2. Сделайте описания параметров подробными

Плохое описание параметра:

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

Хорошее описание параметра:

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

3. Используйте перечисления для ограничения вариантов

Для ограниченного набора вариантов используйте перечисления, чтобы помочь LLM понять:

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

4. Приведите примеры значений

Приведите примеры в описании, чтобы помочь LLM понять формат:

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

Пример определения утилиты

1. Инструмент-калькулятор

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

2. Инструмент поиска

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. Инструмент работы с файлами

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"]
    }
}

Лучшие практики определения инструментов.

  • Краткое и понятное имя:Используйте глаголы в начале, напримерget_、calculate_、search_
  • Полное и подробное описание: Опишите назначение инструмента, применимые сценарии и ограничения
  • Достаточная проверка параметров: Определите типы, диапазоны и требования к формату параметров
  • Укажите значение по умолчанию.: Предоставляйте разумные значения по умолчанию для необязательных параметров
  • Учитывайте случаи ошибок: Опишите возможные ошибки и ограничения в описании

Проверка определения инструмента

После определения инструмента следует выполнить проверочные тесты:

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}")

Извлечение и проверка параметров.

После того как LLM извлекает параметры из вопроса пользователя, эти параметры необходимо проверить и обработать, чтобы инструмент мог выполниться корректно.

Процесс извлечения параметров

Когда LLM решает вызвать инструмент, она анализирует ввод пользователя и извлекает параметры, требуемые инструментом:

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

Сложности при извлечении параметров

При извлечении параметров могут возникнуть следующие проблемы:

  • Отсутствующая информация: Пользователь не предоставил всю необходимую информацию
  • Несоответствие формата: Формат, предоставленный пользователем, не соответствует требованиям инструмента
  • Разрешение неоднозначности: Одну и ту же информацию можно интерпретировать по-разному
  • Зависимость от контекста: Параметры нужно понимать в контексте истории диалога

Метод проверки параметров

1. Проверка типов

Убедитесь, что типы параметров соответствуют требованиям:

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. Проверка диапазона

Проверьте, находятся ли значения параметров в допустимом диапазоне:

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. Проверка обязательных параметров

Убедитесь, что все обязательные параметры предоставлены:

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

Полная система проверки параметров

Пример

class ParameterValidator:
    """Валидатор параметров"""

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

    def validate(self, params):
        """Выполнить полную проверку параметров"""
        all_errors = []

        # Проверить обязательные параметры
        required_errors = self.validate_required(params)
        all_errors.extend(required_errors)

        # Проверить типы параметров
        type_errors = self.validate_type(params)
        all_errors.extend(type_errors)

        # Проверить диапазоны параметров
        range_errors = self.validate_range(params)
        all_errors.extend(range_errors)

        # Проверить наличие лишних параметров (неопределённых параметров)
        extra_errors = self.validate_extra(params)
        all_errors.extend(extra_errors)

        return len(all_errors) == 0, all_errors

    def validate_required(self, params):
        """Проверить обязательные параметры"""
        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_name}")

        return errors

    def validate_type(self, params):
        """Проверить типы параметров"""
        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"Параметр '{param_name}' должен быть строкового типа, но имеет тип {type(param_value).__name__}")
                elif expected_type == "integer" and not isinstance(param_value, int):
                    errors.append(f"Параметр '{param_name}' должен быть целочисленного типа, но имеет тип {type(param_value).__name__}")
                elif expected_type == "number" and not isinstance(param_value, (int, float)):
                    errors.append(f"Параметр '{param_name}' должен быть числового типа, но имеет тип {type(param_value).__name__}")
                elif expected_type == "boolean" and not isinstance(param_value, bool):
                    errors.append(f"Параметр '{param_name}' должен быть логического типа, но имеет тип {type(param_value).__name__}")

        return errors

    def validate_range(self, params):
        """Проверка диапазонов параметров"""
        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]

                # Проверить минимальное значение
                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

    def validate_extra(self, params):
        """Проверка неопределённых дополнительных параметров"""
        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"Предоставлены неопределённые параметры: {', '.join(extra_params)}")

        return errors

# Пример использования
validator = ParameterValidator(weather_tool)
params = {"city": "Пекин", "date": "завтра", "extra": "параметр, которого не должно существовать"}
is_valid, errors = validator.validate(params)

if is_valid:
    print("Проверка параметров пройдена")
else:
    print("Проверка параметров не пройдена:")
    for error in errors:
        print(f" - {error}")

Полный процесс извлечения и проверки параметров.

Стратегия исправления параметров

Когда проверка параметров не проходит, можно применить следующие стратегии:

  • Спросить пользователя: Напрямую спросить у пользователя о недостающих или неверных параметрах
  • Использование значений по умолчанию: Для необязательных параметров используйте предопределённые значения по умолчанию
  • Интеллектуальное определение: Определение разумных значений параметров на основе контекста
  • Преобразование формата: Преобразование формата, предоставленного пользователем, в формат, требуемый инструментом
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

Обработка ошибок и механизм повторных попыток.

В практических приложениях вызовы инструментов могут завершаться неудачей. Надёжная обработка ошибок и механизм повторных попыток — ключ к созданию надёжного AI-агента.

Распространённые ошибки при вызове инструментов

  • Сетевые ошибки: Тайм-аут вызова API, сбой соединения
  • Ошибки параметров: Ошибка проверки параметров, неверный формат
  • Ошибки разрешений: Недействительный API-ключ, недостаточно прав
  • Ошибки ресурсов: Сервис недоступен, достигнут лимит вызовов
  • Логические ошибки: Внутренние логические ошибки в инструменте

Стратегия обработки ошибок

1. Многоуровневая обработка ошибок

Применяйте разные стратегии обработки в зависимости от типа ошибки:

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. Понятные пользователю сообщения об ошибках

Преобразуйте технические ошибки в сообщения, которые могут понять пользователи:

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 "系统繁忙,请稍后重试"

Механизм повторных попыток

Для ошибок, допускающих повторную попытку, следует реализовать интеллектуальную стратегию повторных попыток:

1. Повтор с экспоненциальной задержкой

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. Шаблон автоматического выключателя

Предотвращение лавинообразного отказа системы из-за последовательных сбоев:

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)

Полный фреймворк вызова инструментов

Объединив все вышеперечисленные компоненты, мы можем построить полный фреймворк вызова инструментов:

Пример

class ToolExecutor:
    """Исполнитель инструментов"""

    def __init__(self):
        self.tools = {}  # Зарегистрированные инструменты
        self.validator = ParameterValidator
        self.error_handler = ErrorHandler()
        self.retry = ExponentialBackoffRetry()
        self.breaker = CircuitBreaker()

    def register_tool(self, name, tool_def, func):
        """Зарегистрировать инструмент"""
        self.tools[name] = {
            "definition": tool_def,
            "function": func
        }

    def execute_tool(self, tool_name, params):
        """Выполнить инструмент"""
        if tool_name not in self.tools:
            return {
                "success": False,
                "error": f"Инструмент '{tool_name}' не зарегистрирован"
            }

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

        # Проверить параметры
        validator = self.validator(tool_def)
        is_valid, errors = validator.validate(params)

        if not is_valid:
            return {
                "success": False,
                "error": "Ошибка проверки параметров",
                "details": errors
            }

        # Выполнить через предохранитель (с повторными попытками)
        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:
            # Обработка ошибок
            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):
        """Обработать запрос пользователя (упрощённая версия)"""
        # 1. Пусть LLM выберет инструмент и извлечёт параметры (здесь упрощено)
        # В реальных приложениях здесь будет вызываться LLM для выбора инструмента и извлечения параметров

        # Имитировать вывод LLM
        if "weather" in user_input:
            tool_name = "get_weather"
            # Просто извлекаем город (в реальных приложениях LLM справилась бы лучше)
            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"}  # Значение по умолчанию
        else:
            return "Извините, я не могу обработать этот запрос"

        # 2. Выполнить инструмент
        result = self.execute_tool(tool_name, params)

        # 3. Сгенерировать окончательный ответ
        if result["success"]:
            weather = result["result"]
            return f"{params['city']} погода сегодня: {weather['condition']}, температура {weather['temperature']}°C"
        else:
            return f"Не удалось получить информацию о погоде: {result['error']}"

# Пример использования
executor = ToolExecutor()

# Зарегистрировать инструмент погоды
def mock_weather_api(city, date):
    """Имитированный API погоды"""
    # Имитировать задержку вызова API
    time.sleep(0.1)
    return {"temperature": 22, "condition": "Облачно"}

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

# Обработать запрос пользователя
response = executor.handle_user_request("Какая сегодня погода в Пекине?")
print(response)
Другие расширения