Когда мы начали работать над AI‑консультантом для интернет‑магазина, идея была достаточно простой: помочь пользователю быстрее находить нужную информацию о товарах и упростить выбор.
В интернет‑магазинах уже есть поиск, фильтры и карточки товаров. Но этого часто недостаточно, когда человек приходит не с конкретным названием товара, а с задачей. Например: «Мне нужен инструмент для ремонта квартиры» или «Чем отличаются эти два товара и какой подойдет лучше?». В офлайн‑магазине в такой ситуации можно обратиться к консультанту: объяснить задачу, уточнить детали и получить несколько вариантов. Мы хотели перенести похожий сценарий на сайт.
Но довольно быстро стало понятно, что схема «пользователь → LLM → ответ» для этого не подходит. Нужно понять запрос, сохранить контекст разговора, решить, нужен ли каталог, найти в нем товары, получить актуальную цену, при необходимости обратиться к базе знаний и только после этого дать модели данные для ответа.
Проект мы разрабатывали командой ASAP, и в этой статье разберем, как мы подошли к проектированию AI‑консультанта: устроили взаимодействие LLM с каталогом более чем из 200 тысяч товаров, какие задачи оставили коду, как работаем с актуальными ценами и остатками и почему вокруг языковой модели в итоге пришлось построить достаточно большой слой классической backend‑логики.
Как устроен AI‑консультант
Backend написан на Python и FastAPI. Для внутреннего хранения используется SQLite, виджет на сайте написан на vanilla JavaScript. Все разворачивается через Docker. В качестве LLM используется Qwen.
При разработке мы также использовали Claude, но в рабочий pipeline консультанта он не входит. Через него прорабатывали сценарии поведения и инструкции для Qwen. Работа строилась через агентов: задавали ролевую модель, описывали контекст и конкретный проблемный кейс. Например, что делать, если пользователь пытается увести консультанта от исходных инструкций или неожиданно меняет язык разговора. Claude предлагал вариант поведения, после чего его переносили в сценарии консультанта.
Сам запрос проходит через систему так:

LLM здесь не управляет всей цепочкой. Модель классифицирует запрос и формирует текст, а источниками данных и критичными ветками управляет Python‑код.
Что происходит до первого вызова LLM
Первым вызывается не Qwen. Сначала сообщение проходит HTTP‑слой FastAPI. Для запросов действует rate‑limit по IP со скользящим окном в 60 секунд. Пустое сообщение возвращает 400, сообщение длиннее 1000 символов — 413.
Еще до вызова модели проверяется состояние диалога. Если разговор уже находится в статусе waiting или operator, AI не должен отвечать. Новое сообщение сохраняется, но дальше передается человеку. То есть подключение оператора реализовано не только инструкцией для модели, а отдельной веткой приложения, которая срабатывает до LLM.
После этих проверок сообщение сохраняется в SQLite, и только затем начинается его обработка.
Первый вызов LLM: понять, что хочет пользователь
На первом вызове Qwen работает как классификатор. Она определяет два параметра:
intent — что пользователь хочет сделать;
domain — к какой товарной области относится запрос.
Результат возвращается в структурированном виде и дальше превращается в набор флагов для backend. Например, запрос о подборе товара должен привести систему к каталогу, а вопрос о доставке — нет.
Решение об этом принимает уже код:
if not bflags["skip_catalog"] and catalog_api.enabled():
То есть каталог вызывается только в том случае, если для ответа действительно нужны товарные данные и API магазина включен. Вопрос: «Какие у вас условия доставки?» не должен запускать поиск среди 200 тысяч товаров.
Как сохраняется тема разговора
Для разных этапов используется разный объем истории диалога. В классификатор, поиск и финальную генерацию не нужно каждый раз передавать всю переписку. Но предыдущие сообщения важны, когда пользователь продолжает уже начатый разговор.
Например:
— Нужен шуруповерт.
— Для каких работ?
— Профессиональных.
— Какой бюджет?
— До 10 тысяч.
В последнем сообщении нет слова «шуруповерт», но система не должна потерять предмет разговора. Поэтому поисковый запрос собирается не только из последней реплики.
Как строится запрос к каталогу
Здесь мы столкнулись с особенностью поиска в 1С‑Битрикс. Поиск лексический и объединяет слова по условию «И». Из‑за этого одно лишнее слово может полностью обнулить выдачу. Например, запрос: «интересует утеплитель» мог вернуть ноль результатов, а «утеплитель» — пять.
Поэтому пользовательскую фразу нельзя просто целиком отправить в каталог. Система старается отделить слова, которые относятся непосредственно к товару, от ограничений, которые удобнее применить уже в коде.
Например: «Нужна дрель до 6000 рублей». В каталог при этом отправляется только товарная часть запроса — дрель, а бюджет применяется уже поверх полученной выдачи.
Если ничего не нашли, запрос постепенно упрощается
Для одного пользовательского запроса система может подготовить несколько поисковых вариантов. Сначала выполняется наиболее точный. Если он ничего не дал, формулировка постепенно упрощается: убирается часть слов, затем проверяются отдельные ключевые слова.
Здесь важно не потерять название самого товара. Например: шуруповерт аккумуляторный профессиональный нельзя бездумно свести к «аккумуляторный». Иначе поиск начнет возвращать любые аккумуляторные товары, и нужная категория может исчезнуть из первых результатов. Поэтому варианты, в которых сохраняется существительное — сама товарная сущность, — проверяются раньше остальных.
Перебор ограничен восемью вариантами. При этом API магазина отвечает от 2 до 8 секунд, поэтому последовательная проверка всех вариантов в худшем случае заставила бы пользователя ждать почти минуту.
На весь перебор установлен бюджет в 15 секунд. Первый, наиболее точный запрос выполняется всегда. Следующие — только пока остается время. Для соединения с магазином также используются разные таймауты: 4 секунды на установку соединения и 12 секунд на чтение ответа.
Как из выдачи получаются 1–3 товара
По умолчанию API запрашивает восемь позиций. Если пользователь назвал бюджет, выборка увеличивается до 30.
Допустим, человек ищет товар до 5000 рублей. Первые восемь результатов могут подходить по названию, но все оказаться дороже. Если взять только эти восемь позиций и потом применить фильтр по цене, получится пустая выдача, хотя более дешевые товары в каталоге есть. Поэтому при наличии бюджета система сначала получает более широкую выборку и только потом фильтрует ее.
Дальше путь выглядит так:

