Сегодня многие переусложняют свой RAG-стек. Разработчики сразу переходят к эмбеддингам, векторным базам данных и пайплайнам реранкинга. При этом пользователям нужно всего лишь найти документ, в котором написано «Как сбросить пароль».

В инженерии для каждой задачи существует подходящий инструмент. В системах retrieval для AI ситуация не отличается.

Факторы принятия решения

Прежде чем переходить к рецептам, стоит определить, когда какой подход использовать. Ключевые факторы:

1. Требования к свежести данных — обновления в реальном времени (новости, соцсети) требуют подходов с лёгкой переиндексацией. Ежедневные или еженедельные обновления хорошо работают с гибридными подходами. Стабильный корпус (обновления раз в месяц или квартал) делает разумным предварительный эмбеддинг.

2. Характеристики корпуса — высокая текучесть (более 10% изменений в день) означает, что полного предварительного эмбеддинга стоит избегать. Стабильные документы хорошо работают с pre-embedding. Распределение с длинным хвостом (90% документов никогда не запрашивается) говорит в пользу эмбеддинга на лету.

3. Паттерны запросов — запросы с преобладанием ключевых слов стоит начинать с полнотекстового поиска. Семантические или разговорные запросы выигрывают от эмбеддингов. Смешанные паттерны требуют гибридных подходов.

4. Масштаб и производительность — менее 1000 запросов в день означает, что достаточно простых подходов. От 1К до 10К запросов в день требуют избирательной оптимизации. Более 10К запросов в день оправдывают полную оптимизацию.

5. Возможности команды — без экспертизы в ML стоит оставаться на полнотекстовом поиске с переформулированием запросов. Некоторый опыт в ML делает управляемым гибридный поиск. Наличие ML-команды делает жизнеспособными продвинутые подходы.

Теперь — к книге рецептов. Начинать стоит с самого верха. Двигаться вниз — только когда есть данные, доказывающие такую необходимость.

Рецепт 1: MVP — только полнотекстовый поиск

Что это

Старый добрый BM25. Elasticsearch. Полнотекстовый поиск Postgres. То, что существовало ещё до того, как «эмбеддинг» стал глаголом.

Когда использовать

На старте проекта. Когда пользователи пишут запросы в стиле ключевых слов («pandas merge dataframe»). Когда важны точные совпадения («счёт №12345»). Когда нужен ноль сложности из мира ML. Когда в корпусе присутствует собственная терминология (подробнее об этом дальше).

Плюсы

Нулевые затраты на API. Быстро (менее 10 мс). Легко отлаживать (видно точно, почему документ совпал с запросом). Удивительно эффективно (закрывает множество сценариев). Не нужна стратегия чанкинга — работает с полными документами. Не нужна сложная оценка качества — легко тестировать и валидировать. Нет риска устаревания модели (BM25 не меняется).

Минусы

Пропускает синонимы («car» против «automobile»). Не справляется с семантическими запросами («Как мне...?»). Не понимает намерение за пределами ключевых слов.

По существу

По собственному опыту автора, такой подход закрывает значительную часть сценариев использования. Пропускать этот шаг не стоит — можно удивиться тому, как далеко получится продвинуться.

Почему это недооценивают

Переходя сразу к эмбеддингам, приходится сразу сталкиваться с вопросами: какой размер чанка? (512 токенов? 1024?) Какое перекрытие? (50 токенов? 100?) Семантический чанкинг или фиксированного размера? Как оценить, хорош ли чанкинг?

С полнотекстовым поиском всего этого можно избежать. Документы остаются документами. Поиск просто работает.

Рецепт 2: Агентное переформулирование запросов

Что это

Использование LLM для преобразования неаккуратных пользовательских запросов в чистые поисковые фразы по ключевым словам.

Суть идеи

Большинство проблем «семантического поиска» на самом деле — это проблемы формулировки запроса.

Схема потока

Когда использовать

