Расширенная RAG 01 поиск от малого к большому

Расширенный поиск RAG 01 от малого к большому

Child-Parent RecursiveRetriever и извлечение окна предложения с помощью LlamaIndex

Системы RAG (Retrieval-Augmented Generation) извлекают соответствующую информацию из заданной базы знаний, позволяя при этом генерировать фактическую, контекстно согласованную и предметно-специфичную информацию. Однако RAG сталкивается с множеством проблем, когда дело доходит до эффективного извлечения соответствующей информации и генерации качественных ответов. В этой серии блоговых записей / видео я расскажу о передовых техниках RAG, направленных на оптимизацию рабочего процесса RAG и решение проблем в наивных системах RAG.

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

Существуют две основные техники:

  1. Меньшие дочерние фрагменты, относящиеся к большим родительским фрагментам: Сначала извлекаем меньшие фрагменты во время извлечения, затем ссылаемся на идентификаторы родительского фрагмента и возвращаем большие фрагменты.
  2. Извлечение окна предложения: Извлекаем одно предложение во время извлечения и возвращаем окно текста вокруг предложения.

В этой блог-записи мы рассмотрим реализацию этих двух методов в LlamaIndex. Почему я не делаю это в LangChain? Потому что уже существует много ресурсов о передовом RAG с LangChain. Я предпочитаю не дублировать усилий. Кроме того, я использую как LangChain, так и LlamaIndex. Лучше понимать больше инструментов и гибко их использовать.

Вы можете найти весь код в этом ноутбуке.

Обзор базового RAG

Начнем с базовой реализации RAG с 4 простыми шагами:

Шаг 1. Загрузка документов

Мы используем PDFReader для загрузки файла PDF и объединяем каждую страницу документа в один объект Document.

loader = PDFReader()docs0 = loader.load_data(file=Path("llama2.pdf"))doc_text = "\n\n".join([d.get_content() for d in docs0])docs = [Document(text=doc_text)]

Шаг 2. Разбиение документов на текстовые фрагменты (узлы)

Затем мы разбиваем документ на текстовые фрагменты, которые в LlamaIndex называются “узлами”, где мы задаем размер фрагмента равным 1024. Идентификаторы узлов по умолчанию представлены случайными текстовыми строками, после чего мы форматируем идентификатор узла в определенном формате.

node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)base_nodes = node_parser.get_nodes_from_documents(docs)for idx, node in enumerate(base_nodes):node.id_ = f"node-{idx}"

Шаг 3. Выбор модели эмбеддинга и LLM

Нам нужно определить две модели:

  • Модель эмбеддинга используется для создания векторных эмбеддингов для каждого из текстовых фрагментов. Здесь мы вызываем модель FlagEmbedding от Hugging Face.
  • LLM: запрос пользователя и соответствующие текстовые фрагменты передаются в LLM, чтобы он мог генерировать ответы с соответствующим контекстом.

Мы можем объединить эти две модели вместе в ServiceContext и использовать их позже на этапах индексации и запроса.

embed_model = resolve_embed_model(“local:BAAI/bge-small-en”)llm = OpenAI(model="gpt-3.5-turbo")service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)

Шаг 4. Создание индекса, восстановителя и поисковой системы запросов

Индекс, восстановитель и поисковая система запросов – это три основных компонента для задания вопросов о данных или документах:

  • Индекс – это структура данных, которая позволяет нам быстро получать связанную информацию для пользовательского запроса из внешних документов. Индекс Vector Store берет текстовые фрагменты/узлы и создает векторные вложения текста каждого узла, готовые для запроса LLM.
base_index = VectorStoreIndex(base_nodes, service_context=service_context)
  • Восстановитель используется для получения и извлечения соответствующей информации, исходя из пользовательского запроса.
base_retriever = base_index.as_retriever(similarity_top_k=2)
  • Поисковая система запросов создается поверх индекса и восстановителя, предоставляя общий интерфейс для задания вопросов о ваших данных.
query_engine_base = RetrieverQueryEngine.from_args(    base_retriever, service_context=service_context)response = query_engine_base.query(    "Можете ли вы рассказать мне о ключевых концепциях для тонкой настройки безопасности")print(str(response))

Расширенный метод 1: Меньшие дочерние фрагменты, ссылающиеся на большие родительские фрагменты

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

Шаг 1: Создание более мелких дочерних фрагментов