На стороне backend отсеиваются неподходящие позиции, применяются ограничения по цене и учитывается наличие.
В prompt отправляется максимум шесть товаров. Каждый передается в сжатом виде: название, цена, наличие и категория. Полное описание не отправляется, потому что оно сильно увеличивает контекст.
Изображение модели тоже не нужно — оно передается виджету отдельно в структуре карточки. После этого Qwen выбирает из кандидатов 1–3 позиции и формирует объяснение для пользователя. То есть LLM не ищет сама среди 200 тысяч SKU. К моменту второго вызова модели выбор уже сокращен до нескольких реальных позиций из каталога.
Работа с базой знаний
Для справочной информации магазина используется отдельная база знаний. Поиск по ней работает через embeddings. В контекст модели передается максимум два подходящих фрагмента, каждый примерно до 700 символов.
Embeddings самих запросов кешируются: если одинаковый вопрос уже встречался, повторно обращаться к API embeddings не нужно. Отдельной vector database для этого слоя нет — поиск работает с текущей базой знаний приложения.
В результате к финальной генерации система собирает несколько небольших блоков: историю разговора, найденные товары, ранее показанные позиции и при необходимости фрагменты базы знаний.
Где мы не полагаемся только на LLM
Часть правил консультанта задается в prompt: сценарии общения, ограничения безопасности, правила работы с товарами. Но для критичных действий одной инструкции модели недостаточно.
Хороший пример — передача разговора оператору. Модель может поставить в ответе служебный маркер [[HANDOFF]]. Но если пользователь прямо просит позвать человека, система не рассчитывает только на то, что LLM правильно распознает этот запрос. Такой случай дополнительно определяется классификатором и проверяется по ключевым словам еще до финальной генерации. Поэтому явный запрос оператора может быть обработан кодом независимо от ответа модели.
Похожий подход используется с товарными карточками. Для управления ответом используются служебные маркеры [[SHOW]] и [[HANDOFF]]. Строгий JSON оставили для классификатора, где результат действительно представляет собой структуру. Для основного ответа отдельная JSON‑обертка не понадобилась.
После генерации backend разбирает ответ и решает, какие карточки можно показать. Модель может, например, сократить полное название товара, поэтому простого точного сравнения строк недостаточно. Карточка привязывается к ответу по нескольким характерным словам из названия.
Есть и дополнительная проверка: если консультант написал, что подходящих товаров не нашлось, карточки не показываются, даже если в ответе остался служебный маркер.
То есть ответ Qwen — это еще не готовый ответ интерфейса:

Как управляем поведением консультанта
Системная инструкция не хранится одним большим prompt. Она собирается из отдельных блоков в зависимости от запроса пользователя.
Есть общая инструкция, которая входит в каждую сборку, и сценарии для разных типов запросов: подбор товара, сравнение, расчет материалов, консультация по выбору и эксплуатации, заказ и другие.
Один запрос при этом может относиться сразу к нескольким сценариям. Классификатор определяет до трех классов на сообщение. Для некоторых сценариев есть дополнительные уточнения — более узкие правила поведения внутри основного класса.

