Proxy-Pointer RAG: гибридный метод для масштабируемого поиска | Гайд 2026 | AiManual
AiManual Logo Ai / Manual.
05 Апр 2026 Гайд

Proxy-Pointer RAG: пошаговое руководство по гибридному методу для масштабируемого и точного поиска

Полное руководство по Proxy-Pointer RAG на 2026 год. Узнайте, как совместить векторный и лексический поиск для масштабируемых RAG-систем с минимальными затратам

Почему ваш RAG падает с ростом базы данных? Проблема не там, где вы ищете

Представьте: ваша RAG-система работала идеально на 1000 документах. Вы добавили ещё 100 тысяч — и всё. Задержки растут, точность падает, счета за GPU летят в стратосферу. Знакомо? Стандартный векторный поиск упёрся в математический потолок.

Проблема в фундаментальном ограничении embedding-моделей. Они не умеют искать по миллионам документов без колоссальных затрат. Гибридный поиск помогает, но только до определённого предела — после 500k документов вы снова упираетесь в производительность и стоимость.

К 2026 году большинство production RAG-систем содержат от 1 до 10 миллионов документов. Прямой векторный поиск по таким объёмам требует либо гигантских GPU-кластеров, либо неприемлемых задержек в 2-3 секунды на запрос.

Proxy-Pointer RAG: архитектура, которая не пытается искать иголку в стоге сена

Вместо того чтобы искать точный ответ во всей базе, Proxy-Pointer RAG делает две вещи:

  1. Создает легковесные proxy-документы — сжатые версии оригиналов (10-20% от размера)
  2. Использует LLM как "умный указатель", который определяет, какие proxy релевантны, а затем точно извлекает фрагменты из оригиналов

Результат? Поиск по 1 млн документов работает на дешёвом CPU-сервере с задержкой менее 500 мс. Точность выше, чем у чистого векторного поиска, потому что LLM понимает контекст лучше, чем cosine similarity.

💡
Proxy-Pointer RAG особенно эффективен для длинных документов — техническая документация, юридические тексты, медицинские записи. Там, где традиционный RAG извлекает нерелевантные фрагменты из-за размытых эмбеддингов, наш метод сохраняет точность даже на 100+ страницах.

Архитектура изнутри: как работает двухуровневый поиск

Забудьте про "просто добавить 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

  1. Дешёвые proxy убивают точность
    Если вы создаёте proxy простым обрезанием текста, LLM-указатель будет работать с мусором. Инвестируйте в качественную суммаризацию или извлечение ключевых предложений. Проверяйте proxy вручную на 100 документах перед индексацией.
  2. Неправильный баланс гибридного поиска
    Параметр alpha (вес векторного поиска) нельзя ставить "на глазок". Измеряйте точность (nDCG, MRR) на тестовых запросах при разных значениях alpha. Для технической документации часто оптимально alpha=0.3-0.4, для креативных текстов — 0.6-0.7.
  3. 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 году ожидаю три изменения:

  1. Автоматическая оптимизация proxy — LLM будет сама решать, как лучше сжимать документы разных типов
  2. Мультимодальные proxy — сжатие не только текста, но и таблиц, схем, изображений в единое представление
  3. Дешёвые специализированные указатели — маленькие модели (1-3B параметров), обученные только на задачу указания, с точностью как у больших LLM

Проблема масштабируемого и точного поиска не исчезнет — объёмы данных растут быстрее, чем мощность GPU. Архитектуры вроде Proxy-Pointer — это не временное решение, а новая парадигма. Она признаёт, что искать нужно умно, а не грубо.

Совет напоследок: не реализуйте Proxy-Pointer слепо по этой статье. Адаптируйте под свои данные. Тестируйте каждый компонент отдельно. Измеряйте точность до и после. И помните — самая частая ошибка в RAG в 2026 году не техническая, а методологическая: отсутствие измеримых метрик и A/B тестов.

Подписаться на канал