Версионирование схем инструментов без поломки агентов
Версионирование схем инструментов помогает менять поля и правила без сбоев: как вводить версии, сохранять совместимость и ловить ошибки заранее.

Почему агент ломается после смены поля
Агент не "понимает" инструмент так, как человек читает документацию. Он запоминает шаблон: имя функции, названия полей, допустимые значения, примеры вызова из промпта и прошлые удачные ответы. Когда команда меняет контракт функции, агент часто продолжает собирать аргументы по старой схеме.
Самый частый сбой выглядит банально. Вчера get_order_status(order_id) работал, сегодня функция ждет order_id и channel. Агент по привычке отправляет только старый набор аргументов, и валидация сразу режет вызов.
Но поломка не всегда видна сразу. Если вы меняете тип поля, модель тоже спотыкается. Поле было числом, стало строкой. Был свободный текст, стал enum из нескольких значений. Агент все еще выбирает аргументы по старой логике и начинает слать то, что раньше проходило.
Есть и более тихий вариант. Функция формально отрабатывает, но возвращает уже не тот смысл. Например, поле order_id заменили на external_order_id, а агент по старой памяти кладет туда внутренний номер. Ошибки на уровне JSON нет, ответ приходит, но пользователь получает статус чужого заказа или пустой результат. Такие сбои хуже явных, потому что их замечают поздно.
Обычно проблема сводится к четырем причинам: агент помнит старые имена полей, не знает о новом обязательном аргументе, продолжает выбирать старый тип или старое значение enum, либо берет примеры из промпта, которые уже не совпадают со схемой.
Последний пункт часто недооценивают. Схему обновили в коде, а системный промпт, few-shot примеры и тестовые сценарии оставили старыми. В этот момент модель получает два разных сигнала и нередко верит примерам сильнее, чем новой декларации.
В системах с tool calling это особенно заметно. Даже если вызовы идут через единый OpenAI-совместимый шлюз, сама стабильность endpoint не спасает, если схема инструмента изменилась без совместимого перехода. Поэтому версионирование схем инструментов - не формальность, а защита от скрытых ошибок в поведении агента.
Что входит в контракт инструмента
Контракт инструмента - это не только JSON-схема в описании функции. Для агента это весь набор ожиданий: как называется функция, какие поля он должен передать, какие типы данных допустимы и что он получит в ответ.
Имя функции уже задает смысл. Если инструмент называется check_order_status, агент ожидает проверку статуса заказа, а не расчет скидки или поиск клиента. Даже небольшое переименование может сбить поведение, потому что модель опирается не только на описание, но и на само имя.
Во входной части контракта агент читает поля почти как форму. Он смотрит, какие аргументы есть, какие из них обязательны и в каком виде их нужно заполнять. Если вчера поле order_id было строкой, а сегодня стало числом, часть вызовов начнет падать. Если вы сделали обязательным новое поле region, шанс успешного вызова тоже падает: агенту просто неоткуда взять это значение в старом сценарии.
Сюда же входят формат значений и структура ответа. Для человека дата 2025-04-27 и дата 27.04.2025 одинаково понятны. Для агента это два разных правила. То же самое с суммами, валютами и идентификаторами: 001234, 1234 и ORD-1234 могут вести в разные ветки логики.
Ответ инструмента тоже часть контракта. Агент строит следующий шаг по полям ответа, а не по вашим намерениям. Если раньше он получал status: "paid", а теперь получает вложенный объект payment.state, он может не понять, нужно ли подтверждать заказ, просить оплату или вызывать другой инструмент.
Это особенно заметно в системах, где агентный слой живет дольше одной версии промпта и одной модели. Команда может не менять SDK и не трогать endpoint, но смена контракта функции все равно меняет поведение агента. Простое правило здесь такое: контрактом считается все, что модель читает до вызова и после него.
Когда нужна новая версия
Новую версию схемы выпускают не тогда, когда хочется "навести порядок", а тогда, когда старый вызов уже не может жить по-старому. Если агент отправляет прежний набор полей и получает тот же понятный ответ, новая версия обычно не нужна. Это и есть нормальная обратная совместимость API.
Самый безопасный случай - вы добавляете новое необязательное поле. Старые агенты его не передают, и ничего не ломается. Новые могут начать использовать поле сразу, а старые продолжат работать без правок.
Новая версия нужна, когда меняется сам контракт функции. Обычно это видно в четырех случаях: поле меняет тип, необязательное поле становится обязательным, поле получает другой смысл, либо ответ функции меняется так, что старый агент уже не сможет его разобрать.
Смена смысла поля - один из самых неприятных сценариев. Если status="paid" раньше означал факт оплаты, а теперь обозначает этап внутренней обработки, агент начнет принимать неверные решения. Старому полю нельзя quietly давать новый смысл. Проще добавить новое поле, а старое оставить до конца переходного периода.
Полезное правило для версий полей простое: сначала добавьте, потом пометьте устаревание, потом удаляйте. Не наоборот. Пометка deprecated в схеме, документации и системном промпте снижает шанс, что команда забудет о старом формате.
Часто помогает и параллельная поддержка двух версий. Некоторое время инструмент принимает и v1, и v2, а код внутри приводит их к одной внутренней модели. Так команда видит, кто еще сидит на старом контракте, и может мигрировать без спешки.
Это особенно полезно там, где один и тот же инструмент вызывают разные модели и разные промпты. Обновление почти никогда не происходит в один день для всех клиентов и всех сценариев. Короткий период двойной поддержки обычно дешевле, чем срочный разбор поломок в продакшене.
Если сомневаетесь, задайте один вопрос: сможет ли старый агент вызвать функцию без изменений и получить тот же смысл ответа. Если нет, выпускайте новую версию.
Как менять схему без поломки
Агент ломается не из-за самого изменения, а из-за несовпадения ожиданий. Вчера инструмент принимал order_id, сегодня ждет order.id, а промпт и код агента по-прежнему собирают старый вызов. В итоге валидация падает, ответ пустеет или агент начинает бесконечно повторять один и тот же tool call.
Сначала найдите точку поломки. Проверьте реальные вызовы: какие поля агент отправляет сейчас, какие из них обязательны, где встречаются пустые строки, старые имена и старые типы. Так вы быстро поймете, слегка ли вы меняете форму данных или ломаете контракт целиком.
Рабочий порядок обычно такой:
- Добавьте
v2рядом сv1и не переписывайте старую схему поверх существующей. - На входе примите оба варианта. Если раньше приходил
order_id, а теперь нужен объектorder, поддержите оба формата. - Сведите оба входа к одной внутренней модели. Бизнес-логика должна работать с единым набором полей, а не ветвиться по версиям.
- Пишите предупреждение в лог для старых вызовов. Запрос лучше обработать, чем сломать агенту сценарий.
- Удаляйте
v1только после наблюдения за реальным трафиком, а не после локального теста.
Простой пример: check_order_status_v1 принимает order_id: string, а check_order_status_v2 принимает { order: { id: string, source: string } }. На сервере не стоит до конца держать две разные ветки обработки. Лучше сразу привести оба формата к внутренней структуре вроде normalized_order_id и normalized_source, а дальше вызывать один и тот же код.
Лог предупреждения тоже должен быть полезным. Запишите имя инструмента, версию, клиента, частоту вызовов и старые поля, которые еще приходят. По таким данным видно, какой агент или сервис все еще сидит на старом контракте и сколько трафика это дает.
Частая ошибка выглядит аккуратно только на бумаге: команда публикует новую схему, обновляет описание инструмента и сразу удаляет старые поля из валидатора. Для людей это кажется чистой архитектурой. Для агентов с tool calling это просто резкий обрыв. Намного безопаснее держать переходный период, смотреть на трафик несколько дней или недель и выключать v1 только тогда, когда старые вызовы почти исчезли.
Пример с функцией проверки заказа
У службы поддержки есть инструмент check_order. В первой версии агент отправляет два аргумента: phone и order_number. Так работает бот в чате, голосовой ассистент и несколько внутренних сценариев. Все привыкли к этому контракту, поэтому резкая замена почти всегда бьет по продакшену.
Через время команда меняет внутреннюю модель данных. Теперь система ищет заказ не по номеру телефона и номеру заказа, а по customer_id и order_id. Для новой логики это лучше: ID не зависят от формата номера, дублей меньше, а поиск идет быстрее. В v2 схема инструмента уже просит customer_id и order_id.
Плохой путь очевиден: удалить v1 и ждать, пока все обновятся. Агент с tool calling продолжит слать старые поля, валидатор отклонит вызов, а пользователь увидит пустой ответ или "заказ не найден", хотя дело не в данных, а в сломанном контракте.
Нормальный переход выглядит иначе. На входе остается совместимый слой адаптации. Он принимает старые и новые аргументы, а затем переводит их в один внутренний формат. Если пришли phone и order_number, адаптер ищет customer_id и order_id через CRM и систему заказов. Если пришли customer_id и order_id, запрос идет дальше без преобразований. Если агент прислал смешанный набор полей, код берет новые поля и пишет предупреждение в лог.
Так агент еще месяц может отправлять v1 и не падать. Пользователь ничего не замечает, а команда видит по логам реальную картину: какой агент еще живет на старой схеме, сколько таких вызовов осталось, где адаптер часто ошибается при поиске соответствий.
Для этого хватает нескольких меток в журнале: имя агента, версия схемы, сработал ли адаптер, удалось ли найти соответствие старых полей новым. С такими данными команда не гадает, когда отключать v1. Она смотрит на факты.
На практике схема редко ломает все сама по себе. Обычно проблему создает спешка. Пока старые поля еще нужны, держите их на границе системы, а внутри работайте только с новой моделью. Тогда миграция идет спокойно, а срок отключения v1 опирается на логи, а не на надежду.
Совместимость в коде, схеме и промпте
Когда функция меняет контракт, агент чаще всего ломается не из-за самой схемы, а из-за мелких расхождений между кодом, описанием инструмента и примерами в промпте. Один слой совместимости между старыми и новыми полями обычно спасает лучше, чем срочный переход на v2 для всех.
Один слой совместимости
Держите одну таблицу соответствия старых и новых полей. Не в голове команды и не в заметках, а в коде и рядом со схемой. Если раньше агент отправлял customer_id, а теперь функция ждет client_id, обработчик должен принять оба варианта и свести их к одному внутреннему полю.
Старым полям дайте те же значения по умолчанию, что были раньше. Если в v1 поле include_details по умолчанию было false, не меняйте его молча на true в v2. Агент может не передавать это поле явно, и тогда поведение изменится в самый неприятный момент.
Ответ тоже лучше держать одинаковым для v1 и v2, если смысл функции не поменялся. Если агент привык читать status, message и result, не стоит возвращать в новой версии только data и ждать, что он сам перестроится. Внутри сервиса вы можете хранить новый формат, но наружу безопаснее отдавать одну и ту же структуру.
Нормальное правило простое: внутреннюю реализацию можно менять сколько угодно, а внешний ответ лучше сохранять стабильным так долго, как это возможно.
Примеры и тесты
Промпт стареет быстрее, чем код. Если вы обновили схему, сразу обновите и примеры вызова. Агент часто повторяет именно пример, а не читает описание полей так внимательно, как хочется команде.
Перед релизом проверьте хотя бы четыре вещи: старый пример вызова все еще проходит без ошибок, новый пример дает тот же тип ответа, агент понимает старое имя поля и новое, а значения по умолчанию не меняют результат молча.
После этого прогоните один и тот же промпт на старом и новом контракте. Смотрите не только на JSON, но и на поведение агента: выбрал ли он тот же инструмент, передал ли нужные аргументы, понял ли ответ без лишних уточнений.
Если вы ведете LLM-трафик через один шлюз, такие проверки удобнее встраивать в общий слой инструментов. Например, в AI Router можно прогнать один и тот же сценарий через один OpenAI-совместимый endpoint и сравнить, как разные модели заполняют вызов инструмента без смены SDK. Но даже без отдельного шлюза правило то же: совместимость нужно держать в коде, в схеме и в примерах одновременно. Если один из этих слоев отстанет, агент начнет ошибаться.
Ошибки, которые ломают агентов чаще всего
Агенты редко ломаются из-за сложной причины. Обычно команда меняет схему так, будто ее читает человек, а не модель и код вокруг нее. Для агента даже маленькая правка может стать поломкой.
Самая частая ошибка - сделать поле обязательным без переходного срока. Вчера агент отправлял customer_id, и все работало. Сегодня вы добавили обязательный region, но старый промпт и старый код о нем не знают. Результат предсказуем: вызовы начинают падать, а модель пытается угадать значение из воздуха.
Вторая ошибка не менее болезненна: команда меняет тип поля с string на object в той же версии. Для человека это выглядит как обычное развитие функции. Для агента это уже другой формат того же вызова. Если раньше он передавал address: "Алматы", а теперь вы ждете { city, street }, старые вызовы перестают проходить валидацию.
Часто ломает и работа с enum. Допустим, инструмент принимал статусы new, paid, cancelled. Потом вы решили оставить только paid и cancelled, потому что new больше не нужен. Но старые агенты, тесты или сохраненные сценарии все еще отправляют new. Если вы сужаете допустимые значения, старые значения стоит принимать хотя бы на переходный период и явно маппить.
Есть и более тихая поломка: описание поля осталось тем же по форме, но изменилось по смыслу. Поле date сначала означало дату создания заказа, а после правки стало означать дату доставки. Схема формально та же, валидация молчит, но функция делает уже другое. Такие изменения опаснее явных, потому что их замечают поздно.
Отдельная проблема - релиз без логов и простых метрик. Если вы не смотрите, какие аргументы агент реально отправляет, вы не увидите сбой в первый день. После выката полезно следить хотя бы за четырьмя сигналами: долей ошибок валидации по каждому инструменту, неизвестными и пропущенными полями, старыми значениями enum, которые еще приходят, и долей успешных вызовов по версиям схемы.
Хорошее правило здесь простое: если меняется форма данных или смысл поля, не маскируйте это как мелкую правку. Дайте новой версии пожить рядом со старой, соберите логи и удаляйте старый контракт только тогда, когда трафик на нем почти исчез.
Короткая проверка перед выпуском
Если релиз меняет контракт хотя бы на одно поле, агент может сломаться не сразу, а через несколько дней. Проблема часто прячется не в самой схеме, а в том, что старые вызовы еще живут в коде, промптах и кешах.
Перед выкладкой полезно пройти короткую проверку:
- Прогоните старый вызов без ручных правок. Возьмите реальный payload из логов или тестов и отправьте его как есть.
- Сравните не только ответ API, но и итог для бизнеса. Новый вызов должен давать тот же результат, что и старый.
- Разведите логи по версиям. В трассировке, метриках и аудит-записи должно быть видно, пришел
v1илиv2. - Добавьте тесты на грязные входы: пустые строки, старые названия полей, неожиданные
enumи лишние аргументы. - Зафиксируйте дату отключения старой версии и донесите ее до разработчиков, владельцев интеграций и команды поддержки.
Если хотя бы один пункт не проходит, релиз лучше сдвинуть. Один лишний день перед выпуском почти всегда дешевле, чем тихая поломка в проде, когда функция формально отвечает, но делает уже не то, что вы ждете.
Что делать дальше
Не пытайтесь сразу привести в порядок все инструменты. Возьмите одну функцию, которая менялась чаще других за последние месяцы. Обычно это и есть место, где агент уже ошибался: путал старое имя поля, пропускал новый обязательный аргумент или строил вызов по старому контракту.
Дальше зафиксируйте простое правило для команды. Если вы добавляете необязательное поле, сохраняете старые имена и старый смысл ответа, можно жить в той же версии. Если меняете тип поля, делаете его обязательным, переименовываете аргумент или меняете смысл результата, выпускайте v2. Такое правило убирает споры в ревью и делает версионирование схем инструментов обычной инженерной привычкой.
Полезный минимум на один короткий спринт такой: выбрать одну проблемную функцию, описать ее текущий контракт в одном месте, включить логи по версиям схемы и ошибкам валидации, а затем прогнать 10-15 реальных сценариев на нескольких моделях. Последний шаг особенно важен. Одна модель спокойно терпит лишнее поле и достраивает вызов сама, а другая начинает пропускать обязательный аргумент или отправляет старый формат. Тест на одной модели почти ничего не гарантирует.
Логи тоже не нужно усложнять. Сохраняйте версию схемы, имя инструмента, текст ошибки валидации и входные аргументы после маскировки чувствительных данных. Тогда вы быстро поймете, агент сломался из-за промпта, модели или нового контракта.
Если команда уже гоняет LLM-трафик через AI Router, новую схему удобно проверять на одном endpoint и сравнивать поведение нескольких моделей без смены клиентского кода. Но даже в более простой архитектуре смысл тот же: сначала совместимость, потом удаление старой версии.
Этого достаточно, чтобы следующие изменения не ломали агентов случайно. Версионирование схем инструментов работает лучше всего тогда, когда оно становится обычной частью релиза, а не аварийной мерой после сбоя.