Например, в сценарии подбора отдельно задается, какие параметры нужно выяснить у пользователя и когда уже можно переходить к рекомендации. Для запроса, в котором человек описывает задачу, а не конкретный товар, есть дополнительное уточнение: сначала определить подходящий тип товара, а уже затем искать конкретные позиции.
Поверх сценария добавляется товарный домен. Сейчас их восемь. Они нужны для правил, которые зависят уже не от типа вопроса, а от самой товарной области. Например, для «Стройки и отделки» отдельно прописана работа с расчетами материалов и вопросами об условиях применения. Для других групп используются свои правила.
В конце добавляются технические инструкции. Они не редактируются через интерфейс, потому что от них зависит работа приложения после ответа модели — в том числе служебные маркеры [[SHOW]] и [[HANDOFF]].

В итоге системная инструкция собирается примерно так:
"Общая инструкция + 1–3 сценария запроса + уточнения сценариев + товарный домен + технические правила".
Сейчас в системе 13 классов запросов, 14 подклассов и восемь товарных доменов — всего 36 блоков ожидаемого поведения плюс общая инструкция и технические правила.
При этом для изменения обычного сценария не нужно менять Python‑код и заново разворачивать сервис. Стандартная версия хранится в коде, а переопределение — в базе. Через админку блок можно изменить, отключить или сбросить к стандартной версии. При следующей сборке prompt система возьмет актуальное значение.
Как собирается prompt
Постоянные правила и данные конкретного запроса разделены. В системной части находятся роль консультанта, сценарий, товарный домен и технические правила. Данные текущего запроса передаются отдельно:
SYSTEM: общая инструкция + сценарий + товарный домен + технические правила;
USER: информация магазина + найденные товары + ранее показанные товары + вопрос покупателя.
Модель получает только те данные, которые система решила передать ей на этом ходе. У нее нет инструментов, чтобы самостоятельно сходить в каталог и запросить дополнительные товары. Если какого‑то товара нет в переданном наборе, консультант не должен представлять его как позицию магазина.
Что происходит, если LLM недоступна
Вызов модели сделан с повторными попытками при rate‑limit, таймаутах и ошибках 5xx. Во время разработки здесь обнаружился отдельный сценарий.
Раньше технический сбой LLM мог привести к передаче разговора оператору. Оператор закрывал обращение, следующий запрос снова попадал в недоступную модель, после чего создавалась новая передача. В результате очередь могла заполняться обращениями не потому, что пользователи хотели поговорить с человеком, а из‑за технической ошибки.
Логику поменяли: при недоступности модели консультант просит повторить запрос позже. Передача человеку происходит только тогда, когда для нее действительно есть основание.
Контекст тоже пришлось ограничивать
Передавать Qwen все накопленные данные нет смысла. Для разных частей контекста установлены ограничения.
В финальную генерацию попадает максимум шесть найденных товаров и до четырех ранее показанных. Из базы знаний — не больше двух релевантных фрагментов примерно по 700 символов.
Для финальной генерации используется до десяти сообщений истории, для классификации — до пяти. Для построения поискового запроса — до восьми, а для извлечения фильтров используются последние четыре сообщения пользователя. Есть ограничения и на генерацию: до 700 токенов для финального ответа и до 300 для классификации.
Задача здесь не в том, чтобы передать модели максимум доступной информации, а в том, чтобы дать ей только то, что нужно на конкретном этапе. На практике сокращение prompt дало и более стабильные ответы: лишняя информация могла уводить модель в сторону от текущего вопроса.
Где на самом деле теряется время
Самым нестабильным по времени участком оказался API товарного каталога. Один запрос к магазину занимает от 2 до 8 секунд.
В обычном сценарии на одно сообщение пользователя приходится два обращения к chat‑LLM: первое для классификации, второе для генерации. Дополнительно может быть один вызов embeddings.
С каталогом количество запросов зависит от результата поиска:
Компонент |
Запросов на сообщение |
Chat‑LLM |
2 |
Embeddings |
0–1 |
API каталога |
обычно 1, максимум 8 при поиске |
Сверка товарных данных |
при необходимости |
При этом не каждый запрос пользователя требует обращения к магазину. Если человек спрашивает про доставку или оплату, после классификации каталог просто пропускается.
Что кешируем, а что получаем заново
Кешировать товарные данные нужно осторожно: цена и наличие могут измениться. Поэтому актуальную информацию по товарам система получает из API магазина.
Кеш используется там, где это не создает такой проблемы. Например, кешируются embeddings повторяющихся запросов и ответ на первое сообщение диалога. При попадании в кеш сообщение все равно сохраняется, а необходимые действия приложения продолжают выполняться: кеш не должен ломать историю диалога и аналитику.
Что с нагрузкой
Полноценного нагрузочного тестирования пока не проводили, поэтому говорить, сколько одновременных пользователей выдержит текущая архитектура, рано. На этом этапе мы смотрим на время работы отдельных компонентов. Пока наиболее заметное узкое место — API товарного каталога.
Как разбираем неправильные ответы
Все диалоги сохраняются, поэтому проблемный ответ можно разобрать по этапам: что написал пользователь, как система классифицировала запрос, что искала в каталоге, какие товары получила и что в итоге передала модели.
Ошибка в финальном ответе не обязательно означает ошибку LLM. Проблема с аккумуляторным шуруповертом, которую мы разбирали выше, возникала раньше — при формировании поискового запроса. Если в контекст уже попали неподходящие товары, изменение финального prompt проблему не исправит.
При разборе идем по всей цепочке:
Запрос пользователя - Классификация - Поисковый запрос - Ответ каталога - Контекст LLM - Финальный ответ
В зависимости от того, где появилась ошибка, меняется и исправление: где‑то нужно доработать prompt или сценарий, а где‑то — Python‑логику. При этом текущий мониторинг пока нельзя считать законченным. Самую показательную аномалию во время пилота обнаружил клиент, а не автоматический мониторинг.
Как проверяем изменения
Для работы с prompt сделали панель «Проверка сборки». В нее можно ввести фразу покупателя и посмотреть, какие классы сработали, с какой уверенностью, какой товарный домен определился и какой системный prompt в итоге будет отправлен модели. Это полезно, потому что итоговая инструкция собирается из нескольких частей. Если редактировать отдельный блок и не видеть результат целиком, сложно понять, что в итоге получила модель.
Во время пилота также используется отладочная информация: построенный поисковый запрос, фактический запрос к магазину после упрощения, примененные фильтры, классификация и найденные позиции.
Помимо технической отладки, в консультанте предусмотрена отдельная аналитика работы с пользователями. В ней собираются не только количество диалогов и сообщений, но и продуктовые события: показы товарных карточек, переходы на сайт, добавления в корзину и передачи оператору.
Отдельно считается воронка взаимодействия с карточками и пользовательская оценка ответов. Отчет можно смотреть за выбранный период и выгружать в CSV.

