Перейти к основному содержимому
megapromoting
Давайте обсудим

Экспертиза · Базы знаний и семантический поиск

Мы строим корпус, из которого ассистенту разрешено отвечать, с правилами о том, что входит, как это делится, как ищется и что происходит, когда источник меняется.

Ассистент, который отвечает клиентам, ровно настолько хорош, насколько хорош корпус, из которого он читает. Мы занимаемся корпусом: извлечением из документов и сайта, разделением на фрагменты, индексацией, поиском, воротами ручного одобрения и обновлением. Включает ответ на вопрос, когда НЕ стоит использовать семантический поиск.

Уже построеноExistă implementări proprii pentru fiecare etapă, dar starea lor diferă și trebuie spusă pe față. Ce rulează cu vectori proprii: memoria unui board intern folosit zilnic, cu coloană `vector(1536)`, index `ivfflat` pe distanță cosinus, prag de similaritate 0,35 și o buclă care re-indexează la 30 de minute doar ce s-a schimbat. Ce rulează cu vectori la furnizor: baza de cunoștințe a agenților vocali, sincronizată către motorul de regăsire al furnizorului cu fragmente de 500 și suprapunere de 100. Ce rulează în producție **fără** vectori: baza de cunoștințe a platformei de asistenți text — extragere, împărțire, configurare per asistent și regăsire hibridă sunt scrise și funcționează, dar calea de indexare scrie coloana de vector cu `null`, deci scorul hibrid degradează la lexical, iar codul își spune singur asta prin eticheta `keyword-only`. Ce e scris și încă nu e pornit în producție: pipeline-ul propriu pe `pgvector` al platformei vocale, a cărui migrare poartă în antet mențiunea „LOCAL ONLY — do not push to prod”. Un serviciu se vinde cu harta asta pe masă, nu fără ea.

Ассистент, который разговаривает с клиентами, ничего не «знает». На каждый вопрос кто-то заранее ищет несколько текстовых фрагментов и подаёт их ему, а он формулирует ответ из них. Качество ответа почти целиком решается на этом этапе поиска, а не на выборе модели. Поэтому эта услуга — о корпусе и о поиске, а не о модели.

Работа состоит из четырёх частей, которые ломаются независимо друг от друга. Извлечение: из каких файлов и каких страниц мы берём пригодный текст и что делаем, когда не можем. Разбиение: насколько велики фрагменты, насколько они перекрываются, где режется предложение. Поиск: как выбираются фрагменты для конкретного вопроса, сколько их и по какому критерию. Обновление: что происходит, когда исходный документ меняется. Система может быть безупречной в трёх из них и бесполезной из-за четвёртой.

Есть ещё один этап, который многие пропускают и который полностью меняет результат: ворота утверждения. Корпус, который растёт сам из разговоров, быстро начинает содержать ответы, которые никто не валидировал. Построенный нами цикл идёт в обратную сторону: когда агент говорит „у меня нет этой информации”, вопрос записывается со статусом „в ожидании”, человек получает уведомление и отвечает, а его ответ переходит в статус „утверждено” и только тогда попадает в корпус. Знание растёт из реальных пробелов, с человеком в цепочке.

И непопулярная часть: семантический поиск не всегда является правильным выбором. Для собственной публичной платформы мы сознательно отказались от него. Корпус — это собственный текст платформы, разбитый на короткие пасажи, каждый из которых несёт страницу, из которой он взят, с наложением по словам. Без векторов, без векторного хранилища, без дополнительного сетевого запроса — и, что важнее, без какого-либо источника вне хранилища. Ассистент не может ничего, кроме как повторять то, что уже написано на сайте, а любой ответ проверяется на странице, написанной человеком. Это и есть весь механизм против выдумывания.

Что входит

Работа, по составляющим

Извлекаем текст и прямо говорим, когда не можем

Из обычного текста, markdown, журналов, CSV, JSON, DOCX и PDF. DOCX распаковывается как архив, и читается XML-документ внутри; PDF проходит через стандартный extractor с сохранением разметки страницы, а если его нет, есть резервный вариант, который читает текстовые операторы прямо из файла. Важный случай — когда это не работает: отсканированный PDF, который является изображением, не содержит выбираемого текста, и система возвращает явное сообщение — загрузите DOCX или текст, либо сначала прогоните файл через оптическое распознавание. Оптическое распознавание у нас не реализовано, и мы не делаем вид, что оно есть.

