Назад в блог

Валидация аргументов инструментов: схема как контракт, а не документация

Аргументы от модели почти правильные — и «почти» исполняется как баг. Три границы схемы: strict для модели, рантайм-валидация с починкой, политики поверх схемы на шлюзе.

Иллюстрация к материалу: Валидация аргументов инструментов: схема как контракт, а не документация

Когда LLM вызывает инструмент, аргументы, которые она генерирует, часто почти правильные: число прислано строкой ("3" вместо 3), boolean написан как "yes", enum — не в том регистре ("Celsius" вместо "celsius"), весь объект обёрнут в json-fence или зарыт в предложение, поверх — лишние поля, которых схема не знает. С этим месивом инструмент делает два плохих исхода: падает (лучший случай) или исполняется не так (худший — days: "soon" превращается в дефолт, о котором никто не просил). Большинство команд относится к JSON Schema инструмента как к документации для модели. Производственный подход 2026 года — как к типизированному API-контракту, enforced на трёх границах: у модели, в рантайме и на ответе.

Почти правильные аргументы

Каталог типовых дефектов из практики валидаторов (toolcall-guard, tool-call-validator) стабилен across моделей и провайдеров: скаляр не того типа, enum не в том регистре, missing required при наличии мусора, unknown properties, JSON в прозе и fences, smart quotes и trailing commas, boolean словами. Это не «модель плохая» — это природа генерации: модель пишет текст, похожий на правильный JSON. Разница между демо и продом — есть ли слой, превращающий «похожий» в «валидный» до исполнения, а не после инцидента.

Три границы схемы

Производственный паттерн (AppScale) требует, чтобы схема работала в трёх местах — с разной ролью в каждом:

Граница 1. Модель. Описание и форма параметров внутри промпта: что инструмент делает, какие поля, какие обязательны, какие значения допустимы, примеры входов там, где схема недоопределяет usage. Хорошие описания снижают долю кривых вызовов на порядок — это самая дешёвая валидация.

Граница 2. Рантайм. Валидация эмитированных аргументов до инвокации: парсинг, проверка по схеме, починка безопасного, возврат на доработку небезопасного. Ни один невалидный вызов не должен достичь тела инструмента.

Граница 3. Ответ. Структурированный выход инструмента тоже валидируется: контракт двусторонний. Агент, получивший от инструмента мусор, принимает решения на мусоре — проверяйте и эту сторону.

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

Strict schemas: что значит строгий

Строгий режим (OpenAI Agents SDK, AI SDK strict: true) — это набор жёстких правил: объекты закрыты (additionalProperties: false), объявленные свойства обязательны, юнионы и перечисления нормализованы, неподдерживаемые формы схем отклоняются на построении инструмента, а не в проде. Провайдеры со strict tool calling генерируют только валидные вызовы по заданной схеме — но поддерживают не все конструкции, и что поддерживается, зависит от провайдера. Практические правила: держите схемы плоскими и явными; закрывайте объекты; не тащите в схему то, что модель не может породить из JSON (сложные union, рекурсии); тестируйте конвертеры обоих путей (Chat Completions и Responses различаются); не мутируйте чужие shared-схемы при нормализации — копируйте на границе.

Coercion vs reject

Не каждую ошибку стоит отклонять. Зрелая политика — два режима:

Coercion — чинить безопасное. Приведение "3" к 3, нормализация регистра enum, снос fences и прозы, отбрасывание unknown keys при закрытой схеме — то, что не меняет смысл вызова. Инструмент исполняется, в журнал пишется факт починки.

Reject — отклонять меняющее смысл. Отсутствующее required-поле, значение вне enum после нормализации, тип, который нельзя привести без догадок, — вызов не исполняется. Угадывание намерения модели — это исполнение чужой догадки вашими правами.

Граница между режимами — политика, а не вкусовщина: зафиксируйте таблицу «чиним / отклоняем» на инструмент и покройте её тестами. Всё, что чинится молча, должно быть видно в журнале — иначе отладка «почему вызвалось так» невозможна.

