Перейти к основному содержимому

Tool Calling @ Yandex Cloud with API, часть 1

Интерактивные ноутбук и полезные утилиты, использованные в руководстве.
Код покрыт тестами и поддерживается.

Мотивация​

Tool Calling, мотивация с точки зрения бизнеса

Представим себе, что мы создаем бота-ассистента для онлайн-маркетплейсе, который будет ассистировать клиенту покупать товары. Давайте отправим следущий запрос к YandexGPT:

messages = []
messages.append({"role": "user", "text": "Сколько денег у меня на балансе?"})

response = chat_completion_request(messages)
messages.append(response['result']['alternatives'][0]['message'])

pretty_print_conversation(messages)
user: Сколько денег у меня на балансе?

assistant: К сожалению, я не могу ответить на этот вопрос, так как у меня нет доступа к информации о вашем банковском счёте или балансе. Чтобы узнать, сколько денег у вас на балансе, вам нужно проверить свой банковский счёт через мобильное приложение банка или официальный веб-сайт.

В идеале, мы хотим, чтобы бот-ассистент мог получить ответ на этот вопрос, самостоятельно обратившись к базе данных. Для подобного поведения нам потребуется использовать Tool Calling.

Флоу работы Tool Calling​

Пример флоу работы чат-бота с использованием внешних инструментов:

Параметры внутри тела запроса 1:

request_body = {
"messages": [
{role:system, text:""}, # Системный промпт
{role:user, text:""}, # Сколько денег у меня на балансе?
],
"tools" [...] # Список инструментов, которые мы можем использовать
}

Ответ 1:

"message": [
{role:assistant, toolCallList:[...]}, # описание инструментов в виде json schema
]

Если toolCallList есть в сообщении, то нам нужно испольнить соответствующие функции, передав в них соответствующие аргументы. Например, для функции def get_balance(user_id: str) -> str: нам нужно передать аргумент user_id, который мы достанем из toolCallList.

Добавляем toolResultList:[...] дополнительным сообщением в историю. Запрос 2:

"messages": [
{role:system, text:""},
{role:user, text:""},
{role:assistant, toolCallList:[...]},
{role:assistant, toolResultList:[...]}, # результаты выполненных инструментов, с 'content': '100500'
],
"tools" [...] # Обязательно 1 в 1 тот же список инструментов, что и в запросе 1

Ассистент пишет ответ пользователю, основываясь на результатах инструментов. Ответ 2:

"message": [
{role:assistant, text:""}, # ответ натуральным текстом, "На вашем балансе 100500 рублей"
]

Запрос 3 (дальнейшее общение) :

"messages": [
{role:system, text:""},
{role:user, text:""},
{role:assistant, toolCallList:[...]},
{role:assistant, toolResultList:[...]},
{role:assistant, text:""},
{role:user, text:""}, # уточняющий вопрос пользователя
],
"tools" [...]

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

Системный промпт сильно влияет на логику работы LLM в этом плане:

  • вы можете управлять последовательными, параллельными вызовами
  • заставлять модель делать вызов инструмента постоянно или не делать вообще
  • заставлять модель делать вызов только какого-то одного инструмента
  • и следовать любой другой логике, с которой модель справится

Как устроена JSON Schema​

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

Общая структура JSON Schema выглядит следующим образом:

# Пример JSON Schema для инструмента получения погоды из какого-то API
weather_tool = {
"function": {
"name": "weatherTool",
"description": "Ходит в API и получает прогноз погоды в городе", # Старайтесь делать описание функции подробным
"parameters": {
"type": "object",
"properties": {}, # Словарь параметров
"required": [], # Список обязательных параметров
}
}
}

Внутри properties мы описываем параметры, которые нам нужны для вызова инструмента. Например:

"properties": {
"city": {
"type": "string", # Тип параметра
"description": "Название города, для которого нужно получить погоду." # Описание параметра
},
"units": {
"type": "string",
"enum": ["metric", "imperial"], # Список допустимых значений
"default": "metric", # Значение по умолчанию
"description": "Единицы измерения температуры. 'metric' для Цельсия, 'imperial' для Фаренгейта."
},
"days": {
"type": "integer",
"default": 1,
"minimum": 1, # Выставление ограничений для параметров, подробнее по ссылке в конце
"maximum": 5,
"description": "Количество дней, для которых нужно получить погоду, начиная с текущего дня."
}
"regions": {
"type": "array", # Массивы
"default": ["center"],
"maxItems": 3,
"description": "Части города, для которых нужно получить погоду. Например: 'center', 'west', 'west-east' и т.д."
},
}

"required": ["city"] # Список обязательных параметров, без которых функция не сможет отработать
Что еще можно указать в JSON Schema

1. Базовые типы данных

{
"type": "string", // строка
"type": "number", // число (включая дробные)
"type": "integer", // целое число
"type": "boolean", // логическое значение
"type": "array", // массив
"type": "object", // объект
"type": "null" // null
}

