Почему ваш RAG падает с ростом базы данных? Проблема не там, где вы ищете
Представьте: ваша RAG-система работала идеально на 1000 документах. Вы добавили ещё 100 тысяч — и всё. Задержки растут, точность падает, счета за GPU летят в стратосферу. Знакомо? Стандартный векторный поиск упёрся в математический потолок.
Проблема в фундаментальном ограничении embedding-моделей. Они не умеют искать по миллионам документов без колоссальных затрат. Гибридный поиск помогает, но только до определённого предела — после 500k документов вы снова упираетесь в производительность и стоимость.
К 2026 году большинство production RAG-систем содержат от 1 до 10 миллионов документов. Прямой векторный поиск по таким объёмам требует либо гигантских GPU-кластеров, либо неприемлемых задержек в 2-3 секунды на запрос.
Proxy-Pointer RAG: архитектура, которая не пытается искать иголку в стоге сена
Вместо того чтобы искать точный ответ во всей базе, Proxy-Pointer RAG делает две вещи:
- Создает легковесные proxy-документы — сжатые версии оригиналов (10-20% от размера)
- Использует LLM как "умный указатель", который определяет, какие proxy релевантны, а затем точно извлекает фрагменты из оригиналов
Результат? Поиск по 1 млн документов работает на дешёвом CPU-сервере с задержкой менее 500 мс. Точность выше, чем у чистого векторного поиска, потому что LLM понимает контекст лучше, чем cosine similarity.
Архитектура изнутри: как работает двухуровневый поиск
Забудьте про "просто добавить BM25 к векторам". Proxy-Pointer — это другая философия:
| Слой | Что делает | Технологии (актуально на 05.04.2026) |
|---|---|---|
| Proxy-индекс | Хранит сжатые версии документов для быстрого поиска | SentenceTransformers v3.0, ColBERTv3, BM25 с Bayesian оптимизацией |
| LLM-указатель | Анализирует query и выбирает релевантные proxy | DeepSeek-R1-67B (последняя версия), Claude 3.7 Sonnet, локальные модели 7-13B параметров |
| Точный ретривер | Извлекает конкретные фрагменты из оригинальных документов | DensePhrase, UniXD, адаптивные chunking-алгоритмы |
Почему это масштабируется? Потому что 95% поисковой работы делает лёгкий proxy-индекс. LLM работает только с десятком кандидатов, а не с миллионом. Оригинальные документы лежат в холодном хранилище и извлекаются только по точным ссылкам.
1 Подготовка данных: создание proxy-документов
Не делайте ошибку 90% инженеров — не используйте тупое обрезание текста. Proxy должен сохранять смысл, а не просто первые N слов.
from transformers import AutoTokenizer, AutoModelForSeq2Seq
import torch
# На 2026 год модели для суммаризации стали значительно лучше
# Используем FLAN-T5-XXL v2.0 или аналогичную
model_name = "google/flan-t5-xxl-v2.0"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2Seq.from_pretrained(model_name)
def create_proxy_document(text, max_proxy_length=500):
"""Создаёт сжатый proxy-документ, сохраняющий ключевую информацию"""
# Для длинных документов используем иерархическую суммаризацию
if len(text.split()) > 5000:
chunks = split_text_hierarchically(text)
summaries = []
for chunk in chunks:
inputs = tokenizer(
f"summarize: {chunk}",
return_tensors="pt",
max_length=1024,
truncation=True
)
summary_ids = model.generate(
inputs["input_ids"],
max_length=max_proxy_length,
min_length=100,
length_penalty=2.0,
num_beams=4
)
summary = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
summaries.append(summary)
return " ".join(summaries)
else:
# Прямая суммаризация для коротких документов
inputs = tokenizer(
f"summarize: {text}",
return_tensors="pt",
max_length=1024,
truncation=True
)
summary_ids = model.generate(
inputs["input_ids"],
max_length=max_proxy_length,
min_length=100,
length_penalty=2.0,
num_beams=4
)
return tokenizer.decode(summary_ids[0], skip_special_tokens=True)
# Альтернатива: извлечение ключевых предложений с помощью TextRank или BERT-эмбеддингов
def extract_key_sentences(text, num_sentences=10):
"""Извлекает наиболее репрезентативные предложения"""
# Современные методы на 2026 используют контекстуальные эмбеддинги
# вместо простого TF-IDF
sentences = sent_tokenize(text)
if len(sentences) <= num_sentences:
return text
# Используем модель для эмбеддингов предложений
embeddings = sentence_model.encode(sentences)
# Кластеризация и выбор центральных предложений каждого кластера
# ...
return selected_sentences
Не экономьте на создании proxy! Плохой proxy = бесполезный поиск. Если ваш документ технический, proxy должен содержать спецификации, API endpoints, ключевые параметры. Если юридический — статьи, пункты, определения. Общие фразы убивают точность.
2 Индексирование: строим гибридный индекс для proxy
Здесь мы применяем гибридный поиск, но не к оригинальным документам, а к proxy. Это в 5-10 раз быстрее и дешевле.
from sentence_transformers import SentenceTransformer
from rank_bm25 import BM25Okapi
import numpy as np
import pickle
# Актуальные модели на 2026 год
# Используем e5-multilingual-v3 или более новые аналоги
embedding_model = SentenceTransformer('intfloat/e5-multilingual-v3')
class HybridProxyIndex:
def __init__(self):
self.proxy_texts = []
self.original_pointers = [] # Ссылки на оригинальные документы
self.embeddings = None
self.bm25 = None
def add_document(self, proxy_text, original_doc_id, metadata=None):
"""Добавляет proxy-документ в индекс"""
self.proxy_texts.append(proxy_text)
self.original_pointers.append({
'doc_id': original_doc_id,
'metadata': metadata or {}
})
def build_index(self):
"""Строит векторный и лексический индексы"""
# Векторные эмбеддинги
print("Создаю эмбеддинги для proxy...")
self.embeddings = embedding_model.encode(
self.proxy_texts,
show_progress_bar=True,
batch_size=32,
normalize_embeddings=True
)
# BM25 индекс
print("Строю BM25 индекс...")
tokenized_corpus = [doc.split() for doc in self.proxy_texts]
self.bm25 = BM25Okapi(tokenized_corpus)
def search(self, query, top_k=10, alpha=0.5):
"""Гибридный поиск по proxy-документам"""
# Векторный поиск
query_embedding = embedding_model.encode([query], normalize_embeddings=True)[0]
vector_scores = np.dot(self.embeddings, query_embedding)
# BM25 поиск
tokenized_query = query.split()
bm25_scores = self.bm25.get_scores(tokenized_query)
# Нормализация и гибридная комбинация
# Используем softmax для нормализации
vector_scores_norm = np.exp(vector_scores) / np.sum(np.exp(vector_scores))
bm25_scores_norm = np.exp(bm25_scores) / np.sum(np.exp(bm25_scores))
# Комбинация с весовым коэффициентом
hybrid_scores = alpha * vector_scores_norm + (1 - alpha) * bm25_scores_norm
# Топ-K результатов
top_indices = np.argsort(hybrid_scores)[::-1][:top_k]
results = []
for idx in top_indices:
results.append({
'proxy_text': self.proxy_texts[idx],
'score': hybrid_scores[idx],
'pointer': self.original_pointers[idx]
})
return results
Обратите внимание на alpha=0.5 — это параметр баланса между семантическим и лексическим поиском. Настраивайте его на валидационной выборке. Для технических запросов лучше увеличивать вес BM25, для концептуальных — векторного поиска.
3 LLM как умный указатель: от proxy к точным фрагментам
Это сердце системы. LLM получает query и топ-N proxy, затем определяет, какие именно оригинальные документы и какие их части нужно извлечь.
import openai
from typing import List, Dict
# На 2026 год API стали умнее и дешевле
# Используем последние модели с function calling
client = openai.OpenAI(api_key="your-key")
def llm_pointer_analysis(query: str, proxy_results: List[Dict], max_tokens=1000):
"""LLM анализирует, какие оригинальные документы релевантны"""
proxy_context = "\n---\n".join([
f"Proxy {i+1} (релевантность: {res['score']:.3f}):\n{res['proxy_text'][:500]}..."
for i, res in enumerate(proxy_results[:5]) # Берём топ-5 proxy
])
prompt = f"""Ты — интеллектуальный указатель в RAG-системе.
Запрос пользователя: {query}
Найденные proxy-документы (сжатые версии оригиналов):
{proxy_context}
Проанализируй, какие оригинальные документы нужно извлечь полностью или частично.
Для каждого документа укажи:
1. ID документа (из pointer)
2. Конкретные разделы/страницы/абзацы, если можно определить
3. Уверенность в релевантности (0.0-1.0)
4. Краткое обоснование
Если запрос требует информации из нескольких документов — укажи все.
Если ни один proxy не выглядит релевантным — верни пустой список.
Ответ в формате JSON:"""
response = client.chat.completions.create(
model="gpt-4.5-turbo", # На 2026 год уже есть более новые версии
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
max_tokens=max_tokens,
response_format={"type": "json_object"}
)
analysis = json.loads(response.choices[0].message.content)
return analysis
# Для локального развертывания используйте модели типа DeepSeek-R1
def local_llm_pointer(query, proxy_results, model):
"""Альтернатива с локальной LLM"""
# Локальные модели 7-13B параметров в 2026 году
# уже достаточно умны для такой задачи
# ...
pass
Зачем это нужно? Потому что proxy — это только приближение. LLM может понять, что "вопрос про ошибку API на странице 45 документа X, хотя в proxy упоминается только общее описание API". Это уровень понимания, недоступный обычному векторному поиску.
4 Точное извлечение и финальная сборка контекста
Получив указания от LLM, мы загружаем только нужные оригинальные документы (или их части) и готовим финальный контекст для генерации ответа.
class ExactRetriever:
def __init__(self, document_store):
self.document_store = document_store # Хранилище оригиналов
def retrieve_exact(self, llm_analysis, query):
"""Извлекает точные фрагменты на основе анализа LLM"""
exact_contexts = []
for doc_request in llm_analysis.get('documents', []):
doc_id = doc_request['doc_id']
sections = doc_request.get('sections', []) # Например, ['глава 3', 'раздел 2.1']
confidence = doc_request.get('confidence', 0.5)
# Пропускаем низкокачественные указания
if confidence < 0.3:
continue
# Загружаем оригинальный документ
original_doc = self.document_store.get(doc_id)
if not original_doc:
continue
# Если указаны конкретные разделы — извлекаем их
if sections:
extracted = self.extract_sections(original_doc, sections)
if extracted:
exact_contexts.append({
'doc_id': doc_id,
'content': extracted,
'confidence': confidence
})
else:
# Иначе берём весь документ или применяем умное chunking
# на основе query
relevant_parts = self.smart_chunking(original_doc, query)
exact_contexts.append({
'doc_id': doc_id,
'content': relevant_parts,
'confidence': confidence
})
return exact_contexts
def extract_sections(self, document, section_names):
"""Извлекает конкретные разделы из документа"""
# Зависит от структуры документа
# Для PDF с OCR — используем координаты
# Для Markdown — парсим заголовки
# Для HTML — XPath/CSS селекторы
extracted = []
# ...
return "\n\n".join(extracted)
def smart_chunking(self, document, query):
"""Умное разделение документа на релевантные query части"""
# Используем модель для релевантности предложений
# или DensePhrase для точного соответствия
# ...
return most_relevant_parts
Три ошибки, которые гарантированно сломают ваш Proxy-Pointer RAG
-
Дешёвые proxy убивают точность
Если вы создаёте proxy простым обрезанием текста, LLM-указатель будет работать с мусором. Инвестируйте в качественную суммаризацию или извлечение ключевых предложений. Проверяйте proxy вручную на 100 документах перед индексацией. -
Неправильный баланс гибридного поиска
Параметр alpha (вес векторного поиска) нельзя ставить "на глазок". Измеряйте точность (nDCG, MRR) на тестовых запросах при разных значениях alpha. Для технической документации часто оптимально alpha=0.3-0.4, для креативных текстов — 0.6-0.7. -
LLM-указатель без температурного контроля
Температура выше 0.3 заставит LLM "галлюцинировать" указания на несуществующие документы. Всегда используйте temperature=0.1-0.2 для указателя. И добавьте валидацию — проверяйте, что запрашиваемые doc_id действительно существуют.
Проверка на практике: запустите 100 тестовых запросов и посмотрите, сколько раз LLM-указатель запрашивает нерелевантные документы. Если больше 15% — проблема в качестве proxy или в промпте указателя.
Сравнение с другими методами: почему не RAPTOR или GraphRAG?
В 2025-2026 годах появилось множество архитектур RAG. Но у каждой — свои ограничения:
| Метод | Проблемы (на 2026 год) | Когда использовать вместо Proxy-Pointer |
|---|---|---|
| RAPTOR (иерархический RAG) | Сложность настройки, низкая точность на больших документах (см. статью про провал RAPTOR) | Очень структурированные документы с чёткой иерархией |
| GraphRAG | Огромные вычислительные затраты на построение графа, плохая инкрементальность | Анализ связей между сущностями в маленькой базе |
| Прямой гибридный поиск | Масштабируемость ограничена 500k-1M документов, высокие затраты на индексацию | Маленькие базы до 100k документов |
| Pure векторный поиск | Математический потолок точности (см. статью про потолок RAG) | Простые семантические поиски без требований к точности |
Proxy-Pointer выигрывает там, где важны и масштабируемость, и точность. Система из 10 млн документов? Без проблем. Технические запросы, требующие точных цитат из спецификаций? Справится.
Ответы на частые вопросы
Насколько дорого запускать LLM-указатель для каждого запроса?
Дешевле, чем кажется. Указатель работает с 5-10 proxy (короткими текстами), а не со всей базой. На 2026 год вызов gpt-4-turbo для такого промпта стоит $0.002-0.005 за запрос. При 10k запросов в день — $20-50. Локальные модели (7-13B) вообще почти бесплатны после развертывания.
Как обновлять индекс при добавлении новых документов?
Инкрементально. Новый документ → создаём его proxy → добавляем в гибридный индекс. Не нужно перестраивать весь индекс. Это критично для production-систем, где документы добавляются постоянно.
Proxy-Pointer хорошо работает с неанглийскими языками?
Да, если использовать multilingual embedding-модели (например, e5-multilingual-v3) и LLM с поддержкой нужного языка. Для русского, китайского, арабского языков точность остаётся высокой, потому что LLM-указатель понимает семантику, а не просто сопоставляет слова.
Можно ли использовать Proxy-Pointer для поиска по коду?
Идеально подходит. Proxy для кода — это сигнатуры функций, комментарии, названия классов. LLM-указатель отлично понимает, "какую функцию нужно найти по её описанию". Точность выше, чем у специализированных code search инструментов вроде SourceGraph.
Что дальше? Эволюция Proxy-Pointer к 2027 году
К 2027 году ожидаю три изменения:
- Автоматическая оптимизация proxy — LLM будет сама решать, как лучше сжимать документы разных типов
- Мультимодальные proxy — сжатие не только текста, но и таблиц, схем, изображений в единое представление
- Дешёвые специализированные указатели — маленькие модели (1-3B параметров), обученные только на задачу указания, с точностью как у больших LLM
Проблема масштабируемого и точного поиска не исчезнет — объёмы данных растут быстрее, чем мощность GPU. Архитектуры вроде Proxy-Pointer — это не временное решение, а новая парадигма. Она признаёт, что искать нужно умно, а не грубо.
Совет напоследок: не реализуйте Proxy-Pointer слепо по этой статье. Адаптируйте под свои данные. Тестируйте каждый компонент отдельно. Измеряйте точность до и после. И помните — самая частая ошибка в RAG в 2026 году не техническая, а методологическая: отсутствие измеримых метрик и A/B тестов.