ChatGPT не знает ваши внутренние документы. Это очевидно — он обучен на данных до определённой даты и понятия не имеет о вашей документации, базе знаний или корпоративных процессах.
RAG (Retrieval-Augmented Generation) — это способ это исправить. Подключаете AI к своим данным, и он отвечает на вопросы, опираясь именно на них. Не галлюцинирует, не придумывает — цитирует ваши источники.
Сегодня строим это с нуля на Python за один вечер.
Что такое RAG и как он работает
RAG — это не отдельная модель. Это архитектурный паттерн: «поиск + генерация».
Упрощённо: когда пользователь задаёт вопрос, система сначала ищет релевантные куски в вашей базе документов, потом передаёт их в LLM вместе с вопросом. Модель видит контекст и отвечает на его основе.
Три шага:
- Индексация — разбиваем документы на chunks, превращаем их в векторы (числовые представления смысла), сохраняем в векторную БД
- Поиск — входящий вопрос тоже превращается в вектор, ищем ближайшие по смыслу chunks в базе
- Генерация — передаём найденные chunks + вопрос в LLM, получаем ответ
Векторная БД — это база данных, оптимизированная для поиска по смысловой близости, а не по точному совпадению текста. «Как отменить заказ» и «возврат товара» будут рядом, хотя слова разные.
Что будем строить
Чат-бот, который отвечает на вопросы по PDF-файлам. Пример: документация продукта, инструкции, корпоративная база знаний.
Стек:
- Python 3.11+
- LangChain — фреймворк для LLM-приложений
- ChromaDB — локальная векторная БД (работает без сервера, данные в папке)
- Claude API (Anthropic) — языковая модель
- PyPDF2 — парсинг PDF
Установка окружения
python -m venv rag-env
source rag-env/bin/activate # macOS/Linux
# rag-env\Scripts\activate # Windows
pip install langchain langchain-anthropic chromadb pypdf2 sentence-transformersНужен API-ключ от Anthropic. Берём на console.anthropic.com → API Keys. Кладём в переменную окружения:
export ANTHROPIC_API_KEY="sk-ant-..."Шаг 1: Загружаем и разбиваем документ
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Загружаем PDF
loader = PyPDFLoader("документация.pdf")
pages = loader.load()
# Разбиваем на чанки по 1000 символов с перекрытием 200
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ".", " "]
)
chunks = splitter.split_documents(pages)
print(f"Разбили на {len(chunks)} чанков")Параметр chunk_overlap важен — он обеспечивает контекст на границах чанков. Без него можно потерять смысл на стыке кусков.
Шаг 2: Создаём векторную базу
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
# Embeddings — модель для превращения текста в вектор
# all-MiniLM-L6-v2 — маленькая и быстрая, работает локально
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
# Создаём базу и добавляем документы
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # сохраняем локально
)
vectorstore.persist()
print("База создана и сохранена")Первый запуск скачает модель embeddings (~80 МБ) и займёт пару минут. Дальше работает мгновенно — модель кешируется локально.
Инфо
Embeddings — это числовое представление текста в многомерном пространстве. Тексты с похожим смыслом находятся «рядом» в этом пространстве. Именно это позволяет искать по смыслу, а не по ключевым словам.
Шаг 3: Строим цепочку вопрос-ответ
from langchain_anthropic import ChatAnthropic
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# Загружаем уже созданную базу
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings
)
# Модель
llm = ChatAnthropic(
model="claude-3-5-haiku-20241022", # быстрая и дешёвая для тестов
temperature=0
)
# Промпт — явно говорим использовать только контекст
prompt_template = """Ответь на вопрос, используя только информацию из контекста ниже.
Если ответа в контексте нет — скажи об этом прямо.
Контекст:
{context}
Вопрос: {question}
Ответ:"""
PROMPT = PromptTemplate(
template=prompt_template,
input_variables=["context", "question"]
)
# Собираем цепочку
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vectorstore.as_retriever(
search_kwargs={"k": 4} # берём 4 самых релевантных чанка
),
chain_type_kwargs={"prompt": PROMPT},
return_source_documents=True
)temperature=0 — детерминированные ответы, модель не фантазирует. Для RAG это правильно.
Шаг 4: Задаём вопросы
def ask(question: str) -> None:
result = qa_chain({"query": question})
print(f"Вопрос: {question}")
print(f"\nОтвет: {result['result']}")
print("\nИсточники:")
for doc in result['source_documents']:
page = doc.metadata.get('page', '?')
print(f" — стр. {page}: {doc.page_content[:100]}...")
# Тестируем
ask("Как оформить возврат товара?")
ask("Какой срок гарантии на продукт?")
ask("Что делать если устройство не включается?")Пример вывода:
Вопрос: Как оформить возврат товара?
Ответ: Для оформления возврата нужно обратиться в службу поддержки
в течение 14 дней с момента покупки. Потребуется чек и оригинальная упаковка.
Источники:
— стр. 23: Политика возврата. Покупатель вправе вернуть товар
надлежащего качества в течение 14 дней...
Добавляем поддержку нескольких файлов
Реальный случай — не один документ, а папка с PDF:
import os
from langchain.document_loaders import DirectoryLoader
loader = DirectoryLoader(
path="./docs",
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
documents = loader.load()
chunks = splitter.split_documents(documents)
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)Всё то же самое — только DirectoryLoader рекурсивно обходит папку и загружает все PDF.
Типичные проблемы и как их решать
Ответы не точные или галлюцинирует
Проверьте промпт — явно ли написано «используй только контекст». Увеличьте k в search_kwargs с 4 до 6-8, чтобы брать больше чанков. Иногда помогает уменьшить chunk_size — более мелкие куски точнее попадают в поиск.
Не находит очевидную информацию
Попробуйте другую модель embeddings. all-mpnet-base-v2 точнее, но медленнее. Для русскоязычных документов — intfloat/multilingual-e5-large.
Медленно работает
ChromaDB локально медленнее облачных баз. Для больших объёмов (10k+ документов) смотрите в сторону Pinecone, Weaviate или Qdrant.
Внимание
При работе с конфиденциальными документами — используйте локальные модели embeddings (что мы и делаем) и локальный LLM через Ollama. Тогда данные вообще не уходят во внешние API.
Что дальше
Базовая система работает. Вот куда можно двигаться дальше:
История диалога — добавить ConversationBufferMemory из LangChain, чтобы чат помнил контекст предыдущих вопросов.
Гибридный поиск — сочетание векторного поиска с классическим keyword-поиском. Лучше работает на специфических терминах и именах.
Оценка качества — метрики RAGAS (faithfulness, answer relevancy, context recall) — чтобы измерить, насколько хорошо система отвечает.
Стриминг ответов — для интерфейса лучше стримить токены по мере генерации, а не ждать полного ответа.
Это фундамент. Для анализа данных с Python дальше — смотрите гайд по AI-анализу таблиц с Claude. А для более сложных цепочек агентов с памятью — LangGraph.
RAG — не rocket science. Вечер, Python и ваши PDF — этого хватит.



