Kvmzen Блог
← Назад к разделу «Технологии на практике»

Почему AI Agent не может обойтись без JSON? От Tool Calling и Function Calling до MCP: полный разбор потока данных

AIAgent ·~12 мин чтения

Почему AI Agent не может обойтись без JSON? От Tool Calling и Function Calling до MCP: полный разбор потока данных

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

Самое быстрое решение: разделите поток данных JSON в AI Agent на пять независимых проверок — синтаксис, Schema, выбор инструмента, права и состояние вызова, затем отдельно валидируйте результат инструмента.

Эта статья предназначена для трёх групп:

  • разработчиков AI Agent, которым нужно понимать роль JSON на каждом этапе вызова;
  • инженеров эксплуатации, которые ищут причину ошибки в параметрах, статусах или результатах;
  • архитекторов платформ, проектирующих единые события, журналы и версии Schema.

JSON нужен не для рассуждений, а для границ между компонентами

AI Agent не «думает в JSON» в буквальном смысле. Модель может рассуждать во внутреннем представлении, но при взаимодействии с внешней системой ей требуется формализованный контракт. JSON становится таким контрактом между несколькими слоями:

  1. модель предлагает имя инструмента и аргументы;
  2. оркестратор проверяет структуру и решает, разрешён ли вызов;
  3. исполнитель преобразует аргументы в реальный запрос к API или локальной команде;
  4. внешний сервис возвращает данные либо ошибку;
  5. оркестратор связывает результат с исходным вызовом;
  6. модель получает результат и продолжает цикл.

Поэтому стабильность зависит не от того, удалось ли вызвать 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 до бизнес-логики

Порядок должен быть таким:

  1. декодировать строку;
  2. проверить, что результат имеет ожидаемый базовый тип;
  3. применить JSON Schema;
  4. нормализовать значения;
  5. проверить бизнес-ограничения;
  6. только затем обращаться к 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 даёт более управляемый путь: фиксируйте зависимости, отделяйте логи и подключайте машину только на период проверки.

Ограниченное предложение

Больше чем Mac — ваша база разработки в облаке

Выделенные вычисления · Глобальные узлы · Ежемесячная подписка · Без покупки железа

На главную
Ограниченное предложение Посмотреть тарифы