Перейти до основного вмісту
megapromotingПоговорімо

Експертиза · Бази знань і семантичний пошук

Ми будуємо корпус, з якого асистент має право відповідати, з правилами про те, що входить, як це ділиться, як це шукається і що відбувається, коли джерело змінюється.

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

Вже збудованоІснують власні реалізації для кожного етапу, але їхній стан різний, і це треба сказати прямо. Що працює з власними векторами: пам’ять внутрішньої дошки, яку щодня використовують, із колонкою `vector(1536)`, індексом `ivfflat` на косинусній відстані, порогом подібності 0,35 і циклом, що переіндексовує кожні 30 хвилин лише те, що змінилося. Що працює з векторами у постачальника: база знань голосових агентів, синхронізована до рушія пошуку постачальника з фрагментами по 500 і перекриттям 100. Що працює в продукції **без** векторів: база знань платформи текстових асистентів — витяг, поділ, налаштування для кожного асистента і гібридний пошук написані й працюють, але шлях індексації записує колонку векторів у `null`, тож гібридний бал деградує до лексичного, а код сам це називає міткою `keyword-only`. Що написано й досі не ввімкнено в продукції: власний pipeline на `pgvector` для голосової платформи, чия міграція в заголовку має примітку «LOCAL ONLY — do not push to prod». Послугу продають із цією картою на столі, не без неї.

Асистент, який говорить з клієнтами, нічого не «знає». На кожне запитання хтось заздалегідь шукає кілька фрагментів тексту і кладе їх перед ним, а він формулює відповідь із них. Якість відповіді вирішується майже повністю на цьому етапі пошуку, а не на виборі моделі. Тому ця послуга — про корпус і про відновлення, а не про модель.

Робота має чотири частини, які ламаються незалежно одна від одної. Витяг: з яких файлів і яких сторінок ми беремо придатний текст, і що робимо, коли не можемо. Поділ: якими великими є фрагменти, наскільки вони перекриваються, де рветься речення. Відновлення: як обираються фрагменти для конкретного запитання, скільки їх, і за яким критерієм. Оновлення: що відбувається, коли вихідний документ змінюється. Система може бути бездоганною в трьох із них і непридатною через четверту.

Є ще один етап, який багато хто пропускає і який повністю змінює результат: шлюз затвердження. Корпус, що росте сам із розмов, швидко починає містити відповіді, які ніхто не верифікував. Цикл, який ми побудували, працює навпаки: коли агент каже «у мене немає цієї інформації», запит реєструється зі станом «в очікуванні», людина отримує сповіщення і відповідає, а її відповідь переходить у стан «затверджено» і лише тоді потрапляє в корпус. Знання зростає з реальних прогалин, з людиною в ланцюгу.

І непопулярна частина: семантичний пошук не завжди є правильним вибором. Для власної публічної платформи ми свідомо від нього відмовилися. Корпус — це власний текст платформи, поділений на короткі пасажі, які кожен несуть сторінку, з якої походять, і позначені перекриттям слів. Без векторів, без векторного сховища, без додаткового мережевого запиту — і, що важливіше, без жодного джерела поза сховищем. Асистент може лише повторити те, що вже написано на сайті, а будь-яка відповідь перевіряється на сторінці, написаній людиною. Це і є весь механізм проти вигадування.

Що входить

Робота, за складовими

Витягуємо текст і чітко кажемо, коли не можемо

Із простого тексту, markdown, журналів, CSV, JSON, DOCX і PDF. DOCX розпаковується як архів, і читається XML-документ усередині; PDF проходить через стандартний екстрактор зі збереженням розмітки сторінки, а якщо його немає, є резервний варіант, який читає текстові оператори прямо з файла. Важливий випадок — коли не працює: сканований 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

Ми ставимо ворота схвалення та цикл зростання

