Перейти к содержанию

2.6 RAG-системы. Техники для ответов на вопросы

Retrieval Augmented Generation (RAG) соединяет поиск и генерацию, меняя подход к работе с большими корпусами данных для точных QA‑систем и чат‑ботов. Критически важный этап — подача найденных документов вместе с исходным запросом в модель для генерации ответа. После извлечения релевантных материалов их нужно эффективно синтезировать в связный ответ, совмещая содержание с контекстом запроса и задействуя возможности модели. Общий поток выглядит просто: система принимает вопрос; извлекает релевантные фрагменты из векторного хранилища; подаёт найденное вместе с вопросом в LLM для формирования ответа. По умолчанию можно отправить все найденные части в контекст, но из‑за ограничений окна контекста чаще применяют стратегии MapReduce, Refine или MapRerank — они агрегируют или последовательно уточняют ответы по множеству документов.

Перед использованием LLM для QA убедитесь, что окружение настроено: импорты, ключи API, версии моделей и прочее.

import os
from openai import OpenAI
from dotenv import load_dotenv
import datetime

# Загружаем переменные окружения и настраиваем ключ OpenAI API
load_dotenv()
client = OpenAI()

# Настраиваем версионирование LLM
current_date = datetime.datetime.now().date()
llm_name = "gpt-3.5-turbo"
print(f"Используется версия LLM: {llm_name}")

Далее следует извлечение документов, релевантных запросу, из векторной базы данных (VectorDB), где хранятся эмбеддинги.

# Импортируем необходимые библиотеки для векторной базы данных и генерации эмбеддингов
from langchain.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# Указываем директорию, где векторная база данных будет сохранять свои данные
documents_storage_directory = 'docs/chroma/'

# Инициализируем генератор эмбеддингов, используя эмбеддинги OpenAI
embeddings_generator = OpenAIEmbeddings()

# Инициализируем векторную базу данных с указанной директорией хранения и функцией эмбеддинга
vector_database = Chroma(persist_directory=documents_storage_directory, embedding_function=embeddings_generator)

# Отображаем текущее количество документов в векторной базе данных для проверки инициализации
print(f"Количество документов в VectorDB: {vector_database._collection.count()}")

RetrievalQA объединяет извлечение (retrieval) и генерацию (generation): LLM отвечает на основе извлечённых документов. Сначала инициализируем языковую модель,

from langchain_openai import ChatOpenAI

# Инициализируем чат-модель с выбранной версией LLM
language_model = ChatOpenAI(model=llm_name, temperature=0)

затем настроим цепочку RetrievalQA с пользовательским промптом,

# Импортируем необходимые модули из библиотеки langchain
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# Создаем пользовательский шаблон промпта для языковой модели
# Шаблон направляет модель эффективно использовать предоставленный контекст для ответа на вопрос
custom_prompt_template = """To better assist with the inquiry, consider the details provided below as your reference...
{context}
Inquiry: {question}
Insightful Response:"""

# Инициализируем цепочку RetrievalQA с пользовательским шаблоном промпта
question_answering_chain = RetrievalQA.from_chain_type(
    language_model,
    retriever=vector_database.as_retriever(),
    return_source_documents=True,
    chain_type_kwargs={"prompt": PromptTemplate.from_template(custom_prompt_template)}
)

и проверим ответ на простом запросе.

# Задаем запрос системе
query = "Is probability a class topic?"
response = qa_chain({"query": query})
print("Ответ:", response["result"])

Далее — продвинутые типы QA‑цепочек. Техники MapReduce и Refine помогают обходить ограничение окна контекста, обрабатывая множество документов: MapReduce параллельно агрегирует информацию, а Refine последовательно уточняет ответ.

# Настраиваем цепочку ответов на вопросы для использования техники MapReduce
# Эта конфигурация позволяет агрегировать ответы из нескольких документов
question_answering_chain_map_reduce = RetrievalQA.from_chain_type(
    language_model,
    retriever=vector_database.as_retriever(),
    chain_type="map_reduce"
)

