Покупатель пишет: «Нужен компактный блок питания для настольной лампы, штекер 5,5 × 2,1 мм, на 12 В, и чтобы помещался за монитор». Система находит карточку «Компактный адаптер питания для настольных светильников, лёгкий, тихий, подойдёт для рабочего стола». Описание почти дословно совпадает с запросом. Только штекер у него 3,5 × 1,35 мм, а на выходе 5 В. Семантический поиск ответил ровно на ту половину вопроса, которая выражена словами «компактный» и «для лампы». Вторая половина, про разъём и напряжение, оказалась цифрами в таблице характеристик, а векторное сходство к таким различиям почти слепо.
Это не ошибка модели в обычном смысле. Эмбеддинг сжимает смысл текста в вектор, и слова «штекер 5,5 × 2,1» и «штекер 3,5 × 1,35» в нём оказываются очень близкими: тема одна, формат один, отличаются два числа. Для покупателя же это разница между рабочей лампой и сгоревшей лампой или между вставляющимся и не вставляющимся штекером. Дальше речь о том, как устроить поиск, чтобы такие ошибки не доходили до языковой модели вообще.
Две разные породы условий в одной фразе
В запросе про блок питания уживаются два типа требований, и обращаться с ними нужно по-разному.
Первый тип — жёсткие условия. Разъём 5,5 × 2,1 мм, напряжение 12 В. Если товар им не соответствует, он не «хуже подходит», а не подходит совсем. Здесь нет шкалы, на которой можно было бы компенсировать недостаток другим достоинством: красивый корпус не заменит нужный штекер.
Второй тип — пожелания. «Компактный», «помещался за монитор», «тихий». Это градиент. Блок размером 80 мм лучше блока размером 120 мм, но и 120 мм может устроить, если покупатель готов пожертвовать местью за монитором. Такие требования годятся для ранжирования, а не для отсева.
Практическое правило: всё, что при невыполнении делает покупку бессмысленной или опасной, идёт в фильтр; всё, что сдвигает оценку «лучше или хуже», идёт в ранжирование и в текст для модели. Ошибка в обе стороны дорогая. Если пожелание превратить в жёсткое условие, покупатель с «компактным» блоком на 95 мм вместо порога в 90 увидит пустую выдачу, хотя подходящий товар был. Если жёсткое условие оставить пожеланием, в выдаче окажется красивая карточка с неверным штекером.
Полезно проверить, как разбирается сама фраза. «Подойдёт для лампы» — это не условие совместимости, это тематический контекст. «12 В» — условие. «Помещался за монитор» — пожелание к размеру, причём без числа: придётся либо спросить, сколько места, либо ранжировать по возрастанию габаритов, не выдумывая порог. Выдумывать порог за покупателя нельзя: он скажет «ну, примерно с ладонь», и это другой разговор.
Откуда берутся фильтры: каталог должен знать свои поля
Фильтр можно построить только по тому, что хранится в структурированном виде. Если «5,5 × 2,1» существует лишь как строка внутри описания, фильтровать нечего, останется надеяться на текстовое сходство, с чего мы и начали. Поэтому подготовка каталога для такого поиска — это не про эмбеддинги, а про нормализацию характеристик.
Для примера возьмём вымышленный каталог адаптеров питания. Каждая карточка хранит в отдельных полях то, что определяет совместимость, и отдельно текст для семантического поиска:
| Поле | Пример значения | Роль в поиске |
|---|---|---|
| connector | dc_5.5x2.1 | жёсткий фильтр, точное совпадение |
| output_voltage_v | 12 | жёсткий фильтр, точное совпадение |
| max_current_a | 2 | жёсткий фильтр «не меньше потребности» |
| length_mm | 84 | ранжирование по близости к пожеланию |
| spec_source | datasheet_manufacturer | служебное: откуда взято значение |
| description | Компактный адаптер для настольных светильников… | текст для векторного поиска |
Обратите внимание на три вещи. Во-первых, разъём записан кодом из закрытого списка, а не свободной строкой. «5.5x2.1», «5,5×2,1», «5.5 × 2.1 мм» и «штекер 5,5/2,1» — одно и то же для человека и четыре разных значения для фильтра. Нормализация до словаря значений делается один раз при подготовке каталога, и экономит бесконечные «почему не нашлось».
Во-вторых, числовые поля хранятся числами в одной единице. Напряжение в вольтах, длина в миллиметрах. Если у поставщика вдруг пришло «0,084 м» или «12000 мВ», конвертация происходит на входе в каталог, а не в момент запроса. Иначе рано или поздно фильтр «до 90 мм» пропустит «8,4» из сантиметров как совсем крошечный товар или отсечёт его как «слишком большой» в зависимости от направления сравнения.
В-третьих, рядом с характеристикой стоит поле о происхождении значения. Об этом ещё будет речь при проверке ответа: утверждение о совместимости должно опираться на запись, у которой известно, откуда она.
Если карточек у вас много, и данные приходят из разных прайсов, часть полей будет пустой. Это нормально, но пустое поле нельзя молча приравнять ни к «подходит», ни к «не подходит». Об этом отдельный раздел ниже.
Как условия попадают в запрос: AND как способ вычеркнуть лишнее
В векторных базах условия фильтра проверяют поля payload — дополнительные данные, которые хранятся рядом с вектором. В документации Qdrant про фильтрацию сказано, что условия в блоке must объединяются как AND, то есть запись должна удовлетворять каждому из них (Qdrant: Filtering). Для нашей задачи именно это нужно: совместимость — это конъюнкция.
Посмотрим на это глазами исключения, а не включения. Пусть в каталоге есть пять адаптеров:
- А: разъём 5,5 × 2,1, 12 В, 84 мм;
- Б: разъём 3,5 × 1,35, 5 В, 60 мм — наш «идеальный по описанию» вариант;
- В: разъём 5,5 × 2,1, 9 В, 70 мм;
- Г: разъём 5,5 × 2,1, 12 В, 130 мм;
- Д: разъём 5,5 × 2,5, 12 В, 75 мм.
Условие «разъём = 5,5 × 2,1» убирает Б и Д. Условие «12 В» убирает В. Остаются А и Г. Важно, что каждое условие работает как независимая калитка: товар выбывает при первой же несовпавшей характеристике, и никакое количество совпавших остальных не вернёт его обратно. Б, например, имел бы «компактность 60 мм» — лучший показатель по размеру, — и именно поэтому он так опасен: в ранжировании без фильтра он оказался бы вверху. AND превращает «лучший по одному признаку» в «недопустимый по другому» и убирает его до того, как он успеет понравиться.
Пример структуры условия в упрощённом виде (концептуальный набросок, точный синтаксис сверяйте с документацией вашей версии):
{
"must": [
{ "key": "connector", "match": { "value": "dc_5.5x2.1" } },
{ "key": "output_voltage_v", "match": { "value": 12 } },
{ "key": "max_current_a", "range": { "gte": 1.5 } }
]
}Заметьте, что для тока здесь не равенство, а «не меньше». Блок питания на 2 А для лампы, потребляющей 1,5 А, подойдёт; на 1 А — нет. Направление неравенства — часть предметной логики, и ошибиться в нём проще всего. Для напряжения, напротив, нужно точное совпадение. Для полярности центрального контакта тоже нужно точное совпадение, если у вас есть такое поле, а оно в реальном каталоге, как правило, понадобится: одинаковый штекер с другой полярностью — классическая ловушка.
Откуда фильтры берутся из реплики покупателя? Здесь уместно использовать языковую модель, но в узкой роли: извлечь из фразы значения полей. Модель получает список допустимых полей и значений и возвращает структуру — какие условия названы, какие нет, какие пожелания звучат. Она не решает, что подходит; она переписывает запрос в виде данных. Результат стоит валидировать кодом: значение разъёма должно входить в словарь, напряжение быть числом из разумного диапазона. Если модель вернула «штекер 5,5», а в словаре два варианта — 5,5 × 2,1 и 5,5 × 2,5, это не фильтр, это повод для вопроса, и об этом ниже.
Искать и ранжировать только допустимых
Когда допустимый набор определён, подключается семантика. Её задача сузилась: не угадывать совместимость, а выбрать из совместимого то, что лучше всего отвечает по смыслу и по пожеланиям.
Есть два способа расставить этапы. Простой: применить фильтр прямо в запросе к векторному поиску, тогда база вернёт ближайшие векторы только среди записей, прошедших условия. Более гибкий: собрать несколько выборок и объединить. Query API в Qdrant поддерживает предварительные выборки и последующий запрос по их результатам, а результаты нескольких поисков можно объединять (Qdrant: Hybrid queries). Для нас это означает возможную схему: одна выборка берёт кандидатов по смыслу описания, другая по текстовому совпадению с названием модели лампы, а итоговый шаг сливает их. Принципиально, чтобы условие совместимости применялось в каждой ветви, а не только в финальной. Иначе недопустимый товар, пришедший по одной из ветвей, может попасть в объединённый список.
Это тот случай, где легко допустить тонкую ошибку: фильтр аккуратно стоит в основном запросе, а вспомогательная выборка по ключевым словам его не получила. Тестировать нужно именно такой сценарий: запрос, на который заведомо найдётся несовместимый товар с очень сильным текстовым совпадением.
Теперь пожелание «помещался за монитор». У него нет порога, значит, жёстко фильтровать нельзя. Можно ранжировать допустимых кандидатов по длине по возрастанию, смешав это с семантической оценкой. В нашем примере А (84 мм) окажется выше Г (130 мм). Если покупатель позже скажет «до 100 мм», пожелание превратится в условие length_mm ≤ 100 и уйдёт из ранжирования в фильтр. Граница между категориями не фиксирована раз и навсегда: она определяется тем, насколько покупатель категоричен в данной реплике.
Ещё одно следствие порядка. Поскольку фильтр отсекает кандидатов заранее, список может оказаться пустым или очень коротким. Это не сбой, а честный результат: «с таким разъёмом и напряжением в каталоге нашлось два товара, из них один крупнее, чем хотелось бы». Показать два допустимых варианта полезнее, чем десять близких по описанию с неверным штекером. Но пустой результат нужно обработать явно: не отправлять в модель пустой контекст с просьбой «что-нибудь посоветовать», потому что именно тогда она начнёт сочинять.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Неизвестное — это не «да» и не «нет»
В реальном каталоге у части карточек характеристика не заполнена. У адаптера Е в нашем условном списке в поле output_voltage_v пусто: поставщик не прислал данные, а в описании только «универсальный». Что делать?
Три наивных решения плохи по-разному. Считать пустое «подходит» — значит рекомендовать товар вслепую; для блока питания это риск порчи техники. Считать пустое «не подходит» — значит скрыть возможно хороший товар, и покупатель не узнает о нём; зато это безопасно. Позволить модели «догадаться» по тексту — значит вернуться к исходной проблеме: слово «универсальный» в описании ничего не доказывает.
Разумный путь — третье состояние. Условие жёсткого фильтра по умолчанию отсекает записи без значения: если поле не прошло проверку на совпадение, товар в рекомендуемый набор не входит. Но такие записи не исчезают бесследно: их можно держать в отдельной ветви «совместимость не подтверждена» и показывать явно подписанными, без утверждения о совместимости. Для рискованных категорий, а блоки питания, зарядные устройства, аккумуляторы и всё, что касается электробезопасности, к ним относятся, лучше вообще не показывать неподтверждённые товары как кандидатов и ограничиться предложением уточнить данные у продавца или у производителя. Языковая модель не устанавливает совместимость там, где достоверных характеристик нет; для таких товаров нужна проверка профильным специалистом или по документации производителя.
Обратная сторона: неизвестное может быть не в карточке, а в запросе. Покупатель не назвал напряжение. Тогда фильтр по напряжению применить нельзя, и сделать вид, что условие названо, нельзя тоже. Либо вопрос, либо результат с явной оговоркой, что напряжение не учтено. Для устройств, где неверное напряжение вредит, выбирается вопрос.
Когда лучше спросить, чем угадать
Вопрос уместен не в любой неопределённости, иначе бот превратится в анкету. Он нужен, когда выполняются два условия: отсутствующая характеристика влияет на допустимость, и без неё допустимый набор остаётся слишком разнородным.
Можно сформулировать это через число кандидатов и различия между ними. Допустим, покупатель написал: «Блок питания для лампы, штекер 5,5». Разъём 5,5 × 2,1 и 5,5 × 2,5 — два разных значения словаря, различающихся на четверть миллиметра в центральном контакте. Напряжение не названо вовсе. Если в допустимом после первых условий наборе присутствуют и 9 В, и 12 В, и 19 В, то любая рекомендация будет лотереей. Вопрос в этом случае не вежливость, а необходимый шаг.
Хороший вопрос обладает несколькими свойствами. Он один, а не три: покупатель отвечает охотнее, когда видит конец разговора. Он опирается на то, что покупатель может узнать без инструментов: «Что написано на старом адаптере или на корпусе лампы рядом со значком вольт?» понятнее, чем «Укажите номинальное выходное напряжение». Он называет причину: «от этого зависит, не сгорит ли лампа». И он предлагает пути отступления для тех, кто не знает ответа: «Если не нашли надпись, пришлите модель лампы, найдём по ней».
Порядок вопросов тоже имеет значение. Первым спрашивают то, что сильнее всего сужает набор и о чём покупатель реально может ответить. Если по каталогу вариантов разъёма всего два, а напряжений шесть, вначале выгоднее уточнять напряжение. Это можно считать прямо по данным: перед вопросом посчитать, по какому полю допустимые кандидаты различаются больше всего.
Сборка контекста для модели
Только после всех этих шагов в дело вступает генерация. Модель получает ограниченный материал: отфильтрованные карточки с их полями, пожелания покупателя и инструкцию, как формулировать ответ. Её задача — объяснить выбор человеческим языком, сравнить два-три варианта по пожеланиям и честно назвать компромиссы.
Важно, чтобы структурированные поля попадали в контекст как поля, а не растворялись в тексте. Например: «Товар А. Разъём: 5,5 × 2,1 (источник: паспорт производителя). Напряжение: 12 В (там же). Длина: 84 мм (источник: карточка поставщика)». Тогда модель оперирует именно этими значениями и может ссылаться на них, а не на то, что ей показалось в красивом абзаце описания.
В инструкции стоит прямо запретить две вещи: утверждать совместимость, если соответствующего поля нет в переданных данных, и добавлять характеристики, которых нет в контексте. Вторая важна, потому что модели свойственно «достраивать» правдоподобные детали. Правило простое: если в карточке нет полярности центрального контакта, ответ не должен говорить о полярности вообще или должен прямо сказать, что она не указана.
Финальная проверка: каждое утверждение должно иметь опору
Фильтры защищают от неверных кандидатов, но не от неверной формулировки. Модель может получить два правильных товара и всё равно написать «оба совместимы с вашей лампой», хотя о лампе известно только то, что покупатель сообщил на словах. Поэтому после генерации полезен шаг проверки ответа.
Проверка устроена как сверка утверждений с данными. Из ответа извлекаются утверждения о характеристиках: «разъём 5,5 × 2,1», «12 В», «длина 84 мм». Каждое сопоставляется с записью выбранной карточки. Если утверждение есть в ответе, а в записи его нет или значение другое, ответ отклоняется или переписывается. Отдельно проверяется, что в ответе названы только те товары, которые были в отфильтрованном наборе: попытка упомянуть «ещё один вариант, который тоже подойдёт» вне набора — признак того, что модель вышла за пределы контекста.
Второй уровень — происхождение значения. Поле о том, откуда взята характеристика, позволяет различать «из паспорта производителя» и «со слов поставщика» или «извлечено из описания автоматически». Для ответа о совместимости в чувствительных категориях разумно принимать только первый уровень. Остальное — повод для оговорки «уточните у продавца». Это ваше собственное правило качества, а не свойство какого-либо инструмента, и нужно решить его заранее, а не на ходу.
Третий уровень — согласованность с запросом. Если покупатель назвал 12 В, а в ответе нет упоминания, что выбранный товар даёт 12 В, пользователь вынужден доверять на слово. Хороший ответ повторяет условия и показывает, что товар их выполняет: «Подходит по разъёму (5,5 × 2,1) и напряжению (12 В), в длину 84 мм».
Проверочный набор: вопросы, на которые система не должна ошибиться
Чтобы убедиться, что всё это работает, соберите небольшой набор тестовых запросов с заранее известными допустимыми ответами. Нужны не сотни, а удачно подобранные два-три десятка, покрывающие характерные ловушки:
- запрос, на который есть товар с идеальным описанием, но неверным разъёмом — он не должен появиться в выдаче;
- запрос, где подходящие товары есть, но у одного из них нужное поле пустое — он не должен быть назван совместимым;
- запрос без напряжения — ожидается вопрос, а не рекомендация;
- запрос с единицей в другом виде («9 см» вместо «90 мм») — результат должен совпасть с эквивалентным запросом в миллиметрах;
- запрос, где фильтры дают пустой набор — ожидается честное «не нашлось», а не придуманный товар;
- запрос с пожеланием без порога — ожидается ранжирование, а не жёсткое отсечение;
- запрос, в котором вспомогательная выборка по ключевым словам сильно совпадает с несовместимым товаром — он не должен проскочить через объединение результатов.
Для каждого случая записывается ожидаемое поведение, и набор прогоняется после любого изменения каталога, словаря значений или инструкции модели. Метрик, придуманных ради красивых цифр, здесь не нужно: достаточно отмечать, нарушено ли правило. Один недопустимый товар в рекомендациях — уже провал по главному требованию.
Где всё это живёт
Вычислительно такая схема нетребовательна: фильтрация по полям и поиск по вектору делаются в базе, а языковая модель вызывается дважды-трижды за обращение — для извлечения условий, для формулировки ответа и, возможно, для проверки. Нормализация каталога выполняется пакетными задачами. Для небольшого интернет-магазина это обычная нагрузка на один сервер без особых требований. Если вы разворачиваете такой стек самостоятельно, VPS с NVMe, процессорами AMD EPYC и root-доступом, как в предложении VPS MAATRIX, даёт возможность поставить векторную базу, сервис нормализации и обработчик запросов на одной машине. Тарифы различаются по ресурсам — от Ba с 1 vCPU и 2 ГБ RAM до Djed с 16 vCPU и 32 ГБ, — но ресурсы не равны обещанию ёмкости, и подбирать конфигурацию стоит по замерам на вашем каталоге и вашей нагрузке. Модель для генерации при этом может работать как внешний сервис или локально, это зависит от ваших требований; GPU в предложении не заявлен, так что тяжёлые локальные модели нужно планировать отдельно. Базовые шаги развёртывания поиска по документам описаны в статье как поднять RAG по своим документам на сервере; здесь мы сосредоточились именно на фильтрах совместимости.
Разговор, с которого всё начиналось
Вернёмся к покупателю с лампой. Если он написал только «компактный блок питания для настольной лампы, чтобы помещался за монитор», у системы нет ни разъёма, ни напряжения. Допустимый набор — весь каталог адаптеров, и любая уверенная рекомендация будет угадыванием с красивым описанием. Вместо этого система задаёт один вопрос:
Посмотрите на старый блок питания или на корпус лампы: какое напряжение указано рядом с «V» и какой штекер — если есть размер вроде 5,5 × 2,1 мм, пришлите его. Тогда я покажу только то, что точно встанет и не навредит лампе.
Один вопрос, с причиной и с запасным путём. Ответ на него стоит дороже десятка уверенных карточек, потому что превращает «похоже» в «подходит».