← Лендинг Hermes Agent

Опубликовано

Продвинутый RAG: четыре места вокруг ретривера

Автор: Гусев Николай [портфолио]

Обновлено

Темы: ai-agents, kniga, rag

Пятая часть цикла по книге AI Agents and Applications Роберто Инфанте (Manning, 2026). Главы 8-10 книги: продвинутая индексация, трансформации вопросов, роутинг и постобработка. Предыдущие части: фундамент и промпты, суммаризация с живым замером, LangGraph, RAG в глубину.

Серия выходит раз в 1-2 дня, пометки: #AI_Agents_and_Applications #глава_5.

Одна карта на три главы

Главы 6-7 построили RAG из двух шагов: индексация и запрос. Следующие три главы добавляют к ним ещё места вмешательства, и весь материал раскладывается по одной карте:

Карта продвинутого RAG: приёмы глав 8-10 вокруг ретривера и наши аналоги

Векторная БД остаётся той же во всех трёх главах. Меняется то, что в неё кладут (глава 8), что в неё приходит (глава 9), куда она смотрит и что отдаёт (глава 10).

Глава 8. Что лежит в индексе

Проблема: один эмбеддинг на чанк - всегда компромисс. Крупный чанк даёт контекст, но его вектор размыт. Мелкий точен, но беден контекстом.

Решение - двухслойность. Нижний слой: мелкие сегменты для точного поиска. Верхний: то, что реально уходит в LLM. Четыре приёма книги - четыре варианта нижнего слоя:

  • родитель-ребёнок: родители по 3000 символов, дети по 500, поиск по детям, в LLM уходит родитель;
  • эмбеддинги саммари: в индексе плотное саммари, в docstore оригинал;
  • гипотетические вопросы: LLM генерирует четыре вопроса на чанк, в индекс кладутся вопросы. Работает как мост: вектор вопроса попадает в вектор вопроса, даже если формулировки документа далеки от формулировки пользователя;
  • расширение чанков: индексируем мелкий чанк, в docstore кладём окно из трёх соседних чанков вместо одного.

Последний - самый дешёвый: ноль вызовов LLM на индексации.

Отдельный класс - таблицы и картинки. Их нельзя осмысленно нарезать на чанки, поэтому они индексируются через текстовую проекцию: саммари в векторную БД, исходник в docstore. Общий принцип: вектор всегда про текст, LLM всегда получает оригинал.

"Заметки на полях". Наш book2rag режет книги по структуре markdown-заголовков - иерархический чанкинг из этого раздела: чанк это раздел, семантика не рвётся посреди таблицы. Метаданные (глава, страница, источник) кладутся в каждый чанк с первого дня - именно это потом сделало возможной фильтрацию. Крайняя форма текстовой проекции - memvid: вся память агента в одном видеофайле, эмбеддинги в кадрах.

Отдельная статья: memvid: память для AI-агентов в одном файле.

Глава 9. Что входит в поиск

Поиск проседает часто не из-за отсутствия данных, а из-за самого вопроса. Четыре приёма:

  • Rewrite-Retrieve-Read: перед поиском дешёвая модель переписывает вопрос. "Tell me some fun things I can enjoy in Cornwall" превращается в "fun activities to do in Cornwall" - и выдача перестаёт быть тремя кусками одной секции.
  • Multi-query: вопрос с несколькими неявными вопросами разворачивается в пять независимых. Самый дорогой приём: пять поисков плюс пять вызовов LLM на один вопрос, и без ранжирования на выходе пять списков идут в промпт как есть.
  • Step-back: не дробить, а обобщить. "Tips for a trip to Brighton?" становится "general tips for a coastal city", поиск идёт по обоим. Нюанс автора: дешёвые модели генерируют слишком общие абстракции, в книге сюда поставлена дорогая модель.
  • HyDE: индекс не трогаем, под вопрос генерируется гипотетический ответ-документ и ищется по нему. Риск, который книга не обсуждает: гипотетический документ может быть уверенно неверным, и синтез потом опирается на выдумку.

