К содержимому
Инвойсбокс

Ассистенты без MCP

MCP — не единственный способ, которым ассистент вызывает инструменты. YandexGPT, навыки Алисы, локальные модели в своём контуре и свои сборки на вызове функций подключаются через слой функций: каталог инструментов тот же, меняется только форма вызова.

Сначала проверьте, точно ли слой нужен. MCP умеет не только модель: чаще его поддерживает фреймворк, в котором собран агент. Для GigaChat это LangChain и LangGraph — MCP-клиент даёт пакет langchain-mcp-adapters (транспорты stdio и HTTP), а сама модель подключается через langchain-gigachat. В такой сборке наш сервер работает как есть, по быстрому старту, и слой функций не нужен вовсе. То же у GigaAgent от Сбера и AIRI: внешние сервисы он подключает каталогом коннекторов и серверами MCP, а модель берёт из LangChain — GigaChat, ChatGPT или другую.

Слой функций готовится. Ядро — перевод каталога в формат функций, разбор вызовов, защита от повторов — написано и покрыто тестами. Транспорт для этих клиентов и адаптеры под конкретные платформы пока нет: контракты вызова функций у провайдеров меняются, и писать их по памяти мы не станем. Нужна конкретная платформа — напишите в поддержку, мы уточним контракт и вернёмся с примером.

Как это устроено

Инструмент описан один раз: имя, параметры, область действия, нужно ли подтверждение. Адаптер переводит описание в форму, понятную конкретному клиенту, а проверки, защита от дублей, подтверждения и журнал остаются общими. Иначе поведение начнёт различаться, и документация станет врать одной из половин.

flowchart LR
    core["Каталог инструментов:<br/>проверки, дубли,<br/>подтверждения, журнал"]
    mcp["Нативный MCP<br/>stdio и HTTP"]
    fn["Формат функций<br/>вызов функций у модели,<br/>свои SDK"]
    tag["Разбор из текста<br/>локальные модели"]
    api["API Инвойсбокс"]
    core --> mcp
    core --> fn
    core --> tag
    core --> api

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

  • Схема сводится к общему подмножеству. Плоские объекты, строки, числа, перечисления и массивы объектов — без anyOf и oneOf: часть платформ отбрасывает такое поле вместе со значением. Пара «единица измерения или её код» превращается в два необязательных поля, а согласованность проверяет сервер.
  • Описания короче. У провайдеров есть предел длины описания функции и поля; сервер отдаёт сжатый вариант, сохраняя главное.
  • Подтверждение только двухфазное. Метки «изменяет данные» в формате функций нет вовсе, и попросить человека посреди вызова платформа не умеет. Поэтому первый вызов возвращает сводку и одноразовый токен, а второй исполняет — то же, что в безопасности.
  • Описания по-русски. Русскоязычным моделям описания отдаются на русском: на переводе теряются «счёт-фактура» и «УПД», и модель начинает путать их с инвойсом.

Как выглядит функция

{
  "name": "create_order",
  "description": "Выставляет счёт покупателю и возвращает ссылку на оплату. Покупателем может быть организация, ИП или физлицо. Подтверждение в два шага: первый вызов возвращает сводку и confirmation_token, второй исполняет.",
  "parameters": {
    "type": "object",
    "additionalProperties": false,
    "required": ["description", "customer", "basket_items", "amount", "vat_amount", "currency_id", "expiration_date"],
    "properties": {
      "description": { "type": "string" },
      "amount": { "type": "string", "description": "сумма в копейках строкой: 12200 = 122,00 ₽" },
      "vat_amount": { "type": "string" },
      "currency_id": { "type": "string", "enum": ["RUB", "USD", "EUR", "GBP", "CNY"] },
      "customer": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "enum": ["legal", "private"] },
          "name": { "type": "string" },
          "vat_number": { "type": "string" }
        }
      },
      "confirmation_token": { "type": "string" }
    }
  }
}

Локальные модели, которые вызывают инструмент разметкой

Модели семейства Hermes и родственные вставляют вызов в текст ответа, а сервер вывода разбирает его. Разбор ненадёжен, поэтому сервер отклоняет невалидное вместо того, чтобы дополнять догадками.

<tool_call>
{"name": "get_order", "arguments": {"order_id": "01771534-1a57-f184-dee3-ebeb91dded75"}}
</tool_call>
  • Невалидный JSON, выдуманное имя функции и аргументы массивом отклоняются с причиной.
  • Два одинаковых вызова в одном ответе — реальное поведение части моделей — сводятся в один: иначе получился бы второй счёт.
  • В контуре без авторизации и ролей права по умолчанию — только чтение. Запись включается явно, и ответственность за действия ассистента остаётся на владельце контура: сервер не отличает «модель ошиблась» от «оператор попросил» — кроме подтверждений и потолков.

Что ломается на своём узле — и как это лечится

Проверено на своём узле с моделью в августе 2026. Причина каждый раз в сервере вывода, поэтому то же самое встретит любого, кто подключает MCP к модели у себя.

  • Схема с шаблоном во вложенном поле роняет вызов инструментов. Серверы вывода на llama.cpp строят по схеме грамматику. Шаблон (pattern) у поля верхнего уровня они принимают, а тот же шаблон внутри объекта или элемента массива отвечает 400 failed to parse grammar — причём на весь набор инструментов сразу, и по отказу не видно, чья схема виновата. Лечение: снимать шаблоны перед отправкой модели, а требование к формату переносить в описание поля. Проверять — подавая инструменты по одному.
  • А вот format снимать нельзя. Без него модель отправляет дату вместо метки времени и получает отказ по кругу: по формату она понимает, чего требует поле.
  • Ссылки внутри схемы ($ref) понимают не все. Указатели вида #/properties/…, которыми генераторы схем убирают повторы, разбирает не каждый сервер вывода. Надёжнее раскрыть их в самодостаточную схему.
  • Рассуждение вслух съедает ответ. У модели с размышлением весь отведённый запас может уйти в рассуждение, и вместо ответа приходит пустота. Давайте запас с избытком, а для показа рассуждение лучше выключить средствами своего сервера вывода.
  • Один слот и сон по простою. Локальный сервер часто держит один запрос за раз и выгружает модель из памяти по таймауту: первый вызов после простоя ждёт загрузки. Показывайте это состояние человеку отдельно от «недоступно», иначе диалог выглядит сломанным.
  • Дважды один отказ — это круг. Модель правит не то поле и повторяет вызов. Ставьте предел на шаги и останавливайтесь на втором одинаковом отказе, пересказав причину человеку.
  • Модель может не знать сегодняшнюю дату и поставить счёту вчерашний срок. Дату и требуемый формат (ISO 8601 со временем и зоной) называйте в системной подсказке.
  • Ставка НДС — текущая. Модель охотно берёт код с прежней ставкой 20 %, а контур отвечает vat_not_found. Действующая ставка — 22 %, коды — в справочнике.

Чего мы не поддерживаем

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