# Выполняем технику MapReduce с запросом, предоставленным пользователем
response_map_reduce = question_answering_chain_map_reduce({"query": query})

# Выводим агрегированный ответ, полученный через технику MapReduce
print("Ответ MapReduce:", response_map_reduce["result"])

# Настраиваем цепочку ответов на вопросы для использования техники Refine
# Этот подход позволяет последовательно уточнять ответ на основе запроса
question_answering_chain_refine = RetrievalQA.from_chain_type(
    language_model,
    retriever=vector_database.as_retriever(),
    chain_type="refine"
)

# Выполняем технику Refine с тем же запросом, предоставленным пользователем
response_refine = question_answering_chain_refine({"query": query})

# Выводим уточнённый ответ, демонстрируя процесс итеративного улучшения
print("Ответ Refine:", response_refine["result"])

Практически стоит учитывать: выбор между MapReduce и Refine зависит от задачи (первый — для быстрой агрегации из множества источников, второй — для высокой точности и итеративного улучшения); производительность в распределённых системах упирается в сетевые задержки и сериализацию; эффективность подходов меняется от данных — стоит экспериментировать.

Среди ограничений RetrievalQA заметно отсутствие сохранения истории диалога, что ухудшает обработку уточняющих вопросов. Демонстрация ограничения:

# Импортируем цепочку ответов на вопросы из гипотетической библиотеки
from some_library import question_answering_chain as qa_chain

# Определяем первоначальный запрос, связанный с содержанием курса
initial_question_about_course_content = "Does the curriculum cover probability theory?"
# Генерируем ответ на первоначальный запрос, используя цепочку ответов на вопросы
response_to_initial_question = qa_chain({"query": initial_question_about_course_content})

# Определяем уточняющий запрос без явного сохранения контекста беседы
follow_up_question_about_prerequisites = "Why are those prerequisites important?"
# Генерируем ответ на уточняющий запрос, снова используя цепочку ответов на вопросы
response_to_follow_up_question = qa_chain({"query": follow_up_question_about_prerequisites})

# Отображаем ответы на оба запроса — первоначальный и уточняющий
print("Ответ на первоначальный запрос:", response_to_initial_question["result"])
print("Ответ на уточняющий запрос:", response_to_follow_up_question["result"])

Это подчеркивает важность интеграции памяти диалога в RAG-системы.

Заключение

Продвинутые техники QA в RAG обеспечивают более динамичные и точные ответы. Аккуратная реализация RetrievalQA и работа с его ограничениями позволяют строить системы, способные вести содержательные диалоги с пользователем.

Что почитать дальше

  • Изучите последние достижения LLM и их влияние на RAG.
  • Исследуйте стратегии интеграции памяти диалога в RAG-фреймворки.

Эта глава служит базой для понимания и практики продвинутых QA-техник в RAG и дальнейших инноваций в AI-интеракциях.

Теоретические вопросы

  1. Назовите три стадии QA в RAG.
  2. Какие ограничения имеет окно контекста и как MapReduce/Refine помогают их обойти?
  3. Зачем нужна векторная БД (VectorDB) для ретривала в RAG?
  4. Как RetrievalQA объединяет ретривал и генерацию?
  5. Сравните подходы MapReduce и Refine.
  6. Какие практические факторы важны в распределённых системах (сетевые задержки, сериализация)?
  7. Почему важно экспериментировать с обоими подходами?
  8. Как отсутствие истории диалога влияет на обработку последующих вопросов?
  9. Зачем интегрировать память диалога в RAG?
  10. Что следует изучать дальше для углубления в RAG?

Практические задания

  1. Инициализируйте векторную БД (Chroma + OpenAIEmbeddings) и выведите число документов в ней.
  2. Настройте RetrievalQA с кастомным промптом, указав модель и директорию для хранения данных.
  3. Продемонстрируйте работу MapReduce и Refine на одном запросе, выведите полученные результаты.
  4. Симулируйте уточняющий вопрос без сохранения контекста диалога, чтобы показать ограничение RetrievalQA.