Модель сформировала JSON, но вызов инструмента не состоялся, результат MCP не прошёл проверку или API вернул отказ без понятной причины.
Самое быстрое решение: разделите поток данных JSON в AI Agent на пять независимых проверок — синтаксис, Schema, выбор инструмента, права и состояние вызова, затем отдельно валидируйте результат инструмента.
Эта статья предназначена для трёх групп:
- разработчиков AI Agent, которым нужно понимать роль JSON на каждом этапе вызова;
- инженеров эксплуатации, которые ищут причину ошибки в параметрах, статусах или результатах;
- архитекторов платформ, проектирующих единые события, журналы и версии Schema.
JSON нужен не для рассуждений, а для границ между компонентами
AI Agent не «думает в JSON» в буквальном смысле. Модель может рассуждать во внутреннем представлении, но при взаимодействии с внешней системой ей требуется формализованный контракт. JSON становится таким контрактом между несколькими слоями:
- модель предлагает имя инструмента и аргументы;
- оркестратор проверяет структуру и решает, разрешён ли вызов;
- исполнитель преобразует аргументы в реальный запрос к API или локальной команде;
- внешний сервис возвращает данные либо ошибку;
- оркестратор связывает результат с исходным вызовом;
- модель получает результат и продолжает цикл.
Поэтому стабильность зависит не от того, удалось ли вызвать JSON.parse(). Вызов может пройти синтаксический разбор и всё равно быть непригодным для работы. В документации OpenAI отдельно указано, что аргументы функции генерируются моделью в JSON-формате, но приложение обязано проверять их до выполнения: модель может создать невалидный JSON или добавить параметры, которых нет в Schema. (документация OpenAI по вызовам функций)
Полезно различать четыре типа данных:
- описание инструмента — имя, назначение и
inputSchema; - вызов инструмента — идентификатор, имя и аргументы;
- результат выполнения — полезные данные, статус и структурированная ошибка;
- событие оркестрации — версия Schema, права, попытки, тайм-аут и связь с диалогом.
Именно смешивание этих уровней создаёт большинство трудноуловимых неисправностей.
Почему JSON может быть корректным, а Agent всё равно завершает вызов ошибкой?
Синтаксически правильный JSON не означает соответствие Schema
Рассмотрим аргументы:
{
"path": "/Users/shared/report.csv",
"limit": "100"
}
Это корректный JSON. Однако он нарушает контракт, если limit должен быть целым числом:
{
"type": "object",
"properties": {
"path": { "type": "string" },
"limit": { "type": "integer", "minimum": 1 }
},
"required": ["path", "limit"],
"additionalProperties": false
}
Для исполнителя это уже другая ошибка: не «JSON сломан», а «значение не соответствует типу». Аналогичные проблемы возникают, когда:
- отсутствует обязательное поле;
- вместо объекта пришёл массив;
- передана дата в неподдерживаемом формате;
- добавлено неизвестное поле;
- значение
nullиспользуется там, где разрешена только строка; - вложенный объект имеет другую структуру.
Если вы исправляете только парсер, а не валидатор Schema, причина останется.
У разных API поддерживается разный поднабор JSON Schema. OpenAI указывает, что строгий режим для вызовов функций поддерживает не всю спецификацию, а ограниченный набор конструкций. Gemini описывает декларации функций через подмножество схемы OpenAPI, а не как универсальный контракт для любого сценария JSON Schema. Поэтому перенос одной Schema между платформами без адаптера — рискованный подход. Сверяйте ограничения с официальной документацией OpenAI по function tools и документацией Gemini по function calling. (документация OpenAI)
Schema соответствует требованиям, но выбран неправильный инструмент
Даже идеальные типы не объясняют модели бизнес-смысл. Agent может выбрать delete_file, когда пользователь просил проверить наличие файла, если:
- имена инструментов слишком похожи;
- описание не говорит, когда инструмент использовать нельзя;
- в контекст передано слишком много кандидатов;
- нет явного различия между чтением и изменением;
- задача пользователя сформулирована шире, чем действие API.
Не пытайтесь лечить ошибочный выбор бесконечным усложнением типов. Сначала исправьте семантику:
Хорошее описание:
Проверяет существование файла и возвращает его размер. Не изменяет содержимое и не удаляет файл. Используйте только для проверки локального пути.
Плохое описание:
Работа с файлами.
Проверьте также границы выбора:
- показывайте модели только инструменты, относящиеся к текущей задаче;
- используйте разные глаголы в именах:
inspect_file,upload_file,remove_file; - явно описывайте побочные эффекты;
- добавляйте ограничения по окружению;
- перед разрушительными действиями требуйте отдельного подтверждения.
Преимущества узкого набора инструментов:
- меньше конкурирующих вариантов;
- проще анализировать журналы;
- ниже риск случайного изменения состояния;
- легче тестировать подсказки и описания.
Недостатки чрезмерного ограничения:
- Agent может не найти нужное действие;
- появятся обходные вызовы;
- сложные задачи придётся вручную разбивать на этапы.
Параметры правильные, но API отклоняет запрос
После проверки Schema начинается другой слой. Исполнитель должен самостоятельно проверять:
- наличие и срок действия токена;
- права пользователя или сервисной учётной записи;
- доступ к конкретному ресурсу;
- состояние ресурса;
- сетевой маршрут и DNS;
- лимиты частоты;
- бизнес-правила API;
- идемпотентность повторного запроса.
Например, аргумент environment: "staging" может быть безупречным, но API отклонит запрос, если у токена нет доступа к этому окружению. Модель не должна самостоятельно угадывать, что означает код 403, а затем повторять вызов с теми же параметрами.
Исполнитель обязан возвращать структурированную ошибку:
{
"ok": false,
"error": {
"kind": "authorization_denied",
"message": "Токен не имеет доступа к окружению staging",
"retryable": false,
"request_id": "req_7f31"
}
}
Разделяйте как минимум:
invalid_arguments— ошибка до обращения к API;authentication_failed— проблема удостоверения;authorization_denied— удостоверение есть, права отсутствуют;resource_state_conflict— ресурс находится в неподходящем состоянии;network_timeout— временная проблема соединения;rate_limited— запрос следует повторить после задержки;upstream_error— сбой внешнего сервиса.
Так модель получает объяснимый результат и может выбрать корректное действие: запросить разрешение, изменить параметры, подождать или остановить цикл.
Кто создаёт и кто выполняет JSON в Function Calling?
В Function Calling JSON-аргументы обычно создаёт модель, но исполняет их не модель. Между ними находится ваш код:
запрос пользователя
↓
модель выбирает функцию
↓
JSON с аргументами
↓
оркестратор проверяет Schema и права
↓
исполнитель вызывает API
↓
результат связывается с call_id
↓
модель получает результат
В OpenAI вызов содержит имя функции, аргументы и идентификатор вызова; сообщение с результатом должно ссылаться на исходный tool_call_id. Anthropic использует блоки tool_use и tool_result, где tool_use_id связывает результат с конкретным запросом. Эти поля выполняют одинаковую архитектурную функцию, но не являются взаимозаменяемыми названиями. (документация OpenAI по вызовам инструментов)
В этом месте часто возникает опасная ошибка: разработчик сохраняет только name и arguments, считая ID технической деталью. При последовательных вызовах это приводит к неправильной маршрутизации результата. При параллельных вызовах последствия ещё серьёзнее:
- результат инструмента A может попасть к вызову B;
- один вызов будет считаться незавершённым;
- Agent повторит уже выполненное действие;
- цикл остановится после первого ответа;
- система не сможет доказать, какой запрос изменил ресурс.
Для каждого вызова сохраняйте минимум:
{
"trace_id": "trace_912",
"conversation_id": "conv_204",
"call_id": "call_18",
"tool_name": "inspect_file",
"schema_version": "file-inspect.v3",
"arguments_hash": "sha256:...",
"status": "started"
}
trace_id связывает весь процесс, а call_id связывает конкретный запрос инструмента с его результатом. Если платформа поддерживает previous_response_id, previous interaction или аналогичный указатель состояния, его также следует сохранять в событии. Нельзя полагаться только на позицию сообщения в массиве: история может сжиматься, ветвиться или восстанавливаться после сбоя.
Важно: ID вызова — это не идентификатор пользователя и не идентификатор ресурса. Не используйте его для авторизации и не заменяйте им бизнес-ключ операции.
Как MCP возвращает результат инструмента модели?
Model Context Protocol вводит отдельный контракт для описания и вызова инструментов. В актуальной спецификации инструмент может иметь name, description, inputSchema и необязательную outputSchema. Запрос tools/call содержит имя инструмента и объект arguments, а ответ — content, необязательный structuredContent и флаг isError. (спецификация MCP для инструментов)
Упрощённая цепочка выглядит так:
AI-клиент
→ tools/list
MCP-сервер
→ name + description + inputSchema + outputSchema
AI-клиент
→ tools/call + arguments
MCP-сервер
→ content + structuredContent + isError
AI-клиент
→ проверка результата
модель
→ продолжение ответа
Здесь важно не считать content и structuredContent одним и тем же.
contentпредназначен для текстового или мультимедийного представления результата;structuredContentсодержит данные, которые клиент может обработать как объект;outputSchemaописывает ожидаемую структуру результата;isError: trueсообщает, что выполнение инструмента завершилось ошибкой.
Спецификация MCP рекомендует вместе со структурированным результатом возвращать сериализованный JSON в текстовом блоке для обратной совместимости. Это особенно важно, если часть клиентов не умеет работать с одинаковым набором структурированных полей. Ошибка инструмента обычно должна передаваться внутри результата с isError: true, чтобы модель могла её увидеть и скорректировать действие; протокольный ответ следует оставлять для ошибок обнаружения инструмента или неподдерживаемого вызова. (правила обработки результатов MCP)
В 2026 году вокруг MCP продолжается развитие совместимости Schema. Предложение SEP-2106 расширяет поддержку JSON Schema 2020-12 и допускает более широкий набор значений в structuredContent, но это не означает, что любой существующий клиент уже принимает массивы или примитивы вместо объекта. Для совместимых систем безопаснее сохранять текстовый резервный вариант и проверять фактическую версию клиента. (предложение MCP по JSON Schema 2020-12)
Как построить проверяемый контур от модели до API?
Используйте следующий порядок внедрения.
Шаг 1. Опишите границы каждого слоя
Зафиксируйте в техническом документе:
- какие поля генерирует модель;
- какие поля создаёт оркестратор;
- где выполняется Schema-валидация;
- где принимается решение о правах;
- какой компонент вызывает API;
- кто формирует результат для модели.
Если поле появляется на двух слоях, назначьте одного владельца. Например, call_id должен создаваться в слое оркестрации либо приниматься от платформы, но не генерироваться повторно исполнителем.
Шаг 2. Введите версии входной и выходной Schema
Не изменяйте существующую Schema без версии. Используйте идентификаторы вроде:
deploy-service.input.v2
deploy-service.output.v1
В журнале храните не только название инструмента, но и точную версию. Иначе после обновления обязательного поля вы не поймёте, какой контракт использовал старый вызов.
Шаг 3. Проверяйте JSON до бизнес-логики
Порядок должен быть таким:
- декодировать строку;
- проверить, что результат имеет ожидаемый базовый тип;
- применить JSON Schema;
- нормализовать значения;
- проверить бизнес-ограничения;
- только затем обращаться к API.
Не смешивайте Schema и права. Поле project_id может быть строкой и соответствовать Schema, но пользователь всё равно не должен иметь доступ к этому проекту.
Шаг 4. Добавьте явный этап выбора инструмента
Перед выполнением сохраняйте решение:
{
"candidate_tools": ["inspect_file", "upload_file"],
"selected_tool": "inspect_file",
"selection_reason": "Требуется только чтение",
"policy_decision": "allowed"
}
Это позволяет отличить ошибку модели от ошибки маршрутизации. Если selected_tool неверен, исправляйте описания и контекст. Если выбранный инструмент верен, переходите к параметрам и исполнению.
Шаг 5. Сделайте ошибки машиночитаемыми
Каждая ошибка должна включать тип, повторяемость, безопасное сообщение и идентификатор запроса. Не возвращайте модели необработанный HTML, стек исключения или строку, где смешаны код, секреты и текст прокси-сервера.
Шаг 6. Сохраняйте связь запроса и результата
Для каждой операции записывайте:
trace_id;- идентификатор диалога;
call_idилиtool_use_id;- имя инструмента;
- хэш аргументов;
- время начала и окончания;
- статус;
- ID внешнего запроса;
- версию Schema;
- результат политики;
- размер и тип ответа.
При параллельных вызовах завершайте операции по ID, а не по порядку поступления.
Шаг 7. Валидируйте результат MCP на стороне клиента
Даже если сервер объявляет outputSchema, клиент не должен слепо передавать structuredContent последующим системам. Проверьте:
- наличие
structuredContent; - соответствие
outputSchema; - корректность
content; - значение
isError; - допустимость версии протокола;
- наличие текстового резервного варианта.
Так вы обнаружите несовместимость до того, как она превратится в ошибку бизнес-сервиса.
Шаг 8. Проведите четыре обязательные репетиции
Перед запуском воспроизведите:
- отсутствующее обязательное поле;
- отказ по правам при корректных аргументах;
- потерю или подмену ID вызова;
- MCP-ответ, не соответствующий
outputSchema.
Для каждого теста заранее определите ожидаемые события журнала. Если после отказа невозможно восстановить, где именно он возник, система ещё не готова к длительным автономным задачам.
Короткая проверка перед запуском Agent
- [ ] У каждого инструмента есть уникальное имя и описание с ограничениями.
- [ ] Входная Schema имеет версию и проверяется до вызова API.
- [ ] Исполнитель отдельно проверяет аутентификацию и авторизацию.
- [ ] Для каждого вызова сохраняются
trace_idиcall_id. - [ ] Результат ошибки имеет машинный тип и признак повторяемости.
- [ ] Параллельные ответы сопоставляются по ID, а не по позиции.
- [ ] MCP-клиент проверяет
structuredContentпоoutputSchema. - [ ] Для структурированного MCP-результата предусмотрен текстовый резервный вариант.
- [ ] Логи не содержат токены, пароли и полный набор персональных данных.
- [ ] Четыре отрицательных сценария воспроизводятся в изолированном окружении.
Три таблицы для выбора архитектуры и диагностики
| Слой | Что передаётся в JSON | Кто отвечает за проверку | Типичная ошибка |
|---|---|---|---|
| Модель → оркестратор | Имя инструмента и аргументы | Оркестратор | Неверный инструмент или поле |
| Оркестратор → исполнитель | Нормализованные аргументы и ID | Оркестратор и политика доступа | Потеря call_id |
| Исполнитель → API | HTTP-параметры, заголовки, тело | Исполнитель | Отказ по правам или состоянию |
| MCP-сервер → клиент | content, structuredContent, isError |
MCP-клиент | Несовместимый результат |
| Клиент → модель | Результат с сохранённой связью | Оркестратор | Ответ попал не к тому вызову |
| Симптом | Вероятный слой | Что проверить первым | Исправление |
|---|---|---|---|
| JSON не декодируется | Генерация или транспорт | Полный текст аргументов и потоковые фрагменты | Собрать финальную строку и вернуть ошибку параметров |
| JSON декодируется, но не проходит Schema | Контракт | Типы, required, лишние поля |
Версионировать Schema и добавить адаптер |
| Инструмент не относится к задаче | Выбор | Описание, имена, список кандидатов | Сузить набор и уточнить семантику |
| API отвечает отказом | Исполнение | Токен, права, состояние ресурса | Вернуть структурированный тип ошибки |
| Модель получает чужой результат | Состояние | call_id, история, параллельность |
Сопоставлять ответы по идентификатору |
| MCP-данные не видит последующая система | Протокол | outputSchema, structuredContent, резервный текст |
Валидировать на клиенте и поддерживать content |
| Условия задачи | Предпочтительный подход | Когда выбрать альтернативу |
|---|---|---|
| Один провайдер и несколько простых функций | Нативный Function Calling с адаптером Schema | Если инструменты должны подключаться от разных серверов |
| Несколько моделей и единый каталог инструментов | Единый внутренний контракт и слой преобразования | Если команда ещё не готова поддерживать версии |
| Инструменты распределены между средами | MCP с проверкой результата на клиенте | Если все действия выполняются внутри одного процесса |
| Длинные задачи с повторными попытками | Событийная модель с trace_id, call_id и идемпотентностью |
Для коротких одноразовых запросов можно использовать упрощённый журнал |
| Инструменты управляют macOS-средой | Изолированный исполнитель и журнал состояния окружения | Для API без доступа к локальной системе достаточно контейнера |
Как выбрать среду для воспроизведения ошибок
Если Agent обращается к macOS-инструментам, проблема может находиться не в JSON. Команда может завершаться иначе из-за отсутствующего разрешения, другого пользователя, переменной окружения, версии CLI или недоступного файла. Поэтому при длительном тестировании полезно связывать события вызова с состоянием удалённой машины:
- версия macOS и установленного инструмента;
- пользователь, от имени которого выполняется команда;
- переменные окружения без секретных значений;
- доступность рабочего каталога;
- статус SSH или VNC-сессии;
- версия SDK и валидатора;
- сетевой маршрут к API.
Для разовых проверок можно использовать локальную систему. Но если ошибка должна воспроизводиться несколькими инженерами, изолированная среда обычно удобнее: зависимости фиксируются, журналы разделяются, а состояние машины не смешивается с личным рабочим компьютером. В каталоге Kvmzen можно изучить варианты аренды Mac mini для удалённой разработки, а для тестов с азиатскими API — сравнить аренду Mac mini в Сингапуре и аренду Mac mini в Японии.
Вам не обязательно переносить всю рабочую нагрузку на удалённый Mac. Рациональный вариант — выделить отдельное окружение для регрессионных тестов, проверки MCP-серверов и повторения отказов, а продуктивную инфраструктуру оставить без изменений.
Текущий подход — локальный Mac, общий сервер или универсальный Linux-хост — часто имеет три практических недостатка: состояние окружения меняется между тестами, доступ к macOS-инструментам приходится вручную восстанавливать, а журналы вызовов смешиваются с другими задачами. Покупка отдельного Mac оправдана при постоянной нагрузке и необходимости физических интерфейсов, но для временной диагностики это избыточные капитальные затраты. Если вам нужно регулярно воспроизводить ошибки JSON, MCP и Tool Calling в изолированной macOS-среде, аренда Mac у Kvmzen даёт более управляемый путь: фиксируйте зависимости, отделяйте логи и подключайте машину только на период проверки.
