Головна / Статті

RAG від нуля: фрагментування, вбудовування, зберігання, отримання, відповідь з контекстом

Покроковий огляд процесу RAG із використанням LangChain та FAISS: розділення на частини, створення ембеддингів, пошук інформації, прив’язка запитів до даних, поширені помилки та можливості подальшого вдосконалення.

Видавець: REACTAPP.TOP8 хв читання1915 слів

Джерела: RAG, Explained the Way I Wish Someone Had Explained It to Me

Якщо запитати модель чату, що передбачає політика повернення грошей певної компанії, вона рідко визнає свою некомпетентність. Частіше вона генерує гладкий, правдоподібний абзац, який не має нічого спільного з реальною політикою. Причина цього не в дефекті. Модель мови навчається один раз, а потім її статус застигає. Вона ніколи не читала приватних PDF-файлів, внутрішніх вікі чи робочих просторів Notion та не знає нічого, що було написано після завершення її навчання.

Метод генерації з підтримкою пошуку, який зазвичай скорочують до RAG, є стандартним рішенням, і його ідея насправді дуже проста. Замість того, щоб очікувати, що модель запам’ятає відповідь, система знаходить відповідний текст та вставляє його безпосередньо у запит, а потім просить модель прочитати його та відповісти. Усе інше — це інженерні рішення для забезпечення надійності цього пошуку. Наведений нижче приклад — це бот підтримки, який відповідає на запитання з PDF-файлу з правилами компанії.

Два студенти, один іспит

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

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

Два канали обробки, а не один

Будь-яка система RAG, якою б складною вона не була, складається з двох окремих каналів обробки, з’єднаних у середині.

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

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

Створення індексу

Завантаження документів

Спочатку необхідно перетворити первинний вміст у звичайний текст. Для PDF, файлів Word, веб-сторінок та експортованих нот потрібен засіб завантаження, який повертає текст.

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("company_policy.pdf")
documents = loader.load()

print(f"Loaded {len(documents)} pages")

Розділення тексту на частини

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

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

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # characters per chunk
    chunk_overlap=80,    # overlap between consecutive chunks
    separators=["\n\n", "\n", ". ", " "]
)

chunks = splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks")

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

Вбудовування кожного фрагмента

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

from langchain_openai import OpenAIEmbeddings

embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")

# under the hood, this is what happens per chunk:
vector = embedding_model.embed_query("30-day refund window for annual plans")
print(len(vector))   # e.g. 1536 numbers representing this sentence's meaning

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

        "refund policy" •
                          \
                           • "money-back guarantee"
   "vacation days" •
                     \
                      • "paid time off"

Фраза на кшталт «політика повернення грошей» з’являється поруч із «гарантією повернення грошей», хоча слова різні, і саме в цьому секрет семантичного пошуку. Це пояснює, чому технологія RAG перевершує старий спосіб пошуку за ключовими словами за допомогою Ctrl+F.

Зберігання векторів

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

from langchain_community.vectorstores import FAISS

vectorstore = FAISS.from_documents(chunks, embedding_model)
vectorstore.save_local("faiss_index")

FAISS — це зручний варіант для початку, оскільки він безкоштовний та працює на локальній машині, а Chroma також досить простий у використанні. Управлінські сервіси на кшталт Pinecone чи Weaviate стають доцільними, коли доходить до масштабування в продакшені. Концепція залишається однаковою у всіх варіантах, відрізняються лише способи зберігання та інфраструктура.

Індексування вже завершено. Результатом є пошукова система, створена на основі документів, яку потрібно оновлювати лише тоді, коли змінюються ці документи, а не з кожним запитом користувача.

Відповідь на запитання

Ця частина виконується щоразу, коли хтось вводить запит.

Вбудувати запитання за допомогою того самого моделювання

query = "Can I get a refund on my annual subscription?"

Питання потрібно інкорпорувати саме з тією моделлю, яка використовувалася під час індексації. Люди постійно на це наражаються. Якщо індекс створений за допомогою однієї моделі інкорпорації, а запит надсилається з іншою, вектори знаходяться у різних просторах значень, і пошук за схожістю тихо перестає працювати. Не з’являється жодної помилки — лише погані результати, тому правило полягає у використанні однієї моделі протягом усього процесу.

Отримати найближчі фрагменти

retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
relevant_chunks = retriever.invoke(query)

for chunk in relevant_chunks:
    print(chunk.page_content[:100], "...")

Усередині FAISS виконується пошук схожості. Він вимірює відстань, зазвичай косинусову схожість, між вектором запиту та вектором кожного збереженого фрагмента, а потім повертає найближчі до нього. Це знову ж таки схема кластерів на карті, застосована у зворотному напрямку для знаходження кластера, до якого належить запит.

Вставте фрагменти у запит

Саме цей крок забезпечує роботу технології RAG: отриманий текст буквально вставляється у запит.

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("""
Answer the question using ONLY the context below.
If the answer isn't in the context, say "I don't have that information."

Context:
{context}

Question: {question}
""")

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

