LangChain RAG: поиск по документам и генерация ответов | Курс LangChain урок 8
Цель урока
Вы научитесь загружать документы, разбивать их на чанки, строить векторное хранилище и выполнять семантический поиск. И затем собирать полноценную RAG цепочку, которая отвечает на вопросы на основе ваших данных, а не выдумывает факты.
Необходимые знания:
- Уроки 1–5 (LCEL, промпты, нелинейные пайплайны)
- Базовое понимание, что такое векторы и similarity
Ключевые концепции:
- Зачем нужен RAG и чем он лучше fine-tuning
- Document loaders
- Text splitters и стратегии чанкинга
- Embeddings
- VectorStore и FAISS
- Retriever
- Цепочка RAG
Зачем нужен RAG
У LLM есть два фундаментальных ограничения:
- Знания ограничены датой обучения - модель не знает о событиях после последней даты в обучающих данных (cutoff)
- Нет доступа к приватным данным - документация компании, внутренние базы знаний, личные файлы
Fine-tuning обучает модель на новых данных, но это дорого, медленно и плохо работает для фактических знаний, модели склонны галлюцинировать даже после файн-тюнинга.
RAG (Retrieval-Augmented Generation) решает это иначе:
Вопрос пользователя
↓
Поиск релевантных фрагментов в базе знаний
↓
Передача найденных фрагментов + вопрос в модель
↓
Ответ на основе реальных документов
Преимущества RAG:
- Актуальные данные без переобучения
- Модель отвечает на основе конкретных источников
- Легко обновлять базу знаний
- Можно указать источник каждого факта
Архитектура RAG-системы
RAG состоит из двух фаз:
Индексирование (делается один раз или при обновлении данных):
Документы → Загрузка → Разбивка на чанки → Создание эмбеддингов → Сохранение в VectorStore
Запрос (при каждом вопросе пользователя):
Вопрос → Эмбеддинг вопроса → Поиск похожих чанков → Передача в LLM → Ответ
Document Loaders: загрузка данных
LangChain поддерживает десятки форматов. Пример самых распространённых:
# Текстовый файл
from langchain_community.document_loaders import TextLoader
loader = TextLoader("readme.txt", encoding="utf-8")
docs = loader.load()
# docs — список Document объектов
# PDF
from langchain_community.document_loaders import PyPDFLoader
loader = PyPDFLoader("document.pdf")
docs = loader.load()
# Каждая страница — отдельный Document
# Веб-страница (требует: pip install beautifulsoup4)
from langchain_community.document_loaders import WebBaseLoader
loader = WebBaseLoader("https://docs.python.org/3/library/asyncio.html")
docs = loader.load()
# Директория с файлами
from langchain_community.document_loaders import DirectoryLoader
loader = DirectoryLoader("./docs/", glob="**/*.md", loader_cls=TextLoader)
docs = loader.load()
Каждый Document содержит:
page_content- текстmetadata- источник, страница и другие метаданные
from langchain_community.document_loaders import TextLoader
loader = TextLoader("readme.txt", encoding="utf-8")
docs = loader.load()
doc = docs[0]
print(doc.page_content[:200]) # текст документа
print(doc.metadata) # {"source": "readme.txt"}
Text Splitters: разбивка на чанки
Документы обычно слишком длинные, чтобы передавать целиком. Нужно разбить на чанки, так чтобы каждый чанк содержал законченную мысль, а поиск находил максимально релевантный фрагмент.
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
loader = TextLoader("readme.txt", encoding="utf-8")
docs = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # максимальный размер чанка в символах
chunk_overlap=200, # перекрытие между чанками (для сохранения контекста)
separators=["\n\n", "\n", ". ", " ", ""], # разделители по приоритету
)
chunks = splitter.split_documents(docs)
print(f"Документов: {len(docs)}, чанков: {len(chunks)}")
print(f"Первый чанк ({len(chunks[0].page_content)} символов):")
print(chunks[0].page_content[:300])
print(chunks[0].metadata) # метаданные сохраняются
Выбор размера чанка
| Размер | Когда использовать |
|---|---|
| 200–500 символов | Точный поиск по конкретным фактам |
| 500–1500 символов | Общий случай, рекомендован по умолчанию |
| 1500–3000 символов | Когда нужен широкий контекст, длинные рассуждения |
chunk_overlap 10–20% от chunk_size. Если предложение разорванное на границе чанков, попадет в оба.
Splitter для кода
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
loader = TextLoader("script.py", encoding="utf-8")
docs = loader.load()
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=1500,
chunk_overlap=200,
)
chunks = python_splitter.split_documents(docs)
# Разбивает по границам функций/классов, а не по символам
Embeddings: превращаем текст в векторы
Что такое эмбеддинг
Эмбеддинг - это числовое представление текста в виде вектора (списка чисел с плавающей точкой). Специальная нейросеть (embedding model) принимает текст и возвращает вектор из N чисел, например [-0.02, 0.14, 0.87, ..., 0.03].
Схожие по смыслу тексты получают близкие векторы. Это работает потому, что embedding-модель обучена на огромных объёмах текста и научилась кодировать семантику в числа.
"Как работает asyncio?" → [0.12, -0.05, 0.87, ...]
"event loop в Python" → [0.11, -0.04, 0.89, ...] ← похоже по смыслу
"Рецепт борща" → [-0.73, 0.92, -0.11, ...] ← совсем другой смысл
Косинусное сходство
Чтобы сравнить два вектора, используют косинусное сходство (cosine similarity):
similarity = cos(угол между векторами) = (A · B) / (|A| × |B|)
- 1.0 — векторы идентичны (одинаковый смысл)
- 0.0 — векторы перпендикулярны (нет связи)
- -1.0 — противоположный смысл
FAISS по умолчанию использует L2 distance (евклидово расстояние), чем меньше значение, тем ближе векторы. При поиске, результаты с меньшим score, более релевантны.
Размерность
Размерность вектора это количество чисел в нём. Больше размерность, больше деталей о смысле, но дороже хранить и медленнее искать.
| Модель | Размерность | Применение |
|---|---|---|
all-MiniLM-L6-v2 (HuggingFace) |
384 | Быстрая, бесплатная, локальная |
all-mpnet-base-v2 (HuggingFace) |
768 | Лучшее качество из бесплатных |
text-embedding-3-small (OpenAI) |
1536 | Хорошее качество, платная |
text-embedding-3-large (OpenAI) |
3072 | Максимальное качество, платная |
Почему используем HuggingFace Embeddings
В этом курсе API провайдер (Rus-GPT) пока не поддерживает embedding API, только чат-модели. Поэтому для эмбеддингов используем локальные модели через HuggingFace. Это бесплатно и модели при первом запуске скачиваются автоматически (~90 МБ).
Для продакшн с OpenAI API, просто замените HuggingFaceEmbeddings на OpenAIEmbeddings.
Пример
from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# Один текст
vector = embeddings.embed_query("Как работает asyncio?")
print(f"Размерность: {len(vector)}") # 384
print(f"Первые 5 значений: {vector[:5]}") # [-0.02, 0.01, ...]
# Несколько текстов сразу (эффективнее)
vectors = embeddings.embed_documents([
"Python — интерпретируемый язык",
"FastAPI — веб-фреймворк для Python",
"Django — полнофункциональный фреймворк",
])
print(f"Создано {len(vectors)} эмбеддингов")
Модель all-MiniLM-L6-v2 это хороший баланс скорости и качества для большинства задач.
Для лучшего качества используйте all-mpnet-base-v2.
VectorStore: хранение и поиск
FAISS - это быстрое локальное векторное хранилище, хорошо подходит для разработки и небольших баз.
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
# Локальная модель не требует API ключа, скачивается при первом запуске (~90 МБ)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
# Создаем хранилище из текстов
texts = [
"FastAPI — современный веб-фреймворк Python с автоматической документацией.",
"Django ORM позволяет работать с базами данных как с объектами Python.",
"asyncio в Python реализует конкурентность через event loop и корутины.",
"Pydantic обеспечивает валидацию данных через аннотации типов.",
"SQLAlchemy это ORM, поддерживает много СУБД.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
# Семантический поиск
results = vectorstore.similarity_search("как работать с базой данных в Python", k=2)
for doc in results:
print(doc.page_content)
print()
# Найдет про Django ORM и SQLAlchemy, семантически близкие фрагменты
Поиск с оценкой релевантности
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
texts = [
"FastAPI — современный веб-фреймворк Python с автоматической документацией.",
"Django ORM позволяет работать с базами данных как с объектами Python.",
"asyncio в Python реализует конкурентность через event loop и корутины.",
"Pydantic обеспечивает валидацию данных через аннотации типов.",
"SQLAlchemy это ORM, поддерживает много СУБД.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
results_with_scores = vectorstore.similarity_search_with_score(
"как работать с базой данных в Python", k=3
)
for doc, score in results_with_scores:
print(f"Score: {score:.4f} | {doc.page_content[:80]}")
# Чем меньше score у FAISS (L2 distance), тем ближе документ
Сохранение и загрузка индекса
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
texts = ["FastAPI — веб-фреймворк для Python.", "asyncio — библиотека для конкурентности."]
vectorstore = FAISS.from_texts(texts, embeddings)
# Сохраняем на диск
vectorstore.save_local("faiss_index")
# Загружаем при следующем запуске, не нужно пересоздавать эмбеддинги
vectorstore = FAISS.load_local(
"faiss_index",
embeddings,
allow_dangerous_deserialization=True,
)
Retriever: интерфейс для поиска
Retriever это унифицированный интерфейс поверх VectorStore,
который умеет встраиваться в LCEL.
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
texts = [
"FastAPI — современный веб-фреймворк Python с автоматической документацией.",
"Django ORM позволяет работать с базами данных как с объектами Python.",
"asyncio в Python реализует конкурентность через event loop и корутины.",
"Pydantic обеспечивает валидацию данных через аннотации типов.",
"SQLAlchemy это ORM, поддерживает много СУБД.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
retriever = vectorstore.as_retriever(
search_type="similarity", # тип поиска
search_kwargs={"k": 4}, # количество результатов
)
# Retriever - это Runnable: принимает строку, возвращает список Document
docs = retriever.invoke("как работать с базой данных?")
print(f"Найдено: {len(docs)} документов")
MMR: разнообразие результатов
По умолчанию similarity search может вернуть похожие чанки. MMR (Maximal Marginal Relevance) балансирует релевантность и разнообразие.
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
texts = [
"FastAPI — современный веб-фреймворк Python с автоматической документацией.",
"Django ORM позволяет работать с базами данных как с объектами Python.",
"asyncio в Python реализует конкурентность через event loop и корутины.",
"Pydantic обеспечивает валидацию данных через аннотации типов.",
"SQLAlchemy это ORM, поддерживает много СУБД.",
]
vectorstore = FAISS.from_texts(texts, embeddings)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 20}, # из 20 кандидатов выбрать 4 разных
)
docs = retriever.invoke("как работать с базой данных?")
print(f"Найдено: {len(docs)} документов")
Полная цепочка RAG
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
load_dotenv()
# --- Индексирование ---
# 1. Загрузка
# Создайте файл knowledge_base.txt с любыми данными и задайте вопрос по ним
loader = TextLoader("knowledge_base.txt", encoding="utf-8")
docs = loader.load()
# 2. Разбивка
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(docs)
# 3. Эмбеддинги и VectorStore
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = FAISS.from_documents(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# --- RAG-цепочка ---
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Ты ассистент, отвечающий на вопросы на основе предоставленного контекста.
Правила:
- Отвечай только на основе контекста ниже
- Если ответа нет в контексте — честно скажи об этом
- Не придумывай информацию
Контекст:
{context}"""),
("human", "{question}"),
])
def format_docs(docs: list) -> str:
"""Форматирует список документов в единый текст."""
return "\n\n---\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
# Задайте вопрос по содержимому вашего knowledge_base.txt
answer = rag_chain.invoke("Ваш вопрос здесь")
print(answer)
Ключевая строка retriever | format_docs. Retriever принимает вопрос,
возвращает документы, format_docs собирает их в единый текст для промпта.
RAG с источниками
Когда нужно показать пользователю из каких документов взят ответ.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
load_dotenv()
# Пример базы знаний в памяти
texts = [
"asyncio — библиотека Python для написания конкурентного кода с async/await.",
"FastAPI — современный Python веб-фреймворк с автоматической документацией.",
]
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = FAISS.from_texts(texts, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
prompt = ChatPromptTemplate.from_messages([
("system", "Отвечай только на основе контекста:\n{context}"),
("human", "{question}"),
])
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt | model | StrOutputParser()
)
# Параллельно получаем и контекст для промпта, и исходные документы
rag_chain_with_sources = RunnableParallel(
answer=rag_chain,
sources=retriever,
)
result = rag_chain_with_sources.invoke("Как работает asyncio?")
print("Ответ:")
print(result["answer"])
print("\nИсточники:")
for doc in result["sources"]:
source = doc.metadata.get("source", "неизвестно")
print(f" - {source}: {doc.page_content[:80]}...")
Индексирование из нескольких источников
Этот пример показывает загрузку из нескольких источников сразу. Перед запуском подготовьте структуру:
./pdfs/ - положите сюда любые PDF файлы
./docs/ - положите сюда любые MD файлы
Если какой-то источник вам не нужен, то просто удалите соответствующий блок из кода.
from dotenv import load_dotenv
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import (
DirectoryLoader,
PyPDFLoader,
WebBaseLoader,
TextLoader,
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
load_dotenv()
all_docs = []
# PDF документы
pdf_loader = DirectoryLoader("./pdfs/", glob="**/*.pdf", loader_cls=PyPDFLoader)
all_docs.extend(pdf_loader.load())
# Markdown файлы (loader_kwargs передаёт аргументы в TextLoader)
md_loader = DirectoryLoader(
"./docs/", glob="**/*.md",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"},
)
all_docs.extend(md_loader.load())
# Веб-страницы (требует: pip install beautifulsoup4)
urls = [
"https://docs.python.org/3/library/asyncio.html",
"https://fastapi.tiangolo.com/",
]
web_loader = WebBaseLoader(urls)
all_docs.extend(web_loader.load())
print(f"Загружено документов: {len(all_docs)}")
# Разбиваем всё вместе
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(all_docs)
print(f"Чанков: {len(chunks)}")
# Строим единый индекс
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = FAISS.from_documents(chunks, embeddings)
vectorstore.save_local("combined_index")
Распространённые ошибки
1. Слишком маленькие чанки, теряется контекст
# Плохо: чанк в 100 символов это обрывок предложения без смысла
splitter = RecursiveCharacterTextSplitter(chunk_size=100)
Начинай с 500–1000 символов и корректируй по качеству ответов.
2. Нулевой overlap, разрыв на границе
# Плохо: ключевая информация может оказаться на стыке чанков
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
Всегда используй overlap 10–20% от chunk_size.
3. Слишком много результатов в контексте
# Плохо: 20 чанков по 1000 символов = 20k символов контекста
retriever = vectorstore.as_retriever(search_kwargs={"k": 20})
Оптимально: 3–6 чанков. Если больше, то дороже и часто хуже (модель теряет суть).
4. Модель галлюцинирует без четких инструкций
# Плохо: нет инструкции отвечать только по контексту
prompt = ChatPromptTemplate.from_messages([
("human", "Контекст: {context}\n\nВопрос: {question}"),
])
Явно укажи: "Отвечай только на основе предоставленного контекста. Если ответа нет, скажи об этом."
5. Переиндексирование при каждом запуске
# Медленно и дорого: пересоздаём эмбеддинги при каждом старте
vectorstore = FAISS.from_documents(chunks, embeddings) # каждый раз
Сохраняй индекс на диск и загружай при старте. Пересоздавай только при обновлении данных.
Пример кода
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
load_dotenv()
# --- База знаний (в реальном проекте загружается из файлов) ---
raw_texts = [
"""FastAPI — современный высокопроизводительный веб-фреймворк для создания API на Python.
Основан на стандартных аннотациях типов Python. Автоматически генерирует документацию OpenAPI.
Использует Pydantic для валидации данных. Поддерживает асинхронный код через async/await.""",
"""asyncio — библиотека Python для написания конкурентного кода с использованием синтаксиса async/await.
Event loop управляет выполнением корутин. asyncio.gather() запускает несколько корутин параллельно.
asyncio.sleep() используется вместо time.sleep() в асинхронном коде.""",
"""Pydantic — библиотека для валидации данных и управления настройками через аннотации типов Python.
BaseModel — базовый класс для создания схем данных. Field() позволяет добавлять метаданные к полям.
Pydantic v2 значительно быстрее v1 благодаря реализации на Rust.""",
"""SQLAlchemy — мощная ORM-библиотека для Python. Поддерживает PostgreSQL, MySQL, SQLite и другие СУБД.
Предоставляет два API: Core (низкоуровневый SQL) и ORM (объектно-ориентированный).
Alembic — инструмент для миграций базы данных, разработанный командой SQLAlchemy.""",
"""Docker — платформа для контейнеризации приложений. Dockerfile описывает образ контейнера.
docker-compose.yml позволяет запускать несколько контейнеров вместе.
Volumes используются для сохранения данных между перезапусками контейнера.""",
"""pytest — фреймворк для тестирования Python-приложений. Фикстуры (fixtures) позволяют
переиспользовать тестовые данные и настройки. parametrize позволяет запускать один тест
с разными входными данными. pytest-asyncio нужен для тестирования асинхронного кода.""",
]
# Создаем Document-объекты
docs = [Document(page_content=text, metadata={"source": f"doc_{i}"})
for i, text in enumerate(raw_texts)]
# --- Индексирование ---
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
chunks = splitter.split_documents(docs)
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
INDEX_PATH = "demo_index"
if os.path.exists(INDEX_PATH):
vectorstore = FAISS.load_local(INDEX_PATH, embeddings, allow_dangerous_deserialization=True)
print("Индекс загружен с диска")
else:
vectorstore = FAISS.from_documents(chunks, embeddings)
vectorstore.save_local(INDEX_PATH)
print(f"Индекс создан: {len(chunks)} чанков")
retriever = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10})
# --- RAG-цепочка ---
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Ты технический ассистент. Отвечай только на основе контекста ниже.
Если ответа нет в контексте — скажи: "В доступных документах нет информации по этому вопросу."
Контекст:
{context}"""),
("human", "{question}"),
])
def format_docs(docs):
return "\n\n".join(f"[{doc.metadata.get('source', '?')}]\n{doc.page_content}" for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
# RAG с источниками
rag_with_sources = RunnableParallel(
answer=rag_chain,
sources=(retriever),
)
# --- Тестирование ---
questions = [
"Как запустить несколько корутин параллельно в asyncio?",
"Чем Pydantic v2 отличается от v1?",
"Как хранить данные в Docker между перезапусками?",
"Как тестировать асинхронный код в pytest?",
"Как настроить Kubernetes?", # нет в базе знаний
]
for q in questions:
print(f"\nВопрос: {q}")
result = rag_with_sources.invoke(q)
print(f"Ответ: {result['answer']}")
sources = {doc.metadata.get('source') for doc in result['sources']}
print(f"Источники: {', '.join(sources)}")
print("-" * 60)
Практическое задание
Создайте RAG-систему для ответов на вопросы о библиотеках Python.
Требования:
1) Создайте базу знаний. Минимум 10 текстовых фрагментов о разных библиотеках (requests, httpx, aiohttp, celery, redis, fastapi, sqlalchemy, выберите любые)
2) Реализуйте индексирование с сохранением на диск. При повторном запуске, загружать с диска, не пересоздавать
3) Постройте цепочку RAG с:
- MMR ретривером (k=3)
- Промптом, который запрещает модели выходить за пределы контекста
- Выводом источников вместе с ответом
4) Добавьте команду /search, выводит топ-3 чанка по запросу без генерации ответа, только поиск
5) Реализуйте интерактивный режим. Пользователь вводит вопросы в цикле, /quit - выход
Ожидаемое поведение:
> Как сделать асинхронный HTTP запрос?
Ответ: [ответ на основе документов]
Источники: doc_2, doc_5
> /search redis pub/sub
Результаты поиска:
1. [doc_7] Redis поддерживает паттерн pub/sub...
2. [doc_3] ...
3. [doc_8] ...
> Как настроить Kubernetes?
Ответ: В доступных документах нет информации по этому вопросу.
> /quit
Итоги урока
Мы построили базовый RAG. Но у него есть слабое место, retriever ищет по исходному вопросу пользователя, а он может быть размытым, неточным или зависеть от контекста диалога. В следующем уроке разберём продвинутые техники RAG: переформулировку запроса, гибридный поиск, re-ranking и добавление памяти в RAG систему.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru