Как превращать ошибки чат-бота в тесты грамматики, а не в повод отпустить тормоза
Методика построения цикла «починки грамматики» для чат-ботов с детерминированным парсером: ловим ошибки, фиксируем намерение, добавляем фикстуру, проверяем семантические сдвиги — и не позволяем нейросети молча выполнить команду за пользователя.
Каждое непонимание чат-бота — это не приглашение ослабить парсер, а потенциальный регрессионный тест, который ещё не написан.
Проблема, которую все знают, но редко решают правильно
Есть два соблазна, когда чат-бот не понимает пользователя. Первый — расширять грамматику на каждый чих: добавить ещё одно правило, сделать парсер «умнее». Второй — махнуть рукой на формальный синтаксис и отдать всё на откуп языковой модели, которая сама разберётся, что имелось в виду.
Оба подхода ведут к одному итогу: непредсказуемость. В первом случае вы не знаете, какие старые команды сломались после очередного изменения правил. Во втором — не можете гарантировать, что «разумный» на вид ответ модели не выполнит действие, которого пользователь не просил.
Автор оригинальной публикации на dev.to предлагает третий путь: держать исполнение команд строго детерминированным, но использовать каждый случай непонимания как сырьё для улучшения грамматики — с обязательным циклом валидации на каждом шаге.
Разделение интерпретации и исполнения — не формальность
Ключевая идея звучит почти банально, пока не задумаешься о последствиях: парсер выдаёт результат, а не действие. Только одна из трёх возможных реакций может привести к выполнению команды:
- accepted — синтаксис разобран, валидация домена пройдена, можно выполнять.
- no_match — грамматика не распознала ввод.
- invalid — синтаксис совпал, но нарушено доменное ограничение (например, количество корма вне допустимого диапазона).
Предложение нейросети, ответ поддержки или частично разобранная команда никогда не приравниваются к accepted. Это принципиальное ограничение, и оно не техническая мелочь — это вопрос безопасности. Пользователь спрашивает не «может ли нейросеть угадать?» (часто может), а «не станет ли угадывание незаметно действием?»
Как устроен ремонтный цикл
На конкретном примере — бот для кормления кошек с командами вроде feed Luna 20g или remind me to feed Miso at 19:30 — автор демонстрирует полный workflow.
Шаг 1. Минимальный парсер
Грамматика строится на PEG.js (Peggy) и умеет распознавать базовые конструкции: «feed», «remind me to feed», опциональные вежливые префиксы, имена в кавычках и без, единицы измерения. При этом она сознательно не проверяет доменные ограничения: является ли 99:99 допустимым временем и не слишком ли 5000 граммов корма. Эти проверки живут отдельно — в функции валидации.
Это важный архитектурный выбор: грамматика отвечает за «что пользователь сказал», а валидация — за «можно ли это выполнить». Смешение двух задач — источник трудноуловимых багов.
Шаг 2. Фикстуры вместо скриншотов чата
Лог переписки объясняет, что произошло один раз. Фикстура предотвращает повторение проблемы навсегда.
Автор предлагает хранить тестовые случаи в JSON-файле — по одному объекту на каждый ожидаемый сценарий. Каждый объект содержит ввод пользователя и ожидаемую структуру команды. Тестовый раннер пробегает весь корпус при каждом запуске.
Правило: когда новая формулировка не распознаётся, сначала добавляем фикстуру, потом меняем грамматику. Не наоборот.
Шаг 3. Подтверждение намерения — прежде всего
Вот где большинство команд спотыкается. Пользователь написал «could you give Luna twenty grams?» — парсер не понял. Но прежде чем писать новое правило, нужно точно знать, что имелось в виду. Предположение разработчика или модели — не замена.
Автор предлагает явный жизненный цикл каждого случая починки:
needs_clarification— ввод не распознан, намерение неизвестноintent_confirmed— пользователь или владелец продукта подтвердил, что именно команда должна была означатьfixture_added— тестовый случай добавлен в корпусgrammar_changed— грамматика обновленаverified— весь корпус пройден, семантических сдвигов нетreleased— исправление в продакшене
Этот порядок нельзя сокращать. Подскочить с шага 1 сразу на шаг 4 — значит записать в грамматику чьё-то предположение как факт.
Шаг 4. Проверка семантических диффов
Изменение грамматики — это изменение кода. Но самый опасный дифф может не проявиться в файле правил. Переупорядочивание альтернатив в PEG-парсере способно перенаправить разбор уже существующих вводов, при этом все новые тесты будут зелёными.
Перед релизом автор рекомендует прогнать обе версии парсера (старую и новую) по всему корпусу и классифицировать переходы:
no_match → accepted— ожидаемое расширение, окaccepted → no_match— вероятный регресс, нужно разбиратьсяaccepted → accepted— проверить, не изменился ли объект команды (даже если интент тот же)invalid → accepted— убедиться, что правило домена изменилось осознанно
Особенно коварен третий случай: количество граммов изменилось с 20 на 200, а интент остался feed — и тесты молчат.
Где нейросеть полезна, а где ей нельзя доверять
После no_match языковая модель вполне способна предложить разумную интерпретацию перефразированной команды. Она может сгруппировать похожие сбои, предложить варианты формулировок для фикстур, ускорить работу мейнтейнера.
Но это предложение, а не приказ. Список того, что модель может натворить, если её не ограничить:
- выдумать сущность, которой нет в системе;
- выбрать неверный интент из неоднозначного предложения;
- проигнорировать доменные ограничения;
- дать разные ответы на один и тот же ввод;
- выполнить инструкции, вшитые в текст пользователя;
- уверенно угадать там, где нужно переспросить.
Поэтому каждый кандидат от модели проходит валидацию схемы (в примере — через Zod) и проверку на соответствие известным сущностям. Даже прошедший проверку кандидат не выполняется автоматически — он превращается в вопрос пользователю:
Вы имели в виду «feed Luna 20 grams»?
Подтверждение пользователя отправляет новую детерминированную команду через обычный парсер. Оно не превращает ретроактивно ответ модели в авторизацию.
Таблица границ ответственности
| Ситуация | Роль AI | Контроль человека |
|---|---|---|
| Черновик фикстуры | Предлагает формулировку | Мейнтейнер утверждает |
| Группировка сбоев | Рекомендует кластеры | Мейнтейнер решает, покрыты ли они одним правилом |
| Чтение (lookup) | Предлагает интерпретацию | Пользователь подтверждает, если неоднозначность влияет на результат |
| Планирование, изменение состояния | Только уточнение | Подтверждённая команда проходит детерминированную валидацию |
| Оплата, безопасность, удаление | Не выводить авторизацию | Явные аутентифицированные контроли |
Граница определяется последствиями, а не тем, насколько гладко говорит модель.
Типичные ловушки, о которых стоит помнить
Разработчик записал предположение как факт. Пользователь написал «feed Luna at seven» — разработчик решил, что семь граммов, а имелось в виду семь вечера. Не добавляйте фикстуру до подтверждения. Некоторые формулировки должны навсегда остаться неоднозначными и требовать уточнения — и это нормально.
Широкое правило перехватывает узкое. PEG-парсеры используют упорядоченный выбор. Новое «мягкое» правило выше по списку может молча перехватить вводы, которые раньше попадали в конкретное. Для перекрывающихся форм нужны отдельные фикстуры.
Кнопка «Продолжить» скрывает суть. Если интерфейс предлагает «Продолжить» после ответа модели, пользователь не понимает, что именно произойдёт. Показывайте предложенную команду на языке домена (Feed Luna 20 grams now), требуйте отдельного подтверждения и отправляйте его через стандартный парсер.
Отрицательные примеры исчезают из корпуса. Если в тестах только валидные команды, всё будет толкать парсер в сторону всё большей «вседозволенности». Храните негативные фикстуры: нерелативный болтовню, некорректные времена, запредельные значения, неподдерживаемые действия.
Репорт поддержки не воспроизводится. Если в отчёте нет версии грамматики, исходного результата парсера и точной утверждённой формулировки, мейнтейнер может тестировать против другого поведения. Захватывайте эти поля автоматически, давая пользователю право отредактировать или отказаться от сохранения примера.
Чек-лист перед релизом
Прежде чем выпускать починку грамматики, убедитесь:
- Исходный сбой имеет утверждённую и проверенную на приватность фикстуру.
- Неоднозначная формулировка уточнена, а не «угадана».
- Фикстура падала до изменения грамматики.
- Существующие распознанные команды по-прежнему дают тот же структурированный вывод.
- Негативные фикстуры по-прежнему отклоняются.
- Доменная валидация работает вне грамматики.
- Кандидаты от AI не могут напрямую вызвать исполнение.
- Действия с состоянием показывают явное подтверждение.
- Записи о починке содержат версию грамматики.
- Ответ пользователю говорит, что изменилось — или что уточнение по-прежнему необходимо.
Почему это важно
Методика, описанная в оригинальной статье, решает проблему, которая свойственна почти всем чат-ботам с формальным парсером: каждый новый сценарий разговора толкает команду «сделать парсер послабее». Через полгода вы получаете систему, которая старается понять всё, а реально предсказуемо работает только с подмножеством вводов — потому что изменения накапливались хаотично и никто не проверял побочные эффекты.
Цикл «сбой → подтверждение → фикстура → изменение → дифф → релиз» превращает неизбежную эрозию грамматики в управляемый процесс. Это не громко и не модно, но именно так строятся системы, которые не ломаются тихо.
Оригинальная публикация также упоминает инструмент Knocket для организации канала связи с пользователями — но, по честному признанию автора, это один из возможных вариантов, а не универсальная рекомендация. Суть методики транспортно-agnostic: подойдёт и GitHub Issues, и почтовый алиас, и встроенный чат.