Большие языковые модели (LLM) значительно повышают эффективность поиска и анализа документов благодаря технологии Retrieval-Augmented Generation (RAG).
В первой части статьи рассмотрим, как реализовать RAG-модель на Python с использованием открытой LLM LLAMA 2 и векторного хранилища FAISS для быстрого поиска по большим массивам данных.
Во второй части мы покажем, как достичь тех же результатов без программирования — с помощью no-code платформы Epsilon Workflow, которая позволяет создавать решения на основе RAG без программирования.
Обзор инструментов: Llama 2 и FAISS
Llama 2
Llama 2 — это большая языковая модель, способная работать с огромными объёмами данных. В зависимости от конфигурации, её размеры могут варьироваться от 7 до 70 миллиардов параметров, что позволяет ей справляться с широким спектром задач — от простых до действительно сложных и ресурсоёмких. Особенно полезна версия Llama-2-Chat, которая обучена вести диалоги. Это делает её идеальной для ситуаций, где нужно не только генерировать текст, но и интегрировать внешние данные в ответы. Например, она может использоваться в чат-ботах для поддержки клиентов, создания умных ассистентов или автоматизации бизнес-процессов.
FAISS
FAISS — это инструмент для поиска похожих объектов, только не среди текста или картинок напрямую, а среди их векторных представлений. Он хорошо подходит для решения задач Retrieval-Augmented Generation (RAG), где нужно находить релевантные данные на основе пользовательских запросов.
Одним из главных преимуществ FAISS является инвертированный векторный индекс (IVF), который ускоряет поиск даже при работе с большими объёмами данных.
FAISS также поддерживает дополнительные методы, такие как квантование продукта (PQ) и графы ближайших соседей (HNSW), которые позволяют проводить поиск ближайших векторов в наборах данных, содержащих миллиарды векторных представлений. Эти технологии обеспечивают возможность обработки больших массивов данных, таких как базы изображений, текстов или аудиофайлов.
Шаг 1: Предобработка документов: очистка данных и разбиение текста на фрагменты (chunks)
Сначала очищаем текст от ненужных элементов, таких как форматирование, специальные символы и разметка. Например, удаляем HTML-теги и Markdown-разметку. Также устраняем повторяющиеся пробелы, табуляции и другие служебные символы.
Затем текст разбивается на фрагменты. Чтобы сохранить контекст и предотвратить потерю ключевых деталей, которые могут находиться на границах, важно использовать перекрытие между фрагментами. Перекрытие значит, что конец одного фрагмента будет частично повторяться в начале следующего. Выбор подходящей длины фрагмента и перекрытия зависит от конкретной задачи и структуры текста.
В нашем случае оптимальными параметрами стали фрагмент размером 1200 символов и перекрытие в 300 символов. Например, фрагмент 1 будет включать в себя символы 1–1200, а фрагмент 2 — символы 901–2100.
Для каждого фрагмента добавляем метаданные, такие как название или источник документа, что улучшит затем качество поиска и работы с несколькими файлами.
Поддерживаются форматы .md, .pdf и .txt.
Для начала работы устанавливаем Python-библиотеки:
pip install markdown pip install langchain pip install pdfminer.six
Этот код реализует предобработку файлов разных форматов (Markdown, PDF, текстовые файлы), их очистку и разбиение на фрагменты:
import os import re import markdown from pdfminer.high_level import extract_text as extract_text_from_pdf from io import StringIO from html.parser import HTMLParser from langchain.text_splitter import RecursiveCharacterTextSplitter # Класс для очистки HTML-тегов из текста class MLStripper(HTMLParser): def __init__(self): super().__init__() self.reset() self.strict = False self.convert_charrefs = True self.text = StringIO() def handle_data(self, d): self.text.write(d) def get_data(self): return self.text.getvalue() def strip_tags(html): """Удалить HTML-теги из строки.""" s = MLStripper() s.feed(html) return s.get_data() def clean_markdown(text): """Очистить синтаксис Markdown из текста.""" # Удалить ссылки в формате Markdown text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) # Удалить маркеры жирного и курсивного текста text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) text = re.sub(r'\*([^*]+)\*', r'\1', text) text = re.sub(r'__([^_]+)__', r'\1', text) text = re.sub(r'_([^_]+)_', r'\1', text) # Удалить изображения и их ссылки text = re.sub(r'!\[[^\]]*]\([^)]*\)', '', text) # Удалить маркеры заголовков text = re.sub(r'#+\s?', '', text) # Удалить другой синтаксис Markdown (например, таблицы, маркеры списка) text = re.sub(r'\|', ' ', text) text = re.sub(r'-{2,}', '', text) text = re.sub(r'\n{2,}', '\n', text) # Удалить лишние пустые строки return text def extract_text_from_md(md_path): """Извлечь и очистить текст из Markdown-файла.""" with open(md_path, "r", encoding="utf-8") as file: md_content = file.read() html = markdown.markdown(md_content) text = strip_tags(html) return clean_markdown(text) def extract_text_from_file(file_path): """Извлечь текст из файла на основе его расширения.""" if file_path.endswith('.pdf'): return extract_text_from_pdf(file_path) elif file_path.endswith('.md'): return extract_text_from_md(file_path) elif file_path.endswith('.txt'): with open(file_path, 'r', encoding='utf-8') as file: return file.read() else: return "Неподдерживаемый формат файла." # Директория, содержащая документы для обработки directory = r'LLM/docs' # Параметры для разбиения текста chunk_size = 1200 chunk_overlap = 300 # Список для хранения всех частей документов all_docs = [] allowed_extensions = ['.md', '.pdf', '.txt'] # Обработка каждого файла в директории for root, dirs, files in os.walk(directory): for filename in files: # Получить расширение файла _, file_extension = os.path.splitext(filename) if file_extension in allowed_extensions: file_path = os.path.join(root, filename) # Полный путь к файлу # Удалить расширение ".md", ".pdf" или ".txt" из имени файла file_name_without_extension = os.path.splitext(filename)[0] # Открыть и прочитать файл file_content = extract_text_from_file(file_path) # Разбить текст на части text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap) docs = text_splitter.split_text(file_content) for i, chunk in enumerate(docs): # Определить метаданные для каждой части (можно настроить по своему усмотрению) metadata = { "File Name": file_name_without_extension, "Chunk Number": i + 1, } # Создать заголовок с метаданными и именем файла header = f"File Name: {file_name_without_extension}\n" for key, value in metadata.items(): header += f"{key}: {value}\n" # Объединить заголовок, имя файла и содержимое части chunk_with_header = header + file_name_without_extension + "\n" + chunk all_docs.append(chunk_with_header) print(f"Обработано: {filename}")
Шаг 2: Векторизация и индексация с помощью FAISS
После предварительной обработки и разбивки на фрагменты наш следующий шаг заключается в векторизации и индексации, которые являются важными шагами при создании любой модели RAG.
Векторизация
Векторизация — это процесс преобразования текста в числовые векторы, которые затем можно индексировать и использовать для быстрого поиска. Инструменты, такие как FAISS, позволяют работать с векторными пространствами, содержащими миллионы или даже миллиарды объектов.
Каждый текстовый фрагмент преобразуется в многомерный вектор, где каждое измерение (координата) отражает определённые характеристики текста, например, частоту слов или их значение в контексте. Таких измерений у векторов может быть сотни или тысячи.
Для этого используются модели встраивания (embedding), которые обучены на больших наборах данных и выявляют сложные взаимосвязи между словами и их контекстом.
Например, слово «замок» может иметь разные значения в зависимости от контекста — это может быть как здание, так и механизм для запирания двери. Модель встраивания анализирует соседние слова и общий смысл предложения, чтобы корректно интерпретировать значение. В результате предложения «Мы подошли к старому замку на вершине холма» и «Ключ не подходит к замку на двери» будут преобразованы в разные векторы, несмотря на одно и то же слово.
Модели преобразуют текстовые фрагменты таким образом, что векторы текстов, схожих по смыслу, располагаются ближе друг к другу, а текстов с разным смыслом — дальше. Для измерения расстояния между векторами обычно используются косинусное или евклидово расстояние. Это позволяет сравнивать тексты, сводя задачу к сравнению расстояний между их векторами.
Векторное представление текста упрощает задачи семантического поиска, кластеризации и классификации.
Для создания векторных представлений можно использовать модели из библиотеки Hugging Face.
Индексация
Для индексации векторных представлений будем использовать FAISS — библиотеку которая позволяет быстро находить объекты, похожие на данный вектор.
Сохраняем векторы в FAISS.
FAISS поддерживает разные типы индексов, такие как Flat Index для точного поиска и IVFPQ для ускоренного поиска в крупных наборах данных.
FAISS создаёт индекс, который позволяет эффективно извлекать релевантные фрагменты на основе запросов пользователей.
Одним из ключевых преимуществ FAISS является возможность сохранять созданные индексы локально. Это позволяет повторно использовать проиндексированные данные без необходимости их обработки заново, что экономит ресурсы и ускоряет поиск.
Индексация играет важную роль в задачах Retrieval-Augmented Generation (RAG), где FAISS помогает находить наиболее подходящие фрагменты, которые затем используются для генерации текста языковыми моделями.
from langchain.vectorstores import FAISS from langchain.embeddings import HuggingFaceInstructEmbeddings # Инициализация HuggingFaceInstructEmbeddings model_name = "hkunlp/instructor-large" model_kwargs = {'device': 'cpu'} encode_kwargs = {'normalize_embeddings': True} hf_embedding = HuggingFaceInstructEmbeddings( model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs ) # Встраивание и индексация всех документов с использованием FAISS db = FAISS.from_texts(all_docs, hf_embedding) # Сохранение индексаированных данных локально db.save_local("faiss_AiDoc")
Шаг 3: Загрузка модели Llama 2 и выполнение запросов
На этом этапе загружается модель LLAMA2 для генерации ответов на основе контекстной информации, полученной из индексированных документов. LLAMA2 — это языковая модель, специально настроенная для задач генерации текста, поэтому часто используется для реализации Retrieval-Augmented Generation (RAG).
Для этого примера мы выбрали модель «llama-2-7b-chat.Q6_K.gguf», которую можно скачать с сайта Hugging Face.
Запрос информации
После загрузки LLAMA2 и индексации документов с помощью FAISS, пользователи могут вводить запросы.
Когда поступает поисковый запрос, он преобразуется его в вектор с использованием того же метода, что и для текстовых фрагментов.
Затем система использует FAISS для поиска наиболее релевантных фрагментов следующим образом: вектор поискового запроса сравнивается с векторами всех проиндексированных фрагментов. FAISS вычисляет расстояние между вектором запроса и векторами документов и находит фрагменты, векторы которых находятся ближе всего к вектору запроса. Эти фрагменты считаются наиболее релевантными и выбираются для дальнейшего использования.
FAISS возвращает список из 5 фрагментов, наиболее соответствующих запросу.
Эти фрагменты затем служат контекстом для генерации ответа с использованием LLAMA2.
Процесс генерации
Модель LLAMA2 получает найденные на предыдущем шаге фрагменты и использует их в качестве контекстных подсказок.
На основе этих данных LLAMA2 формулирует точный ответ.
Качество ответа напрямую зависит от точности найденных фрагментов, что делает FAISS важным элементом процесса.
Пример запроса и ответа
Запрос пользователя: «Какие преимущества использования FAISS в системе RAG?»
Ответ, сгенерированный LLAMA2: «Одним из ключевых преимуществ использования FAISS в системе Retrieval-Augmented Generation является скорость поиска релевантной информации среди больших наборов данных. FAISS эффективно индексирует векторные представления документов, что позволяет быстро находить релевантные фрагменты для дальнейшей генерации ответов.»
В этом примере LLAMA2 использует фрагменты, предоставленные FAISS, для построения ответа на основе контекстной информации из проиндексированных документов.
from langchain.llms import LlamaCpp from langchain import PromptTemplate, LLMChain from langchain.callbacks.manager import CallbackManager from langchain.callbacks.streaming_stdout import ( StreamingStdOutCallbackHandler ) # Шаблон для вопросно-ответного запроса template = """Вопрос: {question} \n\nОтвет:""" # Инициализация шаблона запроса и менеджера обратных вызовов prompt = PromptTemplate(template=template, input_variables=["question"]) callback_manager = CallbackManager([StreamingStdOutCallbackHandler()]) # Локальный путь к загруженной модели Llama2 model_path = "llama-2-7b-chat.Q4_K_M.gguf" # Инициализация модели LlamaCpp llm = LlamaCpp(model_path=model_path, temperature=0.2, max_tokens=4095, top_p=1, callback_manager=callback_manager, n_ctx=6000) # Создание LLMChain llm_chain = LLMChain(prompt=prompt, llm=llm) # Определение запроса для поиска в проиндексированных документах query = "<<здесь должен быть вопрос пользователя>>?" # Поиск семантически похожих фрагментов и возвращение топ-5 фрагментов search = db.similarity_search(query, k=5) # Шаблон для генерации итогового запроса template = '''Контекст: {context} Исходя из контекста, ответьте на следующий вопрос: Вопрос: {question} Предоставьте ответ только на основе предоставленного контекста, без использования общих знаний. Ответ должен быть непосредственно взят из предоставленного контекста. Пожалуйста, исправьте грамматические ошибки для улучшения читаемости. Если в контексте нет информации, достаточной для ответа на вопрос, укажите, что ответ отсутствует в данном контексте. Пожалуйста, включите источник информации в качестве ссылки, поясняющей, как вы пришли к своему ответу.''' # Создание шаблона для финального запроса prompt = PromptTemplate(input_variables=["context", "question"], template=template) # Форматирование итогового запроса с учетом вопроса и результатов поиска final_prompt = prompt.format(question=query, context=search) # Запуск LLMChain для генерации ответа на основе контекста llm_chain.run(final_prompt)
RAG для LLM в Epsilon Workflow
Epsilon Workflow от Epsilon Metrics предоставляет интерфейс без использования кода для выполнения запросов RAG, что упрощает нашим пользователям интеграцию искусственного интеллекта в свои процессы обработки данных.
Ниже приведён скриншот компонента AI Prompt с поддержкой RAG, использующего большую языковую модель YandexGPT (LLM) и встроенные функции для улучшенного поиска данных и генерации контекстных ответов.
Заключение
Внедрение модели RAG с использованием открытых технологий, таких как FAISS и LLAMA2, упрощает поиск информации и ускоряет доступ к релевантным данным, особенно в условиях работы с большими объёмами информации.
С помощью Epsilon Workflow это можно сделать без необходимости писать код и знать Python и другие языки программирования. Все настройки выполняются через удобный no-code интерфейс.
Если вы хотите попробовать Epsilon Workflow в своих проектах и на своих данных, свяжитесь с нами для демонстрации.