Главная / Статьи / Выбор и настройка моделей эмбеддингов для систем RAG в производственных целях

Выбор и настройка моделей эмбеддингов для систем RAG в производственных целях

Узнайте, как модели эмбеддингов преобразуют текст в векторы для поиска, почему словарный запас конкретной области мешает семантическому поиску, и как выбирать, компрессировать и настраивать модели для использования в системах RAG.

6521 слов

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

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

Тем не менее, ничто из этого пока не может быть найдено с помощью векторного индекса.

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

Именно эта зависимость является темой этой части серии.

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

Что на самом деле вычисляет модель эмбеддингов

По сути, модель эмбеддингов принимает последовательность токенов и выделяет один плотный вектор, обычно с числом измерений от 384 до 3072 в зависимости от конкретной модели. Этот вектор предназначен для компрессированной передачи смысла входных данных.

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

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

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

Эта разница между формулировками повседневных запросов и формулировками в специализированных документах называется несоответствием словаря, и она является основной причиной проблем с качеством поиска в корпоративных системах RAG.

Токенизация и окно контекста

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

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

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

Семантическое пространство и моменты его нарушения

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

Такая структура работает хорошо, пока запросы и документы используют ту же терминологию, стиль изложения и концептуальную основу, что и материалы, на которых обучалась модель. Однако в корпоративных системах 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. Однако для корпусов данных финансовых услуг, где одна и та же концепция политики описывается по-разному в разных юрисдикциях и при различных редакциях документов, такое расширение может значительно увеличить охват результатов поиска.

    Матрёшкинные вкладыши: регулируемый размер вектора для контроля затрат

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

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

    Семейство моделей text-embedding-3 от OpenAI поддерживает этот подход непосредственно с помощью параметра размеров.

    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. Поскольку представление больше не является непрерывным, сходство измеряется с помощью расстояния Хэмминга вместо косинусного сходства.

    Этот метод наиболее эффективен для моделей, у которых распределения выходных значений естественным образом симметричны относительно нуля, так что для любого входного данных примерно половина измерений находится по обе стороны от нуля. Если измерения модели имеют склонность к асимметрии вместо сбалансированности, бинарная квантизация приводит к значительно большей потере качества. В разработке Embed v3 компании Cohere учитывалась именно эта ограниченность, и в опубликованной 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 эти позитивные пары можно собрать из нескольких практических источников:

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

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

    Записей запросов аналитиков вместе с фрагментами текста, которые действительно были получены при правильном ответе.

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

    Среди этих методов генерация синтетических запросов, как правило, является наиболее приемлемым подходом, когда уже имеется мало помеченных данных.

    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 запросов, разделённых на четыре категории: запросы на получение узкоспециализированных фактов, запросы с пороговыми условиями, запросы, требующие использования нескольких источников информации, и запросы на синтез данных.

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

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

    Аспекты пропускной способности при массовом использовании API для генерации эмбеддингов

    Обработка 100 000 фрагментов через API для генерации эмбеддингов по одному запросу за раз является медленной и дорогостоящей. В производственных решениях потоки обработки эмбеддингов объединяют входные данные в пакеты, что позволяет повысить пропускную способность, соблюдать лимиты скорости, быстро восстанавливаться после сбоев и обеспечивать детерминированную генерацию результатов.

    Объединение запросов через API поставщиков

    Эндпоинт генерации эмбеддингов 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 процентов — такой компромисс может на самом деле не помочь большой языковой модели, поскольку предоставление ей трех практически одинаковых текстов вместо одного полезного не добавляет реальных доказательств.

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

    Инфраструктура в виде конвейера эмбеддингов

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

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

    • Идемпотентность. Если блок перегенерируется из-за смены версии модели, это должно заменить существующую запись, а не создать дубликат.
  • Отслеживание версий. Каждый раз, когда меняется модель эмбеддинга, необходимо либо создавать индекс заново, либо четко разделять его по версиям модели. Позволение векторам из разных моделей сосуществовать в одном индексе приводит к получению показателей сходства, которым нельзя доверять.
  • Распространение правил контроля доступа. Если при загрузке блок данных была присвоена метка CONFIDENTIAL, эта метка должна сохраниться после процесса эмбеддинга и остаться нетронутой в векторном индексе. Слой поиска затем должен учитывать её.
  • Наблюдаемость. Необходимо вести логи о задержке эмбеддинга для каждой партии данных, количестве токенов, коэффициентах ошибок API и сбоях на уровне блоков, причем все это должно сопровождаться структурированными метаданными. Безвозвратные изменения в поведении поиска, происходящие незаметно, — именно то, что должна выявлять система мониторинга, а не оставаться незамеченным.
  • Отслеживание затрат. При масштабном использовании затраты, генерируемые через API, быстро накапливаются. Разделяйте расходы по типам документов и этапам обработки, чтобы иметь возможность оценивать выбор моделей и методы снижения размерности на основе реальных затрат, а не предположений.
  • Ничто из этого не является опциональным в режиме производства. Именно эти критерии отличают пайплайн, который работает только в демо-режиме, от такого пайплайна, который можно использовать, аудитировать и обслуживать в течение времени в регулируемой среде.

    Вернуться к предложению о кредите в размере 12 миллионов евро

    Исходный вопрос менеджера по работе с клиентами остался прежним: какой уровень разрешения требуется для одобрения этой сделки и какие меры контроля необходимо пройти перед её подачей?

    • В части 4 рассматривался способ проектирования единиц поиска, которые сохраняют эти ответы в их первоначальной форме: пункт политики EDD, строка матрицы утверждения, указывающая полномочия GCC, и диаграмма рабочего процесса, описывающая контрольные меры перед подачей.
    • В этой части мы преобразовали эти единицы поиска в векторы для поиска с использованием модели, отработанной на специфической банковской терминологии, доработанной для понимания связи регуляторных аббревиатур с контекстом их применения, а также сопровождаемой полными метаданными происхождения, позволяющими впоследствии подтверждать версию политики и юрисдикцию при поиске.

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

    На уровень генерации поступают точные, полные и отслеживаемые данные.

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

    Прежде чем перейти к векторному индексированию: чек-лист

    Прежде чем вводить векторный индекс фрагменты с встраиваемыми данными, убедитесь в следующем:

    • Правильно ли установлен параметр типа входных данных в модели эмбеддингов как для операций индексации, так и для запросов? Такое скрытое несоответствие снижает точность поиска.
    • Проверялась ли длина токенов каждого фрагмента перед его преобразованием в эмбеддинг? Безвозвратное сокращение меняет содержание текста, преобразованного в эмбеддинг, при этом нигде в процессе обработки не возникает ошибок.
    • Содержит ли каждая запись вектора полную информацию о происхождении — версию документа, идентификатор политики, юрисдикцию и дату вступления в силу?
    • Проходила ли модель эмбеддингов реальные тесты на собственном наборе данных и шаблонах запросов, а не выбиралась исключительно на основе рейтингов в публичных тестах?
    • Если вы настраивали модель под конкретные задачи, имеются ли у вас данные о показателях поиска до и после настройки, полученные на отдельном наборе запросов?
  • Основаны ли ваши решения по квантизации на результатах собственного набора для оценки поиска, а не на предположениях, заимствованных из опубликованных тестов?
  • Является ли весь процесс идемпотентным, учитывающим версии и доступным для отслеживания, как того требует стандарт регулируемой производственной системы?
  • Если что-то из этого отсутствует, векторный индекс не сможет этого выявить — он просто сохранит всё, что ему передадут. В базе данных не появится никаких сигналов о векторе, созданном из неполного фрагмента, запросе с неверным типом входных данных или фрагменте, включённом с использованием версии модели, несинхронизированной с остальной частью индекса. Такие пробелы не проявляются сразу; они вновь возникают позже в виде проблем с качеством поиска, которые снаружи выглядят как проблемы с самой большой языковой моделью.

    Ранее в этой серии, в части 4, было показано, как преобразовать чистые документы в единицы поиска, сохраняя при этом их структуру нетронутой. В данной части рассматривается, как превратить эти единицы поиска в векторы для поиска, сохраняя при этом информацию об их происхождении.

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

    Связанные материалы

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