Плюс декомпозиция зависимых вопросов: "средняя температура августа на самом популярном песчаном пляже Корнволла" - сначала пляж, потом температуры для него, потом среднее. Честная оговорка авторов: в SQL этот вопрос решается одним AVG, то есть LLM городит цепочку там, где база справится сама.

"Заметки на полях". Наш аналог декомпозиции - детектор составных запросов из auto-rag. Отличие от книги принципиальное: детекция детерминированная, словарём доменных слов, а не LLM-промптом. Ноль вызовов модели, ноль латентности, покрыто юнит-тестами. Если в вопросе есть слова и из домена продуктов, и из домена инфраструктуры - вопрос составной, разворачивается в два под-запроса по своим коллекциям:

def detect_compound(query: str, dcd: dict | None = None) -> list[dict]:
    ql = query.lower()
    has_product = any(w in ql for w in _COMPOUND_PRODUCT_WORDS)
    has_infra = any(w in ql for w in _COMPOUND_INFRA_WORDS)
    if not (has_product and has_infra):
        return []
    subqueries = []
    if has_product:
        subqueries.append({"query": query, "domain": "astra-products",
                           "collection": "astra-products"})
    if has_infra:
        infra_terms = [w for w in _COMPOUND_INFRA_WORDS if w in ql]
        infra_query = f"{query} {' '.join(infra_terms[:3])}" if infra_terms else query
        subqueries.append({"query": infra_query, "domain": "devops",
                           "collection": "deployment"})
    return subqueries

Вопрос "миграция ALD Pro на MS AD" содержит слова обоих словарей - получает два под-запроса: по коллекции продуктов и по коллекции развёртывания. Полный файл: compound.py в материалах выпуска. Роутинг по типу вопроса мы разбирали отдельно: Домашний Perplexity, часть 2: кто думает.

Глава 10. Куда идёт поиск

Когда источников несколько - векторная БД, SQL, граф - вопрос классифицируется и уходит в нужную ветку. В книге роутер сделан через структурированный вывод с ограничением на два значения. Два пункта из summary, которых нет в основном тексте и которые критичны: few-shot примеры в промпт роутера и fallback на альтернативный источник при пустой выдаче. Классификатор всегда ошибается, а пустая выдача без fallback выглядит для пользователя как "не знаю".

Self-querying: вопрос разделяется на семантическую часть и метаданные, БД сначала фильтрует, потом ищет по смыслу в отобранном. Узкое место - не LLM, а разметка: поле destination в примере расставлено руками по 20 URL. Без качественной разметки на индексации LLM будет уверенно генерировать фильтр по несуществующему полю.

Text-to-SQL: главная находка из цитируемой статьи Rajkumar и коллег - галлюцинации в SQL это в первую очередь выдуманные имена таблиц и колонок, и они резко падают, если в промпт класть CREATE TABLE плюс примеры строк. Отдельно чистка: модель заворачивает SQL в markdown-обвязку, и исполнитель падает на тройных кавычках перед SELECT.

*"Заметки на полях". Наш роутер dcd_router тоже детерминированный: 15 доменов словарём ключевых слов с весами и anti-keywords (слова, которые наоборот уводят из домена). Цена вопроса - микросекунды вместо вызова LLM. Полный файл - dcd_router.py в материалах выпуска (ссылка следом). LLM-классификатор из книги хорош там, где домены нельзя выразить словарём; у нас домены известны заранее, и словарь дешевле, предсказуемее и тестируем.

Файл: dcd_router.py в материалах выпуска.

Глава 10. Что выходит из поиска

Постобработка отсекает и улучшает то, что дошло от ретривера, до отправки в LLM:

  • порог по схожести. Число 0.6 в примере с потолка: зависит от модели эмбеддингов и метрики, между проектами не переносится;
  • ключевые слова: обязательные и запрещённые, пересечение по токенам;
  • временное взвешивание: скор плюс штраф за давность обращения. Побочный эффект - штрафуются вечные корректные статьи, которые давно не открывали;
  • RRF, Reciprocal Rank Fusion: слияние нескольких выдач по рангам, формула 1/(rank+k), без калибровки абсолютных скоров. Естественный мост к гибридному поиску: dense-выдача плюс BM25, слитые по рангам.

