26 янв. 2026 г.·7 мин чтения

Контракты федерации моделей для стабильных вызовов инструментов

Контракты федерации моделей сохраняют стабильность вызовов инструментов при смене моделей. Узнайте, какие правила запроса, ответа, ошибок и роллаута нужно определить.

Контракты федерации моделей для стабильных вызовов инструментов

Почему инструменты ломаются при смене моделей

Инструменты обычно перестают работать по простой причине: модель перестаёт выдавать результат в той точной форме, которую ожидает ваш код.

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

{"tool":"lookup_order","arguments":{"order_id":"4821"}}

Обычный текстовый ответ может выглядеть похоже на ответ человека, но это не одно и то же:

"I should call lookup_order for order 4821."

Человек прочитает это и поймёт. Ваш парсер обычно — нет. Как только вы начинаете добавлять специальные правила для свободного текста, каждая новая модель становится поводом для очередного исправления.

Небольшие изменения формата тоже ломают инструменты. Поломка не обязана быть драматичной. Одна модель может отправлять arguments как объект. Другая — как JSON-строку. Третья может изменить lookup_order на LookupOrder или добавить одну дружественную фразу перед JSON.

Большинство сбоев начинается с крошечных различий: лишний текст перед полезной частью, изменённые имена полей, числа как строки, пропущенные или переименованные имена инструментов или невалидный JSON с одной лишней запятой.

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

Это ещё хуже, когда команды хотят поменять модель. Рабочий процесс может работать с одной моделью сегодня, а завтра сломаться при переключении на Claude, GPT или открытую модель. Инструмент не менялся. Изменился обёртка вокруг него.

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

Что нужно заложить в контракт с первого дня

Хороший контракт делает смены моделей скучными. Если одна модель формулирует вызовы инструментов чуть по‑другому, агент всё равно должен получать те же факты в тех же местах.

Каждый запрос должен иметь фиксированную внешнюю форму. На практике это обычно означает request_id, session_id или id беседы, историю сообщений в одном стандартном массиве, определения инструментов со стабильными именами и правилами ввода, и метаданные модели в отдельном объекте.

Это разделение важнее, чем кажется. Данные инструмента должны описывать сам инструмент: имя, аргументы и, возможно, tool_call_id. Метаданные модели должны располагаться в другом месте. Если имена провайдеров, количество токенов или флаги вендора протекают в полезную нагрузку инструмента, вы потратите время на исключения для каждой модели.

Ответы требуют той же дисциплины. Возвращайте оригинальный request_id, response_id, верхнеуровневый status, вывод ассистента в одном стандартном формате сообщения, стандартный массив для вызовов инструментов или результатов инструментов и один объект ошибки с кодом, сообщением и опциональными деталями.

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

Небольшой пример помогает представить это проще. Допустим одна модель просит вызвать find_invoice, а другая просит тот же инструмент, но с иным форматированием от вендора. Если оба ответа всё ещё возвращают одинаковый status, одинаковый массив tool_calls[] и один и тот же формат ошибок, агенту не важно, какая модель их сгенерировала.

Выберите форму запроса в первую очередь

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

Хороший запрос остаётся простым. Он требует одних и тех же полей каждый раз, даже если модель за адаптером меняется. Для многих команд достаточно messages, tools, tool_choice и небольшого блока metadata или trace id.

Держите историю чата отдельно от определения инструментов. Messages содержат беседу. Tools описывают, какие вызовы агент может делать прямо сейчас. Когда команды смешивают всё в один большой полезный груз, кэширование становится сложнее, а отладка — медленнее. Изменение описания инструмента также может повлиять на старые подсказки непредсказуемым образом.

Используйте одни и те же типы аргументов во всех адаптерах. Выберите одну JSON‑форму и придерживайтесь её. Если user_id строка в одном адаптере, пусть он будет строкой везде. Если limit число, не позволяйте другому адаптеру превратить его в "10". Дрейф типов вызывает реальные сбои инструментов.

Настройки провайдера нужно держать в своей полосе. Поместите их в отдельный объект, например provider_options, и ограничьтесь этим. Ваше приложение может передавать reasoning, temperature или флаги кеширования в адаптер для Claude или OpenAI, но остальная система не должна зависеть от этих полей. Периферийные инструменты должны видеть только общий формат запроса и ответа.

