Почему «ответьте JSON» недостаточно
Локальную языковую модель часто подключают к CRM, сервис-деску или учётной системе через простой договор: модель получает письмо или документ и возвращает JSON с типом обращения, реквизитами и следующим действием. На демонстрации это работает. В эксплуатации появляются лишняя запятая, другое имя поля, свободный комментарий перед объектом или значение, которого нет в справочнике.
Обычный промпт лишь просит соблюдать формат. Он не мешает модели выбрать токен, после которого строка перестанет соответствовать ожидаемой структуре. Повторный запрос «исправь JSON» уменьшает число ошибок, но добавляет задержку, расход вычислений и новую недетерминированность.
Управляемая генерация решает более узкую задачу: на каждом шаге разрешает только токены, которые ещё могут привести к строке, допустимой грамматикой или схемой. В документации `llama.cpp` формат GBNF описан именно как способ ограничить вывод; стек также умеет преобразовывать поддерживаемое подмножество JSON Schema в грамматику и принимать схему в параметрах сервера.
Это важное улучшение интерфейса, но не автоматическая гарантия бизнес-результата. Валидный JSON может содержать неверный ИНН, несуществующего клиента, дату в закрытом периоде или уверенно придуманную сумму. Поэтому рабочая архитектура состоит не из одной схемы, а из нескольких последовательных ворот.
Что именно гарантирует ограничение генерации
Грамматика контролирует форму ответа: допустимые скобки, поля, типы, перечисления и некоторые ограничения длины или количества элементов. Исследование авторов Outlines показывает общий принцип: допустимые продолжения можно вычислять как переходы конечного автомата и маскировать остальные токены модели.
Практически это даёт три преимущества:
- парсер перестаёт ловить случайный текст вокруг объекта;
- интеграция получает предсказуемые имена и типы полей;
- часть ошибок обнаруживается во время генерации, а не после записи в систему.
Но граница гарантии проходит по синтаксису и описанной структуре. Схема не проверяет правдивость извлечённых фактов, полномочия пользователя, состояние сделки и последствия действия. Более того, `llama.cpp` прямо предупреждает: схема ограничивает вывод, но не внедряется в промпт, поэтому ожидаемый смысл полей нужно объяснить модели отдельно. Поддерживается подмножество JSON Schema, а сложные грамматики могут влиять на скорость.
Отсюда правило: **ограниченная генерация заменяет ремонт JSON, но не заменяет валидацию данных и контроль действий**.
Минимальная схема для классификации заявки
Допустим, входящее обращение нужно превратить в карточку для CRM. Начните с малого объекта, а не пытайтесь описать весь интерфейс системы:
```json
{
"type": "object",
"properties": {
"request_type": {
"type": "string",
"enum": ["sales", "support", "billing", "unknown"]
},
"customer_id": {"type": ["string", "null"]},
"summary": {"type": "string", "minLength": 1, "maxLength": 500},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"evidence": {
"type": "array",
"items": {"type": "string", "maxLength": 300},
"maxItems": 5
}
},
"required": ["request_type", "customer_id", "summary", "confidence", "evidence"],
"additionalProperties": false
}
```
`required` нужен явно: в JSON Schema перечисление полей в `properties` само по себе не делает их обязательными. `additionalProperties: false` закрывает путь для неожиданных ключей. Перечисление ограничивает маршруты заранее утверждённым справочником, а `unknown` позволяет модели честно не выбирать между неподходящими вариантами.
Поле `confidence` не следует трактовать как измеренную вероятность. Это сигнал для маршрутизации, который нужно откалибровать на собственных примерах. Поле `evidence` полезнее: оно заставляет систему вернуть короткие фрагменты основания, которые человек или правило могут сопоставить с исходным сообщением.
Архитектура из семи ворот
**1. Нормализация входа.** Почта, форма или документ превращаются в единый объект. Вложения проходят антивирусную проверку, OCR и очистку. Исходник получает неизменяемый идентификатор.
**2. Разделение инструкции и данных.** Системная инструкция описывает задачу и смысл полей. Текст клиента передаётся как недоверенные данные. Команды внутри письма не получают приоритета над политикой приложения.
**3. Генерация по схеме.** Сервер локальной модели применяет JSON Schema или подготовленную GBNF-грамматику. Версии модели, шаблона чата, схемы и сервера фиксируются в журнале.
**4. Независимый валидатор.** Ответ повторно проверяется обычной библиотекой JSON Schema вне модели. Это защищает от несовместимой функции движка, ошибки конфигурации и регрессии после обновления. В открытых задачах `llama.cpp` встречались сообщения о конфигурациях, где параметр принимался, но ограничение фактически не применялось; интеграционный тест должен замечать такой отказ.
**5. Бизнес-правила.** `customer_id` ищется в CRM, сумма сверяется с документом, дата — с открытым периодом, категория — с доступной очередью. Эти проверки детерминированы и не передаются модели.
**6. Очередь исключений.** Низкая уверенность, конфликт полей, неизвестный клиент или нарушение правила создают задачу человеку. Оператор видит исходник, извлечённые значения, основания и конкретную причину остановки.
**7. Идемпотентная запись.** Только после подтверждения система создаёт или обновляет карточку с ключом исходного события. Повторная доставка письма не должна породить вторую сделку. Для внешнего письма, платежа или удаления требуется отдельное явное подтверждение.
Такой конвейер позволяет использовать локальную модель там, где важна конфиденциальность, но не делает её владельцем справочников и бизнес-правил.
Какие данные и инфраструктура нужны
Для пилота достаточно одного сервера модели, небольшого сервиса-оркестратора, валидатора, очереди и тестового контура целевой системы. Видеокарта зависит от выбранной модели, объёма контекста и целевой задержки; сама структурированная генерация не отменяет нагрузочного теста.
Подготовить нужно не «все данные компании», а четыре набора:
- 100–300 реальных обезличенных входов выбранного процесса;
- эталонные структурированные ответы, подтверждённые владельцем процесса;
- справочники допустимых клиентов, категорий, статусов и единиц измерения;
- редкие и конфликтные примеры: пустые вложения, несколько клиентов, противоречивые суммы, неизвестный формат.
Схему храните как версионируемый контракт рядом с кодом интеграции. Изменение имени поля или перечня статусов — это миграция интерфейса, а не правка промпта «на лету». На переходный период потребитель должен понимать старую и новую версии либо получать явное поле версии.
В журнале достаточно сохранять хеш или идентификатор исходника, версии компонентов, результат структурной и бизнес-проверки, решение человека, время и стоимость обработки. Чувствительный исходный текст не нужно копировать во все логи.
Как тестировать до подключения CRM
Сначала запускайте модель в режиме «только черновик». Для каждого примера измеряйте отдельно:
- долю ответов, прошедших независимую проверку схемы;
- точность каждого бизнес-поля, а не среднюю оценку объекта;
- полноту: сколько обязательных фактов найдено;
- долю `unknown` и долю ручной очереди;
- критические ложные срабатывания, например неверного клиента;
- медиану и 95-й перцентиль задержки;
- стоимость одного принятого результата и время проверки человеком.
Обязательный отрицательный тест должен намеренно требовать поле или значение, запрещённое схемой. Если сервер возвращает его и HTTP-ответ выглядит успешным, ограничение не работает. Повторите тест после каждого обновления движка, модели или шаблона чата.
Для бизнес-качества разделите выборку на настройку и контроль. Не редактируйте схему и промпт, глядя на контрольную часть, иначе получите красивый тест без прогноза на новые обращения. Критические поля проверяйте правилом или человеком даже при высокой общей точности.
Ограничения и типовые ошибки
Первая ошибка — слишком широкая схема. Свободные строки на тысячи символов и разрешённые дополнительные поля возвращают почти всю неопределённость текстового ответа. Поля должны соответствовать конкретному следующему шагу процесса.
Вторая — слишком жёсткая схема без `unknown` и `null`. Модель будет вынуждена выбрать формально допустимое, но ложное значение. Возможность остановиться — часть надёжности.
Третья — вера в `confidence`. Самооценка модели не заменяет калибровку. Порог выбирают по стоимости разных ошибок: пропущенная заявка и неверно выставленный счёт имеют разные последствия.
Четвёртая — запись сразу после валидации JSON. Корректная строка не доказывает, что клиент существует и пользователь имеет право менять его карточку.
Пятая — обновление `llama.cpp` без контрактных тестов. Документация указывает на поддержку подмножества схем и возможные проблемы производительности у некоторых грамматик. Зафиксируйте рабочую сборку, затем обновляйте через стенд.
Шестая — один огромный запрос. Извлечение реквизитов, классификация, поиск клиента и решение о действии лучше разделить. Детерминированный поиск по идентификатору не нужно поручать генеративной модели.
Модельная экономика пилота
Рассмотрим условный сервисный отдел с 4 000 обращений в месяц. Ручная классификация и перенос пяти полей занимают в среднем две минуты — около 133 часов. Если система автоматически принимает 70% обращений, а проверка исключения занимает одну минуту, потенциально высвобождается примерно 73 часа: 93 часа автоматизированной работы минус 20 часов проверки оставшихся 30%.
Это модельный расчёт, а не обещание. Он предполагает стабильный поток, отсутствие времени на исправление скрытых ошибок и нулевую стоимость инфраструктуры. В реальном бюджете добавьте сервер, разработку интеграции, разметку выборки, сопровождение схемы и контроль качества.
Главная метрика — не цена одного вызова и не процент валидного JSON. Считайте стоимость принятой без существенной правки карточки и отдельно стоимость критической ошибки. Если ручная очередь остаётся большой, более компактная схема или правило до модели часто выгоднее перехода на более крупную LLM.
Пилот на две недели
В первые два дня выберите один объект: например, заявку поддержки. Согласуйте 5–8 полей, допустимые значения и действия, которые система не имеет права выполнять.
Дни 3–5 посвятите обезличенной выборке и JSON Schema. Добавьте `unknown`, запрет дополнительных полей и явную версию контракта.
На второй неделе запустите локальную модель в теневом режиме. Ответы не записываются в CRM, а сравниваются с работой оператора. Проведите отрицательные тесты ограничения, измерьте поля и причины исключений.
Подключайте запись сначала только для низкорисковых карточек и через идемпотентный API. Расширять автономность можно после стабильной контрольной выборки и разбирательства каждого критического промаха.
Что взять руководителю
Начните не с вопроса «какая модель лучше пишет JSON», а с контракта между ИИ и системой: компактная схема, независимый валидатор, бизнес-правила, очередь исключений и подтверждение действий. Такой слой обычно приносит больше надёжности, чем ещё один раунд промпт-инжиниринга.
Внутрик может идеально подобрать кубик к отверстию. Решение, в какой ящик отправится заявка и можно ли менять CRM, всё равно остаётся у людей и правил процесса.
