Когда 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, а не как к подсказке модели: строгость для генерации, валидация с починкой в рантайме, политики поверх схемы на шлюзе. «Почти правильный» вызов должен либо стать правильным до исполнения, либо вернуться модели на доработку — но никогда не исполняться как есть. Исполнение догадок вашими правами — это не интеграция, это лотерея.