Як працюють ланцюги Retrieval-Augmented Generation: так просто, що…

текст перекладу
a middle school student could understand it and could make their first RAG app at first try.

Вступ.

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

### Невеликий приклад ###  

context = """  
Retrieval-Augmented Generation (RAG) — це процес оптимізації результатів роботи великої мовної моделі, щоб вона зверталася до авторитетної бази знань за межами своїх тренувальних даних перед генерацією відповіді.  
Великі Мовні Моделі (LLM) тренуються на великих обсягах даних і використовують мільярди параметрів для генерації оригінальних результатів для таких завдань, як відповідь на запитання, переклад мов або доповнення речень.  
RAG розширює вже потужні можливості LLM для специфічних доменів або внутрішніх баз знань організацій, все це без необхідності перенавчання моделі.
текст перекладу
Це економічно ефективний підхід до покращення результатів роботи LLM, щоб вони залишалися релевантними, точними та корисними в різних контекстах.  
"""  

llm = OllamaLLM(model="llama3.1", temperature=0.5)  

prompt = """  
Використовуй лише наданий контекст для відповіді на наступне запитання. Якщо ти не знаєш відповіді, відповідай, що не впевнений.  

Контекст: {context}  

Запитання: {question}  
"""  

prompt_template = PromptTemplate.from_template(prompt)  

chain = prompt_template | llm  

print(chain.invoke({  
 "context": context,  
 "question": "Що таке Retrieval-Augmented Generation?"  
}))

Іноді, надаючи LLM конкретне запитання або завдання, вам знадобиться правильний контекст, тому що:

  1. Деякі речі, які ви хочете зробити, виходять за межі контексту LLM, наприклад, якщо ви попросите пояснити сюжет Arcane Season 2 (2024), коли LLM була тренувана та створена в 2021 році.
    2.
    текст перекладу
    Запитане завдання чи питання залежить від знань, що виходять за межі тренувальних даних, які були надані LLM.

У попередньому фрагменті коду я надав правильний контекст для концепції "RAG", взятий з блогу Amazon Web Services

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

pic

Перший вихід LLM на запитання "Що таке RAG?"

Отже, що буде, якщо я запитаю щось поза межами контексту?

Запитання — У чому різниця між Retrieval-Augmented Generation та семантичним пошуком?

pic

🙁

LLM не справився із завданням, це сталося тому, що, як було сказано раніше, запит виходить за межі наданого контексту.

Правильне рішення полягає в тому, щоб надати LLM інший контекст або список контекстів для відповіді.

context_list = ["context 1","context 2","context 3","context 4"]  

context = "\n".join(context_list)

Це працювало б, як очікується, але є кілька причин, чому це не практично…

1.
текст перекладу
Оскільки чим більше контекстів ви додаєте до списку, кожен з яких буде споживатися запитом, залежно від параметра контекстного вікна (ctx) кожного LLM, з часом ваш запит перевищить це значення, і LLM втратить контекст.
2. Це болісно для серверів виробництва, оперативної пам'яті (RAM), розробників, LLM і Бога.

Розбиття тексту, вбудовування, векторні сховища та RAG рятують ситуацію.

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

Щоб зробити це, я надам LLM повний запис блогу AWS, про який згадував раніше, як контекст.

Спочатку завантажимо сайт.

За допомогою WebBaseLoader ми можемо завантажити запис блогу в форматі, який Langchain може ефективно прочитати.

Примітка_.
текст перекладу
WebBaseLoader потребує пакетів bs4 для роботи, не забудьте виконати “pip install bs4” перед використанням.

from langchain_community.document_loaders import WebBaseLoader

Тепер ми пишемо код для завантаження URL.

loader = WebBaseLoader('https://aws.amazon.com/what-is/retrieval-augmented-generation/')  

data = loader.load()  

print(data)

pic

Сайт був завантажений у “data”

Функція завантаження створила список з документом та інформацією про метадані та контент.

