English translation is not available yet. Showing Russian content.

Как вы переключаете агента между инструментами (function calling) с разными сигнатурами?

Краткий тезис

Переключение агента между инструментами с разными сигнатурами — это задача динамического выбора и вызова внешних функций на основе семантики запроса пользователя. Агент использует описание инструментов в формате JSON Schema, выбирает подходящий инструмент (часто через LLM-рассуждение), генерирует аргументы, строго соответствующие схеме, валидирует их (например, с помощью Pydantic), и обрабатывает ошибки. Ключевой элемент — test-time compute: модель тратит дополнительные токены на размышление о том, какой инструмент вызвать и как сформировать аргументы, что повышает надёжность переключения.


1. Термин: Function Calling (вызов функций)

Function calling — это механизм, позволяющий LLM (Large Language Model) не только генерировать текст, но и возвращать структурированные запросы на вызов внешних инструментов (API, функций, баз данных). Агент получает список доступных инструментов с их сигнатурами, и на основе запроса решает, какой инструмент вызвать и с какими аргументами.

Зачем нужно переключение между инструментами
В реальных сценариях агент должен уметь:

  • получать погоду (инструмент get_weather)
  • искать документы (инструмент retrieve_docs)
  • выполнять вычисления (инструмент calculate)
  • отправлять email (инструмент send_email)

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


2. Описание инструментов через JSON Schema

Для того чтобы LLM могла вызывать инструменты, их сигнатуры описываются в формате JSON Schema — стандарте описания структуры JSON-данных. Каждый инструмент представляется как объект с полями:

  • name — уникальное имя инструмента
  • description — текстовое описание, когда и как его использовать (важно для семантического выбора)
  • parametersJSON Schema объекта, описывающая ожидаемые аргументы (типы, обязательные поля, ограничения)

Пример описания двух инструментов с разными сигнатурами:

[
  {
    "name": "get_weather",
    "description": "Получить текущую погоду для города",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "Название города"
        },
        "units": {
          "type": "string",
          "enum": ["metric", "imperial"],
          "default": "metric"
        }
      },
      "required": ["city"]
    }
  },
  {
    "name": "calculate",
    "description": "Выполнить математическое вычисление",
    "parameters": {
      "type": "object",
      "properties": {
        "expression": {
          "type": "string",
          "description": "Математическое выражение, например '2 + 2'"
        }
      },
      "required": ["expression"]
    }
  }
]

Зачем JSON Schema

  • LLM обучается на таких схемах и умеет генерировать корректные JSON-вызовы.
  • Схема позволяет валидировать аргументы на стороне агента до реального вызова.
  • Разные сигнатуры кодируются в разных схемах, что даёт агенту возможность переключаться.

3. Процесс выбора инструмента

Агент не просто вызывает первый попавшийся инструмент — он должен выбрать правильный на основе запроса. Есть несколько стратегий:

3.1 Прямое указание LLM (наиболее распространённое)

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

Пример:
Запрос: «Какая погода в Москве?»
Модель выбирает get_weather, а не calculate.

3.2 Классификация запроса (rule-based или ML)

Можно предварительно классифицировать намерение пользователя (intent) и на основе этого выбрать набор инструментов. Например, если запрос содержит слово «погода» → активируется инструмент погоды.

3.3 Многошаговое рассуждение (ReAct, Chain-of-Thought)

Агент сначала генерирует «мысли» (reasoning), а потом решает, какой инструмент вызвать. Это часть test-time compute: модель тратит дополнительные токены на обдумывание, что повышает точность выбора.


4. Генерация аргументов

После выбора инструмента LLM должна сгенерировать аргументы, строго соответствующие его JSON Schema. Обычно это делается в формате JSON-объекта.

Проблема разные сигнатуры требуют разных полей. Например, get_weather требует city (строка), а calculate требует expression (строка). Модель должна не перепутать.

Как это решается

  • В промпте явно указывается: «Для вызова функции используй JSON с полями, описанными в схеме».
  • Современные LLM (GPT-4, Claude 3, Llama 3) обучены генерировать корректные JSON по схеме.
  • Используется constrained decoding — техника, при которой генерация ограничивается грамматикой JSON Schema (например, библиотека outlines или guidance).

5. Валидация аргументов (Pydantic)

Даже если LLM сгенерировала JSON, его нужно проверить перед реальным вызовом. Для этого удобно использовать Pydantic — библиотеку Python для валидации данных на основе моделей.

Пример:

from pydantic import BaseModel, Field
from typing import Optional

class GetWeatherArgs(BaseModel):
    city: str = Field(..., description="Название города")
    units: Optional[str] = "metric"

class CalculateArgs(BaseModel):
    expression: str = Field(..., description="Математическое выражение")

# Валидация
try:
    args = GetWeatherArgs(**json.loads(llm_output))
    # вызов get_weather(args.city, args.units)
except ValidationError as e:
    # обработка ошибки: повторная генерация или fallback

Преимущества

  • Автоматическая проверка типов, обязательных полей, значений по умолчанию.
  • Чёткие сообщения об ошибках, которые можно передать обратно LLM для исправления.
  • Легко расширять: добавление нового инструмента = новая Pydantic модель.

6. Обработка ошибок при вызове

Даже после валидации возможны ошибки выполнения (инструмент недоступен, неверные данные). Агент должен уметь:

  • Повторная генерация если аргументы не прошли валидацию, отправить LLM сообщение об ошибке и попросить сгенерировать заново.
  • Fallback: если инструмент не сработал, переключиться на другой (например, если не удалось получить погоду через API, использовать парсинг HTML).
  • Логирование: записывать все вызовы и ошибки для отладки.

Пример цикла

1. Пользователь: "Сколько будет 2+2?"
2. LLM решает вызвать calculate с аргументом {"expression": "2+2"}
3. Валидация: CalculateArgs(expression="2+2") — успешно
4. Вызов calculate("2+2") → возвращает 4
5. LLM формирует ответ: "Результат: 4"

Если на шаге 3 ошибка (например, поле expression отсутствует), агент отправляет LLM: «Ошибка: не хватает поля expression. Попробуй ещё раз».


7. Переключение между инструментами в multi-step сценариях

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

  • Запрос: «Какая погода в столице Франции и сколько будет 5!?»
  • Агент сначала вызывает get_weather для города «Париж», затем calculate для «5!».
  • Результаты объединяются в финальный ответ.

Управление состоянием агент должен помнить, какие инструменты уже вызваны и какие результаты получены. Для этого используется память (conversation history) и контекст (переменные окружения).

Параллельные вызовы если инструменты независимы, можно вызвать их одновременно (например, через asyncio), что сокращает время ответа.


8. Test-time compute и рассуждение перед вызовом

Test-time compute — это парадигма, при которой модель тратит дополнительные вычислительные ресурсы во время инференса на «размышление» перед генерацией ответа. В контексте function calling это означает:

  • Chain-of-Thought (CoT): модель сначала пишет рассуждение: «Пользователь спрашивает о погоде, значит нужно вызвать get_weather. Город — Москва, единицы — метрические.»
  • ReAct (Reasoning + Acting): чередование мыслей и действий (вызовов инструментов).
  • Self-ask модель задаёт себе уточняющие вопросы, чтобы понять, какой инструмент нужен.

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


9. Пример реализации на Python (OpenAI API)

import json
from openai import OpenAI
from pydantic import BaseModel, ValidationError

client = OpenAI()

# Определение инструментов
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "units": {"type": "string", "enum": ["metric", "imperial"]}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Evaluate a math expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string"}
                },
                "required": ["expression"]
            }
        }
    }
]

# Pydantic модели для валидации
class GetWeatherArgs(BaseModel):
    city: str
    units: str = "metric"

class CalculateArgs(BaseModel):
    expression: str

# Функции-исполнители
def get_weather(city, units="metric"):
    return f"Погода в {city}: 20°C"

def calculate(expression):
    return eval(expression)  # упрощённо, в реальности безопасный eval

# Основной цикл агента
def agent_loop(user_input):
    messages = [{"role": "user", "content": user_input}]
    response = client.chat.completions.create(
        model="gpt-4",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    msg = response.choices[0].message
    if msg.tool_calls:
        for tool_call in msg.tool_calls:
            func_name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            # Валидация
            try:
                if func_name == "get_weather":
                    validated = GetWeatherArgs(**args)
                    result = get_weather(validated.city, validated.units)
                elif func_name == "calculate":
                    validated = CalculateArgs(**args)
                    result = calculate(validated.expression)
                else:
                    result = "Unknown tool"
            except ValidationError as e:
                result = f"Validation error: {e}"
            # Добавляем результат в историю
            messages.append(msg)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result)
            })
        # Второй вызов LLM для финального ответа
        final_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )
        return final_response.choices[0].message.content
    else:
        return msg.content

print(agent_loop("Какая погода в Лондоне?"))
print(agent_loop("Сколько будет 2+2?"))

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


10. Сравнение подходов к переключению

ПодходОписаниеПлюсыМинусы
Прямое указание LLMМодель сама выбирает инструмент на основе описанияГибкость, простотаЗависит от качества модели, возможны галлюцинации
Rule-based классификацияПредопределённые правила (ключевые слова)Быстро, детерминированноНе масштабируется, плохо для сложных запросов
Многошаговое рассуждение (CoT/ReAct)Модель «думает» перед вызовомВысокая точность, интерпретируемостьДороже по токенам, медленнее
Иерархический выборСначала выбирается категория, потом конкретный инструментХорошо для большого числа инструментовСложнее в реализации

Пет-проект для закрепления

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

  • get_stock_price(ticker: str) — цена акции
  • get_news(topic: str, limit: int = 5) — новости по теме
  • translate(text: str, target_lang: str) — перевод текста

Инструменты Python, OpenAI API (или локальная LLM через Ollama), Pydantic, FastAPI (для веб-интерфейса).

Шаги:

  1. Определите JSON Schema для каждого инструмента.
  2. Реализуйте Pydantic модели для валидации.
  3. Напишите функцию-агента, которая принимает запрос, вызывает LLM с инструментами, обрабатывает ошибки.
  4. Добавьте возможность последовательных вызовов (например, «Найди новости про Apple и переведи заголовки на русский»).
  5. Протестируйте на разных запросах, включая неоднозначные (например, «Что с акциями?» — нужно уточнить тикер).

Ожидаемый результат Рабочий агент, который корректно выбирает инструмент, генерирует аргументы, валидирует их и возвращает ответ. Вы научитесь обрабатывать ошибки валидации и повторные вызовы.


Связь с другими вопросами

ВопросТема
145Как агент планирует последовательность действий (multi-step reasoning)?
147Что такое ReAct и как он используется в агентах?
148Как обрабатывать ошибки при вызове инструментов?
149Как агент управляет состоянием (памятью) в длинных сессиях?
151Как тестировать агента с инструментами (unit-тесты, симуляция)?
155Как объединять результаты нескольких инструментов в один ответ?

Навигация