#
Что такое Инструменты?

Одним из важнейших аспектов AI Агентов является их способность предпринимать действия. Как мы видели, это происходит благодаря использованию Инструментов.

В этом разделе мы узнаем, что такое Инструменты, как их эффективно разработать и как интегрировать их в вашего Агента с помощью Системного Сообщения.

Предоставив своему агенту правильные инструменты и четко описав, как они работают, вы сможете значительно расширить возможности своего AI. Давайте погружаться!

##
Что такое AI Инструменты?

Инструмент - это функция, предоставленная LLM. Эта функция должна выполнять четкую цель.

Вот некоторые часто используемые в AI агентах инструменты:

Инструмент | Описание
--- | ---
Веб-поиск | Позволяет агенту получать актуальную информацию из Интернета.
Генерация изображений | Создает изображения на основе текстовых описаний.
Извлечение | Извлекает информацию из внешнего источника.
| Интерфейс API

Это лишь примеры, поскольку на самом деле вы можете создать инструмент для любого случая использования!

Хороший инструмент должен быть чем-то, что дополняет возможности LLM.

Например, если вам нужно выполнить арифметические действия, то предоставление вашему LLM калькулятора обеспечит лучшие результаты, чем полагаться на собственные возможности модели.

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

Например, если вы спросите у LLM напрямую (без инструмента поиска) о сегодняшней погоде, LLM потенциально может выдать случайную погоду в виде галлюцинаций.

- Инструмент должен:

  - иметь текстовое описание того, что делает функция.
  - быть Вызываемым (Callable) (чем-то, что выполняет действие).
  - иметь Аргументы с типизацией.
  - (Необязательно) иметь Выходные данные с типизацией.

##
Как работают инструменты?

Как мы видели, LLM могут только получать текстовые данные на вход и генерировать текстовые данные на выход. У них нет возможности самостоятельно вызывать инструменты. Когда мы говорим о предоставлении инструментов агенту, мы имеем в виду, что мы обучаемLLM существованию инструментов и просим модель генерировать текст, который будет вызывать инструменты, когда это необходимо. Например, если мы предоставим инструмент для проверки погоды в определенном месте из Интернета, а затем спросим LLM о погоде в Париже, LLM распознает этот вопрос как релевантную возможность использовать инструмент “weather”, которой мы его научили. LLM сгенерирует текст в виде кода, чтобы вызвать этот инструмент. Ответственность Агента** заключается в том, чтобы проанализировать вывод LLM, распознать, что требуется вызов инструмента, и вызвать его от имени LLM. Выходные данные от инструмента будут отправлены обратно в LLM, которая составит окончательный ответ для пользователя.

Выходные данные после вызова инструмента - это еще один тип сообщений в диалоге. Шаги вызова инструмента обычно не демонстрируются пользователю: агент извлекает диалог, вызывает инструмент(ы), получает выходные данные, добавляет их в новое сообщение диалога и снова отправляет обновленный диалог в LLM. С точки зрения пользователя это выглядит так, как будто LLM использовал инструмент, но на самом деле это сделал наш код приложения (Агент).

Мы поговорим об этом процессе подробнее на следующих занятиях.

##
Как мы даем инструменты LLM?

Полный ответ может показаться непомерно сложным, но мы, по сути, используем системную подсказку для предоставления текстовых описаний доступных модели инструментов:

Чтобы это сработало, мы должны быть очень точны и аккуратны в отношении:

1. Что делает инструмент.
2. Каких именно входных данных он ожидает.

Именно по этой причине описания инструментов обычно предоставляются с использованием выразительных, но точных структур, таких как компьютерные языки или JSON. Не обязательно делать это именно так, подойдет любой точный и последовательный формат.

Если это кажется слишком теоретическим, давайте разберемся на конкретном примере.

Мы реализуем упрощенный калькулятор, который будет просто перемножать два целых числа. Это может быть наша реализация на Python:

    def calculator(a: int, b: int) -> int:
        """Multiply two integers."""
        return a * b