Разбиваем на фрагменты с перекрытием, разрезая между предложениями, а не внутри них

Используемые нами значения по умолчанию: 500 лексических единиц и 50 перекрытия для голосового pipeline, 1.200 символов и 150 перекрытия для текстового; на стороне голосового поставщика — 500 и 100. Перекрытие существует потому, что предложение, разрезанное посередине между двумя фрагментами, становится непригодным для обоих. Разделитель предложений написан для языков, с которыми мы работаем: он распознаёт конец предложения, за которым следует латинская **или кириллическая** заглавная буква, включая румынские диакритики. Разделитель, написанный для английского, плохо режет на румынском и ещё хуже на русском.

Индексируем с векторами там, где это запущено, и говорим, где нет

Модель представления по умолчанию — `text-embedding-3-small`, с 1.536 измерениями, запрашиваемая через наш шлюз моделей, а не напрямую у поставщика — так можно сменить поставщика, не трогая код базы знаний. В базе колонка имеет тип vector с индексом `ivfflat` по косинусному расстоянию. В миграции есть резервная стратегия: если векторное расширение недоступно, схема создаётся в любом случае с trigram-индексом по тексту, а поиск переключается на лексическое сходство. Это не гипотеза — это написано именно потому, что права на расширение не гарантированы на любой установке.

Гибридный поиск, с весами на виду

Итоговый балл — 0,7 от векторного сходства плюс 0,3 от совпадения слов, с нормализованной лексической частью. Оценивается набор кандидатов и сохраняются первые фрагменты, а контекст, отправляемый модели, обрезает каждый фрагмент до максимальной длины, чтобы один длинный документ не занял все пространство. Конфигурация задается для каждого ассистента, по умолчанию: поиск включен или выключен, максимальное число фрагментов на вопрос (по умолчанию 5, ограничено от 1 до 20), порог сходства, тип поиска — семантический, лексический или гибридный — и вес лексической части.

Ворота одобрения: знание растет из пробелов, с человеком в цепочке

Когда агент отвечает, что у него нет информации, вопрос записывается в таблицу со статусом «в ожидании», помеченный каналом, из которого он пришел — голос или виджет. Человек получает уведомление и отвечает прямо в сообщении; webhook забирает ответ, переводит запись в статус «одобрен», а оттуда она попадает в контекст агента. Возможных статусов четыре: в ожидании, одобрен, проигнорирован, дубликат — а дубликаты связываются с исходным вопросом через ссылку, чтобы не накапливались десять формулировок одного и того же пробела. Передача агенту выполняется один раз на каждую одобренную запись.

Обновление: что происходит, когда источник меняется

При добавлении вычисляется отпечаток SHA-256 содержимого; если текст идентичен чему-то уже индексированному, он переиспользуется и за новую векторную репрезентацию не платят. Когда документ изменяется, его фрагменты полностью удаляются и переписываются — полная замена по источнику, а не частичное обновление, потому что частичное обновление оставляет после себя сиротские фрагменты старой версии, а именно они и дают ответы, противоречащие сайту. У источника есть видимый цикл статусов: в ожидании, в обработке, в повторной обработке, готово или неудача, а переиндексацию можно запросить точечно, по источнику.

Мы не переиндексируем то, что не изменилось

В цикле, который запускается каждые тридцать минут по списку, который только растет, перерасчет представления для не тронутого элемента — это повторная оплата за вектор, который не мог сдвинуться. Наш цикл сравнивает момент последнего изменения с моментом последней индексации и пропускает неизмененное. Представления запрашиваются пакетами, а не по одному. Это детали затрат, но ежемесячная стоимость базы знаний почти полностью решается именно здесь.

Подключенный каталог — это не загруженный список

Разница не в формате, а во времени. Загруженный список является верным на момент загрузки: цена, запас и новые товары остаются замороженными, пока кто-то не вспомнит загрузить их снова — и обычно выясняется, что никто не вспоминает, когда клиент спрашивает о цене трехмесячной давности. Подключенный каталог читается в момент вопроса, из источника, который ведет учет. Что мы построили в этом направлении: извлекатель, который распознает коллекции товаров в JSON или CSV по обычным ключам и, когда структура необычная, просит помощи у модели с обязательством отвечать строго в заданной форме; и парсер, который читает цену со живой страницы магазина. Лучшим маршрутом остается собственный структурированный интерфейс магазина, там, где он есть — но это проверяется по каждому магазину, а не предполагается.

Обход сайта: сначала карта, потом исследование

