Вызов инструментов Function calling Агенты Несколько провайдеров Совместимость с OpenAI

Портативный вызов инструментов в OpenAI, Claude и Gemini

Сопоставьте поля инструментов OpenAI, Claude и Gemini, проверяйте аргументы через JSON Schema и поддерживайте единый цикл вызовов через шлюз.

AveMujica API 8 мин чтения

Портативный вызов инструментов означает, что агенты определяют инструменты один раз — обычно в формате tools из OpenAI Chat Completions — и могут вызывать их через модели OpenAI, Claude, Gemini и других провайдеров, не поддерживая в коде приложения три расходящихся диалекта схем. Надёжный путь к этому результату — не «надеяться, что все нативные SDK совпадут», а стандартизировать один клиентский контракт, проверять аргументы до выполнения побочных действий и направлять запросы через OpenAI-совместимый шлюз, который сопоставляет различия провайдеров на границе системы.

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

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

Function calling создаёт вторую поверхность API — интерфейс инструментов. Этот интерфейс должен жить дольше любого отдельного бренда моделей. При этом нативные форматы провайдеров всё ещё различаются:

  • OpenAI описывает tools, tool_choice и сообщения с вызовами инструментов в руководстве по function calling.
  • Нативный Messages API Anthropic использует блоки содержимого tool-use / tool-result. Путь совместимости с OpenAI SDK принимает OpenAI-совместимые клиенты для оценки, но для большинства приложений Anthropic не рекомендует его как долгосрочный основной вариант для production.
  • Gemini описывает function calling в документации Google AI и также предоставляет OpenAI-совместимый endpoint для существующих клиентов OpenAI.

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

Шпаргалка по сопоставлению полей

Сохраняйте в продуктовом коде диалект OpenAI Chat Completions. Шлюзы или адаптеры должны примерно сопоставлять поля следующим образом (названия зависят от версии; проверяйте их, прежде чем полагаться на пограничные случаи):

ПонятиеOpenAI Chat CompletionsClaude Messages (нативный)Gemini (function calling / совместимость с OpenAI)
Каталог инструментовtools[].type=function + function.{name,description,parameters}tools[] с name, description, input_schemafunction_declarations / OpenAI tools на совместимом endpoint
Принудительный или автоматический режимtool_choice: none / auto / required / именованный инструментtool_choice в формах, специфичных для Claudetool_config провайдера или OpenAI-совместимый tool_choice
Модель запрашивает вызовmessage.tool_calls[] с id, function.name, function.arguments (JSON в строке)блоки content[] с type=tool_use, id, name, input (объект)части functionCall или OpenAI-совместимые tool_calls
Приложение возвращает результатсообщение role=tool + tool_call_id + строка contentблок tool_result, ссылающийся на tool_use_idчасти function response / OpenAI role=tool в режиме совместимости
Параллельные вызовынесколько элементов tool_callsнесколько блоков tool_useнесколько вызовов функций, когда это включено
Строгая схемаstrict / structured outputs (зависит от провайдера)нативные structured outputs; совместимость с OpenAI игнорирует strictпроверяйте актуальную документацию Gemini по structured output; не считайте поведение OpenAI strict гарантированным

Практическое правило: нормализуйте данные к формату OpenAI на входе в продуктовый код, даже если шлюз взаимодействовал с Claude или Gemini в нативном формате. Тогда журналы и стенды оценки сравнивают одну форму данных.

JSON Schema, сохраняющая портативность

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {"type": "string", "minLength": 1},
        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
      },
      "required": ["city"],
      "additionalProperties": false
    }
  }
}

Сохраняйте имена стабильными, описания точными, а наборы enum небольшими. Портативность исчезает, если в каждом эксперименте с провайдером поле city переименовывают в location только потому, что «так лучше звучит промпт».

Эталонный клиентский цикл с настоящей валидацией

Одного json.loads недостаточно для валидации. До любых побочных действий используйте список разрешённых имён инструментов вместе с JSON Schema или Pydantic:

from openai import OpenAI
import json
from typing import Any, Callable

# pip install jsonschema
from jsonschema import Draft202012Validator

client = OpenAI(
    api_key="GATEWAY_API_KEY",
    base_url="https://your-gateway.example.com/v1",
)

TOOL_PARAMS = {
    "get_weather": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "minLength": 1},
            "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
        },
        "required": ["city"],
        "additionalProperties": False,
    }
}

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city",
            "parameters": TOOL_PARAMS["get_weather"],
        },
    }
]