Реєстрація запитань без відповіді, сповіщення особи, яка затверджує, шлях відповіді, перенесення в корпус і зв’язування дублікатів. Ми здаємо робочу петлю та письмову інструкцію для того, хто затверджує — зокрема, що не копіюється із запитання клієнта.

05

Передаємо процедуру оновлення, з іменем того, хто її виконує

Яке джерело оновлюється, як часто, хто натискає і як видно, що це сталося. Тут ми прямо кажемо про межу: на платформах, які ми обслуговуємо, повторне індексування — це запитувана дія, а не та, що запускається автоматично за розкладом. Процедура без імені людини навпроти неї — це процедура, яка не виконується.

DocumenteConversațiiДжерелаSintezăAcțiuneEchipăКонтекстуалізуєАвторизовані джерела. Перевірені дії.
Шлях запиту крізь систему: затверджені джерела потрапляють у витяг і поділ, фрагменти йдуть в індекс; на запит ранжуються кандидати — векторно та лексично — і лише кілька фрагментів потрапляють у контекст; а коли нічого не знаходиться, запит виходить на гілку людського затвердження і повертається в корпус.

Дані

Чого торкаємося, де воно лежить і скільки лишається

Запитання, які ставить кожен, у кого є відповідальний за захист даних, — поставлені тут раніше, ніж їх поставить він.

Що потрапляє в корпус
Лише те, що явно схвалено: завантажені документи, обрані сторінки, пари запитання-відповідь, тексти, написані від руки. Джерела типізовані — текст, документ, сайт, пари запитання-відповідь — і мають заявлену мету: продукти, документація, поширені запитання або інше. Кожне джерело має власний перемикач, а перемикач перевіряється під час кожного запиту на пошук, а не лише в інтерфейсі — вимкнене джерело не може потрапити в контекст навіть випадково.
Де зберігаються фрагменти та їхні представлення
У базі даних відповідної платформи, поруч з рештою даних клієнта, а не в окремому зовнішньому сервісі пошуку. Для встановлень на PostgreSQL — у власних таблицях із явно активованим векторним розширенням; для платформи текстових асистентів — у MySQL, з представленням у стовпці JSON і обчисленням відстані в додатку. Другий варіант працює у малому масштабі й не масштабується до сотень тисяч фрагментів — це архітектурне обмеження, сказане як є.
Персональні дані в розмовах
Корпус не повинен містити персональних даних, але петля навчання може їх занести, бо запитання клієнта — це текст, написаний клієнтом. Тому потрапляння в корпус проходить через схвалення людини: там відбувається фільтрація. Операційне правило, яке з цим іде: той, хто схвалює відповіді, отримує явну інструкцію не копіювати в корпус імена, номери телефонів або деталі замовлення з оригінального запитання.
Що зберігається щодо запитів
Існує таблиця журналу запитів на пошук, зі здобутими оцінками, часом пошуку та кількістю повернених фрагментів. Це інструмент, з яким відповідають на «чому він це сказав» — без нього налаштування порогів перетворюється на вгадування. Журнал зберігається стільки, скільки він корисний для налаштування, і входить до строків зберігання платформи, а не до окремого режиму.
Чого не існує і що прямо зазначено як таке
Не застосовується строк придатності до документів: поле оголошене в схемі, але ніхто його не читає, тож документ сам не зникає з корпусу. Немає автоматичного повторного індексування, запланованого на платформах клієнта — оновлення запитується явно, по джерелу. Немає переупорядкування з окремою моделлю і немає лексичного оцінювання типу BM25; те, що ми називаємо «гібридним», — це зважена сума між зіставленням слів і векторною відстанню, і саме таке формулювання є правильним.

Випадок

База знань, що виросла з запитань, на які агент не вмів відповісти

Ситуація