Для каждого текстового фрагмента размером 1024 мы создаем еще более мелкие текстовые фрагменты:

  • 8 текстовых фрагментов размером 128
  • 4 текстовых фрагмента размером 256
  • 2 текстовых фрагмента размером 512

Мы добавляем исходный текстовый фрагмент размером 1024 в список текстовых фрагментов.

sub_chunk_sizes = [128, 256, 512]sub_node_parsers = [    SimpleNodeParser.from_defaults(chunk_size=c) for c in sub_chunk_sizes]all_nodes = []for base_node in base_nodes:    for n in sub_node_parsers:        sub_nodes = n.get_nodes_from_documents([base_node])        sub_inodes = [            IndexNode.from_text_node(sn, base_node.node_id) for sn in sub_nodes        ]        all_nodes.extend(sub_inodes)    # также добавляем исходный узел в узел    original_node = IndexNode.from_text_node(base_node, base_node.node_id)    all_nodes.append(original_node)all_nodes_dict = {n.node_id: n for n in all_nodes}

Когда мы взглянем на все текстовые фрагменты ‘all_nodes_dict’, мы можем видеть, что к каждому из исходных текстовых фрагментов, например ‘node-0’, связаны множество более мелких фрагментов. Фактически, все более мелкие фрагменты ссылаются на большой фрагмент в метаданных с указанием параметра index_id на идентификатор индекса большого фрагмента.

Шаг 2: Создание индекса, восстановителя и поисковой системы запросов

  • Индекс: Создание векторных вложений для всех текстовых фрагментов.
vector_index_chunk = VectorStoreIndex(    all_nodes, service_context=service_context)
  • Восстановитель: главное здесь – использовать RecursiveRetriever, чтобы просматривать связи между узлами и извлекать узлы на основе “ссылок”. Этот восстановитель рекурсивно исследует связи от узлов к другим восстановителям/поисковым системам запросов. Для полученных узлов, если любой из них является IndexNode, то он будет исследовать связанный восстановитель/поисковую систему запросов и этот восстановитель будет запрашивать их.
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=2)retriever_chunk = RecursiveRetriever(    "vector",    retriever_dict={"vector": vector_retriever_chunk},    node_dict=all_nodes_dict,    verbose=True,)

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

  • Теперь с помощью тех же шагов, что и раньше, мы можем создать поисковый движок в качестве общего интерфейса для задания вопросов о наших данных.
query_engine_chunk = RetrieverQueryEngine.from_args(    retriever_chunk, service_context=service_context)response = query_engine_chunk.query(    "Можете рассказать мне о ключевых концепциях для настройки безопасности")print(str(response))

Улучшенный метод 2: Извлечение по окну предложений

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

В этом случае одиночные предложения будут аналогичны концепции “дочернего” фрагмента, которую мы упомянули в методе 1. “Окно” предложений (5 предложений с обоих сторон исходного предложения) будет аналогично концепции “родительского” фрагмента. Другими словами, мы используем одиночные предложения во время извлечения и отправляем извлеченное предложение вместе с окном предложений в LLM.

Шаг 1: Создание парсера узлов окна предложений

# create the sentence window node parser w/ default settingsnode_parser = SentenceWindowNodeParser.from_defaults(    window_size=3,    window_metadata_key="window",    original_text_metadata_key="original_text",)sentence_nodes = node_parser.get_nodes_from_documents(docs)sentence_index = VectorStoreIndex(sentence_nodes, service_context=service_context)

Шаг 2: Создание поискового движка

При создании поискового движка мы можем заменить предложение окном предложений с использованием MetadataReplacementPostProcessor, чтобы окно предложений было отправлено в LLM.

query_engine = sentence_index.as_query_engine(    similarity_top_k=2,    # the target key defaults to `window` to match the node_parser's default    node_postprocessors=[        MetadataReplacementPostProcessor(target_metadata_key="window")    ],)window_response = query_engine.query(    "Можете рассказать мне о ключевых концепциях для настройки безопасности")print(window_response)

Извлечение по окну предложений смогло ответить на вопрос “Можете рассказать мне о ключевых концепциях для настройки безопасности”:

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

Заключение

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

Ссылки:

By Sophia Yang on November 4, 2023

Connect with me on LinkedIn, Twitter, and YouTube and join the DS/ML Book Club ❤️