Итак, наш инструмент называется `calculator`, он перемножает два целых числа, и ему требуются следующие входные данные:

- `a` (int): Целое число.
- `b` (int): Целое число.

На выходе получается другое целое число, которое можно описать следующим образом:

- (int): Произведение `a` и `b`.

Все эти детали очень важны. Давайте соберем их вместе в текстовую строку, которая описывает наш инструмент для понимания LLM.

    Tool Name: calculator, Description: Multiply two integers., Arguments: a: int, b: int, Outputs: int

> Напоминание: Это текстовое описание - то, что мы хотим, чтобы LLM знала об инструменте.

Когда мы передадим предыдущую строку как часть входных данных в LLM, модель распознает ее как инструмент, и будет знать, что ему нужно передавать в качестве входных данных и что ожидать от выходных данных.

Если мы хотим предоставить дополнительные инструменты, мы должны быть последовательными и всегда использовать один и тот же формат. Этот процесс может быть хрупким, и мы можем случайно упустить некоторые детали.

Есть ли лучший способ?

###
Автоформатирование секции Инструменты

Наш инструмент написан на Python, и его реализация уже предоставляет все, что нам нужно:

- Описательное название того, что он делает: `calculator`.
- Более длинное описание, представленное в комментарии к docstring функции: `Multiply two integers.`.
- Входные данные и их тип: функция явно ожидает два `int`.
- Тип выходных данных.

Люди не просто так используют языки программирования: они выразительны, кратки и точны.

Мы могли бы предоставить исходный код Python в качестве спецификации инструмента для LLM, но способ реализации инструмента не имеет значения. Важно лишь его название, то, что он делает, какие входные данные он ожидает и какие выходные данные он предоставляет.

Мы воспользуемся возможностями интроспекции Python, чтобы изучить исходный код и автоматически составить описание инструмента. Все, что нам нужно, - это чтобы реализация инструмента использовала подсказки типов, строки документации и разумные имена функций. Мы напишем код для извлечения нужных фрагментов из исходного кода.

После этого нам останется только использовать декоратор Python, чтобы указать, что функция `calculator` является инструментом:

    @tool
    def calculator(a: int, b: int) -> int:
        """Multiply two integers."""
        return a * b

    print(calculator.to_string())

Обратите внимание на декоратор `@tool` перед определением функции.

С помощью реализации, которую мы рассмотрим далее, мы сможем автоматически извлекать следующий текст из исходного кода с помощью функции `to_string()`, предоставляемой декоратором:

    Tool Name: calculator, Description: Multiply two integers., Arguments: a: int, b: int, Outputs: int

Как видите, это то же самое, что мы уже писали вручную!

###
Универсальная реализация Инструмента

Мы создаем общий класс `Tool`, который мы можем использовать каждый раз, когда нам нужно использовать инструмент.

> Отказ от ответственности: Этот пример реализации является вымышленным, но очень похож на реальные реализации в большинстве библиотек.

    class Tool:
        """
        Класс, представляющий многократно используемый фрагмент кода (инструмент).

        Атрибуты:
            name (str): Имя инструмента.
            description (str): Текстовое описание того, что делает инструмент.
            func (вызываемый): Функция, которую оборачивает этот инструмент.
            arguments (список): Список аргументов.
            outputs (str или list): Возвращаемые обернутой функцией типы.
        """
        def __init__(self,
                     name: str,
                     description: str,
                     func: callable,
                     arguments: list,
                     outputs: str):
            self.name = name
            self.description = description
            self.func = func
            self.arguments = arguments
            self.outputs = outputs

        def to_string(self) -> str:
            """
           Возвращает строковое представление инструмента,
            включая его название, описание, аргументы и выходные данные.
            """
            args_str = ", ".join([
                f"{arg_name}: {arg_type}" for arg_name, arg_type in self.arguments
            ])

            return (
                f"Tool Name: {self.name},"
                f" Description: {self.description},"
                f" Arguments: {args_str},"
                f" Outputs: {self.outputs}"
            )

        def __call__(self, *args, **kwargs):
            """
            Вызов базовой функции (вызываемой) с указанными аргументами.
            """
            return self.func(*args, **kwargs)