Это особенно важно в мульти‑модельных настройках. Oleg Sotnikov часто работает с командами, которые маршрутизируют задачи через Claude, GPT и открытые модели в одном рабочем потоке. Когда каждый адаптер принимает одинаковую форму запроса, маршрутизатор может менять модели без переписывания кода инструментов, и ошибки становится проще прослеживать.

Стабильная форма запроса — не что‑то эффектное. Она строга там, где нужно, по умолчанию компактна и открыта только на границах.

Выберите форму ответа, которая остаётся предсказуемой

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

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

Компактному ответу обычно нужны четыре поля: role, content, tool_calls и error. Устанавливайте role в assistant каждый раз. content хранит текст для пользователя или пустую строку, когда модель только вызывает инструмент. Делайте tool_calls массивом, даже если там один вызов. errornull при успехе и объект при ошибке.

Вызовы инструментов не должны прятаться в свободном тексте типа "I will now call search_orders". Поместите их в выделенное поле tool_calls с именем инструмента, id вызова и структурированными аргументами. Текст — для людей. Объекты вызовов инструментов — для кода.

Этот выбор спасёт много проблем в будущем. Если одна модель возвращает текст, похожий на JSON, а другая — чистый объект инструмента, уровень агента начнёт угадывать. Угадывание годится для демо. В продакшне оно проваливается.

Держите стриминг отдельно от финального сообщения

Стриминг требует другой формы, чем финальный ответ ассистента. Во время стриминга присылайте чанки как фреймы событий, например text_delta, tool_call_delta или status. Когда поток заканчивается, собирайте эти части в тот же финальный объект ассистента каждый раз.

Не смешивайте частичные чанки с готовым объектом ответа. Вызывающий должен знать, читает ли он живые обновления или полный ответ. Если обе формы выглядят похоже, рано или поздно кто‑то распарсит не то.

Простое правило помогает: фреймы событий для стриминга, один полный объект ассистента для завершения.

Делайте ошибки простыми для чтения

Ошибки требуют структуры, но им также нужен простой язык. Хороший объект ошибки даёт машинам что‑то стабильное и людям — информацию, которую можно просканировать за две секунды.

Используйте поля вроде code, message и retryable. Держите message коротким и прямым, например "Tool arguments failed validation" или "Model returned malformed JSON". Избегайте расплывчатых формулировок вроде "unexpected issue occurred".

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

Ясно определите имена инструментов, входы и ошибки

Review Your Tool Contract
Let Oleg spot weak request and response shapes before they break production.

В командах, которые переключаются между Claude, GPT и открытыми моделями, сбои инструментов часто начинаются с мелких ошибок в именах. Инструмент под именем search, web_search и runSearch может означать одно и то же для людей, но модели не воспринимают их одинаково.

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

Вводы требуют такого же внимания. Каждый аргумент должен иметь простую метку и тип, который оставляет мало места для толкований. email говорит модели больше, чем target. start_date и end_date безопаснее, чем range, потому что модель не вынуждена придумывать структуру.

Также нужно правило на случай плохого ввода. Решите, что происходит, когда обязательное поле отсутствует, когда значение имеет неправильный тип и когда модель присылает лишние поля, которые вы не хотите. Если один инструмент игнорирует лишние данные, а другой отвергает их, агент будет вести себя по‑разному после смены модели.

Небольшой пример очевиден. Представьте инструмент create_ticket с тремя входами: title как текст, priority одно из low, medium или high, и assignee_id как число. Если модель присылает priority: "urgent", все модели должны получить одинаковую форму ответа. Не позволяйте одному инструменту молча приводить это значение, а другому возвращать кастомную строку ошибки.

Держите один формат ошибок для всех инструментов. Простая схема работает хорошо: код ошибки вроде missing_field или invalid_type, короткое сообщение для человека, имя поля, если проблема вызвана конкретным полем, и опциональная подсказка с ожидаемым значением.

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

Стройте контракт малыми шагами

Начните с одной страницы JSON. Держите её простой. Опишите один объект запроса, один объект ответа и короткую заметку для каждого поля.

Если три человека прочитали страницу и дали три разных значения полю, контракт не готов. Чёткие имена побеждают умные игры.

Начните с формы запроса. Оставьте только те поля, которые вам действительно нужны: messages, tools, tool choice, model id и небольшой блок metadata, если вы его используете. Отложите специфичные для провайдера дополнения.