Пользователи задают вопросы в разговорной форме. Есть несовпадение словаря (пользователи пишут «fix bugs», в документации — «debugging»). Есть внутренний жаргон (собственный фреймворк называется «Atlas»). Нужна гибкость для быстрой итерации над стратегиями запросов.

Стоимость

~$0.001 за запрос (при использовании GPT-4o-mini для переформулирования)

В чём магия

LLM может убрать стоп-слова («how do I» превращается в ничто). Может добавить синонимы («car» превращается в «car automobile vehicle»). Может перевести доменные термины («speed up code» превращается в «optimize performance»). Может разложить сложный запрос на части («read CSV and plot» превращается в [«read CSV», «plot data»]). Может учиться на глоссарии (через системный промпт).

Почему это гибче эмбеддингов

С эмбеддингами, если результаты плохие, нужно подстраивать стратегию чанкинга, переэмбеддить весь корпус, прогонять регрессионные тесты на eval-наборе и надеяться, что стало лучше.

С переформулированием запросов, если результаты плохие, достаточно подправить системный промпт. Всё. Проверка мгновенная.

Многошаговое агентное переформулирование

Ещё лучше — можно построить цикл:

def agentic_search(query, max_iterations=3):
    for i in range(max_iterations):
        # Rewrite query
        optimized = query_rewriter.rewrite(query, iteration=i)
        
        # Search
        results = bm25_search(optimized)
        
        # Evaluate quality
        quality = evaluate_results(results, query)
        
        if quality > threshold:
            return results
        
        # Agent learns and tries again
        query = refine_based_on_feedback(query, results, quality)
    
    return results

Агент способен итерировать, обучаться и адаптироваться — без переэмбеддинга чего-либо.

Пример: проблема собственной терминологии

Допустим, у компании есть Python-фреймворк с названием «Atlas». При использовании общих эмбеддингов:

General embedding model (trained on internet):
"Atlas" = [vectors pointing toward: Greek mythology, maps, geography]
Your actual Atlas docs = [vectors about data processing]
Similarity score: 0.15 (terrible!)

Модель понятия не имеет о существовании этого «Atlas». Она опирается на то, что усвоила при обучении. Но с переформулированием запросов:

system_prompt = """
  Domain-specific terms (NEVER modify these, use as exact keywords):
    - Atlas: our internal data processing framework
    - Mercury: our messaging system
    - Zeus: our auth service

    Preserve these terms exactly and optimize the rest of the query.
"""

# User: "How do I use Atlas for batch jobs?"
# Agent: "Atlas batch jobs data processing pipeline"
# BM25: Perfect match on "Atlas" ✓

Для собственных терминов точное совпадение по ключевым словам побеждает семантическое понимание.

Рецепт 3: Гибридный поиск (Sparse + Dense Reranking)

Что это

Использование BM25 для получения кандидатов (топ 50-100), затем реранкинг с помощью эмбеддингов (топ 10).

Почему это работает

BM25 быстр и хорош в сопоставлении ключевых слов. Эмбеддинги хороши в семантическом понимании. Вместе они компенсируют слабости друг друга.

Когда использовать

Пользователи задают семантические вопросы («найди альтернативы X»). BM25 плюс переформулирование запросов сами по себе не справляются (и есть данные, это подтверждающие). Допустима задержка 100-500 мс. Корпус относительно стабилен (не меняется каждую минуту).

Пайплайн

Схема потока

Соображения по стоимости

Посчитаем по текущим ценам (OpenAI text-embedding-3-small — $0,02 за 1М токенов):

  • Эмбеддинг 50 документов на запрос (в среднем по 500 токенов) означает 50 документов × 500 токенов = 25 000 токенов

  • Стоимость: 25 000 × $0,00002 = ~$0,0005 за запрос. При 1000 запросов в день × 30 дней = ~$15 в месяц.

На самом деле вполне приемлемо. Но есть нюанс: задержка.

Эмбеддинг 50 документов на лету добавляет 200-500 мс к каждому запросу. Для поиска, ориентированного на пользователя, это заметно. Именно здесь и заключается реальный компромисс — не в стоимости, а в скорости.

Важное соображение: проблема чанкинга возвращается

