Главная / Статьи

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 году без диплома по информатике: семь областей навыков, небольшие проекты, практический опыт и эффективное налаживание контактов.