Портативный вызов инструментов в OpenAI, Claude и Gemini
Сопоставьте поля инструментов OpenAI, Claude и Gemini, проверяйте аргументы через JSON Schema и поддерживайте единый цикл вызовов через шлюз.
Портативный вызов инструментов означает, что агенты определяют инструменты один раз — обычно в формате 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 Completions | Claude Messages (нативный) | Gemini (function calling / совместимость с OpenAI) |
|---|---|---|---|
| Каталог инструментов | tools[].type=function + function.{name,description,parameters} | tools[] с name, description, input_schema | function_declarations / OpenAI tools на совместимом endpoint |
| Принудительный или автоматический режим | tool_choice: none / auto / required / именованный инструмент | tool_choice в формах, специфичных для Claude | tool_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 через один и тот же клиент шлюза:
- Инструмент не нужен, требуется только текст
- Один обязательный инструмент с корректными аргументами
- Отсутствует обязательный аргумент
- Недопустимое значение enum
- Неизвестное имя инструмента
- Параллельный вызов двух инструментов, если продукт это разрешает
- Принудительный
tool_choiceдля именованного инструмента - Идемпотентная повторная попытка инструмента изменения
Для случаев 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. Различия в том, вызывается ли инструмент, являются продуктовым сигналом; различия в форме оболочки ответа — ошибкой сопоставления шлюза.
Официальные источники
- Руководство OpenAI по function calling
- Совместимость Anthropic с OpenAI SDK
- Совместимость Gemini с OpenAI
- OWASP Top 10 для LLM-приложений
Последняя проверка: 2026-07-15. Перед расширением побочных действий агента повторно проверяйте tool_choice, параллельные вызовы инструментов и поддержку строгих схем у каждого провайдера.
Частые вопросы
Должна ли каждая команда изучать нативный формат инструментов каждого провайдера?
Для кода продуктовых приложений — нет. Сохраняйте одну входную схему, обычно OpenAI tools, а особенности провайдеров изолируйте в шлюзе или единой библиотеке адаптеров.
Достаточно ли OpenAI-совместимого вызова инструментов для production-агентов?
Это сильная стратегия транспорта и схемы, но production-системе всё равно нужны списки разрешённых инструментов, валидация схем, авторизация, идемпотентность и бюджеты. Совместимость не заменяет эти средства контроля.
Может ли MCP заменить портативный function calling?
MCP дополняет его: протокол стандартизирует публикацию инструментов для хостов, но провайдерам моделей всё равно нужен способ запрашивать вызовы. Детали топологии и управления описаны в статьях о маршрутизации MCP, ссылки на которые приведены выше.
Запустите матрицу до мультипровайдерного failover
Возьмите одну production-схему инструмента, запустите матрицу из восьми случаев для псевдонимов OpenAI, Claude и Gemini через /v1/chat/completions и устраните пробелы в валидации до включения мультипровайдерного failover для этого агента. Только после этого расширяйте трафик.