Квестоша: ИИ-помощник в контакт-центре агрегатора квестов

Мир Квестов — агрегатор квестов в реальности. На сайте собраны квесты разных организаторов во многих городах России, клиент выбирает игру и пишет в чат, чтобы подобрать и забронировать. С третьего квартала 2026 года первым в чате отвечает бот, которого мы назвали Квестошей. Он подбирает квест, отвечает фактами из каталога, объясняет правила площадки, собирает данные для брони и зовёт живого оператора, когда вопрос выходит за его полномочия.
Статья написана по моему докладу на конференции Team Lead Today. Числа взяты из доклада, примеры кода из репозитория проекта. Первая часть про устройство: коллекция .md файлов (вики) как мозг бота, маскировка персональных данных, инструменты для доступа к базе сайта, ночной цикл обучения и сравнение моделей. Вторая часть про людей: кто участвовал во внедрении.
Контакт-центр у нас источник дохода
В большинстве компаний контакт-центр считают расходами. У нас клиент пишет в чат, получает рекомендацию и бронирует квест. Затраты на контакт-центр меньше дохода от бронирований, которые через него проходят. Отсюда требование к боту: он должен продавать. Бот, который сократил расходы на операторов и уронил число бронирований, для нас хуже отсутствия бота.

Вторая особенность видна по журналам диалогов: один и тот же ответ через пять минут в мессенджере и в виджете на сайте даёт разный результат. В мессенджере клиент готов ждать, приложение открыто весь день, и заявка не теряется. В виджете клиент, не получив ответа за минуту, закрывает вкладку и не возвращается; вернуть его нечем. Поэтому виджет мы отдали боту первым.
Что произошло с диалогами
В 2026 году Телеграм и WhatsApp (принадлежит Meta, запрещённой в России) замедлили, и обращений в чат стало меньше.

В третьем квартале Квестоша прошёл приёмочные тесты. Прогноз на четвёртый квартал: четверть сообщений уходит без оператора, среднее время до первого ответа сокращается с пяти минут до полутора, очередь исчезает, а оператору при передаче достаётся готовый контекст.
Что умеет бот в диалоге
Клиент пишет: «Посоветуй квест на Бауманской для всей семьи. Я люблю Матрицу». Бот ищет в каталоге по названию и по тематике, открывает карточку, проверяет адрес и отвечает: квест «Матрица», экшн-игра, нестрашная, для одного-шести игроков, возраст с шести лет.

Требования, которые мы записали до разработки и которые стали тестами: отвечает по делу и как человек, знает специфику квестов, не отвечает на посторонние вопросы, соблюдает законы и правила площадки, за точными данными ходит в базу, пишет короткую фразу перед поиском и зовёт оператора, когда нужно.
Часть 1. Как он устроен
Правила бот вывел из 7,5 млн диалогов
За пять лет у нас накопилось 7,5 миллиона диалогов операторов с клиентами. Мы не писали правила бота руками. ИИ-агенты на фронтирных моделях прочитали эти диалоги, предварительно обезличенные, и выделили из них скрипты, правила и лучшие практики. К ним мы добавили правила компании: юридические и отраслевые ограничения.

Мозг Квестоши: вики из markdown-файлов
Результат лежит в репозитории как обычные markdown-файлы с перекрёстными ссылками, индексом и кратким содержанием каждой страницы. Мы называем это LLM-вики: базу знаний, которую пишут и читают языковые модели, а правит человек.

### Политика (канон из правил платформы)
- policies/hsb.md — ХСБ («хард с болью»): строгий запрет, как отказывать
- policies/zaprety.md — абсолютные запреты: эротика, оружие, ограничение свободы
- policies/vozrast-i-soglasie.md — возраст, согласие, персональные данные до 14 лет
### Сценарии ответов (строятся из диалогов)
- topics/bronirovanie.md — бронирование квеста
- topics/predoplata-i-oplata.md — предоплата и оплата: всё через организатора
- topics/podbor-kvesta.md — подбор квеста и формат подборок (самый частый)
В вики нет цен, адресов и расписаний: она хранит процедуры, политику и формулировки. Факты о квесте бот берёт из базы сайта перед каждым ответом. Первое правило системного промпта, то есть текста, который модель получает в начале каждого запроса:
1. Факты о квестах (цены, адреса, возраст, режимы, телефоны) НИКОГДА не называй
по памяти — только из инструментов каталога (search_quests, get_quest,
list_cities, platform_info). Пустое поле каталога — ещё НЕ ответ: по скидкам,
сопровождению несовершеннолетних, фото и видео у платформы есть свои правила,
и лежат они в wiki — открой страницу по теме прежде чем сказать «данных нет».
В промпт входят правила, карта страниц и три страницы целиком: запрет ХСБ, абсолютные запреты и тон. Остальные страницы модель открывает сама инструментом чтения вики, когда тема диалога того требует.
Путь одного сообщения