2. Валидация строк

{
"type": "string",
"minLength": 2, // минимальная длина
"maxLength": 100, // максимальная длина
"pattern": "^[A-Za-z]+$", // регулярное выражение
"format": "email" // предопределенный формат
}

3. Валидация чисел

{
"type": "number",
"minimum": 0, // минимальное значение
"maximum": 100, // максимальное значение
"exclusiveMinimum": 0, // исключающий минимум
"exclusiveMaximum": 100, // исключающий максимум
"multipleOf": 5 // кратность числа
}

4. Валидация массивов

{
"type": "array",
"items": { // схема для элементов массива
"type": "string"
},
"minItems": 1, // минимальное количество элементов
"maxItems": 10, // максимальное количество элементов
"uniqueItems": true // все элементы должны быть уникальными
}

5. Сложные объекты

{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer"
}
},
"patternProperties": { // свойства по регулярному выражению
"^S_": { "type": "string" }
},
"additionalProperties": false, // запрет дополнительных свойств
"minProperties": 1, // минимальное количество свойств
"maxProperties": 10 // максимальное количество свойств
}

6. Условная валидация

{
"type": "object",
"if": {
"properties": {
"type": { "const": "user" }
}
},
"then": {
"required": ["username"]
},
"else": {
"required": ["adminCode"]
}
}

7. Комбинирование схем

{
"anyOf": [ // хотя бы одна схема должна подойти
{ "type": "string" },
{ "type": "number" }
],
"allOf": [ // все схемы должны подойти
{ "type": "object" },
{ "required": ["id"] }
],
"oneOf": [ // ровно одна схема должна подойти
{ "type": "string" },
{ "type": "null" }
],
"not": { // схема не должна подойти
"type": "string"
}
}

8. Встроенные форматы

{
"type": "string",
"format": "date-time", // ISO 8601 дата/время
// Другие встроенные форматы:
// "date" // только дата
// "time" // только время
// "email" // email адрес
// "ipv4" // IPv4 адрес
// "ipv6" // IPv6 адрес
// "uri" // URI
// "uuid" // UUID
// "hostname" // имя хоста
// "json-pointer" // JSON указатель
}

9. Составные типы

{
"type": ["string", "null"], // поле может быть строкой или null
"properties": {
"status": {
"type": ["string", "integer"],
"enum": ["active", "inactive", 0, 1]
}
}
}
Пример JSON Schema для выполнения любого HTTP-запроса
{
"name": "make_http_request",
"description": "Выполняет HTTP-запрос к указанному URL с заданными параметрами",
"parameters": {
"type": "object",
"properties": {
"method": {
"type": "string",
"description": "HTTP метод запроса",
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
"default": "GET"
},
"url": {
"type": "string",
"description": "URL для выполнения запроса",
"format": "uri"
},
"query_params": {
"type": "object",
"description": "Query параметры для URL",
"additionalProperties": {
"type": ["string", "number", "boolean", "array"],
"items": {
"type": ["string", "number", "boolean"]
}
}
},
"headers": {
"type": "object",
"description": "HTTP заголовки запроса",
"patternProperties": {
"^[A-Za-z0-9-]+$": {
"type": "string"
}
}
},
"data": {
"type": "object",
"description": "Тело запроса для POST/PUT/PATCH методов",
"additionalProperties": true
},
"timeout": {
"type": "number",
"description": "Таймаут запроса в секундах",
"minimum": 0,
"maximum": 300,
"default": 30
},
"verify_ssl": {
"type": "boolean",
"description": "Проверять SSL сертификат",
"default": true
},
"auth": {
"type": "object",
"description": "Параметры аутентификации",
"properties": {
"type": {
"type": "string",
"enum": ["basic", "bearer", "digest", "none"],
"default": "none"
},
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"token": {
"type": "string"
}
},
"dependencies": {
"username": ["password"],
"password": ["username"]
}
},
"retry": {
"type": "object",
"description": "Настройки повторных попыток",
"properties": {
"max_attempts": {
"type": "integer",
"minimum": 0,
"maximum": 5,
"default": 0
},
"delay": {
"type": "number",
"minimum": 0,
"default": 1
}
}
}
},
"required": ["url"],
"additionalProperties": false
}
}

Пример простого GET-запроса:

{
"method": "GET",
"url": "https://api.example.com/users",
"query_params": {
"page": 1,
"limit": 10
}
}

POST-запрос с аутентификацией:

{
"method": "POST",
"url": "https://api.example.com/data",
"headers": {
"Content-Type": "application/json",
"X-API-Version": "1.0"
},
"data": {
"name": "Test User",
"email": "test@example.com"
},
"auth": {
"type": "bearer",
"token": "your-token-here"
}
}

Максимально подробное руководство по JSON Schema