Затем сопоставьте один провайдер с этой формой. Выберите одну модель и постройте адаптер, который превратит ваш контракт в вызов API этого провайдера. Если вы используете Claude, GPT и открытые модели в одном потоке, не пытайтесь поддерживать их все в первый день. Один адаптер покажет, где контракт слишком свободен.

Дальше прогоните один инструмент сквозь всю систему, прежде чем добавлять второй. Выберите что‑то простое, например get_customer_status с маленьким JSON‑вводом и простым JSON‑результатом. Проследите весь путь: входящий запрос, вывод модели, вызов инструмента, результат инструмента, финальный ответ. Именно там обычно проявляются ошибки схемы.

Тесты нужно запускать рано. Покройте несколько простых случаев сначала:

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

После этого заморозьте v1. Не переименовывайте поля только потому, что другой провайдер предпочёл иную форму. Держите публичный контракт стабильным и исправляйте различия внутри адаптеров.

Логируйте каждое несоответствие. Фиксируйте отсутствующие аргументы, неверные типы, неизвестные инструменты, невалидный JSON и пустые ответы. Эти логи покажут, сломался ли адаптер или сам контракт нуждается в доработке.

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

Простой пример с двумя моделями

Fix Model Adapter Drift
Get a practical review of adapters, schemas, and validation across Claude, GPT, and open models.

Представьте агента, который назначает звонки в поддержку. Пользователь пишет: "I need help with deployment tomorrow afternoon." Агент должен собрать время, адрес электронной почты и короткую причину звонка, затем передать эти данные одному инструменту бронирования.

Теперь отправьте одно и то же определение инструмента двум разным моделям. Модель A может ответить чистым вызовом book_support_call и аккуратным JSON‑вводом. Модель B может выбрать тот же инструмент, но немного иначе форматировать поля, например customer_email вместо email или time_slot вместо start_time. Обе модели поняли задачу. Они просто выразили её по‑разному.

Инструменту не должно приходиться угадывать, что имела в виду модель. Слой агента должен нормализовать оба вывода, прежде чем что‑то дойдёт до календаря.

Обычный нормализатор выполняет четыре задачи:

  • отображает альтернативные имена полей на утверждённое имя
  • преобразует даты и время в единый формат
  • заполняет отсутствующие необязательные поля значениями по‑умолчанию
  • раннее отклонение неизвестных инструментов или неверных вводов

После нормализации оба вывода моделей становятся одним и тем же внутренним запросом. Ваша система может превратить любой ответ в одну форму: имя инструмента, аргументы, request id и статус валидации, если вы его отслеживаете. Тогда инструмент бронирования запускается один раз, предсказуемо.

Путь возврата должен оставаться так же строг. Если бронирование прошло успешно, возвращайте один и тот же результат любому вызывающему: status, booking id, scheduled time и безопасное для пользователя сообщение. Если бронирование провалилось, возвращайте тот же формат ошибки с ясным кодом, например slot_unavailable или missing_email.

Такая последовательность делает модель взаимозаменяемой. Вы сможете менять модель ради цены, скорости или качества без переписывания инструмента. Модель может меняться. Контракт — нет.

Ошибки, которые вызывают поломки позже

Большинство поломок начинается с малого. Команда добавляет одно специфичное для провайдера упрощение, никто не замечает, и через месяц один агент посылает неверный полезный груз в инструмент.

Первая ловушка — позволять каждому провайдеру называть аргументы по‑своему. Одна модель шлёт query, другая — search_term, третья — q. Инструменту теперь нужен кастомный маппинг для каждой модели, и каждая новая модель добавляет ещё одно место для ошибок. Выберите одно имя поля и держите его везде, даже если провайдеры под капотом используют другие метки.

Ещё одна путаница возникает, когда команды смешивают ошибки инструмента и ошибки модели. Это не одно и то же. Если модель таймаутилась — это одна ошибка. Если инструмент вернул "customer not found" или "rate limit reached" — это другая. Размещайте их в разных полях, чтобы агент мог решать, что повторить, что показать пользователю, а что залогировать.

Свободный текст тоже наносит тихий вред. Если результат инструмента приходит в виде абзаца вроде "I found three matching orders and one is delayed", следующая модель должна угадывать, где именно данные. Это работает, пока не перестаёт работать. Возвращайте структурированные поля, такие как status, data и error, а человекочитаемый текст держите как отдельное опциональное поле.