Это может показаться сложным, но если мы медленно пройдемся по нему, то сможем понять, что он делает. Мы определяем класс `Tool`, который включает в себя:

- `name` (str): Название инструмента.
- `description` (str): Краткое описание того, что делает инструмент.
- `function` (callable): Функция, которую выполняет инструмент.
- `arguments` (list): Ожидаемые входные параметры.
- `outputs` (str или list): Ожидаемые выходные данные инструмента.
- `__call__()`: Вызывает функцию при вызове экземпляра инструмента.
- `to_string()`: Преобразует атрибуты инструмента в текстовое представление.

Мы можем создать инструмент с помощью этого класса, используя следующий код:

    calculator_tool = Tool(
        "calculator",                   # имя
        "Multiply two integers.",       # описание
        calculator,                     # функция для вызова
        [("a", "int"), ("b", "int")],   # водные данные (имена и типы)
        "int",                          # выходные данные
    )

Но мы также можем использовать модуль Python `inspect`, чтобы получить всю информацию за нас! Вот что делает декоратор `@tool`.

> Если вам интересно, вы можете посмотреть на реализацию декоратора в следующем разделе.

код декоратора

    def tool(func):
        """
        Декоратор, создающий экземпляр Tool из заданной функции.
        """
        # Получение сигнатуры функции
        signature = inspect.signature(func)

        # Извлеките пары (param_name, param_annotation) для входных данных
        arguments = []
        for param in signature.parameters.values():
            annotation_name = (
                param.annotation.__name__
                if hasattr(param.annotation, '__name__')
                else str(param.annotation)
            )
            arguments.append((param.name, annotation_name))

        # Определите аннотацию возврата
        return_annotation = signature.return_annotation
        if return_annotation is inspect._empty:
            outputs = "No return annotation"
        else:
            outputs = (
                return_annotation.__name__
                if hasattr(return_annotation, '__name__')
                else str(return_annotation)
            )

        # Используйте строку документации функции в качестве описания (по умолчанию, если None)
        description = func.__doc__ or "Описание не представлено."

        # Имя функции становится именем Инструмента
        name = func.__name__

        # Возвращаем новый экземпляр Инструмента
        return Tool(
            name=name,
            description=description,
            func=func,
            arguments=arguments,
            outputs=outputs
        )

Повторимся, что с этим декоратором мы можем реализовать наш инструмент следующим образом:

    @tool
    def calculator(a: int, b: int) -> int:
        """Multiply two integers."""

        return a * b

    print(calculator.to_string())

И мы можем использовать метод `Tool` `to_string` для автоматического получения текста, подходящего для использования в качестве описания инструмента для LLM:

    Tool Name: calculator, Description: Multiply two integers., Arguments: a: int, b: int, Outputs: int

Описание вставляется в системную подсказку. Если взять пример, с которого мы начали этот раздел, то вот как он будет выглядеть после замены `tools_description`:

В разделе Действия мы узнаем, как агент может вызвать инструмент, который мы только что создали.

Инструменты играют решающую роль в расширении возможностей AI агентов.

Подводя итоги, мы узнали:

- Что такое инструменты: Функции, которые предоставляют LLM дополнительные возможности, такие как выполнение вычислений или доступ к внешним данным.

- Как определить инструмент: Предоставить четкое текстовое описание, входы, выходы и вызываемую функцию.

- Почему инструменты необходимы: Они позволяют агентам преодолевать ограничения статического обучения модели, решать задачи в реальном времени и выполнять специализированные действия.

Теперь мы можем перейти к [Рабочему процессу Агента] (agent-steps-and-structure), где вы увидите, как Агент наблюдает, думает и действует. Это собирает воедино все, что мы изучили до сих пор, и закладывает основу для создания вашего собственного полнофункционального AI Агента.

Но сначала - еще один короткий тест!

< > Update on GitHub