При внедрении эмбеддингов нужно решить, как чанковать документы (фиксированный размер? семантический? по разделам?). Нужно определить, какой размер чанка и перекрытие использовать. Нужно обрабатывать чанки, которые охватывают важный контекст.

Это добавляет сложность, которой чистый полнотекстовый поиск избегает.

Рецепт 4: Эмбеддинг на лету (ставка на свежесть данных)

Суть идеи

Если данные меняются часто, зачем платить за переэмбеддинг всего подряд?

Что это

Схема потока

Когда использовать

Высокая текучесть документов (более 10% документов обновляется ежедневно). Контент в реальном времени (новости, соцсети, живые обновления). Идёт эксперимент с моделями эмбеддингов (переиндексация не нужна). Свежесть данных критична (документы должны быть актуальными). Небольшое K для реранкинга (20-50 документов).

Время посчитать

On-the-fly / online (1000 queries/day, 50 docs/query):
- Embedding cost: ~$15/month (ongoing)
- Storage: $0 (just store text)
- Latency: 200-500ms per query
- Freshness: Perfect (always current)
- Model switching: Easy (just change the API call)

Преимущество в контексте устаревания моделей

Вот о чём говорят недостаточно часто: модели эмбеддингов устаревают.

OpenAI прекратила поддержку text-embedding-ada-002 в пользу text-embedding-3. Если предварительно проэмбеддить 10 миллионов документов старой моделью, теперь придётся переэмбеддить все 10 миллионов документов новой моделью, обновить векторную базу данных, прогнать регрессионные тесты на eval-наборе, подтвердить, что качество не деградировало, справиться с периодом переключения и разобраться с любыми изменениями API.

С эмбеддингом на лету / online

Достаточно буквально изменить одну строку кода. Готово.

Недостаток

Задержка. Документы эмбеддятся при каждом запросе. Такой вариант жизнеспособен только если приемлема задержка 200-500 мс, K небольшое (реранкинг 20-50 документов, а не 500), и сценарий использования отдаёт предпочтение свежести данных перед скоростью.

Рецепт 5: Pre-Embedding с горячими/холодными тирами (прагматичная ставка)

Что это

Предварительный эмбеддинг часто запрашиваемых документов («горячий тир»), эмбеддинг на лету редко запрашиваемых документов («холодный тир»).

Суть идеи

Паттерны обращения следуют распределению Парето. 20% документов получают 80% трафика.

# Track access patterns
access_counts = Counter()

def adaptive_search(query):
    # BM25 to get candidates
    candidates = bm25_search(query, top_k=100)
    
    # Separate hot and cold
    hot = [d for d in candidates if d.id in hot_tier]
    cold = [d for d in candidates if d.id not in hot_tier]
    
    # Hot docs: use pre-computed embeddings (fast)
    hot_scores = vector_db.similarity_search(query_emb, hot)
    
    # Cold docs: embed on-the-fly (slower, but rare)
    cold_scores = embed_and_score(cold, query_emb)
    
    return merge_and_rank(hot_scores, cold_scores)

## Periodically promote frequently accessed docs to hot tier
def update_tiers_weekly():
    frequently_accessed = [doc_id for doc_id, count 
                          in access_counts.items() 
                          if count > threshold]
    
    # Only re-embed the new hot docs
    newly_hot = set(frequently_accessed) - set(hot_tier)
    embed_and_index(newly_hot)

Когда использовать

Есть чёткие паттерны обращения (некоторые документы запрашиваются намного чаще других). Корпус среднего или крупного размера (более 100К документов). Смесь стабильного и меняющегося контента. Нужна хорошая задержка для частых запросов. Есть желание минимизировать переэмбеддинг при обновлении моделей.

Преимущества

Быстро для 80% запросов (попадание в предварительно проэмбеддженный кэш). Свежо для редко запрашиваемых документов. Переэмбеддинг только горячего тира при смене моделей (20% корпуса). Адаптация к меняющимся паттернам обращения. Лучший компромисс между задержкой, стоимостью и гибкостью.