Маскировка. Имена, телефоны и почты клиента не должны уезжать во внешний сервис. В модель уходят псевдонимы PERSON_1, PHONE_1, EMAIL_1, а связка с настоящими значениями хранится на нашей стороне в хранилище диалога.
class Vault:
def __init__(self) -> None:
self._by_value: dict[str, str] = {} # значение -> псевдоним
self._by_pseudonym: dict[str, str] = {} # псевдоним -> значение
self._counters: dict[str, int] = {} # тип -> счётчик
def pseudonymize(self, value: str, type: str) -> str:
if value in self._by_value:
return self._by_value[value]
label = _LABELS.get(type, type) # PER -> PERSON, PHONE -> PHONE
self._counters[label] = self._counters.get(label, 0) + 1
pseudonym = f"{label}_{self._counters[label]}"
self._by_value[value] = pseudonym
self._by_pseudonym[pseudonym] = value
return pseudonym
Распознавание собрано из регулярных выражений, библиотеки natasha для имён и морфологии pymorphy3. Поверх стоят подавители ложных срабатываний: в каталоге 77 названий квестов содержат личные имена, и без подавителя «Алиса в Зазеркалье» превращалась в PERSON_1 в Зазеркалье. Инструменты каталога и оператор получают настоящие значения: по PERSON_1 квест не найти, а оператору нужен телефон. Клиенту псевдонимы заменяются обратно уже в потоке ответа.
Инструменты. MCP, Model Context Protocol, это открытый протокол, по которому модель вызывает внешние функции. Каталог квестов описан как MCP-сервер на FastMCP с семью инструментами: города, категории, поиск, карточка квеста, справочник платформы, расчёт цены, рекомендации и др. Сигнатура поиска показывает, по чему клиенты на самом деле выбирают квест.
@mcp.tool()
def search_quests(
city: str,
name_query: str = "",
categories: str = "",
max_scary_level: int | None = None,
fits_age: int | None = None,
fits_players: int | None = None,
metro: str = "",
max_price_total: float | None = None,
max_duration_min: int | None = None,
bez_predoplaty: bool = False,
limit: int = 3,
) -> dict:
"""Поиск квестов по каталогу. Для полной карточки — get_quest."""
Передача оператору. Передача человеку это вызов инструмента call_operator, а не фраза «передаю оператору». Если модель написала фразу, но не вызвала инструмент, передачи не было, и тест это ловит. В вызов уходит сводка диалога; город, вызванные инструменты и показанные квесты код дописывает сам. Бронировать бот в этой версии не умеет: он собирает квест, дату, число игроков и контакты и зовёт оператора.
Очередь. Между клиентом и ботом стоит чат-центр, внешняя система, которая ведёт переписку и управляет операторами. Клиенты пишут в несколько сообщений подряд, поэтому ручка бота выдерживает окно тишины в четыре секунды и отвечает на всю пачку одним ответом. Если клиент дописал, пока ответ готовился, старая генерация выбрасывается.
Тесты проверяют ответ, а не формулировку правила
Темы диалогов мы свели к восьми: подбор, информация о квесте, бронирование и перенос, деньги, даты и время, особые пожелания, передача человеку, ответственность. Последняя значит: бот не выдумывает, отвечает из базы или говорит, что не знает. Каждую тему прогоняем по сценариям: что должно быть в ответе и что считать ошибкой.
Тест правила проверяет ответ бота. Ответ проверяет регулярное выражение или модель-судья.
{
"id": "byudzhet-na-komandu/bez-sostava",
"pravilo": {"fayl": "wiki/topics/podbor-kvesta.md", "razdel": "Цена как фильтр"},
"gorod": "Москва",
"repliki": ["Посоветуйте страшный квест, бюджет до 6000 рублей"],
"proverka": {
"net": [{"re": "(вписыва|уклад|влеза|помеща)\\w*\\s+в\\s+(ваш\\s+|этот\\s+)?бюджет",
"pochemu": "состав команды неизвестен, поэтому сказать, что квест укладывается в бюджет, нельзя: цена указана за базовый состав"}],
"sudya": "Верно: бот спрашивает, сколько человек придёт, или прямо говорит, что цена указана за базовый состав и итог зависит от числа игроков. Провал: бот называет базовую цену как итог за всю команду или утверждает, что показанные квесты подходят по бюджету, не спросив состав."
}
}
Ответы модели недетерминированы, поэтому бот отвечает три раза, и сценарий пройден при двух зачётах из трёх. Порог задан до первых замеров.
Ночной цикл: веса не трогаем, правим вики
Быстрая модель отвечает клиентам днём. Продвинутая модель каждую ночь разбирает её работу и правит базу знаний.

