JSON от российских LLM: как я довёл парсинг до 99.4% и перестал терять данные

JSON от российских LLM: как я довёл парсинг до 99.4% и перестал терять данные

Пятница, 19:47. Задача простая: AI-агент должен разобрать письма и закинуть заявки в CRM. На выходе — «Конечно, вот результат:» перед JSON, лишние запятые и строки вместо чисел. В итоге из сорока писем только пятнадцать дошли до системы, остальное ушло в логи с JSONDecodeError.

Знакомая боль? Я тоже через это прошёл. Расскажу, как мы перестали терять заявки и подняли успешный парсинг до 99.4% на семи тысячах вызовов в сутки.

Почему «верни JSON» почти никогда не работает

LLM — это не JSON-сериализатор. Она просто предсказывает токены. Пишешь в промпте «отдай строго JSON» — модель старается, но на длинных текстах начинает добавлять ```json, одинарные кавычки или вообще забывает про структуру.

С английскими моделями проще — там есть structured output. У нас с GigaChat, YandexGPT и прочими всё печальнее. response_format работает не везде, function calling — только для инструментов. А нам нужна структура без лишних телодвижений.

Реальный кейс: разбор обращений в дилерский центр

Дилер автотехники, 200–400 сообщений в сутки. Нужно вытаскивать intent, класс машины, бюджет, urgency и телефоны. До переделки успех был 73%. 27% писем уходили менеджеру на ручной ввод — семь часов в неделю.

Мы попробовали два подхода. Первый почти не помог, второй выстрелил.

Попытка №1: жёсткий промпт + pydantic

Добавили few-shot примеры, запретили markdown, подключили валидацию. Получили 84%. Уже лучше, но всё равно много ошибок на длинных жалобах — модель начинала пересказывать текст и только потом вспоминала про JSON.

Что реально сработало: три слоя защиты

Слой 1. Tolerant extractor. Не ждём чистый JSON. Берём первый блок, похожий на объект, через простой счётчик скобок (30 строк кода). Снимает все «Конечно, вот ответ» и обёртки.

Слой 2. Мягкая коррекция. Пару регулярок: убираем trailing comma, чиним одинарные кавычки, выкидываем комментарии. +5% к успеху.

Слой 3. Retry с фидбэком. Если валидация падает — шлём второй запрос: «Ты ошибся в поле budget_rub, ожидалось число». Температуру ставим в 0.0. Обычно чинит с первого раза.

Цифры до и после

  • Tolerant extractor → 91%
    • мягкая коррекция → 97%
    • retry → 99.4%

Оставшиеся 0.6% — совсем безнадёжные сообщения, их сразу в ручную очередь.

Ручная работа упала с семи часов до двадцати минут в неделю. Время от письма до карточки — с 4 часов до 90 секунд.

Сколько это стоит и когда не стоит городить свой парсер

Retry добавляет ~4% к счёту за токены. Одна попытка максимум, дальше — в DLQ. Раз в неделю смотрим, что туда попало, и правим промпты.

Если используешь GigaChat Pro с response_format — бери, будет 96–98%. Но свой tolerant extractor всё равно оставляй как страховку, особенно когда нужно работать с несколькими провайдерами или сложными схемами.

Итог

Структурированный вывод от LLM в проде — это не одна функция в SDK, а небольшой инженерный пайплайн. Промпт делает 80%, остальное — валидация и retry. Кто надеется только на промпт, теряет четверть данных и потом удивляется, куда делись заявки.