История со сменой модели

Когда модель эмбеддингов устаревает:

Full pre-embedding: Re-embed 1M docs × $0.01 = $10,000 + downtime
Hot/cold tiers: Re-embed 200K docs × $0.01 = $2,000 + minimal downtime
On-the-fly: Change one line of code = $0 + zero downtime

Рецепт 6: Полный Pre-Embedding (ставка на масштаб)

Что это

Эмбеддинг всего заранее. Хранение в векторной базе данных. Поиск с помощью ANN (приближённых ближайших соседей).

Когда использовать

Очень высокий объём запросов (более 10К запросов в день). Нужна задержка ниже 50 мс. Очень стабильный корпус (менее 5% текучести в месяц). Паттерн обращения широкий (без длинного хвоста). Есть ML-команда для управления инфраструктурой.

Разбор стоимости

Pre-embedding (1M docs):
- One-time embedding: 1M docs × 500 tokens × $0.00002 = $10
- Storage: 1M × 1536 dims × 4 bytes = 6GB (~$10-30/month)
- Search latency: under 50ms (blazing fast!)
- Freshness: Only as fresh as last re-index

Когда НЕ стоит использовать

Документы меняются часто (более 10% в неделю). Идёт эксперимент с моделями эмбеддингов. Низкий объём запросов (менее 1К запросов в день). Более простые подходы ещё не были опробованы.

Кошмар устаревания модели

Именно здесь полный pre-embedding причиняет больше всего боли. При необходимости сменить модель приходится сталкиваться с простоем (поиск деградирует во время переэмбеддинга), вычислительными затратами (переэмбеддинг миллионов документов), нагрузкой на тестирование (полный набор регрессионных тестов на новых эмбеддингах), переоценкой стратегии чанкинга (вдруг новая модель лучше работает с другим размером чанков?) и риском (что если новая модель хуже подходит для данного домена?).

Это избыточно для большинства систем. Мне доводилось видеть команды, которые тратили месяцы на оптимизацию настройки векторной базы данных, тогда как переформулирование запросов решило бы 90% их проблем.

Но если речь о Pinterest, Shopify или обработке массивного масштаба со стабильным корпусом — именно к этому решению и приходят.

Проблема запросов с несколькими намерениями

Здесь всё становится интереснее. До сих пор обсуждались запросы с одним намерением: «Как мне объединить датафреймы?»

Но реальные пользователи спрашивают что-то вроде: «Как мне прочитать CSV-файл, почистить пропущенные данные и построить график результатов?»

Это три отдельных намерения. Искать это как один запрос — всё равно что пытаться найти ресторан, который подаёт пиццу, суши и тако одновременно. Удачи.

Playbook в духе Perplexity

Современные агентные RAG-системы (Perplexity, поиск ChatGPT) справляются с этим элегантно:

Агент понимания запроса

Разбивает запрос на части.

# Input: "read CSV, clean data, plot results"

# Agent output:

{
  "query_type": "complex",
  "sub_queries": [
    "pandas read csv file",
    "pandas clean missing data",
    "matplotlib plot dataframe"
  ],
  "dependencies": ["read > clean > plot"]
}

Параллельная адаптивная обработка

Маршрутизация каждого подзапроса оптимальным способом

Sub-query 1 (simple):
  "pandas read csv"
  Stopwords + lemma, then BM25
  Cost: $0, Latency: 15ms

Sub-query 2 (moderate):
  "pandas clean missing data"
  Synonym expansion, then BM25
  Cost: $0, Latency: 20ms

Sub-query 3 (complex):
  "matplotlib plot dataframe"
  LLM rewrite, then Multi-search
  Cost: $0.001, Latency: 250ms

Total (parallel): $0.002, 250ms (not 285ms!)

Синтез

Объединение результатов в целостный ответ

Here's a complete workflow:

1. Reading CSV Files
   [relevant docs from sub-query 1]
   
2. Cleaning Missing Data
   [relevant docs from sub-query 2]
   
3. Plotting Results
   [relevant docs from sub-query 3]