Сначала ищется карта сайта, в обычных вариантах названия, и разворачиваются вложенные карты на двух уровнях. Только если карты нет, переходят к обходу в ширину, строго по тому же происхождению, с настраиваемым лимитом страниц (по умолчанию 50, максимум 200). Фактически использованная стратегия сообщается обратно — карта или обход — потому что по ней видно, исходит ли бедный результат от сайта или от метода. Учитывается файл исключений сайта.

Как это выглядит

Путь, шаг за шагом.

01

Определяем, что ассистенту разрешено знать, до любого импорта

Список разрешенных источников и, что не менее важно, список отклоненных, с причиной. Внутренний файл закупочных цен, экспорт контактов, архив писем — это именно те вещи, которые попадают в корпус «чтобы был контекст» и потом становятся ответом клиенту. Мы передаем инвентарь источников с решением и причиной для каждого, плюс правило о том, что никогда не должно попасть внутрь.

02

Импортируем, разделяем и проверяем результат, читая фрагменты на деле

Извлечение, разделение с перекрытием, индексирование. Этап проверки не пропускается: случайные фрагменты читаются из полученного корпуса. Там появляются таблицы, превращенные в строки слипшихся слов, сканированные PDF, из которых не извлекся текст, и страницы меню навигации, попавшие как самостоятельные документы. Мы передаем корпус плюс отчет о том, что не удалось извлечь и почему.

03

Настраиваем поиск по реальным вопросам, а не по удобным вопросам

Создается набор вопросов из того, что клиенты реально спрашивают, включая плохо написанные формулировки и смешанные двуязычные, и настраиваются число фрагментов, порог и вес лексической части. Цель не в том, чтобы каждый вопрос получил ответ, а в том, чтобы вопросы без покрытия в корпусе получили «у меня нет этой информации». Мы передаем набор вопросов с результатами и финальной конфигурацией.

04

Ставим ворота одобрения и цикл роста

Регистрация неотвеченных вопросов, уведомление лица, которое утверждает, путь ответа, перенос в corpus и связывание дубликатов. Мы поставляем рабочий цикл и письменную инструкцию для того, кто утверждает, — включая то, что не копируется из вопроса клиента.

05

Передаём процедуру обновления, с именем того, кто её выполняет

Какой источник обновляется, как часто, кто нажимает и как видно, что это произошло. Здесь открыто говорим об ограничении: на платформах, которые мы обслуживаем, переиндексация — это запрашиваемое действие, а не то, что выполняется автоматически по расписанию. Процедура без имени человека напротив неё — это процедура, которая не выполняется.

DocumenteConversațiiSurseSintezăAcțiuneEchipăContexteazăSurse autorizate. Acțiuni revizuite.
Drumul unei întrebări prin sistem: sursele aprobate intră în extragere și împărțire, fragmentele ajung în index; la întrebare se punctează candidații — vectorial și lexical — și doar câteva fragmente ajung în context; iar când nu se găsește nimic, întrebarea iese pe ramura de aprobare umană și se întoarce în corpus.

Данные

К чему прикасаемся, где оно лежит и сколько остаётся

Вопросы, которые задаёт всякий, у кого есть ответственный за защиту данных, — заданы здесь раньше, чем их задаст он.

Что попадает в corpus
Только то, что явно утверждено: загруженные документы, выбранные страницы, пары вопрос-ответ, тексты, написанные от руки. Источники типизированы — текст, документ, сайт, пары вопрос-ответ — и имеют заявленную цель: продукты, документация, часто задаваемые вопросы или другое. У каждого источника есть свой переключатель, и этот переключатель проверяется при каждом запросе извлечения, а не только в интерфейсе — выключенный источник не может попасть в контекст даже случайно.
Где хранятся фрагменты и их представления
В базе данных соответствующей платформы, рядом с остальными данными клиента, а не в отдельном внешнем сервисе поиска. Для установок на PostgreSQL — в собственных таблицах с явно активированным векторным расширением; для платформы текстовых ассистентов — в MySQL, с представлением в колонке JSON и вычислением расстояния в приложении. Вторая модель работает в небольшом масштабе и не масштабируется до сотен тысяч фрагментов — это архитектурное ограничение, сказанное как есть.
Персональные данные из разговоров
Corpus не должен содержать персональные данные, но цикл обучения может их привнести, потому что вопрос клиента — это текст, написанный клиентом. Поэтому попадание в corpus проходит через утверждение человеком: там идёт фильтрация. Операционное правило, которое идёт вместе с этим: тот, кто утверждает ответы, получает явную инструкцию не копировать в corpus имена, номера телефонов или детали заказа из исходного вопроса.
Что сохраняется о запросах
Есть таблица журнала запросов извлечения, со полученными оценками, временем извлечения и количеством возвращённых фрагментов. Это инструмент, с помощью которого отвечают на вопрос «почему он так сказал» — без него настройка порогов превращается в гадание. Журнал хранится, пока он полезен для настройки, и подпадает под сроки хранения платформы, а не под отдельный режим.
Чего не существует и что заявлено как таковое
Не применяется срок истечения к документам: поле объявлено в схеме, но никто его не читает, поэтому документ не исчезает из corpus сам по себе. В клиентских платформах нет автоматического переиндексирования по расписанию — обновление запрашивается явно, по источнику. Нет переупорядочивания с выделенной моделью и нет лексического оценивания типа BM25; то, что мы называем «гибридом», — это взвешенная сумма между сопоставлением слов и векторным расстоянием, и именно такая формулировка является корректной.

Случай

База знаний, выросшая из вопросов, на которые агент не знал, как ответить

Ситуация

У ресторанной сети с доставкой был голосовой агент по телефону и ассистент на веб-странице. Начальный corpus был загружен один раз, при запуске. Проблема была не в том, что агент ошибался, а в том, что он слишком часто говорил «у меня нет этой информации» — на вполне разумные вопросы, которые никто не предвидел, когда писался corpus. Каждый такой ответ был потерянным разговором, а списка их не существовало нигде.

Что мы построили

Мы превратили отсутствие в системную сущность. Когда агент отвечает, что у него нет информации, вопрос записывается в таблицу со статусом «в ожидании» и с каналом, из которого он пришёл — голос или виджет. Ответственное лицо со стороны клиента получает уведомление в канал обмена сообщениями, которым оно и так пользуется, с инструкцией ответить прямо в сообщении. Webhook забирает ответ, переводит запись в статус «утверждено», и оттуда ответ попадает в контекст агента. Разные формулировки одного и того же пробела связываются с исходной записью по ссылке, а передача агенту выполняется один раз на запись, чтобы утверждённый ответ не реинжектировался при каждом цикле.

Что получилось

Corpus перестал быть файлом, загруженным при запуске, и стал списком реальных пробелов, упорядоченным клиентами, а не нашими предположениями. Тот, кто утверждает, видит точно, что было спрошено и в каком канале, поэтому пишет ответ в той форме, в которой он будет использован. Статусов четыре — в ожидании, утверждено, проигнорировано, дубликат — потому что «проигнорировано» — это законный результат: некоторые вопросы вообще не должны получать автоматический ответ.

Чего этот случай не говорит

Цикл работает ровно настолько, насколько работает человек внутри него. Если никто не отвечает на уведомления, записи «в ожидании» накапливаются, и агент по-прежнему ничего не знает — механизм делает отсутствие видимым, а не решает его сам. Второе ограничение, не менее важное: реализация была подключена под поток этого клиента, с таблицей, уведомлением и внедрением, написанными под него. Как общей функции, доступной из интерфейса для любого аккаунта, её пока нет.

Вопросы

О чём нас спрашивают перед тем, как позвонить

Почему ассистент отвечает неправильно, если информация есть в загруженных документах?

Почти всегда потому, что правильный фрагмент не был найден, а не потому, что модель была бы «плохой». Частые причины, в порядке, в котором мы их проверяем: слишком большие фрагменты, в которых полезная информация теряется среди прочего; разрез посреди предложения, без перекрытия; слишком мало фрагментов, вынесенных в контекст; или корпус, в котором одна и та же информация встречается в трёх разных версиях, и старая набирает больший балл. Журнал запросов со полученными оценками показывает, какой именно случай перед вами — без него приходится гадать.

Вы действительно везде используете семантический поиск?

Нет, и честнее сказать точно, где. С собственными векторами: память внутренней доски, используемой ежедневно, в векторном столбце с индексом по косинусному расстоянию и порогом сходства 0,35. С векторами у поставщика: база знаний голосовых агентов, синхронизированная с движком поиска поставщика. **Без** векторов, на сегодня, в продакшене: база знаний текстовых ассистентов платформы — путь индексации пишет пустой векторный столбец, поэтому поиск работает лексически, а код сам помечает свой модуль как `keyword-only`. Есть и полностью написанный собственный pipeline, с миграцией, помеченной «не публиковать в продакшене» до подтверждения прав на векторное расширение. Это и есть реальная карта.