RRF в книге - 20 строк на чистом Python, и в них дыра, которую авторы не замечают: ключ пары это группа и локальный ранг, дедупликации по содержимому нет. Два одинаковых чанка из двух запросов считаются разными документами: дубликат задваивается в промпте, а его двойной вклад искажает ранжирование.

"Заметки на полях". Наша постобработка в gateway собрана ровно из этих деталей. Дедупликация каноническая: документы группируются по каноническому идентификатору, победитель группы выбирается по приоритету источника (живой корпоративный источник бьёт локальный снапшот, тот бьёт память агента, тот - открытый веб), а проигравшие сохраняются в метаданные как альтернативные источники:

_ORIGIN_PREFERENCE = {
    EvidenceOrigin.LIVE_CORPORATE: 4,
    EvidenceOrigin.LOCAL_SNAPSHOT: 3,
    EvidenceOrigin.AGENT_MEMORY: 2,
    EvidenceOrigin.PUBLIC_WEB: 1,
}

def deduplicate_evidence(evidence: Iterable[Evidence]) -> list[Evidence]:
    groups: dict[str, list[Evidence]] = {}
    order: list[str] = []
    for item in evidence:
        canonical_id = item.canonical_id or item.document_id
        if canonical_id not in groups:
            groups[canonical_id] = []
            order.append(canonical_id)
        groups[canonical_id].append(item)
    deduplicated = []
    for canonical_id in order:
        candidates = groups[canonical_id]
        winner = max(candidates, key=lambda item: (
            _ORIGIN_PREFERENCE.get(item.origin, 0),
            item.final_score, item.retrieval_score))
        alternates = [item for item in candidates if item is not winner]
        if alternates:
            metadata = dict(winner.metadata)
            metadata["alternate_sources"] = tuple(i.source for i in alternates)
            winner = replace(winner, metadata=metadata)
        deduplicated.append(winner)
    return deduplicated

Дальше rerank локальным кросс-энкодером ms-marco-MiniLM (около 80МБ, на CPU, без round-trip в LM Studio) и бусты за точное совпадение идентификаторов: если запрос это номер документа или slug - такой документ поднимается фиксированной добавкой к скору. Оба файла лежат в материалах выпуска, ссылки следом. Почему порог схожести и "больше контекста" не работают сами по себе - отдельная статья, ссылка следом.

Статья: Больше контекста - хуже результат.

Файлы: rerank_adapter.py, boosting.py.

Чего нет в книге ни разу

Во всех трёх главах нет ни одного числа. Ни recall@k до и после, ни доли вопросов, где приём дал выигрыш. Вся метрика - "выдача стала разнообразнее". Для книги допустимо, для инженерного решения нет: приём без замера на золотом наборе не внедряется, потому что половина описанных приёмов на среднем корпусе не окупит добавленную сложность и точку отказа.

У нас оценка двухслойная: детерминированный слой без модели и слой LLM-судьи поверх. Приём внедряется, только если метрика на золотом наборе выросла.

"Заметки на полях". Методологию "RAG от простого к сложному" и типовые ловушки мы разбирали в трёх статьях, ссылки следом.

Статьи: RAG от простого к сложному, Что не пишут в "RAG за 5 минут", Что не пишут в "RAG за 5 минут", часть 2.

Что забрать в работу

  1. Двухслойность из главы 8 - самый дешёвый приём: расширение чанков соседями вообще без LLM на индексации.
  2. Rewrite перед поиском - один вызов дешёвой модели, заметный эффект на кривых вопросах.
  3. Роутинг с fallback - всегда. Пустая выдача без fallback это "не знаю" для пользователя.
  4. Дедупликация перед RRF - обязательна, иначе двойной вклад дубликата искажает слияние.
  5. Метрики до и после - приём без замера на золотом наборе не внедряется.

Продолжение - главы 11-12 книги: агенты и мультиагентные системы.

Книга: AI Agents and Applications (Roberto Infante, Manning, 2026), главы 8-10.

#AI_Agents_and_Applications #глава_5