Головна / Статті / Вибір та налаштування моделей ембеддингу для систем RAG у промислових умовах

Вибір та налаштування моделей ембеддингу для систем RAG у промислових умовах

Дізнайтеся, як моделі ембеддингу перетворюють текст на вектори, які можна шукати, чому словниковий запас конкретної галузі псує семантичний пошук, та як вибирати, стискувати та налаштовувати моделі для використання у системах RAG.

6521 слів

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

Проте жоден з цих елементів поки що не може бути знайдений за допомогою векторного індексу.

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

Саме ця залежність є предметом розгляду в цій частині серії.

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

Що насправді обчислює модель ембеддингу

По суті, модель ембеддингу приймає послідовність токенів та генерує один щільний вектор, який зазвичай має від 384 до 3072 вимірів залежно від конкретної моделі. Цей вектор призначений для стиснутого кодування значення вхідних даних.

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

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

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

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

Токенізація та вікно контексту

Перш ніж модель ембеддингу щось обчислить, вона спочатку розбиває вхідний текст на токени за допомогою власного внутрішнього словника. Кількість токенів не завжди прямо відповідає кількості слів чи символів. Частина тексту з 500 токенів англійською мовою може відповідати приблизно 350–400 словам, тоді як така ж кількість токенів у німецькій мові — де слова часто є складеними — може означати меншу кількість окремих ідей.

Кожна модель ембеддингу встановлює максимальну довжину контексту, і все, що перевищує цей ліміт, або обрізається, або потребує особливого оброблення. Моделі типу Sentence-Transformers зазвичай мають ліміт у межах від 256 до 512 токенів. Модель OpenAI text-embedding-3-large обробляє до 8,191 токенів. BGE-M3 підтримує до 8,192 токенів. Моделі ембеддингів Jina v3 також підтримують до 8,192 токенів.

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

Семантичний простір та моменти його порушення

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