[Code example combining all three steps]

Почему это работает

Каждый подзапрос сфокусирован и точен, что даёт лучший retrieval. Параллельное выполнение означает меньшую задержку (максимум, а не сумма). Адаптивная маршрутизация снижает стоимость (за LLM платят только сложные запросы). Структурированный вывод даёт лучший UX.

Сравнение стоимости

Без декомпозиции

  • Переформулирование LLM всего сложного запроса целиком: $0,005

  • Эмбеддинг 50 документов: $0,025

  • Итого: $0,03

С декомпозицией

  • Декомпозиция: $0,001

  • Подзапрос 1 (простой): $0

  • Подзапрос 2 (простой): $0

  • Подзапрос 3 (сложный): $0,001

  • Итого: $0,002

В 15 раз дешевле, качество лучше.

Именно здесь агентный retrieval раскрывается по-настоящему. Агент способен разумно решать, какие подзапросы требуют дорогой обработки (эмбеддинги), а какие можно обработать дешёвыми методами (простая предобработка + BM25).

Дерево решений (или: что когда использовать)

Итак, главный вопрос: «Что строить?»

Начать здесь: есть ли поиск вообще? Если нет — сначала строить BM25. Серьёзно. Хватит читать и начать строить. Если поиск уже есть — продолжаем.

Измерить базовый уровень. Запустить текущий поиск на 2-4 недели и собрать обратную связь пользователей. Довольны ли пользователи результатами? Если да — остановиться, всё готово, можно заниматься другими фичами. Если нет — продолжаем.

В чём главная жалоба?

Если пользователи говорят «Не могу найти документы, которые точно существуют», сначала стоит попробовать переформулирование запросов. При $0,001 за запрос и без переиндексации это стоит проверить. Запустить A/B-тест на 2 недели. Если улучшение заметно — оставить как есть, готово. Если недостаточно — продолжаем.

Если пользователи говорят «Результаты нормальные, но не отличные», стоит A/B-тестировать гибридный поиск (sparse плюс реранкинг эмбеддингами). Оправдана ли добавленная задержка? Если да — определиться с реализацией. Если данные меняются часто — эмбеддинг на лету. Если есть чёткие горячие документы — горячие/холодные тиры. Если корпус стабилен и масштаб большой — полный pre-embedding. Если задержка того не стоит — вместо этого дальше оптимизировать переформулирование запросов.

Если пользователи говорят «Нужно лучшее семантическое понимание», использовать гибридный поиск и выбирать подход исходя из ситуации. Высокая текучесть (более 10% в день) — эмбеддинг на лету. Средний масштаб с чёткими паттернами — горячие/холодные тиры. Массивный масштаб со стабильными данными — полный pre-embedding.

Ключевые факторы принятия решений:

Полнотекстовый поиск с переформулированием запросов даёт идеальную свежесть данных при низкой сложности настройки и задержке запроса ниже 50 мс. Смена модели тривиальна, чанкинг не нужен, подходит для большинства сценариев.

Эмбеддинг на лету даёт идеальную свежесть данных при низкой сложности настройки, но более высокой задержке запроса 200-500 мс. Смена модели тривиальна, чанкинг нужен, лучше всего подходит для сценариев с высокой текучестью.

Горячие/холодные тиры дают смешанную свежесть данных при средней сложности настройки и задержке запроса 50-100 мс. Смена модели проста, чанкинг нужен, обеспечивают сбалансированную производительность для разных потребностей.

Полный pre-embedding имеет устаревшие данные до переиндексации, высокую сложность настройки, но задержку запроса ниже 50 мс. Смена модели болезненна, чанкинг нужен, рассчитан на операции массивного масштаба.

Правило 80/20: 60% систем должны остановиться на полнотекстовом поиске плюс переформулировании запросов. 25% нужен гибридный подход с эмбеддингом на лету или горячими/холодными тирами. 10% нужен полный pre-embedding. 5% нужны кастомные решения.

Итог: не стоит быть тем, кто строит решение для 5% случаев ради задачи из 60%.