Correction loop вместо падения

Отклонённый вызов — не ошибка, а сообщение модели. Паттерн correction loop: валидатор возвращает короткое model-directed сообщение («поле days должно быть integer, получена строка; отсутствует обязательное city; отвечай только исправленным вызовом»), оно добавляется в диалог, модель перевыпускает валидный вызов — цикл агента замыкается вместо краха. LangChain middleware реализует это внутри model node: только финальное валидное сообщение попадает в состояние графа, с лимитом ретраев (обычно 2) и политикой на исчерпание — fail open (пропустить) или fail closed (поднять). Для чувствительных инструментов — только fail closed: «пропустить» означает исполнить непроверенное.

Поверх схемы: allow-list, роли, риск

Схема проверяет форму, но не право и не смысл. Поэтому поверх — политики шлюза:

  • Allow-list инструментов. Неизвестный инструмент отклоняется до валидации аргументов. Нет в реестре — нет вызова.
  • Роли и права. Локальный pre-check роль→действие как defense in depth: даже валидный вызов от роли без гранта не исполняется.
  • Risk detection. Правила риска по аргументам: сумма выше порога, внешний получатель, массовый охват — CRITICAL в отказ, MEDIUM/HIGH на approval. Схема сказала «валидно», риск-движок спрашивает «а можно ли».
  • Approval. Чувствительные вызовы возвращают approval_id вместо исполнения; approver резолвит, агент переисполняет через approval-handle. Разделение обязанностей: запросивший не подтверждает.

Конвейер решения на шлюзе

Сведите всё в детерминированный pipeline решения — по образцу Cerberus из шести стадий: S1 schema_validation (неизвестный инструмент и невалидные аргументы — жёсткий стоп), S2 permission (ролевой pre-check), S3 policy (движок политик), S4 risk_detection (CRITICAL → DENY, MEDIUM/HIGH → REQUIRE_APPROVAL), S5 approval (человек для чувствительного), S6 finalize (сводка вердиктов в одно решение со следом). Отклонённые вызовы не исполняются никогда; чувствительные — только с approval; каждое решение — в hash-chained журнал. Конвейер детерминирован: один и тот же вызов при тех же политиках даёт то же решение — и это тестируется как обычный код.

Чеклист проектирования инструментов

  • У каждого инструмента — закрытая строгая схема: required, enum, additionalProperties false.
  • Описания и примеры входов — для модели; сложные конструкции из схем убраны.
  • Рантайм-валидация до исполнения и до HITL; таблица coercion/reject зафиксирована и покрыта тестами.
  • Correction loop с лимитом ретраев; на чувствительном — fail closed.
  • Ответы инструментов тоже валидируются по схеме выхода.
  • Allow-list инструментов; неизвестное отклоняется до разбора аргументов.
  • Роли, лимиты, risk-правила и approval поверх схемы; конвейер детерминирован и тестируем.
  • Починки и отказы видны в журнале с путём ошибки (path-tagged errors).

Где Codenik

Codenik — это шлюз, где схема enforced, а не рекомендована. Реестр инструментов с версиями: неизвестного нет в природе вызовов. Аргументы валидируются по схеме до исполнения — с починкой безопасного и correction loop для остального. Рискованные значения (суммы, внешние адреса, охваты) уходят на approval с именованным подтверждающим. Каждое решение конвейера — в журнале: разрешено, отклонено, почему, кем подтверждено. Модель может генерировать что угодно — исполняется только то, что прошло схему, права, риск и человека. Граница «модель → вызов» становится инженерным артефактом: версионированным, тестируемым и доказуемым.

Короткий вывод

Относитесь к схеме инструмента как к контракту API, а не как к подсказке модели: строгость для генерации, валидация с починкой в рантайме, политики поверх схемы на шлюзе. «Почти правильный» вызов должен либо стать правильным до исполнения, либо вернуться модели на доработку — но никогда не исполняться как есть. Исполнение догадок вашими правами — это не интеграция, это лотерея.

Источники и дальнейшее чтение