Ця структура працює ефективно, доки запити та документи використовують ту саму термінологію, стиль написання та концептуальну основу, що й матеріал, на якому навчався модель. Однак у корпоративних системах RAG вона зазвичай зазнає невдач у кількох передбачуваних ситуаціях:

  • Специфічна для галузі термінологія призводить до проблем, які легко залишити непоміченими. Аналітик, який запитує про „порогове значення для подання SAR щодо структурування“, може не отримати результатів пошуку, якщо у фрагменті політики використана формулювання „Критерії подання звіту про підозрілу діяльність для структурування транзакцій“, оскільки модель ніколи не навчилася розуміти, що ці дві формулювання означають одне й те саме.
  • Скорочення не діють послідовно. Терміни на кшталт "GCC" (Комітет з кредитування груп), "EDD" (Поглиблена перевірка) та "RFI" (Запит на інформацію) можуть бути інтегровані універсальною моделлю відповідно до їхніх більш поширених значень в інших контекстах — Рада співпраці країн Перської затоки, Доставка електронних документів, Перешкоди від радіочастот.
  • Регуляторні ідентифікатори не мають вбудованого значення для універсальних моделей. Код на кшталт CRD-EU-047, оброблений універсальною моделлю інтеграції, розглядається як довільний рядок. Модель, навчена спеціально на регуляторних текстах, натомість розмістила б його поруч з іншими ідентифікаторами кредитних регуляцій ЄС, які належать до тієї самої концептуальної групи.
  • Числові пороги обробляються загальними моделями лише частково ефективно. Фраза "10 мільйонів євро" сама по собі вставляється поруч з іншими грошовими показниками. Коли вона з’являється разом із текстом про повноваження на схвалення, модель, налаштована спеціально під цю сферу, може виявити зв’язок між цією конкретною сумою та контролем управління, який вона запускає — що загальна модель менш схильна правильно інтерпретувати.
  • Точне розуміння того, де саме загальні моделі ембеддингу починають працювати неефективно, має таке ж значення, як і знання того, яка модель очолює списки лідерів у публічних тестах.

    Вибір моделі ембеддингу у 2025 році

    Сфера моделей ембеддингу значно скоротилась. Наведене нижче порівняння охоплює моделі, які є найважливішими для корпоративних систем RAG у банківській та фінансовій сферах станом на середину 2025 року.

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

    Асиметричне пошукове забезпечення та інформування моделі про тип вхідних даних

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

    Деякі моделі розроблені для прямого виявлення цієї асиметрії. Моделі E5 додають до вхідного тексту префікс „query:“ або „passage:“, щоб модель знала, яку роль вона має виконувати. Embed v3 від Cohere забезпечує це через параметр input_type, де допустимі значення включають „search_query“, „search_document“, „classification“ та „clustering“.

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

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

    import cohere
    from typing import List
    
    co = cohere.Client(api_key="your_api_key")
    
    def embed_documents(chunks: List[str]) -> List[List[float]]:
        """Embed document chunks for indexing with explicit document input type."""
        response = co.embed(
            texts=chunks,
            model="embed-english-v3.0",
            input_type="search_document",
            embedding_types=["float"]
        )
        return response.embeddings.float
    
    def embed_query(query: str) -> List[float]:
        """Embed a search query with explicit query input type."""
        response = co.embed(
            texts=[query],
            model="embed-english-v3.0",
            input_type="search_query",
            embedding_types=["float"]
        )
        return response.embeddings.float[0]
    

    Розріджені вектори: де збіг ключових слів кращий за семантичний пошук

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

    BM25 як надійна база

    BM25 залишається стандартним підходом до пошуку на основі ключових слів. Він оцінює релевантність за кількістю зустрічей терміна в документі, рідкістю цього терміна в усьому корпусі даних та фактором нормалізації, який враховує довжину документа. Не потрібно нічого тренувати, немає вимог до GPU, і не потрібні виклики API для ембеддингів.

    Розгляньмо запит на кшталт "CRD-EU-047 approval authority threshold." BM25 буде високо ранжувати будь-який фрагмент, який містить ці точні терміни. Натомість щільна модель може не показувати такий фрагмент, якщо її корпус навчання випадково не створив міцного зв’язку між цим конкретним кодом політики та поняттям органу, який надає схвалення.

    from rank_bm25 import BM25Okapi
    import re
    from typing import List, Tuple
    
    def tokenise(text: str) -> List[str]:
        """Simple whitespace and punctuation tokeniser for BM25."""
        return re.findall(r'\b\w+\b', text.lower())
    
    class BM25Index:
        def __init__(self, documents: List[str]):
            self.documents = documents
            tokenised = [tokenise(doc) for doc in documents]
            self.bm25 = BM25Okapi(tokenised)
    
        def search(self, query: str, top_k: int = 10) -> List[Tuple[int, float]]:
            """Return (doc_index, score) pairs for the top_k results."""
            tokens = tokenise(query)
            scores = self.bm25.get_scores(tokens)
            ranked = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)
            return ranked[:top_k]
    

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

    У корпусах, побудованих на стабільній, чітко визначеній мові правил, сам BM25 часто досягає рівня точності пошуку, характерного для щільного пошуку, під час вузькоспрямованих пошуків фактів, при цьому використовуючи значно менше ресурсів. Його основною слабкістю є проблема синонімів: запит з фразою „Вимоги EDD“ не знайде уривок, у якому написано лише „Вимоги до посиленого аналізу“, якщо точні фрази не збігаються повністю.

    SPLADE: Розріджені вектори, які навчаються розширенню словникового запасу

    SPLADE (Sparse Lexical and Expansion Model) займає проміжне положення між простим збігом ключових слів та повним щільним пошуком. Під час індексації використовується замаскована мовна модель для доповнення як документів, так і запитів семантично пов’язаним словниковим запасом, який не обов’язково присутній у первинному формулюванні. Результатом є розріджений вектор, чиї виміри відповідають окремим елементам словникового запасу, з вагами, які залежать від того, наскільки важливим є цей елемент для вхідних даних.

    Отже, уривок, закодований за технологією SPLADE та присвятований вимогам EDD, може надавати більшої ваги таким термінам, як „ретельна перевірка клієнта“, „оцінка ризиків“ та „перевірка особи“, навіть якщо жоден із цих точних виразів не зустрічається у вихідному тексті. Таке розширення покращує результати пошуку для запитів, що використовують синоніми, водночас зберігаючи ефективність та зрозумілість, характерні для репрезентацій із розрідженим інвертованим індексом.

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

    Matryoshka Embeddings: регульований розмір вектора для контролю витрат

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

    Ця техніка отримала свою назву від російських матрьошок: 1536-вимірний вектор Матрьошки містить повнофункціональне 256-вимірне представлення у своїх перших 256 положеннях, функціональне 512-вимірне представлення у перших 512 положеннях та так далі.

    Сімейство моделей text-embedding-3 від OpenAI підтримує це безпосередньо за допомогою параметра dimensions.

    from openai import OpenAI
    from typing import List
    
    client = OpenAI()
    
    def embed_with_matryoshka(
        texts: List[str],
        dimensions: int = 256,
        model: str = "text-embedding-3-large"
    ) -> List[List[float]]:
        """
        Embed texts at a specified sub-dimension.
        Lower dimensions reduce storage and index cost.
        Measure retrieval quality drop before committing to a dimension.
        """
        response = client.embeddings.create(
            input=texts,
            model=model,
            dimensions=dimensions
        )
        return [item.embedding for item in response.data]
    

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

    Справжня перевага у продакшені полягає у можливості коригувати баланс між простором зберігання та якістю за потреби, без необхідності перенавчання моделі чи створення індексу з нуля. Для корпусу документів банківської політики можна протестувати ефективність пошуку при 256, 512, 1024 та 3072 вимірах та виявити, що 512 вимірів забезпечують 97% повного рекалу, використовуючи лише 17% простору зберігання, необхідного для повних векторів.

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

    Стиснення ембеддингів без значних втрат точності

    Стандартні ембеддинги зберігають кожен вимір у форматі 32-бітного числа з плаваючою комою. Якщо масштабувати це до мільйона фрагментів документів по 1536 вимірах кожен, то без урахування додаткових витрат на індексацію отримуємо приблизно 6 ГБ необроблених векторів. У корпоративних масштабах такий обсяг зберігання та пов’язані з ним витрати на пам’ять вже не можна ігнорувати через похибки округлення.

    Квантування вирішує цю проблему шляхом зменшення кількості бітів, які використовуються для представлення кожного виміру. На практиці переважають три методи: скалярне квантування (перетворення float32 у int8), бінарне квантування (перетворення float32 у один біт) та квантування продуктом (стиснення кожного вектора у коротший код).

    Скалярне квантування: int8

    Скалярна квантація переносить неперервний діапазон float32 у 256 дискретних цілих значень. Кожен вимір скорочується з 4 байтів до 1, що дозволяє зменшити обсяг зберігання на 75%. Оскільки моделі ембеддингу високого розміру розподіляють інформацію рівномірно по багатьох вимірах, жоден окремий вимір сам по собі не має значного впливу, тому точність, втрачена через це округлення, зазвичай є незначною.

    import numpy as np
    from typing import Tuple
    
    def quantise_to_int8(
        embeddings: np.ndarray
    ) -> Tuple[np.ndarray, float, float]:
        """
        Scalar quantisation to int8.
        Returns quantised array plus the scale and zero_point needed for dequantisation.
        """
        min_val = embeddings.min()
        max_val = embeddings.max()
        scale = (max_val - min_val) / 255.0
        zero_point = -round(min_val / scale)
        quantised = np.clip(
            np.round(embeddings / scale) + zero_point,
            0, 255
        ).astype(np.uint8)
        return quantised, scale, zero_point
    
    def dequantise_from_int8(
        quantised: np.ndarray,
        scale: float,
        zero_point: float
    ) -> np.ndarray:
        """Reconstruct approximate float32 embeddings from int8."""
        return ((quantised.astype(np.float32) - zero_point) * scale)
    

    Бінарна квантація

    Бінарна квантація йде ще далі, зводячи кожен вимір до одного біта, який просто фіксує, чи було початкове значення типу float додатнім чи від’ємним. Це дозволяє зменшити обсяг зберігання приблизно на 97% порівняно з float32. Оскільки представлення більше не є неперервним, схожість вимірюється за допомогою відстані Хеммінга замість косинусної схожості.

    Ця техніка найкраще працює з моделями, чиї розподіли вихідних даних природним чином добре центровані, тож для будь-якого вхідного даних приблизно половина вимірів знаходиться по обидва боки від нуля. Якщо виміри моделі є засмученими, а не збалансованими, бінарна квантація спричиняє значно більшу втрату якості. Cohere створила Embed v3, враховуючи цю обмеження, і опублікована Anthropic оцінка цієї моделі показує зниження якості пошуку менше ніж на 1% разом із скороченням обсягу зберігання на 97% у їхніх тестових наборах. Вважайте цю цифру початковою точкою, а не гарантією, та перевірте її на власному корпусі даних перед тим, як покладатися на неї.

    import numpy as np
    
    def quantise_to_binary(embeddings: np.ndarray) -> np.ndarray:
        """
        Binary quantisation: positive dimensions become 1, negative become 0.
        Packs 8 dimensions per byte using numpy packbits.
        """
        binary_matrix = (embeddings > 0).astype(np.uint8)
        return np.packbits(binary_matrix, axis=1)
    
    def hamming_similarity(
        query_binary: np.ndarray,
        corpus_binary: np.ndarray
    ) -> np.ndarray:
        """Compute normalised Hamming similarity for binary embeddings."""
        n_bits = corpus_binary.shape[1] * 8
        xor = np.bitwise_xor(
            query_binary,
            corpus_binary
        )
        hamming_distances = np.unpackbits(xor, axis=1).sum(axis=1)
        return 1.0 - (hamming_distances / n_bits)
    

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

    Доопрацювання для RAG, специфічного для домену

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

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

    Коли загальні ембеддинги не спрацьовують

    Особливо в банківських системах типу RAG кілька постійно повторюваних моделей невдач виправдовують інвестиції у доопрацювання:

    Скорочення, специфічні для певної галузі, можуть бути неправильно інтерпретовані. Модель загального призначення може пов’язувати „NPA“ з Асоціацією національних парків замість поняття „непродуктивних активів“, а також може лише слабко пов’язувати „KYC“ з концепціями комплаєнсу та процедур налагодження співпраці, які насправді є основними у банківських запитах.

    Зв’язки між пов’язаними концепціями в різних документах залишаються непоміченими. Пошук фрази „положення щодо реструктуризації фінансових засобів“ мав би виявити текст політики про „рамки модифікації кредитів“, але модель, навчена переважно загальним веб-контентом, може ніколи не бачити цих формулювань у достатній мірі близько одне до одного, щоб створити такий зв’язок.

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

    Числові пороги втрачають свій контекст управління. Такий показник, як „10 мільйонів євро“, розміщений окремо, не повинен автоматично відповідати запиту про „орган, який надає дозвіл на великі ризики“ — такий зв’язок виникає лише тоді, коли модель була навчена даними конкретної галузі, які пов’язують це число з його регуляторним значенням.

    Створення пар для навчання з даних специфічної галузі

    Доопрацювання моделей sentence-transformers за допомогою контрасттивної мети залежить від позитивних пар: прикладів, які поєднують запит і уривок тексту, який модель має навчитися розглядати як пов’язаний. Негативні приклади можна обирати вручну або автоматично отримувати з навколишнього корпусу даних.

    У контексті банківських систем RAG ці позитивні пари можна створювати з кількох практичних джерел:

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

    Природна структура документів з правилами, де заголовок у поєднанні з розділком нижче утворює готовий позитивний приклад.

    Запити аналітиків, зафіксовані в журналі, у поєднанні з уривками тексту, які були фактично отримані під час правильної відповіді.

    Запити, створені машиною за допомогою LLM для кожного фрагмента тексту, де сам цей фрагмент використовується як відповідний позитивний уривок.

    Серед цих методів створення синтетичних запитів зазвичай є найбільш придатним підходом, коли вже існує мало позначених даних.

    from openai import OpenAI
    import json
    from typing import List, Dict
    
    client = OpenAI()
    
    def generate_training_queries(
        chunk: str,
        chunk_metadata: Dict,
        n_queries: int = 3
    ) -> List[Dict]:
        """
        Generate synthetic query-passage pairs for fine-tuning.
        The chunk itself is the positive passage for each generated query.
        """
        prompt = f"""You are generating training data for a banking RAG system.
    Given the following policy passage, generate {n_queries} realistic questions
    that a credit analyst, compliance officer, or relationship manager might ask
    that this passage directly answers. Each question should use natural language
    and may use different terminology than the passage itself.
    
    Passage:
    {chunk}
    
    Return a JSON array of objects with keys "query" and "difficulty".
    Difficulty should be "narrow" (single fact) or "synthesis" (multiple facts).
    Return only the JSON array, no other text."""
    
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}
        )
    
        try:
            result = json.loads(response.choices[0].message.content)
            queries = result.get("queries", result) if isinstance(result, dict) else result
            return [
                {
                    "query": q["query"],
                    "passage": chunk,
                    "document_id": chunk_metadata.get("document_id"),
                    "chunk_id": chunk_metadata.get("chunk_id"),
                    "difficulty": q.get("difficulty", "narrow")
                }
                for q in queries
            ]
        except (json.JSONDecodeError, KeyError):
            return []
    

    Контрастне навчання з втратою у стилі трійок

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

    from sentence_transformers import SentenceTransformer, InputExample
    from sentence_transformers.losses import MultipleNegativesRankingLoss
    from torch.utils.data import DataLoader
    from typing import List, Dict
    import logging
    
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    def build_training_examples(
        pairs: List[Dict]
    ) -> List[InputExample]:
        """
        Convert query-passage pairs into InputExample objects.
        MultipleNegativesRankingLoss expects (anchor, positive) pairs.
        Negatives are sampled automatically from other items in the batch.
        """
        return [
            InputExample(texts=[pair["query"], pair["passage"]])
            for pair in pairs
            if pair.get("query") and pair.get("passage")
        ]
    
    def fine_tune_embedding_model(
        base_model_name: str,
        training_pairs: List[Dict],
        output_path: str,
        epochs: int = 3,
        batch_size: int = 16,
        warmup_steps: int = 100
    ) -> SentenceTransformer:
        """
        Fine-tune a sentence-transformers model on domain-specific query-passage pairs.
    
        base_model_name: HuggingFace model identifier or local path.
        training_pairs: List of dicts with "query" and "passage" keys.
        output_path: Directory to save the fine-tuned model.
        """
        model = SentenceTransformer(base_model_name)
        logger.info(f"Loaded base model: {base_model_name}")
        logger.info(f"Training on {len(training_pairs)} query-passage pairs")
    
        examples = build_training_examples(training_pairs)
        loader = DataLoader(examples, shuffle=True, batch_size=batch_size)
        loss = MultipleNegativesRankingLoss(model)
    
        total_steps = len(loader) * epochs
        logger.info(f"Training for {epochs} epochs, {total_steps} total steps")
    
        model.fit(
            train_objectives=[(loader, loss)],
            epochs=epochs,
            warmup_steps=warmup_steps,
            output_path=output_path,
            show_progress_bar=True,
            checkpoint_path=output_path,
            checkpoint_save_steps=len(loader)
        )
    
        logger.info(f"Fine-tuned model saved to: {output_path}")
        return model
    

    Результати тестування: банківський корпус до та після деталізованої налаштування

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

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

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

    Аспекти продуктивності для масштабного батч-ембеддингу

    Передавання 100 000 фрагментів через API ембеддингу по одному запиту є повільним та дорогим. У продакшн-системах ембеддингу вхідні дані обробляються пакетно, що дозволяє підвищити продуктивність, дотримуватися лімітів швидкості, ефективно відновлюватися після збоїв та забезпечувати детерміноване генерацію результатів.

    Батчування через API-провайдерів

    Ендпоїнт embedding в OpenAI дозволяє обробляти до 2 048 вхідних даних за один запит. Ендпоїнт Embed у Cohere обмежується 96 текстами на запит, якщо тільки ви не використовуєте їхній спеціалізований API для пакетної обробки більшого обсягу даних. Виконання інференсу локально за допомогою sentence-transformers забезпечує можливість налаштування розміру пакетів, який обмежується лише доступною пам’яттю GPU.

    import time
    import logging
    from typing import List, Optional
    from openai import OpenAI, RateLimitError, APIError
    
    logger = logging.getLogger(__name__)
    client = OpenAI()
    
    def embed_in_batches(
        texts: List[str],
        model: str = "text-embedding-3-large",
        batch_size: int = 512,
        max_retries: int = 3,
        retry_delay: float = 2.0,
        dimensions: Optional[int] = None
    ) -> List[List[float]]:
        """
        Embed a large list of texts using batched API calls with retry logic.
    
        texts: Pre-chunked text strings. Caller is responsible for ensuring
               no text exceeds the model's token limit.
        batch_size: Number of texts per API call. Stay well below the API limit
                    to avoid hitting per-request token limits.
        dimensions: Optional Matryoshka dimension reduction for supported models.
        """
        all_embeddings: List[List[float]] = []
        total_batches = (len(texts) + batch_size - 1) // batch_size
    
        for batch_idx in range(0, len(texts), batch_size):
            batch = texts[batch_idx: batch_idx + batch_size]
            current_batch = batch_idx // batch_size + 1
            logger.info(f"Embedding batch {current_batch}/{total_batches} "
                        f"({len(batch)} texts)")
    
            kwargs = {
                "input": batch,
                "model": model
            }
            if dimensions is not None:
                kwargs["dimensions"] = dimensions
    
            attempt = 0
            while attempt < max_retries:
                try:
                    response = client.embeddings.create(**kwargs)
                    # Preserve input order: API returns items sorted by index
                    sorted_items = sorted(response.data, key=lambda x: x.index)
                    all_embeddings.extend([item.embedding for item in sorted_items])
                    break
    
                except RateLimitError:
                    attempt += 1
                    wait = retry_delay * (2 ** attempt)
                    logger.warning(f"Rate limit hit on batch {current_batch}. "
                                   f"Waiting {wait:.1f}s before retry {attempt}/{max_retries}")
                    time.sleep(wait)
    
                except APIError as e:
                    attempt += 1
                    logger.error(f"API error on batch {current_batch}: {e}. "
                                 f"Retry {attempt}/{max_retries}")
                    if attempt >= max_retries:
                        raise
                    time.sleep(retry_delay)
    
        logger.info(f"Embedding complete. Total vectors: {len(all_embeddings)}")
        return all_embeddings
    

    Виконання інференсу локально за допомогою sentence-transformers

    Деякі організації стикаються з обмеженнями щодо розташування даних, які не дозволяють надсилати документи з правилами до зовнішнього API. У таких випадках локальний інференс з використанням sentence-transformers є оптимальним рішенням.

    from sentence_transformers import SentenceTransformer
    import numpy as np
    from typing import List, Optional
    import logging
    
    logger = logging.getLogger(__name__)
    
    class LocalEmbeddingPipeline:
        """
        Production-ready local embedding pipeline using sentence-transformers.
        Suitable for data-residency-constrained banking environments.
        """
    
        def __init__(
            self,
            model_name_or_path: str,
            device: str = "cpu",
            batch_size: int = 64,
            normalise: bool = True
        ):
            self.model = SentenceTransformer(model_name_or_path, device=device)
            self.batch_size = batch_size
            self.normalise = normalise
            self.device = device
            logger.info(f"Loaded model: {model_name_or_path} on {device}")
    
        def embed(
            self,
            texts: List[str],
            show_progress: bool = True
        ) -> np.ndarray:
            """
            Embed a list of texts. Returns an (N, D) numpy array.
            Normalises to unit length if normalise=True (required for cosine similarity).
            """
            embeddings = self.model.encode(
                texts,
                batch_size=self.batch_size,
                show_progress_bar=show_progress,
                normalize_embeddings=self.normalise,
                convert_to_numpy=True
            )
            logger.info(f"Embedded {len(texts)} texts. "
                        f"Output shape: {embeddings.shape}")
            return embeddings
    
        def embed_query(self, query: str) -> np.ndarray:
            """Embed a single query. Returns a 1D array."""
            return self.embed([query], show_progress=False)[0]
    

    Перевірка довжини токенів перед вставкою

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

    from transformers import AutoTokenizer
    from typing import List, Tuple
    import logging
    
    logger = logging.getLogger(__name__)
    
    def validate_chunk_lengths(
        chunks: List[str],
        model_name: str,
        max_tokens: int,
        truncation_strategy: str = "warn"
    ) -> Tuple[List[str], List[int]]:
        """
        Validate that all chunks are within the model's token limit.
    
        truncation_strategy:
            "warn"  - Log a warning for oversized chunks and include them (will be truncated by model).
            "skip"  - Remove oversized chunks and return only valid ones.
            "raise" - Raise ValueError on the first oversized chunk.
    
        Returns (validated_chunks, oversized_indices).
        """
        tokeniser = AutoTokenizer.from_pretrained(model_name)
        oversized = []
    
        for idx, chunk in enumerate(chunks):
            token_count = len(tokeniser.encode(chunk, add_special_tokens=True))
            if token_count > max_tokens:
                oversized.append(idx)
                msg = (f"Chunk {idx} has {token_count} tokens, "
                       f"exceeds model limit of {max_tokens}. "
                       f"First 80 chars: {chunk[:80]!r}")
                if truncation_strategy == "raise":
                    raise ValueError(msg)
                else:
                    logger.warning(msg)
    
        if truncation_strategy == "skip" and oversized:
            valid = [c for i, c in enumerate(chunks) if i not in set(oversized)]
            logger.info(f"Removed {len(oversized)} oversized chunks. "
                        f"{len(valid)} chunks remain.")
            return valid, oversized
    
        return chunks, oversized
    

    Додавання метаданих та інформації про походження до ембеддингів

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

    Якщо повернутися до сценарію пропозиції кредиту у розмірі 12 мільйонів євро, кожен вбудований фрагмент має містити принаймні поля, показані тут:

    from dataclasses import dataclass, field
    from typing import Optional, List
    import uuid
    
    @dataclass
    class EmbeddedChunk:
        """
        Production embedding record for a banking policy RAG system.
        The vector enables retrieval. The metadata enables everything else.
        """
        # Vector
        vector: List[float]
        vector_dimensions: int
        embedding_model: str
        embedding_model_version: str
    
        # Content
        text: str
        content_type: str          # "narrative", "table_row", "proposition", "image_description"
    
        # Provenance
        document_id: str
        document_version: str      # e.g. "7.2"
        policy_id: Optional[str]   # e.g. "CRD-EU-047"
        jurisdiction: Optional[str] # e.g. "EU"
        effective_date: Optional[str]
    
        # Chunk structure
        chunk_id: str = field(default_factory=lambda: str(uuid.uuid4()))
        parent_id: Optional[str] = None
        section: Optional[str] = None
        page_number: Optional[int] = None
        source_artifact_path: Optional[str] = None  # path to original image/table
    
        # Access control
        classification: str = "INTERNAL"            # "PUBLIC", "INTERNAL", "CONFIDENTIAL"
        permitted_roles: List[str] = field(default_factory=list)
    
        # Indexing
        indexed_at: Optional[str] = None
        indexing_pipeline_version: Optional[str] = None
    

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

    Оцінка якості вбудовування

    Публічні топ-листи, такі як MTEB, повідомляють про загальні показники ефективності пошуку в широкому спектрі академічних наборів даних. Ці цифри допомагають відсіяти моделі, які явно показують слабкі результати. Однак вони не є корисними при виборі найкращої моделі для спеціалізованого корпусу, такого як внутрішня бібліотека правил банку.

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

    Створення набору для оцінки пошуку

    Набір тестових даних для пошуку, створений для процесу імбеддингу RAG у банківських системах, має охоплювати кілька типів запитів:

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

    Питання на основі порогових значень, які поєднують певну числову умову з приписом, пов’язаним із нею, наприклад, запит про те, яка організація затвердження потрібна, коли корпоративні активи ЄС перевищують 10 мільйонів євро.

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

    Синтетичні питання, які запозичують інформацію з кількох розділів одночасно, наприклад, запит про повний опис системи контролю проти відмивання грошей, яка регулює надання кредитів корпораціям високого ризику.

    Питання, що стосуються кількох документів, актуальні там, де політики посилаються одна на одну у різних документах.

    import numpy as np
    from typing import List, Dict, Set
    
    def recall_at_k(
        retrieved_ids: List[str],
        relevant_ids: Set[str],
        k: int
    ) -> float:
        """
        Compute Recall@k for a single query.
        relevant_ids is the ground truth set of chunk identifiers.
        retrieved_ids is the ordered list of retrieved chunk identifiers.
        """
        if not relevant_ids:
            return 0.0
        top_k_retrieved = set(retrieved_ids[:k])
        return len(top_k_retrieved & relevant_ids) / len(relevant_ids)
    
    def mean_reciprocal_rank(
        retrieved_ids: List[str],
        relevant_ids: Set[str]
    ) -> float:
        """Compute MRR for a single query."""
        for rank, chunk_id in enumerate(retrieved_ids, start=1):
            if chunk_id in relevant_ids:
                return 1.0 / rank
        return 0.0
    
    def evaluate_embedding_model(
        model_name: str,
        evaluation_queries: List[Dict],
        corpus_chunks: List[Dict],
        k_values: List[int] = [1, 5, 10, 20]
    ) -> Dict:
        """
        Evaluate an embedding model on a labelled retrieval dataset.
    
        evaluation_queries: List of dicts with "query" and "relevant_chunk_ids" keys.
        corpus_chunks: List of dicts with "chunk_id" and "text" keys.
        Returns per-query-type and aggregate retrieval metrics.
        """
        from sentence_transformers import SentenceTransformer
    
        model = SentenceTransformer(model_name)
    
        corpus_texts = [c["text"] for c in corpus_chunks]
        corpus_ids = [c["chunk_id"] for c in corpus_chunks]
        corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
    
        results_by_type: Dict[str, List] = {}
        all_recall: Dict[int, List[float]] = {k: [] for k in k_values}
        all_mrr: List[float] = []
    
        for query_item in evaluation_queries:
            query = query_item["query"]
            relevant = set(query_item["relevant_chunk_ids"])
            query_type = query_item.get("query_type", "unspecified")
    
            query_embedding = model.encode(query, normalize_embeddings=True)
            scores = corpus_embeddings @ query_embedding
            ranked_indices = np.argsort(scores)[::-1]
            retrieved = [corpus_ids[i] for i in ranked_indices]
    
            mrr = mean_reciprocal_rank(retrieved, relevant)
            all_mrr.append(mrr)
    
            for k in k_values:
                r = recall_at_k(retrieved, relevant, k)
                all_recall[k].append(r)
    
            if query_type not in results_by_type:
                results_by_type[query_type] = {"mrr": [], "recall": {k: [] for k in k_values}}
            results_by_type[query_type]["mrr"].append(mrr)
            for k in k_values:
                results_by_type[query_type]["recall"][k].append(
                    recall_at_k(retrieved, relevant, k)
                )
    
        aggregate = {
            "model": model_name,
            "n_queries": len(evaluation_queries),
            "mrr": float(np.mean(all_mrr)),
            "recall": {k: float(np.mean(all_recall[k])) for k in k_values}
        }
    
        per_type = {
            qt: {
                "mrr": float(np.mean(data["mrr"])),
                "recall": {k: float(np.mean(data["recall"][k])) for k in k_values},
                "n_queries": len(data["mrr"])
            }
            for qt, data in results_by_type.items()
        }
    
        return {"aggregate": aggregate, "by_query_type": per_type}
    

    Показники отримання інформації не слід оцінювати ізольовано, окремо від того, як вони впливають на якість кінцевої відповіді. Припустимо, показник Recall@5 покращується на три пункти, але частка майже ідентичних фрагментів, які отримуються, зростає на 30 відсотків — такий компроміс може насправді не допомогти ШІ, адже надання йому трьох майже однакових уривків замість одного корисного не додає реальних доказів.

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

    Інфраструктура у вигляді потоку ембеддингів

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

    Це частина інфраструктури, а не одноразовий скрипт. Процеси створення ембеддингів промислового рівня для регульованих банківських середовищ вимагають наступного:

    • Ідемпотентність. Якщо фрагмент перестворюється через зміну версій моделі, це має перезаписати існуючий запис, а не створити дублікат.
  • Відстеження версій. Кожного разу, коли змінюється модель ембеддингу, ви або створюєте індекс заново, або чітко розділяєте його за версіями моделей. Дозвіл векторам з різних моделей існувати разом у одному індексі призводить до отримання показників схожості, яким не можна довіряти.
  • Поширення правил контролю доступу. Якщо під час обробки даних блок було позначено як КОНФІДЕНЦІЙНИЙ, ця мітка повинна зберегтися після процесу ембеддингу та залишитися недоторканою в векторному індексі. Шар пошуку потім має дотримуватися цих правил.
  • Оперативне спостереження. Ви повинні фіксувати час затримки ембеддингу для кожної партії даних, кількість токенів, рівень помилок API та проблеми на рівні окремих блоків, додаючи до всього цього структуровані метадані. Безпосереднє зміна поведінки пошуку внаслідок обрізання даних — саме те, що має виявляти система моніторингу, а не залишатися непоміченим.
  • Відстеження витрат. Включення витрат через API швидко накопичується, коли ви працюєте у масштабі. Розбивка витрат за типом документа та за кожною операцією пайплайну дозволяє об’єктивно оцінити вибір моделі та процес зменшення розмірностей на основі реальних витрат, а не припущень.
  • Ніщо з цього не є опційним у продакшен-середовищі. Саме ці якості відрізняють пайплайн, який просто працює у демо-режимі, від такого, який можна експлуатувати, аудитувати та підтримувати протягом часу в регульованому середовищі.

    Повернення до пропозиції кредиту у розмірі 12 мільйонів євро

    Початкове запитання менеджера зі стосунків з клієнтами залишилося незмінним: які повноваження на схвалення потрібні для цієї угоди, та які контролі необхідно пройти перед її поданням?

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

    Коли надходить запит, векторний індекс знаходить рядок матриці схвалень, який охоплює операції з високим ризиком на суму понад 10 мільйонів євро, положення EDD та діаграму, що показує послідовність контролю — і повертає їх разом із метаданими, які підтверджують, що всі ці елементи належать до політики CRD-EU-047, версія 7.2, юрисдикція ЄС, чинна з 15 січня 2026 року.

    До шару генерації надходять докази, які є точними, повними та простежуваними.

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

    Перш ніж переходити до векторного індексування: чек-лист

    Перш ніж додавати вбудовані фрагменти до векторного індексу, переконайтеся у наступному:

    • Чи правильно встановлений параметр типу вхідних даних у моделі ембеддингу як для запитів на індексацію, так і для запитів? Така невідповідність поступово знижує точність пошуку.
    • Чи перевірялась довжина токенів кожного фрагмента перед створенням ембеддингу? Тихе обрізання змінює те, що насправді представляє текст у ембеддингу, при цьому в жодній частині процесу не виникає помилок.
    • Чи містить кожен запис вектора повну інформацію про походження — версію документа, ідентифікатор політики, юрисдикцію та дату набрання чинності?
    • Чи справді модель ембеддингу тестувалася на власному корпусі даних та зразках запитів, чи її обрали лише на основі результатів публічних тестів?
    • Якщо ви налаштовували модель, чи є у вас показники ефективності пошуку до та після налаштувань, виміряні на окремому наборі запитів?
  • Чи підтверджуються ваші рішення щодо квантування результатами власного набору для оцінки пошуку, а не припущеннями, запозиченими з опублікованих тестових систем?
  • Чи є цей процес ідемпотентним, усвідомлює версію та є прозорим, як це вимагає стандарт регульованої системи виробництва?
  • Якщо щось із цього відсутнє, векторний індекс не допоможе вам це виявити — він просто збереже все, що ви йому надасте. У базі даних не буде жодних попереджень щодо вектора, створеного з скороченої частини даних, запиту із неправильним типом вхідних даних чи частини, яка була включена за допомогою версії моделі, що не синхронізована з рештою індексу. Ці проблеми не проявляються одразу; вони знову з’являються пізніше у вигляді проблем з якістю пошуку, які ззовні виглядають як проблеми з LLM.

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

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

    Пов’язані матеріали

  • Малі спеціалізовані моделі тихо перевершують величезні LLM — Дізнайтеся, як модель логіки з 3 млрд параметрів перевершує модель з 120 млрд параметрів у формальному міркуванні на звичайному обладнанні, та чому відповідність завданню важливіша за простий розмір моделі.