Мережа ресторанів із доставкою мала голосового агента на телефоні та асистента на вебсторінці. Початковий корпус було завантажено один раз, під час запуску. Проблема була не в тому, що агент помилявся, а в тому, що він часто казав «у мене немає цієї інформації» — на цілком розумні запитання, яких ніхто не передбачив, коли писали корпус. Кожна така відповідь була втраченою розмовою, а список цих розмов ніде не існував.

Що ми збудували

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

Що вийшло

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

Чого цей випадок не каже

Петля працює рівно настільки, наскільки працює людина в ній. Якщо ніхто не відповідає на сповіщення, записи «в очікуванні» накопичуються, і агент і далі не знає — механізм робить відсутність видимою, а не розв’язує її сам. Друга межа, така ж важлива: реалізацію було налаштовано під потік цього клієнта, з таблицею, сповіщенням і ін’єкцією, написаними для нього. Як загальна функція, доступна з інтерфейсу для будь-якого облікового запису, її ще немає.

Питання

Про що нас питають, перш ніж зателефонувати

Чому асистент відповідає неправильно, якщо інформація є в завантажених документах?

Майже завжди тому, що правильний фрагмент не було знайдено, а не тому, що модель була б «дурною». Типові причини, у порядку, в якому ми їх перевіряємо: занадто великі фрагменти, у яких корисна інформація губиться серед іншого; обрізання посередині речення, без перекриття; надто мало фрагментів, перенесених у контекст; або корпус, у якому та сама інформація є у трьох різних версіях, і стара набирає кращий бал. Журнал запитів із отриманими оцінками показує, який саме це випадок — без нього лише здогадуються.

Ви справді використовуєте семантичний пошук всюди?

Ні, і чесніше сказати точно де. З власними векторами: пам’ять внутрішньої дошки, що використовується щодня, у векторному стовпці з індексом на косинусну відстань і порогом подібності 0,35. З векторами у постачальника: база знань голосових агентів, синхронізована до механізму пошуку постачальника. **Без** векторів, сьогодні, у продакшені: база знань текстових асистентів платформи — шлях індексації записує порожній векторний стовпець, тож пошук працює лексично, а код сам позначає свій режим як `keyword-only`. Є і повністю написаний власний pipeline, з міграцією, позначеною «не публікувати в продакшен» до підтвердження прав на векторне розширення. Це реальна карта.

Що відбувається, коли ми змінюємо вже індексований документ?

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

Він оновлюється сам, коли змінюється сайт або каталог?

Не автоматично, на платформах, які ми нині обслуговуємо. Переіндексація запитується явно, для джерела, через окрему дію. Є один виняток, у нашій внутрішній системі, де цикл запускається кожні тридцять хвилин і переіндексовує лише те, що змінилося. Автоматичне планування можна побудувати, і це окремий етап; чого ми не робимо, так це не створюємо враження, що воно вже існує. Є і поле строку дії, оголошене в схемі, яке ніхто не читає — тож документ сам не виходить із корпусу.

У чому різниця між підключеним каталогом і завантаженим списком продуктів?

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

Ви можете індексувати скановані PDF?

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

Працює румунською та російською?

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

Коли НЕ варто використовувати семантичний пошук?

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

Ви тримаєте нас заручниками у своїй платформі?

Ваші джерела залишаються в оригінальній формі; фрагменти й представлення зберігаються в базі даних вашого встановлення, а не в зовнішній службі пошуку, до якої доступ маємо лише ми. Модель представлення запитується через власний шлюз моделей, тож зміна постачальника не зачіпає код бази знань. Векторні представлення, однак, пов’язані з моделлю, що їх створила: зміна моделі вимагає повного переіндексування всього корпусу. Це операція, яку слід прорахувати, а не безкоштовна, і краще знати це на початку, ніж дізнатися посередині.

На чому ґрунтуються твердження вище (29 джерел)

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

Що вам хотілося б налагодити?

Розкажіть про свій процес. Разом визначимо, що варто збудувати, що можна підключити і як перевіримо результат.

Поговорімо