Конфликт SQL и векторного поиска в RAG: как приоритизировать свежие данные | AiManual
AiManual Logo Ai / Manual.
14 Фев 2026 Гайд

Когда свежие SQL-данные проигрывают старым векторам: как RAG-системы сходят с ума от конфликта источников

Почему ваша RAG-система возвращает устаревшую информацию и как заставить её доверять свежим SQL-данным больше, чем старым векторам. Практическое решение на 2026

Ваша RAG-система врёт. И знаете почему? Она не может выбрать между правдой вчерашней и правдой сегодняшней

Представьте себе: у вас есть база знаний компании. Вчера клиент написал в поддержку — его запрос зафиксировали в SQL-таблице. Сегодня проблема решилась — статус обновили в той же таблице. А ваш RAG-пайплайн продолжает возвращать ответы на основе вчерашнего чанка, который когда-то завекторизировали и забыли.

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

Реальная история из продакшена: Система технической поддержки на RAG. Клиент спрашивает "Статус моей заявки #12345". В SQL статус уже "Решено", но RAG возвращает ответ на основе старого вектора с текстом "Заявка находится в обработке". Потому что семантически этот чанк ближе к запросу. Катастрофа доверия.

Почему это происходит? Механика конфликта

Когда вы строите RAG-систему с гибридным поиском (векторный + SQL), вы фактически создаёте двух независимых свидетелей. Один — векторная БД — помнит всё, что было сказано когда-либо. Другой — SQL — знает, что происходит прямо сейчас.

При запросе система:

  1. Ищет в векторах семантически близкие чанки
  2. Параллельно опрашивает SQL на предмет актуального состояния
  3. Собирает всё в контекст и отправляет в LLM
  4. Надеется, что модель сама разберётся, чему верить

А вот надеяться не на что. Современные LLM вроде GPT-4o (актуально на февраль 2026) или Claude 3.5 Sonnet не имеют встроенного детектора свежести. Они видят два противоречащих факта и выбирают тот, который лучше вписывается в контекст. Часто — старый.

💡
Это та же проблема, что и в деградации поиска при росте базы, только с временным измерением. Больше данных — больше шансов, что старый релевантный чанк перевесит свежие SQL-данные.

Три подхода, которые НЕ работают (и один, который сломает вам систему)

Перед тем как показать работающее решение, давайте пройдёмся по популярным антипаттернам:

1. Довериться реранкерам

"Давайте поставим реранкер, который будет оценивать свежесть!" — звучит логично. Пока вы не понимаете, что реранкеры в 2026 всё ещё работают на семантике, а не на временных метках. Cohere Rerank, BGE Reranker — все они ищут релевантность, а не актуальность.

2. Добавить временные метки в промпт

"Система: Учитывай, что данные из SQL свежие, а из векторной БД могут быть устаревшими". LLM кивает и делает ровно то, что делала раньше. Потому что промпт — не программа, а вежливая просьба.

3. Полностью отказаться от векторов для изменяемых данных

Радикально, но глупо. Вы теряете главное преимущество RAG — семантический поиск. Теперь находить можно только по точным совпадениям в SQL. Прощай, естественный язык.

4. Hard delete устаревших векторов (осторожно!)

Удалить чанк из векторной БД при изменении данных в SQL. Теоретически работает. Практически — вы теряете историю. А что если пользователь спросит "А какое было описание товара до обновления?" Или "Покажи мне все версии документа?". Упс.

Кстати, если думаете, что ваша проблема уникальна — почитайте обзор свежих исследований RAG. Там GraphRAG пытается решать похожие проблемы через связи между сущностями, но временное измерение всё равно хромает.

Рабочее решение: TTL Gating + Metadata Priority Routing

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

1 Добавляем временные метаданные ко всему

Каждый чанк в векторной БД должен иметь:

{
  "chunk_id": "doc_123_chunk_5",
  "source_id": "doc_123",
  "created_at": "2026-02-13T10:30:00Z",
  "updated_at": "2026-02-13T10:30:00Z",
  "ttl_hours": 24,
  "is_mutable": true,
  "sql_check_required": true
}

Ключевые поля:

  • ttl_hours — через сколько часов чанк считается "подозрительным" на устаревание
  • is_mutable — могут ли данные меняться со временем (статусы, цены, описания)
  • sql_check_required — нужно ли проверять свежесть в SQL при попадании в топ результатов

2 Строим двухэтапный пайплайн поиска

Вместо параллельного запроса к SQL и векторам — последовательный с приоритетами:

