LangChain

LangChain RAG: поиск по документам и генерация ответов | Курс LangChain урок 8

LangChain RAG: поиск по документам и генерация ответов | Курс LangChain урок 8
Mikhail
Автор
Mikhail
Опубликовано 23.02.2026
0,0
Views 5

Цель урока

Вы научитесь загружать документы, разбивать их на чанки, строить векторное хранилище и выполнять семантический поиск. И затем собирать полноценную RAG цепочку, которая отвечает на вопросы на основе ваших данных, а не выдумывает факты.

Необходимые знания:

  • Уроки 1–5 (LCEL, промпты, нелинейные пайплайны)
  • Базовое понимание, что такое векторы и similarity

Ключевые концепции:

  • Зачем нужен RAG и чем он лучше fine-tuning
  • Document loaders
  • Text splitters и стратегии чанкинга
  • Embeddings
  • VectorStore и FAISS
  • Retriever
  • Цепочка RAG

Зачем нужен RAG

У LLM есть два фундаментальных ограничения:

  1. Знания ограничены датой обучения - модель не знает о событиях после последней даты в обучающих данных (cutoff)
  2. Нет доступа к приватным данным - документация компании, внутренние базы знаний, личные файлы

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 систему.

<< Урок 7

Урок 9 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы