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-интеракциях.
Теоретические вопросы
- Назовите три стадии QA в RAG.
- Какие ограничения имеет окно контекста и как MapReduce/Refine помогают их обойти?
- Зачем нужна векторная БД (VectorDB) для ретривала в RAG?
- Как
RetrievalQA
объединяет ретривал и генерацию? - Сравните подходы MapReduce и Refine.
- Какие практические факторы важны в распределённых системах (сетевые задержки, сериализация)?
- Почему важно экспериментировать с обоими подходами?
- Как отсутствие истории диалога влияет на обработку последующих вопросов?
- Зачем интегрировать память диалога в RAG?
- Что следует изучать дальше для углубления в RAG?
Практические задания
- Инициализируйте векторную БД (Chroma + OpenAIEmbeddings) и выведите число документов в ней.
- Настройте
RetrievalQA
с кастомным промптом, указав модель и директорию для хранения данных. - Продемонстрируйте работу
MapReduce
иRefine
на одном запросе, выведите полученные результаты. - Симулируйте уточняющий вопрос без сохранения контекста диалога, чтобы показать ограничение
RetrievalQA
.