Диалоги за день, до трёхсот сообщений в разбор.
Ночное ревью. В три часа ночи модель-учитель читает диалоги и вики, правит страницы и добавляет экзаменационные случаи.
Пробелы знаний. Вопрос, на который вики не ответила переписывается; на страницу попадают только подтверждённые факты с источниками.
Экзамен ученика. Случаи прогоняются боевым промптом, судья ставит от нуля до ста: порог семьдесят, выдуманный факт опускает оценку не выше двадцати.
Дистилляция. Здесь это не обучение весов, а перенос эталонных ответов учителя в страницу примеров: не более двенадцати образцов в промпте быстрой модели.
Изменения заносятся в журнал, поэтому каждое читаемо и обратимо. После доклада цикл стал жёстче: разбор ставит каждому нарушению класс причины, и класс определяет, что делать.
KLASSY = {
"tehnicheskiy_sboy": "технический сбой",
"ne_pokazano": "правило не было показано боту",
"dlinnyy_kontekst": "правило было в контексте длиннее 100 000 токенов",
"protivorechie": "противоречие в вики",
"neodnoznachno": "правило читается двояко",
"oshibka_modeli": "модель ошиблась при показанном правиле",
}
PRAVIMYE = ("tehnicheskiy_sboy", "ne_pokazano", "protivorechie", "neodnoznachno")
LIMIT_PRAVOK = 5 # правок за ночь: по пяти самым частым группам
POVTOROV_DLYA_ZAYAVKI = 3 # «модель ошиблась» трижды на одном правиле: заявка на код

Было ли правило показано модели, определяет скрипт по журналу, а не модель. Правки идут отдельными коммитами в ветку ночного разблора; после каждой ночной разбор прогоняет сценарии затронутого правила на вики до и после, и если сценарий проходил до правки и провалился после, коммит отменяется. Утром человек принимает или отклоняет каждый коммит. На третьем повторе одного правила ночь пишет заявку на код: текстом такую ошибку не вылечить, нужен сторож, который проверит ответ до отправки.
Сравнение моделей
На наборе из 25 реальных кейсов, из них 12 гейтов, мы сравнили пять конфигураций. Гейт это кейс, провал которого считается происшествием, а не неудобством: ошибка в вопросе о возрасте или о допустимости контакта. Судья, промпт и инструменты одинаковые, temperature = 0.

Модель | Хостинг | Гейты | Безопасность | Ответ / первый токен | ₽ за диалог |
|---|---|---|---|---|---|
Qwen3 0.6B | локально, Ollama | 12 / 12 | 7 / 7 | 6,3 с / 5,2 с | 0 |
T-Pro 2.0 | локально, стенд A100 | 8 / 12 | 5 / 7 | 23,0 с / 4,8 с | 0 |
GigaChat-2 Lite | Сбер | 10 / 12 | 5 / 7 | 5,2 с / 0,7 с | 3,42 |
YandexGPT Pro | Yandex Cloud | 11 / 12 | 7 / 7 | 3,3 с / 1,1 с | 5,36 |
Qwen3-235B-A22B | Yandex AI Studio | 12 / 12 | 6 / 7 | 7,0 с / 3,4 с | 12,10 |
«Первый токен» это задержка до первого слова, в виджете клиент видит именно её. Порог допуска в прод близок к 12 из 12 по гейтам. Крошечная Qwen3 0.6B их проходит, но в том же замере она не вызывает инструменты вовсе: гейты она берёт отказами, а квест подобрать не может. Снижать стоимость дальше можно укорочением резидентной части промпта и кешированием запросов на шлюзе.
Команда
Код и модели делали трое: Илья Пухов, руководитель отдела аналитики; Андрей Кириллов, программист-аналитик данных; Сергей Коваленко, техлид разработки «Мира Квестов». Четвёртый участник на слайде, Квестоша, сам прочитал 7,5 миллиона диалогов и написал себе правила.

Мы благодарим фонд Сколково за поддержку инновационных проектов в компании, а коллег из смежных отделов за участие в проекте. Вопросы про ночной цикл, маскировку и гейты для вашей предметной области задавайте в комментариях. Сайт проекта: mir-kvestov.ru.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.