Что происходит, когда мы изменяем уже проиндексированный документ?

Фрагменты источника удаляются полностью и переписываются по новой версии. Мы выбрали полную замену вместо частичного обновления, потому что сложная часть не добавление, а удаление: фрагменты, оставшиеся от старой версии, по-прежнему находятся в поиске и выдают ответы, которым клиент противоречит страницей перед собой. При добавлении есть и проверка отпечатка содержимого — если текст идентичен, ничего не пересчитывается и зря не оплачивается.

Обновляется ли он сам, когда меняется сайт или каталог?

Не автоматически, на платформах, которые мы сейчас обслуживаем. Реиндексация запрашивается явно, по источнику, через отдельное действие. Есть одно исключение, в нашей внутренней системе, где цикл запускается каждые тридцать минут и переиндексирует только то, что изменилось. Автоматическое расписание можно построить, и это отдельный этап; чего мы не делаем — это не создаём впечатление, будто оно уже существует. В схеме есть и объявленное поле истечения срока, которое никто не читает — значит, документ сам из корпуса не выходит.

В чём разница между подключённым каталогом и загруженным списком товаров?

Момент истины. Загруженный список верен в день загрузки; со следующего дня цена, запас и новые товары тихо расходятся, и это расхождение обнаруживается, когда клиент просит цену, которой уже нет. Подключённый каталог читается в момент вопроса, из системы, которая ведёт учёт, значит не может отставать. Цена здесь другая: появляется зависимость от доступности этой системы, и нужно решить, что ассистент говорит, когда источник не отвечает — правильный ответ: «сейчас не могу подтвердить цену», а не последнее известное значение.

Можете индексировать сканированные PDF?

Не напрямую. Сканированный PDF — это изображение в упаковке PDF и не содержит выбираемого текста; наш экстрактор это обнаруживает и возвращает явное сообщение вместо индексации пустого документа — худший возможный результат это источник, помеченный как «готово», который ничего не содержит. Решение — оптическое распознавание перед импортом или предоставление документа в DOCX либо текстом. Оптическое распознавание у нас не реализовано в текущей цепочке.

Работает на румынском и русском?

Да, и есть деталь, которая важнее, чем кажется. Разбиение на фрагменты режет между предложениями, а наше правило распознаёт конец предложения, за которым следует латинская **или** кириллическая заглавная буква, включая румынские диакритики. Разделитель, написанный для английского, не распознаёт начало предложения на кириллице и даёт слипшиеся фрагменты, что напрямую влияет на качество поиска. Для голосовой стороны используемая у поставщика модель представления является мультиязычной.

Когда НЕ стоит использовать семантический поиск?

Когда корпус маленький, стабильный и написан вами — например, содержимое собственного сайта. В собственной публичной платформе мы сознательно выбрали поиск по перекрытию слов по собственному тексту платформы, разбитому на короткие отрывки, каждый из которых содержит исходную страницу. Без векторов, без векторного хранилища, без дополнительного сетевого запроса и, что важно, без какого-либо источника вне хранилища. Ассистент может только повторять то, что уже говорит сайт, а каждый ответ можно проверить на странице, написанной человеком. Когда главная цель — не выдумать ничего, это лучше семантического поиска.

Вы держите нас в заложниках в своей платформе?

Ваши источники остаются в исходном виде; фрагменты и представления хранятся в базе данных вашей установки, а не во внешнем сервисе поиска, к которому имеем доступ только мы. Модель представлений запрашивается через собственный шлюз моделей, поэтому смена поставщика не затрагивает код базы знаний. Векторные представления, однако, связаны с моделью, которая их создала: смена модели требует переиндексации всего корпуса. Это вычислительная операция, а не бесплатная, и лучше знать об этом в начале, чем узнать в середине.

На чём основаны утверждения выше (29 источников)

29 из них — код и файлы из наших репозиториев. Мы не публикуем их название и строку: вместе, на одной странице, они слишком точно описали бы, как устроены системы, которые принадлежат не только нам. Мы разбираем их вместе с тобой, в репозитории, по запросу — проверка остаётся возможной, просто она проходит в разговоре.

Что вы хотели бы наладить?

Расскажите о своём процессе. Вместе определим, что стоит построить, что можно подключить и как проверим результат.

Давайте обсудим