Підключити до ланцюга

Тепер елементи об’єднуються у ланцюг RAG — саме це мають на увазі люди, коли говорять про створення такого ланцюга. У LangChain його зазвичай пишуть за допомогою оператора „|“, який передає результат зліва на наступний крок справа.

from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4o-mini")

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke("Can I get a refund on my annual subscription?")
print(answer)

Читаючи зліва направо, ланцюг сам розповідає свою історію. Запит надходить, фрагменти отримуються та форматуються як контекст, тоді як запит залишається незмінним; шаблон запиту заповнюється, викликається модель, а з її відповіді парсується простий текст.

Якщо це запустити, замість уявного припущення з’явиться щось на кшталт: «Так, щорічні плани підлягають повній компенсації протягом тридцяти днів після покупки, як зазначено у політиці повернення грошей». Відповідь є обґрунтованою та конкретною, і її можна простежити до реального абзацу в PDF.

Чому б не вставити все?

Справедливою запереченням є те, що вікна контексту зараз дуже великі. Чому б не включати весь документ у кожен запит та не обходитися без векторного пошуку?

  • Витрати та затримки. Кожен надісланий токен коштує грошей та займає час. При наявності бази знань з 500 сторінок надсилання всієї інформації до кожного запитання є марним, якщо відповідь міститься в одному абзаці.
  • Погіршена здатність до згадування. Навіть за наявності великих обсягів даних моделі значно гірше використовують інформацію, розташовану посеред запиту, порівняно з інформацією, яка надається безпосередньо та коротко.
  • Масштабування. Вставка всієї інформації призводить до невдачі, як тільки база знань перевищує обсяг даних, який може бути оброблений — наприклад, при наявності тисяч PDF-файлів, повної корпоративної вікі чи років історії запитів на підтримку.

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

Помилки, які з’являються рано

  • Погане розділення тексту на частини псує процес отримання інформації, незалежно від якості моделі. Якщо таблиця розділена навпіл або речення переривається посеред думки, ніщо подальше не може це виправити. Перевіряйте якість отриманої інформації, перш ніж звинувачувати модель.
  • Несумісні моделі ембеддингів роблять показники схожості беззмістовними. Індекс, створений за допомогою text-embedding-3-small, не можна запитувати за допомогою іншої моделі.
  • Отримання занадто малої кількості чи занадто великої кількості частин тексту шкодить процесу. При k=1 можна пропустити справжню відповідь, а при k=20 модель потопає у шумі та збільшується витрата ресурсів. Почніть з трьох-п’яти частин та коригуйте значення на основі результатів.
  • Без альтернативного підходу у разі відсутності інформації модель все одно створюватиме відповідь, навіть коли перед нею є відповідний контекст. Чітко визначте, що робити, коли в контексті бракує потрібної інформації.

Як рухатися далі

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

  • Гібридний пошук поєднує векторну схожість із зіставленням ключових слів, оскільки деякі запити, такі як точні коди продуктів чи назви, краще обробляються за допомогою ключових слів.
  • Поновна класифікація спочатку отримує ширший набір кандидатів, а потім дозволяє другій, більш точній моделі переупорядкувати їх, перш ніж мовна модель їх побачить.
  • Фільтрація метаданих додає до кожного фрагмента таку інформацію, як дата, джерело чи відділ, щоб можна було звузити коло результатів ще до того, як взагалі буде проведено пошук за схожістю.

Жоден з цих факторів не має значення, поки базовий цикл — розділення на фрагменти, вставка ембеддингів, зберігання, пошук та генерація відповідей — функціонує належним чином. Найшвидший спосіб справді це зрозуміти — це завантажити важливий PDF, навмисно створити проблеми, наприклад, використати несумісні моделі ембеддингів чи фрагменти без перетину, та спостерігати за погіршенням якості відповідей. За допомогою RAG способи збоїв навчають більше, ніж успішний сценарій роботи.

Пов’язана література

  • Практичні нотатки: Три рівні Google Cloud RAG: векторний пошук, двигун RAG та механізм отримання даних від агента — Детальний огляд Практичних нотаток: Три рівні Google Cloud RAG: векторний пошук, двигун RAG та механізм отримання даних від агента: контракти, перевірки та шаблони коду для використання.
  • RAG із зберіганням стану в Databricks: багатокрокова пам’ять за допомогою Lakebase та LangGraph — Надання агенту RAG у Databricks справжньої пам’яті для розмов: обробка PDF-файлів, створення векторного пошуку, зберігання записів розмов у Lakebase Postgres, а потім розгортання через MLflow та Model Serving.
  • Як стати інженером штучного інтелекту у 2026 році без диплома з інформатики — План від нуля до працевлаштування для того, щоб стати інженером штучного інтелекту у 2026 році без диплома з інформатики: сім сфер навичок, невеликі проекти, практичний досвід та ефективне налагодження контактів.