Приручаем нейросети
Давно не виделись, уважаемые! Ну что ж, рад вас видеть, сегодня будем говорить и применять новые инструменты для создания RAG, улучшим качество наших результатов относительно прошлой статьи за счет использования других моделей для embeddings. Также затронем использование трушной векторной БД Chroma.
Видит Бог, я не хотел, но нам пора начинать использовать не No-Code платформы, так что сегодня вам понадобится Python. Начнем мы с установки зависимостей, нам понадобятся:
langchain — сам фреймворк для построения RAG, мы его использовали в некотором виде в прошлой статье.
openai — библиотечка для работы с API OpenAI. Мы будем придерживаться традиции использовать локальные модели, но так как LM Studio эмулирует сервер как OpenAI, нам нужна эта библиотека.
chromadb — для хранения наших embeddings.
tiktoken — нужен нам для генерации embeddings, используется другими библиотеками.
Возможно что-то я подзабыл, и у меня это было уже установлено, но не стоит переживать, Python вам сам сообщит, что вам не хватает какой-то библиотечки, установите.
В этот раз, чтобы было интереснее, давайте будем разбирать книгу «Герой нашего времени», все-таки классика. :) Я предварительно ее скачал в формате txt, но вы можете использовать буквально любую информацию, в langchain встроена куча разных loader-ов, со списком можете ознакомится тут.
Начнем с генерации эмбедингов. Собственно, я создал файлик chroma.py, можете называть его как угодно. :) Сделал для удобства пару констант:
CHROMA_PATH = "chroma"
DATA_PATH = "data/books"
CHUNK_SIZE = 750
CHUNK_OVERLAP = 100
Начну с утильки, которая будет отдавать объект эмбедингов:
def get_embeddings():
model_kwargs = {'device': 'cuda'}
embeddings_hf = HuggingFaceEmbeddings(
model_name='intfloat/multilingual-e5-large',
model_kwargs=model_kwargs
)
return embeddings_hf
langchain умный, поэтому при первом запуске он подтянет и сохранит модельку, которую мы тут указываем. Как видите, я остановил свой выбор на intfloat/multilingual-e5-large в этот раз, так как она отлично себя показывает как с русским, так и другими языками. Также я буду запускать это добро на видеокарте, но если вдруг у вас нет такой возможности, можете вместо device: cuda написать cpu. Будет медленнее, но как минимум сможете попробовать.
На случай, если вы используете видеокарту Nvidia и Cuda, вам нужно установить CUDA-драйвера (просто загуглите их и ставьте с официального сайта). После этого небольшой танец с бубном: узнайте версию драйверов, которые вы установили, а затем идите вот сюда и ставьте все по своим параметрам. Сгенерится команда на установку PyTorch, ставим его. Вауля, теперь ваш PyTorch, ну и модели в целом смогут работать с видеокартой.
Перейдем по коду дальше, собственно, подгрузка нашей книги:
def load_documents():
loader = DirectoryLoader(DATA_PATH, glob="*.txt")
documents = loader.load()
return documents
Далее бьем по старой стратегии на чанки наш документ:
def split_text(documents: list[Document]):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
add_start_index=True,
)
chunks = text_splitter.split_documents(documents)
print(f"Разбили {len(documents)} документов на {len(chunks)} чанков.")
return chunks
Размер чанков я выбрал 750, для нашего теста с книгой лучше брать побольше, ибо чем больше контекста будет у LLM-ки, тем качественнее вы получите ответ.
Ну и, собственно, объединяем:
def main():
documents = load_documents()
chunks = split_text(documents)
save_to_chroma(chunks)
Теперь, когда у нас заполнена база знаний, можем переходить к составлению запроса. Я создал файл ask.py. Начнем с промпта для нашей LLM-ки. Кстати, ее я не менял, также будем использовать SaigaMistral.
PROMPT_TEMPLATE = """
Ответь на вопрос базируясь только на этом контексте:
{context}
---
Ответь на вопрос, используя только контекст: {question}
"""
Такой вот простейший промпт будет четко объяснять нашей LLM, что мы от нее хотим.
Далее просто приведу код с комментариями:
def main():
# Читаем аргументы запуска
parser = argparse.ArgumentParser()
parser.add_argument("query_text", type=str)
args = parser.parse_args()
query_text = args.query_text
# Создаем БД
db = Chroma(persist_directory=chroma.CHROMA_PATH, embedding_function=chroma.get_embeddings())
# Ищем по БД
# Мы будем использовать 3 чанка из базы данных, которые наиболее похожи на наш вопрос
# c этим количеством можете экспериментировать как угодно, главное не переборщите, ваша LLM
# должна поддерживать такое количество контекста, чтобы уместить весь полученный промпт
results = db.similarity_search_with_relevance_scores(query_text, k=3)
if len(results) == 0 or results[0][1] < 0.7:
print(f"Нет фрагментов текста, на которые можно опираться для ответа.")
return
# Собираем запрос к LLM, объединяя наши чанки. Их мы записываем через пропуск строки и ---
# помещаем мы контекст в переменную context, которую обозначали еще в самом промпте
# ну и по аналогии вопрос просто записываем в переменную question.
context_text = "\n\n---\n\n".join([doc.page_content for doc, _score in results])
prompt_template = ChatPromptTemplate.from_template(PROMPT_TEMPLATE)
prompt = prompt_template.format(context=context_text, question=query_text)
print(f"Полученный промпт {prompt}")
# Подключение к LM Studio и отправка запроса
model = ChatOpenAI(temperature=0.7, base_url="http://localhost:1234/v1", api_key="not-needed")
response_text = model.predict(prompt)
# Выводим результаты ответа
sources = [doc.metadata.get("source", None) for doc, _score in results]
formatted_response = f"Ответ: {response_text}\nДанные взяты из: {sources}"
print(formatted_response)
Итак, как же теперь воспользоваться всем тем, что мы написали?
Каждый раз, когда вы меняете состав документов или сами документы, вам нужно запустить запись в БД, делаем это командой python chroma.py. Или как вы назвали файл создания БД.
Вот что мы должны получить в выводе:
Теперь не забываем стартануть LM Studio в режиме сервера, подробнее про это я рассказывал в прошлой статье.
После того как база заполнена можем начать задавать вопросы (python ask.py «ваш вопрос»). Например: python ask.py «Кто рассказывает историю Бэлы?»
Ответ будет примерно таким:
Полученный промпт Human:
Ответь на вопрос базируясь только на этом контексте:Сегодня я встал поздно; прихожу к колодцу — никого уже нет. Становилось жарко; белые мохнатые тучки быстро бежали от снеговых гор, обещая грозу; голова Машука дымилась, как загашенный факел; кругом него вились и ползали, как змеи, серые клочки облаков, задержанные в своем стремлении и будто зацепившиеся за колючий его кустарник. Воздух был напоен электричеством. Я углубился в виноградную аллею, ведущую в грот; мне было грустно. Я думал о той молодой женщине с родинкой на щеке, про которую говорил мне доктор… Зачем она здесь? И она ли? И почему я думаю, что это она? и почему я даже так в этом уверен? Мало ли женщин с родинками на щеках? Размышляя таким образом, я подошел к самому гроту. Смотрю: в прохладной тени его свода, на каменной
___
Как она переменилась в этот день! бледные щеки впали, глаза сделались большие, губы горели. Она чувствовала внутренний жар, как будто в груди у ней лежала раскаленное железо.
___
А вы были в Москве, доктор?
Да, я имел там некоторую практику.
Продолжайте.
Да я, кажется, все сказал… Да! вот еще: княжна, кажется, любит рассуждать о чувствах, страстях и прочее… она была одну зиму в Петербурге, и он ей не понравился, особенно общество: ее, верно, холодно приняли.
Вы никого у них не видали сегодня?
Напротив: был один адъютант, один натянутый гвардеец и какая-то дама из новоприезжих, родственница княгини по мужу, очень хорошенькая, но очень, кажется, больная… Не встретили ль вы ее у колодца? — она среднего роста, блондинка, с правильными чертами, цвет лица чахоточный, а на правой щеке черная родинка; ее лицо меня поразило своей выразительностью.
Родинка! — пробормотал я сквозь зубы. — Неужели?
___
Ответь на вопрос, используя только контекст: У кого из героинь на правой щеке была чёрная родинка?
Как видите контекст задается из отрывков книги, где фигурирует родинка. Ну и, собственно, сам ответ:
Ответ: Родительница княгини по мужу.
Данные взяты из: ['data\books\geroy-nashego-vremeni.txt', 'data\books\geroy-nashego-vremeni.txt', 'data\books\geroy-nashego-vremeni.txt']
Возможно на экзамене у вас бы такой ответ не приняли, но ответ по сути-то правильный. :)
Ну что ж, на этом у нас все. В этот раз мы добились меньших галлюцинаций и научились собирать RAG, используя Python. А также запускать в нем локально эмбединги. Уже на основе этого можно собрать что-то под ваши задачи, просто пошаманив с выбором эммбедингов и промптом.