Большие языковые модели (LLMs), такие как GPT-4 и Claude, необычайно мощны, но страдают от фундаментального ограничения: их знания заморожены на момент обучения. Они не могут получить доступ к вашим внутренним документам, вашей базе данных или информации в реальном времени. Генерация с дополненной выборкой (RAG) решает именно эту проблему, сочетая генеративную мощь LLM со способностью извлекать информацию из внешних источников.
Проблема: ограничения LLM
Прежде чем говорить о RAG, важно понять, зачем он нужен.
- Статические знания: LLM знает только то, что видел во время обучения. Если вы спросите о событии, произошедшем после его отсечки, он не сможет ответить.
- Галлюцинации: Когда LLM не знает ответа, он склонен его выдумывать, генерируя правдоподобную, но полностью ложную информацию.
- Нет доступа к приватным данным: Обычный LLM не имеет доступа к внутренней документации вашей компании, тикетам или кодовой базе.
RAG решает все три эти проблемы, предоставляя модели релевантный контекст, извлечённый из внешних источников в момент запроса.
Что такое RAG?
Генерация с дополненной выборкой — это архитектура, которая обогащает промпт, отправляемый LLM, информацией, извлечённой из внешней базы знаний. Вместо того чтобы полагаться исключительно на параметрические знания модели, RAG сначала ищет релевантную информацию, а затем внедряет её в промпт, позволяя модели генерировать точные, обоснованные ответы.
Как RAG работает в деталях
Архитектура RAG состоит из двух основных фаз: Индексация (офлайн) и Извлечение + Генерация (онлайн).
Фаза 1: Индексация (загрузка документов)
Фаза индексации подготавливает ваши документы для семантического поиска. Она состоит из четырёх шагов.
1. Загрузка документов
Документы могут поступать из любого источника: PDF-файлы, веб-страницы, базы данных, Markdown-файлы, API. Document Loader читает эти документы и преобразует их в структурированный текст.
2. Разбиение текста (Chunking)
LLM имеют ограниченное контекстное окно, а документы могут быть очень длинными. Text Splitter делит документы на меньшие фрагменты, называемые chunks. Качество разбиения критически важно: слишком маленькие chunks теряют контекст, а слишком большие размывают релевантность.
Наиболее распространённые стратегии:
- Рекурсивное разбиение по символам: Рекурсивно разбивает текст, используя разделители типа
\n\n,\n,., соблюдая структуру документа. - Семантическое разбиение: Использует embeddings для нахождения естественных точек разрыва в тексте.
- Перекрытие chunks: Включает перекрытие между последовательными chunks для сохранения контекста на границах.
3. Embedding
Каждый chunk преобразуется в числовой вектор (embedding) с помощью модели embedding (например, text-embedding-3-small от OpenAI). Эти векторы захватывают семантическое значение текста: предложения с похожим смыслом будут иметь близкие векторы в многомерном пространстве.
4. Vector Store
Векторы сохраняются в Vector Store (или векторной базе данных), такой как ChromaDB, Pinecone, Weaviate или FAISS. Эта база данных оптимизирована для поиска по сходству: по заданному запросу она находит наиболее похожие векторы (и, следовательно, наиболее релевантные текстовые chunks).
Фаза 2: Извлечение + Генерация
Когда пользователь задаёт вопрос:
- Вопрос преобразуется в embedding с использованием той же модели embedding.
- Vector Store находит наиболее похожие chunks через поиск по сходству (обычно косинусное сходство или евклидово расстояние).
- Извлечённые chunks вставляются в промпт в качестве контекста.
- LLM генерирует ответ на основе предоставленного контекста.
Построение RAG-пайплайна с LangChain
LangChain — самый популярный фреймворк на Python (и JavaScript) для создания приложений на основе LLM. Он предоставляет высокоуровневые абстракции для каждого компонента RAG-пайплайна.
Установка
pip install langchain langchain-openai langchain-community chromadb
Шаг 1: Загрузка документов
LangChain предоставляет десятки Document Loaders для различных источников данных.
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # Load a PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # Load a web page web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # Load all .md files from a directory dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
Шаг 2: Разбиение документов на chunks
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"Original documents: {len(all_docs)}, Chunks: {len(chunks)}")
Параметр chunk_overlap критически важен: он создаёт перекрытие между последовательными chunks, чтобы контекст не терялся на границах.
Шаг 3: Создание Embeddings и Vector Store
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
Шаг 4: Создание Retriever
Retriever — это компонент, который по заданному запросу извлекает наиболее релевантные chunks из vector store.
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("How does authentication work?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
Шаг 5: Построение RAG-цепочки
Теперь соединим всё вместе с LLM и шаблоном промпта.
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" Answer the question based only on the provided context. If the context does not contain enough information, say you don't know. Context: {context} Question: {question} Answer: """) 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() ) response = rag_chain.invoke("How does authentication work in the system?") print(response)
Продвинутые техники RAG
Базовый пайплайн работает хорошо, но существует несколько техник для значительного улучшения качества ответов.
Мульти-запросное извлечение
Иногда запрос пользователя неоднозначен или не совпадает с языком, используемым в документах. Multi-Query Retriever автоматически генерирует варианты исходного вопроса для захвата множества перспектив.
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("What are the security best practices?")
Контекстуальное сжатие
Не весь контент в chunk релевантен запросу. Contextual Compression Retriever использует LLM для извлечения только релевантных частей из каждого полученного chunk.
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
Гибридный поиск
Чисто семантический поиск не всегда оптимален. Гибридный поиск сочетает семантический поиск (embeddings) с лексическим поиском (BM25, сопоставление ключевых слов) для достижения лучших результатов.
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
Разговорный RAG (с памятью)
Чтобы построить RAG-чатбот, который помнит контекст разговора, необходимо добавить память, которая переформулирует вопросы пользователя с учётом истории разговора.
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "Given the chat history and the user's latest question, " "reformulate the question so it is understandable without the history."), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
Лучшие практики
- Выбирайте правильный размер chunk: Экспериментируйте с различными размерами (500-1500 токенов). Меньшие chunks для точных ответов, большие для более широкого контекста.
- Используйте метаданные документов: Добавляйте источник, дату и категорию в качестве метаданных к chunks. Это позволяет фильтровать результаты при извлечении.
- Оценивайте качество: Используйте фреймворки вроде RAGAS для измерения таких метрик, как faithfulness, relevancy и context precision.
- Управляйте обновлениями документов: Реализуйте пайплайн повторной загрузки для поддержания синхронизации vector store с вашими источниками данных.
- Добавьте re-ranker: После первоначального извлечения используйте модель re-ranking (например, Cohere Rerank) для переупорядочивания результатов на основе реальной релевантности.
Заключение
RAG стал стандартной архитектурой для создания AI-приложений, которым нужен доступ к конкретным, актуальным знаниям. LangChain значительно упрощает реализацию, предоставляя абстракции для каждого компонента пайплайна.
Следующие шаги:
- Экспериментируйте локально: Начните с ChromaDB и нескольких документов, чтобы ознакомиться с пайплайном.
- Изучите LangSmith: Используйте LangSmith для мониторинга и отладки ваших цепочек в продакшене.
- Попробуйте разные модели embedding: Сравните модели
text-embedding-3-small,text-embedding-3-largeи open-source модели от Sentence Transformers. - Ознакомьтесь с документацией: Документация LangChain — отличный и постоянно обновляемый ресурс.