Изменения схемят ломают систему и долго после релиза. Крошечное переименование customer_id в client_id может выбить старые агенты, закешированные подсказки или тесты. Версионируйте контракт намеренно. Даже простой schema_version: "1.1" даёт чистый способ поддерживать старые и новые клиенты во время перехода.

Null, пустые и частичные значения требуют больше внимания, чем многие команды им уделяют. Решите, что означает null, когда значение неизвестно; что означает пустая строка, когда пользователь ничего не ввёл; что означает пустой массив, когда инструмент ничего не нашёл; и как представлять частичные данные, когда один шаг провалился, но другие поля всё ещё пригодны.

Небольшой пример прояснит. Если погодный инструмент возвращает temperature: null, означает ли это невалидный город, сбой провайдера или временную недоступность температуры? Если контракт этого не говорит, каждая модель будет догадываться по‑своему.

Хорошие контракты намеренно кажутся скучными. Это обычно хороший признак.

Быстрая проверка перед запуском

Strengthen Multi Model Workflows
Get help routing work across models without rewriting tools each time.

Прежде чем пустить реальный трафик через новую модель, выполните короткую предполётную проверку. Большинство ошибок инструментов начинается с мелких несоответствий: отсутствующее поле, переименованный статус или ошибка, спрятанная в свободном тексте.

Ваш слой инструментов должен видеть одну форму каждый раз, независимо от модели за адаптером.

Используйте небольшой чек‑лист и тестируйте на реальных подсказках, а не только на удачных примерах:

  • Каждый адаптер должен испускать одинаковые поля запроса в одинаковых местах, даже если исходная модель использует разные имена.
  • Каждый нормализованный ответ должен объявлять один явный исход: text, tool_call или error.
  • Логи должны хранить и сырой вывод модели, и нормализованный вывод, который ваша система реально использовала.
  • Одна плохая модель должна падать сама по себе, не блокируя весь конвейер инструментов.

Первая проверка звучит скучно, но экономит много проблем. Если один адаптер отправляет tool_name, а другой — name, раннер инструментов никогда не должен иметь дело с этим несоответствием. Адаптер должен исправить это до исполнения.

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

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

Изоляция — последняя проверка, и её легко пропустить. Не позволяйте одному сломанному адаптеру блокировать все вызовы инструментов в очереди. Добавьте таймауты, обработку ошибок на модель и пути резервного копирования. Если одна модель начнёт возвращать мусор в 2:00 ночи, остальная система должна продолжать работать.

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

Что делать дальше

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

Выберите один инструмент, где ломки причиняют наибольший вред — поиск, запись в файл или создание тикета. Опишите версию 1 для этого инструмента на этой неделе, затем заморозьте формат запроса и ответа достаточно надолго, чтобы протестировать его на реальном трафике. Большинство команд узнают больше от одного живого инструмента, чем от десяти диаграмм на белой доске.

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

  • Выберите инструмент, где ломка причиняет наибольший ущерб, и опишите точную схему для входов, выходов и ошибок.
  • Добавьте тесты адаптеров прежде, чем подключать больше моделей. Отправьте один и тот же вызов через каждый адаптер и сравните финальный JSON, а не формулировки модели.
  • Просмотрите реальные логи через несколько дней. Уточните поля, которые остаются расплывчатыми, опциональными или несогласованными между моделями.
  • Повышайте версию только тогда, когда форма действительно меняется. Небольшие изменения формулировок не должны заставлять каждый агент обновляться.

Логи обычно подсказывают, где контракт слишком свободен. Одна модель может вернуть timeout, другая — tool_error, третья — спрятать ошибку в тексте. Исправьте это рано. Дайте каждой ошибке чистый код, короткое сообщение и место для машинно‑читаемых деталей. Редкие дополнительные поля — это хорошо. Если поле не помогает маршрутизации, повторным попыткам или отладке, уберите его.

Здесь общий слой либо остаётся простым, либо начинает дрейфовать. Команды часто добавляют исключения для одной модели, затем для другой, пока общий слой перестаёт быть общим. Более строгий контракт кажется замедляющим на день‑два. Потом он экономит часы.

Если вы хотите второй обзор перед запуском, Oleg Sotnikov at oleg.is помогает стартапам и небольшим командам ужесточить контракты, адаптеры и рабочие процессы разработки с AI без перестройки всей системы.