Забудьте про Ctrl+F. Вот что происходит на самом деле
Представьте: у вас 500 PDF с технической документацией. Клиент спрашивает про специфичный параметр в договоре от 2023 года. Вы копаетесь в папках, теряете час, находите не то. Знакомо?
Стандартный поиск по тексту ломается на синонимах, контексте, сложных запросах. ИИ вроде GPT-4 умный, но слепой - он не видит ваши документы. Решение? RAG (Retrieval-Augmented Generation). Не модный акроним, а конкретный способ заставить ИИ работать с вашими данными.
К марту 2026 года RAG перестал быть экспериментальной технологией. Это стандарт для корпоративных систем, который работает в 80% компаний из Fortune 500. Но большинство реализаций все еще кривые.
Как работает RAG (без воды)
Разбиваю на атомы:
- Загружаете документы - PDF, Word, Excel, даже презентации
- Дробите на чанки - кусочки по 500-1000 символов с перекрытием
- Превращаете в числа (эмбеддинги) - текстовые векторы, которые понимает ИИ
- Кладете в векторную БД - ChromaDB, Pinecone, Weaviate
- При запросе ищете похожие чанки - семантический поиск, не точное совпадение
- Кормите найденное LLM + вопрос = точный ответ с контекстом
Звучит просто? Так и есть. Сложность в деталях - размер чанков, модель эмбеддингов, стратегия поиска. Ошибетесь на любом этапе - получите бред вместо ответов.
1 Ставим инструменты (актуально на март 2026)
LangChain к 2026-му сильно изменился. Если видите туториалы с версией 0.0.XX - бегите. Мы используем LangChain 0.2+ с модульной архитектурой.
# Базовый набор для работы
pip install langchain==0.2.1 langchain-community==0.2.1
pip install chromadb==0.4.22 # Самая стабильная версия на 2026
pip install pypdf python-docx pymupdf # Для чтения документов
pip install sentence-transformers # Локальные эмбеддинги
pip install ollama # Если хотите локальную LLM
Внимание: LangChain 0.2+ ломает обратную совместимость. Старые импорты типа `from langchain.document_loaders` больше не работают. Теперь все через `langchain_community`.
2 Загружаем документы - здесь ломаются 30% проектов
Самая скучная часть. И самая важная. Плохо загрузили - получили мусор в БД.
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
class DocumentLoader:
def __init__(self, directory_path):
self.directory = directory_path
def load_all(self):
"""Загружает все документы из директории"""
documents = []
for filename in os.listdir(self.directory):
filepath = os.path.join(self.directory, filename)
try:
if filename.endswith('.pdf'):
loader = PyPDFLoader(filepath)
elif filename.endswith('.docx'):
loader = Docx2txtLoader(filepath)
elif filename.endswith('.txt'):
loader = TextLoader(filepath)
else:
continue
docs = loader.load()
# Добавляем метаданные - критично для отслеживания источника
for doc in docs:
doc.metadata["source"] = filename
doc.metadata["file_type"] = filename.split('.')[-1]
documents.extend(docs)
print(f"Загружен {filename}: {len(docs)} страниц/чанков")
except Exception as e:
print(f"Ошибка загрузки {filename}: {str(e)}")
return documents
# Пример использования
loader = DocumentLoader("./documents/")
raw_docs = loader.load_all()
print(f"Всего загружено документов: {len(raw_docs)}")
Видите метаданные `source` и `file_type`? Без них потом не поймете, откуда пришел ответ. Типичная ошибка - потерять источник информации.
3 Дробление текста - магия, а не наука
Размер чанка - священный грааль RAG. 500 токенов? 1000? 2000? Ответ: зависит от ваших документов.
def smart_chunking(documents, chunk_size=1000, chunk_overlap=200):
"""Умное дробление с сохранением контекста"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
separators=["\n\n", "\n", ".", " ", ""], # Важен порядок!
)
chunks = text_splitter.split_documents(documents)
# Статистика для отладки
chunk_lengths = [len(chunk.page_content) for chunk in chunks]
print(f"Создано {len(chunks)} чанков")
print(f"Средняя длина: {sum(chunk_lengths)/len(chunks):.0f} символов")
print(f"Минимум: {min(chunk_lengths)}, Максимум: {max(chunk_lengths)}")
return chunks
# Тестируем разные размеры
for size in [500, 1000, 1500]:
print(f"\nТест с размером чанка {size}:")
chunks = smart_chunking(raw_docs, chunk_size=size)
# Смотрим на конкретный чанк
if chunks:
print(f"Пример чанка (первые 200 символов): {chunks[0].page_content[:200]}...")
4 Эмбеддинги: превращаем текст в математику
Здесь выбор модели определяет качество поиска. OpenAI дорого, но точно. Локальные модели бесплатно, но нужна мощность.
from langchain_community.embeddings import HuggingFaceEmbeddings
from chromadb.config import Settings
import chromadb
class VectorStoreManager:
def __init__(self, persist_directory="./chroma_db"):
"""Инициализация с локальными эмбеддингами"""
# BAAI/bge-large-en-v1.5 - лучшая открытая модель на 2026 для семантического поиска
self.embedding_model = HuggingFaceEmbeddings(
model_name="BAAI/bge-large-en-v1.5",
model_kwargs={'device': 'cpu'}, # или 'cuda' если есть GPU
encode_kwargs={'normalize_embeddings': True} # Критично для косинусного сходства
)
# Настройки ChromaDB
self.client = chromadb.PersistentClient(
path=persist_directory,
settings=Settings(
anonymized_telemetry=False, # Отключаем телеметрию
allow_reset=True
)
)
def create_collection(self, collection_name="documents"):
"""Создаем коллекцию в ChromaDB"""
collection = self.client.get_or_create_collection(
name=collection_name,
metadata={"hnsw:space": "cosine"} # Косинусное расстояние для семантического поиска
)
return collection
def add_documents(self, chunks, collection_name="documents"):
"""Добавляем чанки в векторную БД"""
collection = self.create_collection(collection_name)
# Подготавливаем данные
ids = [f"doc_{i}" for i in range(len(chunks))]
texts = [chunk.page_content for chunk in chunks]
metadatas = [chunk.metadata for chunk in chunks]
# Генерируем эмбеддинги и добавляем
embeddings = self.embedding_model.embed_documents(texts)
collection.add(
embeddings=embeddings,
documents=texts,
metadatas=metadatas,
ids=ids
)
print(f"Добавлено {len(chunks)} документов в коллекцию '{collection_name}'")
return collection
# Использование
manager = VectorStoreManager()
chunks = smart_chunking(raw_docs, chunk_size=1000)
collection = manager.add_documents(chunks)
Почему `normalize_embeddings=True`? Без нормализации косинусное сходство работает некорректно. Мелочь, которая ломает весь поиск.
Хотите облачное решение? Попробуйте Pinecone (платно) или Weaviate Cloud. Но помните про стоимость: 1M токенов эмбеддингов = $0.13-$0.80 в зависимости от модели. Локальные модели бесплатны, но требуют GPU для скорости.
5 Поиск и ответ: где LangChain показывает зубы
Собрали базу. Теперь самое интересное - поиск и генерация ответов.
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
from langchain_community.llms import Ollama # Локальная LLM
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
class RAGSystem:
def __init__(self, vector_store_manager, llm_model="llama3.1:8b"):
"""Инициализация RAG системы с локальной LLM"""
self.manager = vector_store_manager
# Используем Ollama с Llama 3.1 (актуально на 2026)
self.llm = Ollama(
model=llm_model,
temperature=0.1, # Низкая температура для точных ответов
callbacks=[StreamingStdOutCallbackHandler()]
)
# Кастомный промпт - секретный соус хорошего RAG
self.prompt_template = """Ты - помощник по документам. Используй только предоставленный контекст для ответа на вопрос.
Если в контексте нет информации для ответа, скажи "В предоставленных документах нет информации по этому вопросу."
Не придумывай информацию.
Контекст: {context}
Вопрос: {question}
Ответ: """
self.prompt = PromptTemplate(
template=self.prompt_template,
input_variables=["context", "question"]
)
def search_similar(self, query, collection_name="documents", k=5):
"""Семантический поиск похожих документов"""
collection = self.manager.client.get_collection(collection_name)
# Генерируем эмбеддинг для запроса
query_embedding = self.manager.embedding_model.embed_query(query)
# Ищем похожие
results = collection.query(
query_embeddings=[query_embedding],
n_results=k,
include=["documents", "metadatas", "distances"]
)
print(f"\nПоиск по запросу: '{query}'")
print(f"Найдено {len(results['documents'][0])} результатов")
for i, (doc, metadata, distance) in enumerate(zip(
results['documents'][0],
results['metadatas'][0],
results['distances'][0]
)):
print(f"\nРезультат {i+1} (расстояние: {distance:.4f}):")
print(f"Источник: {metadata.get('source', 'Неизвестно')}")
print(f"Текст: {doc[:200]}...")
return results
def answer_question(self, query, collection_name="documents", k=3):
"""Полный RAG пайплайн: поиск + генерация"""
# Сначала ищем релевантные чанки
search_results = self.search_similar(query, collection_name, k)
if not search_results['documents'][0]:
return "Не найдено релевантных документов для ответа на вопрос."
# Объединяем контекст
context = "\n\n".join([
f"[Документ {i+1} из {metadata.get('source', 'Неизвестно')}]\n{doc}"
for i, (doc, metadata) in enumerate(zip(
search_results['documents'][0],
search_results['metadatas'][0]
))
])
# Формируем финальный промпт
final_prompt = self.prompt.format(context=context, question=query)
# Генерируем ответ
print("\n" + "="*50)
print("ГЕНЕРАЦИЯ ОТВЕТА:")
print("="*50 + "\n")
response = self.llm.invoke(final_prompt)
# Добавляем источники
sources = list(set([m.get('source', '') for m in search_results['metadatas'][0]]))
response_with_sources = f"{response}\n\nИсточники: {', '.join(sources)}"
return response_with_sources
# Запускаем систему
rag = RAGSystem(manager)
# Тестовый запрос
question = "Какие условия возврата товара указаны в документах?"
answer = rag.answer_question(question)
print("\n" + "="*50)
print("ФИНАЛЬНЫЙ ОТВЕТ:")
print(answer)
Видите промпт? Он заставляет LLM не выдумывать. Без такого строгого шаблона модель начнет галлюцинировать.
Где все ломается (личный опыт)
Собрали систему? Отлично. Теперь список проблем, которые возникнут через неделю:
- Качество чанков - если разрываете предложения, смысл теряется. Решение: overlap 15-20% и проверка границ.
- Дублирование - один и тот же текст в разных чанках. Решение: дедупликация перед добавлением в БД.
- Скорость эмбеддингов - 1000 документов = часы обработки. Решение: батчинг и GPU.
- Актуальность данных - документы обновляются, а векторная БД нет. Решение: версионирование и переиндексация.
Самый опасный сценарий - отравление базы знаний. Злоумышленник может добавить документы с ложной информацией, и система начнет генерировать некорректные ответы. Защитные механизмы рассмотрены в статье "Атака на RAG-системы".
Что дальше? Производственные улучшения
Базовая система работает. Теперь сделаем ее промышленной:
# 1. Гибридный поиск (семантический + ключевые слова)
from langchain.retrievers import BM25Retriever, EnsembleRetriever
# 2. Переранжирование результатов
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from sentence_transformers import CrossEncoder
# 3. Мультимодальность (если есть сканы/изображения)
# Используйте layoutlm или аналоги для OCR с сохранением структуры
# 4. Кэширование эмбеддингов
import hashlib
import pickle
class EmbeddingCache:
def __init__(self, cache_file="embeddings_cache.pkl"):
self.cache_file = cache_file
self.load_cache()
def get_hash(self, text):
return hashlib.md5(text.encode()).hexdigest()
def load_cache(self):
try:
with open(self.cache_file, 'rb') as f:
self.cache = pickle.load(f)
except:
self.cache = {}
def save_cache(self):
with open(self.cache_file, 'wb') as f:
pickle.dump(self.cache, f)
def get_embedding(self, text, embedding_model):
text_hash = self.get_hash(text)
if text_hash in self.cache:
return self.cache[text_hash]
else:
embedding = embedding_model.embed_documents([text])[0]
self.cache[text_hash] = embedding
return embedding
Кэширование эмбеддингов ускоряет повторную обработку в 50-100 раз. Но требует мониторинга памяти.
Вместо заключения: когда RAG не нужен
Парадоксально, но иногда RAG - overkill. Если:
- У вас меньше 100 документов - проще использовать поиск по ключевым словам с улучшенной логикой
- Документы сильно структурированы (JSON, XML) - парсите их напрямую
- Вопросы всегда однотипны - создайте шаблоны ответов
- Требуется 100% точность - RAG дает 85-95%, остальное - риск
К 2026 году появились более легкие альтернативы. Для браузерных решений смотрите браузерный RAG для юристов - весь пайплайн работает локально без отправки данных.
Главный совет: начните с простого. ChromaDB + локальные эмбеддинги + Ollama. Когда поймете pain points вашего кейса - оптимизируйте. Не стройте монстра на 1000 строк кода в первый день.
RAG-система - не черный ящик, а набор компромиссов. Между скоростью и точностью, между стоимостью и качеством, между простотой и функциональностью. Выбирайте осознанно.