def get_weather(city: str, unit: str = "celsius") -> dict:
    return {"city": city, "unit": unit, "temperature": 22}


HANDLERS: dict[str, Callable[..., dict]] = {
    "get_weather": get_weather,
}


def parse_and_validate(name: str, raw_arguments: str) -> dict[str, Any]:
    if name not in HANDLERS or name not in TOOL_PARAMS:
        raise ValueError(f"tool_not_allowed:{name}")
    try:
        args = json.loads(raw_arguments or "{}")
    except json.JSONDecodeError as exc:
        raise ValueError(f"invalid_json:{name}") from exc
    if not isinstance(args, dict):
        raise ValueError(f"arguments_not_object:{name}")
    Draft202012Validator(TOOL_PARAMS[name]).validate(args)
    return args


MODEL = "claude-sonnet"  # gateway alias; swap to gemini-flash or gpt-* without code rewrites
messages = [{"role": "user", "content": "Weather in Lyon in celsius?"}]

response = client.chat.completions.create(
    model=MODEL,
    messages=messages,
    tools=tools,
    tool_choice="auto",
)

msg = response.choices[0].message
messages.append(msg)

if msg.tool_calls:
    for call in msg.tool_calls:
        try:
            args = parse_and_validate(call.function.name, call.function.arguments)
            result = HANDLERS[call.function.name](**args)
        except Exception as exc:
            result = {"error": "tool_validation_failed", "detail": str(exc)}
        messages.append(
            {
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            }
        )

    final = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)

Чтобы попробовать Gemini или OpenAI, меняйте только псевдоним модели. Исполнитель инструментов и JSON Schema остаются неизменными. В этом состоит основная выгода портативности.

Фрагмент ответа для проверки в тестах

Успешный первый шаг должен иметь примерно следующую форму (часть полей сокращена):

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\":\"Lyon\",\"unit\":\"celsius\"}"
            }
          }
        ]
      }
    }
  ]
}

Тестовый стенд должен проверять, что имя инструмента присутствует в списке разрешённых, arguments является строкой с JSON-объектом, схема успешно валидируется, а второй шаг получает role=tool с тем же tool_call_id.

Матрица несовместимостей: проверяйте, а не предполагайте

Режим отказаЧто наблюдаетсяБезопасная обработка
Почти корректный JSON в аргументахзавершающие запятые, одинарные кавычкиотклонить и вернуть ошибку инструмента; не использовать eval
Отсутствует обязательное полеошибка валидации схемывернуть структурированную ошибку инструмента; разрешить модели одну повторную попытку
Неизвестное имя инструментамодель выдумывает getWeatherотклонить по списку разрешённых; никогда не выполнять
Пустые arguments"" или {} при обязательных поляхсчитать ошибкой валидации
Параллельное разветвление5 и более вызовов инструментов за один ходограничить параллелизм, особенно для изменений состояния
tool_choice=required игнорируетсятекстовый ответ вместо вызова инструментасчитать задачу неуспешной, если побочные действия обязательны
strict=true в режиме совместимости Claudeпо документации Anthropic молча игнорируетсявсегда валидировать локально
Частичные аргументы при streamingфрагментированные строки аргументовнакапливать до финального события вызова инструмента
Провайдер отказывается использовать инструментытолько текст safety / policyзакрывать доступ при ошибке; не выдумывать вызов инструмента в коде приложения
Идемпотентная повторная попыткаодин и тот же create_ticket дваждыиспользовать ключи дедупликации на уровне инструмента

Автоматизируйте проверки схемы. Не включайте субъективную оценку качества в критерий портативности.

Что всё ещё ломается при изменениях у провайдеров

Корректность аргументов

Даже если схема кажется «простой», модели могут выдавать неверные значения enum или пропускать обязательные поля. Портативные системы проверяют аргументы одной и той же JSON Schema до побочных действий. Никогда не выполняйте платёжный инструмент или запись данных с непроверенными аргументами только потому, что «модель обычно соблюдает правила».

Параллельные вызовы инструментов

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

Семантика tool_choice

Режимы auto, none, required и принудительный выбор именованного инструмента реализованы не везде одинаково. Если продукт зависит от обязательного использования инструмента, включите каждый режим в матрицу тестов.

Строгие структурированные инструменты и свободный текст

Строгие structured outputs в стиле OpenAI, нативные structured outputs Claude и structured outputs Gemini — разные продукты. На уровне совместимости Anthropic с OpenAI параметр strict игнорируется. Независимо от формулировок провайдера всегда планируйте валидацию на уровне приложения.

Streaming и инструменты

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

Идемпотентность побочных действий

Портативным инструментам всё равно нужны устойчивые ключи идемпотентности для операций создания и платежей. Иначе повторные попытки модели и клиента приведут к двойной отправке.

Обязанности шлюза для портативных инструментов

AI-шлюз должен:

  • принимать один входной диалект инструментов, обычно OpenAI tools
  • сопоставлять его с Claude, Gemini или OpenAI upstream, не заставляя каждое приложение изучать нативные формы
  • публиковать стабильные псевдонимы моделей, чтобы тестовые стенды инструментов не фиксировали snapshot ID провайдеров
  • записывать журналы использования с псевдонимом модели и причиной завершения для ходов с инструментами
  • применять области действия ключей, чтобы пакетный ключ не мог вызывать агентов с большим числом изменяющих операций

Выбор архитектуры — прокси или шлюз — рассмотрен в статье LLM-прокси и AI-шлюз. Настройка SDK для Claude и Gemini описана в материалах использование Claude с OpenAI SDK и использование Gemini с OpenAI SDK.

Матрица портативности из восьми случаев

Запустите следующие проверки для псевдонимов OpenAI, Claude и Gemini через один и тот же клиент шлюза:

  1. Инструмент не нужен, требуется только текст
  2. Один обязательный инструмент с корректными аргументами
  3. Отсутствует обязательный аргумент
  4. Недопустимое значение enum
  5. Неизвестное имя инструмента
  6. Параллельный вызов двух инструментов, если продукт это разрешает
  7. Принудительный tool_choice для именованного инструмента
  8. Идемпотентная повторная попытка инструмента изменения

Для случаев 3–5 и 8 критерием успеха должно быть поведение валидации или исполнителя, а не то, что «модель вежливо извинилась».

Правила проектирования, сохраняющие портативность инструментов

  • Одно имя — одно значение. Не перегружайте search в несвязанных системах.
  • Предпочитайте простой JSON. Вложенные union и огромные поля свободного текста снижают надёжность между моделями.
  • Версионируйте инструменты явно, например create_ticket_v2, вместо незаметного изменения смысла.
  • Разделяйте инструменты чтения и изменения; для изменений применяйте более строгую авторизацию и идемпотентность.
  • Ограничивайте разветвление и рекурсию. Неограниченные циклы инструментов становятся инцидентами стоимости и безопасности.
  • Журналируйте ввод и вывод инструментов с маскированием. Без доказательств невозможно отлаживать портативность.

Рекомендации OWASP для LLM по-прежнему применимы: чрезмерная самостоятельность и раскрытие конфиденциальных данных часто впервые проявляются именно в системах с инструментами.

Проверка через curl

curl https://your-gateway.example.com/v1/chat/completions \
  -H "Authorization: Bearer $GATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini-flash",
    "messages": [{"role": "user", "content": "What is the weather in Lyon?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }],
    "tool_choice": "auto"
  }'

Проверьте choices[0].message.tool_calls. Повторите запрос с псевдонимами Claude и OpenAI. Различия в том, вызывается ли инструмент, являются продуктовым сигналом; различия в форме оболочки ответа — ошибкой сопоставления шлюза.

Официальные источники

Последняя проверка: 2026-07-15. Перед расширением побочных действий агента повторно проверяйте tool_choice, параллельные вызовы инструментов и поддержку строгих схем у каждого провайдера.

Частые вопросы

Должна ли каждая команда изучать нативный формат инструментов каждого провайдера?

Для кода продуктовых приложений — нет. Сохраняйте одну входную схему, обычно OpenAI tools, а особенности провайдеров изолируйте в шлюзе или единой библиотеке адаптеров.

Достаточно ли OpenAI-совместимого вызова инструментов для production-агентов?

Это сильная стратегия транспорта и схемы, но production-системе всё равно нужны списки разрешённых инструментов, валидация схем, авторизация, идемпотентность и бюджеты. Совместимость не заменяет эти средства контроля.

Может ли MCP заменить портативный function calling?

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

Запустите матрицу до мультипровайдерного failover

Возьмите одну production-схему инструмента, запустите матрицу из восьми случаев для псевдонимов OpenAI, Claude и Gemini через /v1/chat/completions и устраните пробелы в валидации до включения мультипровайдерного failover для этого агента. Только после этого расширяйте трафик.