async def hybrid_search_with_freshness(query: str, user_id: str):
    # Этап 1: Векторный поиск с фильтрацией по времени
    vector_results = await vector_db.search(
        query=query,
        filter={
            "$or": [
                {"is_mutable": False},  # Неизменяемые данные всегда ок
                {
                    "is_mutable": True,
                    "updated_at": {"$gte": datetime.now() - timedelta(hours=24)}
                }
            ]
        },
        limit=20
    )
    
    # Этап 2: Для "старых" изменяемых чанков — проверка в SQL
    fresh_data_from_sql = {}
    for chunk in vector_results:
        if chunk["is_mutable"] and chunk["ttl_expired"]:
            sql_data = await sql_check_freshness(chunk["source_id"])
            if sql_data["is_fresher"]:
                # Помечаем векторный результат как deprecated
                chunk["deprecated_by_sql"] = True
                fresh_data_from_sql[chunk["source_id"]] = sql_data
    
    # Этап 3: Сбор финального контекста с приоритетом SQL
    context_parts = []
    
    # Сначала свежие SQL-данные
    for sql_item in fresh_data_from_sql.values():
        context_parts.append(f"[СВЕЖИЕ ДАННЫЕ из SQL]: {sql_item['content']}")
    
    # Затем не устаревшие векторы
    for chunk in vector_results:
        if not chunk.get("deprecated_by_sql", False):
            context_parts.append(f"[ИЗ ВЕКТОРНОЙ БАЗЫ]: {chunk['content']}")
    
    return await llm.generate(context="\n\n".join(context_parts), query=query)

3 Реализуем фоновую синхронизацию

TTL gating — это хорошо, но ещё лучше предотвращать конфликты заранее:

# Фоновая задача, которая обновляет векторы при изменении SQL
async def background_vector_sync():
    while True:
        # Находим документы, изменённые в SQL за последний час
        changed_docs = await sql_db.query("""
            SELECT doc_id, updated_at 
            FROM documents 
            WHERE updated_at > :last_sync 
            AND vector_sync_required = true
        """, {"last_sync": last_sync_time})
        
        for doc in changed_docs:
            # Реиндексируем чанки с новыми данными
            new_chunks = await chunker.process(doc["content"])
            await vector_db.upsert(
                vectors=embed(new_chunks),
                metadata=[{
                    **chunk.metadata,
                    "updated_at": doc["updated_at"],
                    "sql_check_required": False  # Теперь свежий!
                } for chunk in new_chunks]
            )
        
        await asyncio.sleep(300)  # Каждые 5 минут
💡
Этот подход особенно важен для систем, где SQL и векторы живут в разных сервисах. Если же вы используете что-то вроде pgvector (векторы прямо в PostgreSQL), часть проблем решается на уровне транзакций — но появляются другие. Подробнее в статье про архитектуру локального RAG-пайплайна.

Типичные ошибки, которые всё испортят

Даже с правильной архитектурой можно наступить на грабли:

Ошибка Последствие Как исправить
Единый TTL для всех типов данных Новости устаревают за часы, документация — за годы Динамический TTL на основе типа контента
Игнорирование часовых поясов В 23:59 данные свежие, в 00:01 — уже устаревшие UTC для всех меток времени, конвертация на клиенте
Слишком агрессивная синхронизация Каждое изменение в SQL вызывает реиндексирование Дебаунсинг обновлений (например, раз в 15 минут)
Отсутствие fallback-стратегии При падении SQL система не возвращает ничего Кэширование последних успешных SQL-ответов

А что насчёт мультимодальности? Там всё ещё хуже

Добавьте к этой проблеме изображения, аудио, видео — и получите настоящий ад. Представьте: в SQL обновилось описание товара, но связанное с ним изображение в векторной БД осталось старым. Или транскрипт видео обновили, а эмбеддинги аудио — нет.

Для мультимодальных систем нужна cross-modal синхронизация. Если меняется текстовое описание, нужно пересчитывать эмбеддинги связанных медиа. В 2026 году это всё ещё экспериментальная область, но некоторые фреймворки начинают предлагать решения. Посмотрите мультимодальный RAG в 2025 — там есть наработки по синхронизации модальностей.

Практический стек на 2026 год

Что реально работает сегодня (февраль 2026):

  • Векторные БД с TTL поддержкой: Pinecone с expiration policies, Weaviate с auto-schema для временных меток, Qdrant с фильтрацией по datetime
  • Оркестраторы пайплайнов: Haystack 2.0 с кастомными компонентами для freshness routing, LlamaIndex с hybrid search nodes
  • Мониторинг: Phoenix для трассировки, какой источник данных использовался для ответа
  • LLM: GPT-4o с system prompt, явно описывающим приоритет источников, или открытые модели с fine-tuning на конфликтных сценариях

Не доверяйте "умным" системам, которые обещают автоматическое разрешение конфликтов. В 2026 году все они либо переоценивают векторы, либо недооценивают SQL. Нужна явная, прозрачная логика приоритизации.

Что будет дальше? Прогноз на 2027

Проблема свежести данных в RAG станет только острее. С появлением real-time систем и agentic RAG (где агенты сами обновляют знания), конфликт источников будет возникать чаще.

Мои прогнозы:

  1. Появятся специализированные "временные" векторные БД с встроенной поддержкой versioning чанков
  2. LLM начнут получать доступ к metadata filters напрямую, без промежуточного слоя оркестрации
  3. Стандартом станет multi-source confidence scoring — каждый источник данных будет иметь оценку достоверности и свежести
  4. Мы увидим первые реализации Dreaming Engine для автоматического разрешения конфликтов на основе графа знаний

А пока что — внедряйте TTL gating, явные метаданные и двухэтапные пайплайны. Ваши пользователи не должны страдать от того, что ваша система не может отличить вчера от сегодня.

P.S. Если думаете, что ваша RAG-система не страдает от этой проблемы — задайте ей вопрос о чём-то, что менялось в последние 24 часа. И приготовьтесь к неловкому молчанию.