За период с 7 июня по 3 августа система обработала 45 реальных диалогов от 37 уникальных посетителей и 117 сообщений пользователей. Было 168 показов товарных карточек в 55 ответах, 332 перехода на сайт, 21 добавление товара в корзину и 10 передач оператору.
Эта выборка пока слишком небольшая, чтобы делать выводы об эффективности консультанта. Сейчас эти данные нужны в первую очередь для проверки системы на реальных пользовательских сценариях: можно проследить путь от вопроса до показа товара, перехода на сайт, добавления в корзину или передачи оператору и уже по конкретному диалогу разбирать, что происходило на каждом этапе.
Что оказалось сложнее всего
Самой сложной частью оказалось не подключение LLM. Больше работы потребовали сценарии поведения, классификация запросов, работа с контекстом, поиск по каталогу и обработка нестандартного поведения пользователей.
Даже запрос «нужен шуруповерт» в итоге проходит несколько этапов: нужно сохранить тему между сообщениями, построить поисковую строку, не потерять название товара при ее упрощении, получить актуальные позиции, применить ограничения пользователя и только после этого передать несколько кандидатов модели.
То есть большая часть работы оказалась не в самой генерации текста, а в том, чтобы подготовить для модели правильные данные и контролировать то, что происходит до и после ее ответа.
Что дальше
Сейчас AI‑консультант продолжает работать в пилотном режиме. Основная задача этого этапа — собирать реальные пользовательские сценарии и смотреть, где текущая логика дает сбой: на классификации, поиске, данных каталога или уже на генерации.
Отдельная задача — автоматические тесты. Сейчас основные проверки проводятся вручную, хотя часть логики работы со строками можно тестировать без обращения к LLM.
Пока пилот нужен именно для того, чтобы проверить собранную систему на живых запросах и понять, какие ее части действительно нужно менять дальше.
alterbred
".... при необходимости обратиться к базе знаний...."
А в "базе знаний "? А как решаются вопросы с разными названиями - ушм, болгарка?
Как делается выбор когда клиент не совсем понимает, что ему нужно?
Если подобные вопросы Вам интересны, то может мои идеи про граф смыслов Вас заинтересуют?