print(data[0].page_content + "\n--------\n")  
print(data[0].metadata)  

## Спробуйте вивести це і подивіться, яка інформація та метадані доступні для вас і LLM ##

Але тут є проблема.

Пам'ятаєте, коли я говорив, що великий контекст перевищить контекстне вікно LLM?

Очевидно, ми завантажили величезний текст у змінну data, тому ця проблема виникне.

Але, що якщо я скажу вам, що ми можемо розділити текст на різні частини, і це спростить процес споживання контексту, щоб не перевищити контекстне вікно?

RecursiveCharacterTextSplitter — ваш зручний роздільник.

RecursiveCharacterTextSplitter має деякі переваги над CharacterTextSplitter.

Хоча CharacterTextSplitter краще працює з роздільниками, ніж з розміром частини та перекриттям частин.
текст перекладу
RecursiveCharacterTextSplitter може правильно працювати з розміром частин і, рекурсивно, може працювати з роздільниками.

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

Якщо ваші дані структуровані, формат розділення чіткий, і документ має малий чи середній розмір, можна використовувати CharacterTextSplitter.

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

З сказаними вище відмінностями, давайте перейдемо до коду.

from langchain_text_splitters import RecursiveCharacterTextSplitter

Після імпорту пакету, тепер ми можемо завантажити дані в RCTS і перетворити їх на частини.

splitter = RecursiveCharacterTextSplitter(  
 separators=["\n\n\n\n\n\n\n\n\n\n\n\n\n", "\n\n", " ", "\n\n\n", "\n", "", " "],  
 chunk_size=200,  
 chunk_overlap=100  
)  

chunks = splitter.split_text(data[0].page_content)

У випадку цього прикладу програми, роздільники можуть бути різними, тому я вказав їх у списку separators.

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

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

Ви можете експериментувати з вашими роздільниками, chunksize та chunkoverlap, щоб краще організувати ваші дані.

print(chunks)  
print("\n---------\n")  
print([len(chunk) for chunk in chunks])  

### Надрукувавши це, ви побачите, як ваші дані розділяються ###

pic

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

Тепер у вас є список з багатьох частин даних різного розміру.
текст перекладу
Що далі?.

Наступний крок — створити бібліотеку частин (chunks) для того, щоб LLM міг ітераційно знаходити необхідний контекст.

Надання вашому LLM суперсил з допомогою Embeddings і VectorStores.

Як я вже казав, VectorStores — це щось на зразок бібліотек, де ми можемо зберігати частини даних всередині VectorStores, як якщо б це були книги.

Але після збереження даних, нам потрібно, щоб наш LLM міг корелювати введення користувача з збереженими частинами. Як це зробити?.

Embeddings.

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

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

Примітка.
текст перекладу
Зовні розробники можуть використовувати різні Embeddings, такі як SentenceTransformer, OpenAIEmbeddings, але в цьому випадку, оскільки я працюю з локальною моделлю Ollama і пояснення різниць між ними виходить за межі цієї статті, я використаю OllamaEmbeddings.

from langchain_ollama import OllamaEmbeddings  


embeddings = OllamaEmbeddings(  
 model="nomic-embed-text:latest",  
)  

### Замініть цей код нижче післяmore ###  
input_text = "two plus two is four, minus one that's three, quick maths."  
vector = embeddings.embed_query(input_text)  
print(vector)

Якщо ви виведете цей приклад, він поверне список з великою кількістю чисел в залежності від заданого input_text.
текст перекладу
Що це означає?.

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

Наступний етап: вбудований текст тепер перетворений на вектор, вектор містить список чисел, згенерованих введенням.
текст перекладу
Ми можемо дати моделі вбудовування (embedding) багато різних введень, щоб перетворити їх на списки чисел (embeddings).

### Замініть попередній код на цей ###  

input_texts = ["two plus two is four, minus one that's three, quick maths.", "three plus three is nine, minus one that's eight, quick maths."]  
vectors = embeddings.embed_documents(input_texts)  
print(len(vectors))

Тепер у нас є два текстових рядки в введенні, і вони перетворюються на два різних вектори (списки чисел), які можуть бути схожими або не бути схожими. Наша змінна vectors зберігає ці два embeddings.

Тепер ви, можливо, замислюєтесь, як це нам допоможе.

Якщо ви почали розглядати input_texts як ті самі chunks, які ми створювали раніше, то ви праві.
текст перекладу
Embeddings допомагають нам перетворювати chunks на зручні для аналізу та кореляції вектори, але останні дії не є завданням для embeddings, саме тут нам допоможуть VectorStores.

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

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

pic

Як працюють разом Embeddings і VectorStores.

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

А тепер, сюжетний поворот: як ці два інструменти допоможуть LLM працювати так, як нам потрібно?

pic

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

Тепер давайте повернемося до роботи.

Створення RAG.

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

Спочатку імпортуємо пакет для використання в самому початку коду.

from langchain_chroma import Chroma  
from langchain_core.runnables import RunnablePassthrough  
from langchain_core.output_parsers import StrOutputParser

Тепер, нам потрібно налаштувати embeddings і chunks у параметри Chroma.

vector_store = Chroma.from_texts(  
 texts=chunks,  
 embedding=embeddings  
)  

## Якщо ви очікували щось складне, то...
текст перекладу
Ласкаво просимо до Langchain RAG ##  

Тепер нам потрібно визначити наш retriever (отримувач), який, як і вказано в назві, буде виконувати пошук схожості в тексті.

retriever = vector_store.as_retriever(  
 search_type="similarity",  
 search_kwargs={"k": 20}  
)

Параметр search_kwargs встановлює кількість векторів, які необхідно отримати, це означає, що кількість подібних векторів, які будуть включені в контекст, дорівнюватиме 20, оскільки слова "Semantic Search" і "RAG" зустрічаються в тексті багато разів.

Ви можете грати з цим фрагментом коду у вашому коді, щоб відлагодити та перевірити, чи працює ваш retriever належним чином.

print(retriever.invoke("some question..."))

Останній етап — створити правильний ланцюг для виконання запиту до LLM.

chain = ({"context": retriever, "question": RunnablePassthrough()}  
 | prompt_template  
 | llm  
 | StrOutputParser())

RunnablePassthrough — це корисний інструмент для ланцюгів "context" та "question" (QA LLMs, як цей), вони працюють автоматично, передаючи вхідні дані до retriever та питання.

Отже, завдяки цьому вхідні дані будуть одночасно передані до retriever та питання.

Тепер ми можемо виконати ланцюг і вивести результат.

user_question = "What is the difference between Retrieval-Augmented Generation and semantic search?"  
result = chain.invoke(user_question)  
print(result)

Раніше, під час виклику ланцюга, ми задали словник із ключами "context" та "question", як ви бачили, RunnablePassthrough працює зі словником "context" та "question" автоматично і приймає лише рядок, це працює як магія.

І тепер, момент істини...

pic

(Sweet Victory від Bob Kulick та David Glen Eisley грає на фоні)

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

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

WHOAMI

Я — Хосе Рейєс.

Можете називати мене «Архітектом» тут, я молодший інженер з виявлення загроз та контент-кріейтор. Любитель шахів, людський єнот і кавоман. Обожнюю читати, а ще більше — мати змогу ділитися всім, що я прочитав або дізнався про кібербезпеку, ШІ, програмування, продуктивність та/або технології через Medium та інші соціальні мережі, створюючи неймовірні спільноти інформації та досвіду, що допомагають людям, таким як я або ви (читач), легше знаходити інформацію або знаходити досвід, вартий того, щоб його застосувати в професійному житті.
текст перекладу
Дуже дякую за те, що прочитали мої статті.

pic

Калі каже «Щасливого Різдва»

Перекладено з: How Retrieval-Augmented Generation Chains Work Explained So Easy That…

Leave a Reply

Your email address will not be published. Required fields are marked *