Contextual Retrieval: техника, которая чинит главную проблему RAG за 50 центов на тысячу чанков. bm25.. bm25. Contextual Retrieval.. bm25. Contextual Retrieval. embeddings.. bm25. Contextual Retrieval. embeddings. llm.. bm25. Contextual Retrieval. embeddings. llm. rag.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск. искусственный интеллект.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск. искусственный интеллект. корпоративный ИИ.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск. искусственный интеллект. корпоративный ИИ. Машинное обучение.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск. искусственный интеллект. корпоративный ИИ. Машинное обучение. Поисковые технологии.. bm25. Contextual Retrieval. embeddings. llm. rag. база знаний. Блог компании OTUS. векторный поиск. искусственный интеллект. корпоративный ИИ. Машинное обучение. Поисковые технологии. чанки.

В сентябре 2024 года Anthropic опубликовала технику Contextual Retrieval с обещанием снизить процент неудачных извлечений в RAG‑системах на 49% при использовании только embeddings и на 67% в сочетании с reranking. К середине 2026 года эта техника закрепилась как один из стандартов production‑RAG и применяется в подавляющем большинстве серьёзных корпоративных систем.

Разберём, в чём её суть, почему она вообще работает, как её реализовать и где она ломается.

Корневая проблема classical RAG, которую долго игнорировали

Стандартная схема RAG режет документ на чанки по 256–512 токенов, прогоняет каждый чанк через embedding‑модель, складывает векторы в базу. На запросе считается embedding вопроса, ищется топ‑K ближайших чанков по косинусной близости, найденные чанки склеиваются и подаются в промпт LLM как контекст.

В этой схеме есть глубинная проблема, которая не решается ни выбором лучшей embedding‑модели, ни тонкой настройкой размера чанка, ни добавлением overlap. Проблема в том, что чанк теряет контекст документа, к которому он относится.

Возьмём пример из квартального финансового отчёта компании. Где‑то на 47 странице есть параграф:

«Выручка компании в этом квартале выросла на 3% по сравнению с предыдущим кварталом. Северо‑американский регион показал рост в 12%, тогда как европейский снизился на 5%.»

При chunking этот параграф попадает в отдельный чанк, теряя всё, что было раньше в документе. Embedding модели для этого фрагмента будет похожа на embedding других параграфов про выручку из других кварталов, других компаний, других периодов. Когда пользователь спросит «какая выручка ACME в Q2 2024?», embedding‑поиск может вытащить любой из десятков чанков про выручку, и шансы попасть в правильный неочевидны.

Параметрические знания самой LLM здесь не помогают: модель не знает, какая компания, какой квартал, какой год. Метаданные документа теоретически могут помочь, но они хранятся отдельно от embedding и обычно используются только для фильтрации, не для ранжирования.

Эта проблема видна на любом корпусе, где документы тематически близки между собой. Юридические договоры все говорят про «стороны» и «обязательства», технические спецификации все говорят про «параметры» и «требования», финансовые отчёты все говорят про «выручку» и «расходы». Чанки из разных документов в таких корпусах легко перепутываются, и точность извлечения падает.

Идея Contextual Retrieval в одну фразу

Перед индексацией каждого чанка LLM генерирует короткий контекстный префикс (50–100 токенов), который объясняет, к какому документу и какой его части относится чанк. Префикс добавляется в начало чанка перед вычислением embedding и индексированием в BM25.

Тот же параграф из примера выше после обогащения выглядит так:

«Этот фрагмент взят из квартального финансового отчёта ACME Corporation за Q2 2024 года, раздел про региональные показатели выручки. Выручка компании в этом квартале выросла на 3% по сравнению с предыдущим кварталом. Североамериканский регион показал рост в 12%, тогда как европейский снизился на 5%».

Теперь embedding учитывает компанию, квартал, год, тип документа, раздел. Запрос «выручка ACME в Q2 2024» с большой вероятностью попадёт именно в этот чанк. BM25 тоже выигрывает: keyword‑поиск по «ACME» и «Q2 2024» теперь матчит с этим чанком, тогда как до обогащения никаких таких ключевых слов в чанке не было.

Бенчмарки Anthropic

В оригинальной публикации Anthropic приводит цифры на нескольких бенчмарках (Codebase, Common Resources, FinancialDocs, Knowledge Base):

  • Naive RAG только с embeddings: процент неудачных извлечений 5.7%.

  • С Contextual Embeddings (только embedding обогащённый): 3.7%, улучшение на 35%.

  • С Contextual Embeddings + Contextual BM25: 2.9%, улучшение на 49%.

  • С Contextual Embeddings + Contextual BM25 + Reranking: 1.9%, улучшение на 67%.

Воспроизведение на других корпусах в течение 2025–2026 годов показало похожие цифры: техника работает, эффект существенный, прирост качества стабильный. На некоторых специализированных корпусах (юридические документы, технические спецификации) эффект ещё больше — до 70–80% снижения failure rate.

Метрика, которую измеряет Anthropic, это retrieval failure rate: процент случаев, когда правильный документ не попал в топ-20 результатов поиска. Если правильный документ в топе, дальше его извлекает reranking или просто ставит на нужное место в финальном контексте. Если его в топе нет, никакие дальнейшие шаги не помогут.

Промпт для генерации контекста

В оригинальной публикации Anthropic использует следующий формат промпта:

<document>
{полный текст документа}
</document>

Вот чанк из этого документа:
<chunk>
{текст чанка}
</chunk>

Дай короткое описание (50-100 токенов) того, к чему относится этот чанк
в контексте всего документа. Описание должно помочь поиску правильно
классифицировать этот чанк. Отвечай только описанием, без преамбулы.

LLM получает весь документ целиком в качестве контекста и конкретный чанк, для которого нужно сгенерировать описание.

Описание должно содержать: тип документа, главную тему, конкретный раздел или подтему, к которому относится чанк, при наличии — конкретные сущности (компания, продукт, версия, период). Не должно содержать: пересказ чанка, выводы, оценки, ответы на гипотетические вопросы.

def generate_chunk_context(document: str, chunk: str, llm_client) -> str:
    prompt = f"""<document>
{document}
</document>

Here is the chunk we want to situate within the whole document:
<chunk>
{chunk}
</chunk>

Please give a short succinct context (50-100 tokens) to situate this chunk
within the overall document for the purposes of improving search retrieval
of the chunk. Answer only with the succinct context and nothing else."""
    
    response = llm_client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=200,
        messages=[{"role": "user", "content": prompt}]
    )
    return response.content[0].text.strip()

По моделькам хорошо работает Claude Haiku, потому что задача относительно простая, не требует креативности, а Haiku в 10–15 раз дешевле Sonnet или Opus. На бенчмарках разница в качестве между моделями для этой задачи минимальная.

Стоимость и оптимизация через prompt caching

Главная стоимость Contextual Retrieval — это вызов LLM для каждого чанка. Документ в 100 страниц при размере чанка 500 токенов даёт 50–100 чанков. На 10 тысяч документов это 500 тысяч — 1 миллион вызовов LLM. Без оптимизации стоимость становится заметной.

Anthropic в публикации указывает цифру: 1.02 доллара за миллион токенов с использованием Claude Haiku 3 и prompt caching. К 2026 году с Claude Haiku 4.5 цифра ещё ниже, около 0.50 доллара на тысячу чанков для типичных документов.

Prompt caching — это ключевая оптимизация. Без него каждый вызов LLM передаёт весь документ целиком, что для большого документа (200–1000 чанков) повторяется по 200–1000 раз. С prompt caching документ кэшируется на стороне модели, и при повторных вызовах для других чанков того же документа платится только за кэш‑чтение, что в 10 раз дешевле обычного input.

def generate_contexts_for_document(document: str, chunks: list[str], llm_client) -> list[str]:
    contexts = []
    for chunk in chunks:
        response = llm_client.messages.create(
            model="claude-haiku-4-5",
            max_tokens=200,
            messages=[
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "text",
                            "text": f"<document>n{document}n</document>",
                            "cache_control": {"type": "ephemeral"}
                        },
                        {
                            "type": "text",
                            "text": f"""Here is the chunk we want to situate:
<chunk>
{chunk}
</chunk>

Please give a short succinct context (50-100 tokens) to situate this chunk
within the document for retrieval purposes. Answer with the context only."""
                        }
                    ]
                }
            ]
        )
        contexts.append(response.content[0].text.strip())
    return contexts

cache_control: {"type": "ephemeral"} указывает API кэшировать предыдущий контент на пять минут. При вызове для следующего чанка того же документа, LLM не пересчитывает токены документа, а читает их из кэша. Экономия драматическая.

Дополнительная оптимизация — batch API. Все вызовы для индексации можно отправить batch‑запросом, который выполняется асинхронно за 24 часа и стоит на 50% дешевле обычного. Для разовой индексации корпоративной базы знаний это идеальный вариант.

В итоге индексация корпуса в 10 тысяч документов с 50 чанками каждый (500 тысяч чанков) стоит порядка 200–300 долларов через Haiku 4.5 + batch + prompt caching. Это разовая затрата, дальше контекст переиспользуется на каждом запросе бесконечно.

Реализация end‑to‑end pipeline

Полная схема индексации с Contextual Retrieval выглядит примерно так.

import anthropic
from typing import Iterator

class ContextualRAGIndexer:
    def __init__(self, anthropic_client, embedding_client, vector_store, bm25_store):
        self.llm = anthropic_client
        self.embedder = embedding_client
        self.vectors = vector_store
        self.bm25 = bm25_store
    
    def chunk_document(self, document: str) -> list[str]:
        # Любая стратегия chunking: sliding window, semantic, structural
        # Здесь для примера простой sliding window
        chunks = []
        chunk_size = 500
        overlap = 100
        words = document.split()
        for i in range(0, len(words), chunk_size - overlap):
            chunks.append(" ".join(words[i:i + chunk_size]))
        return chunks
    
    def generate_context(self, document: str, chunk: str) -> str:
        prompt = f"""<chunk>
{chunk}
</chunk>

Please give a short succinct context (50-100 tokens) to situate this chunk
within the overall document for the purposes of improving search retrieval.
Answer only with the succinct context and nothing else."""
        
        response = self.llm.messages.create(
            model="claude-haiku-4-5",
            max_tokens=200,
            messages=[{
                "role": "user",
                "content": [
                    {
                        "type": "text",
                        "text": f"<document>n{document}n</document>",
                        "cache_control": {"type": "ephemeral"}
                    },
                    {"type": "text", "text": prompt}
                ]
            }]
        )
        return response.content[0].text.strip()
    
    def index_document(self, doc_id: str, document: str, metadata: dict):
        chunks = self.chunk_document(document)
        
        for chunk_idx, chunk in enumerate(chunks):
            context = self.generate_context(document, chunk)
            enriched_chunk = f"{context}nn{chunk}"
            
            embedding = self.embedder.embed(enriched_chunk)
            
            chunk_id = f"{doc_id}_chunk_{chunk_idx}"
            chunk_metadata = {
                **metadata,
                "doc_id": doc_id,
                "chunk_idx": chunk_idx,
                "original_chunk": chunk,
                "context": context
            }
            
            self.vectors.upsert(chunk_id, embedding, chunk_metadata)
            self.bm25.add(chunk_id, enriched_chunk, chunk_metadata)

При запросе извлекаются результаты из обеих систем (vectors + BM25), сливаются через Reciprocal Rank Fusion, опционально пропускаются через reranking, и финальный топ передаётся в LLM как контекст для генерации ответа.

При формировании финального промпта для генерации обычно используется поле original_chunk (без контекстного префикса), потому что префикс был нужен только для поиска, а в контексте генерации он избыточен и съедает токены. Метаданные с полем context сохраняются на случай, если они понадобятся для отображения пользователю.

Совместимость с reranking

Contextual Retrieval хорошо сочетается с cross‑encoder reranking. Логика двухэтапная: contextual hybrid retrieval достаёт топ-20-40 кандидатов с обогащёнными embeddings и BM25, потом reranker оценивает релевантность каждого кандидата к запросу и отдаёт топ-5-10 для финального контекста LLM.

Цифры из оригинальной публикации Anthropic показывают, что комбинация даёт мультипликативный эффект: каждая техника отдельно улучшает результаты, в сочетании они складываются почти линейно. Contextual embeddings + Contextual BM25 дают улучшение на 49%, добавление reranker поверх — улучшение до 67%.

В практической реализации reranking работает на чанках вместе с контекстом, потому что reranker тоже выигрывает от увиденного контекста. Чанк с фразой «выручка выросла на 3%» сам по себе мало значит для reranker, потому что не привязан к компании и периоду; с контекстом «отчёт ACME за Q2 2024» reranker точнее оценивает релевантность к конкретному запросу.

Когда Contextual Retrieval не работает или избыточен

Не каждая RAG‑система выигрывает от Contextual Retrieval, и для некоторых сценариев накладные расходы не окупаются.

  1. Малые корпуса (меньше 1000 чанков). Если документов мало и они тематически различны, обычный embedding‑поиск с reranking работает достаточно хорошо. Затраты на индексацию контекстом не окупаются маргинальным улучшением.

  2. Сильно структурированные документы. Если каждый чанк уже содержит явные сущности и контекст (например, JSON‑объекты с явными полями entity_type, entity_id, period), то контекст из него уже извлекается на этапе chunking без LLM. Contextual Retrieval тут дублирует то, что уже есть в данных.

  3. Корпусы с очень высокой динамикой обновления. Если документы переписываются ежечасно, переиндексация с Contextual Retrieval становится постоянным расходом. Для таких систем обычно выгоднее построить специализированную retrieval‑стратегию.

  4. Однотипные документы с очень похожими структурой. Например, тысячи product reviews одного формата. Контекст для каждого ревью получится одинаковым («это ревью продукта X пользователя Y от даты Z»), и его проще генерировать структурно из метаданных, чем через LLM.

  5. Запросы, не требующие конкретного контекста документа. Если пользователи задают общие вопросы вроде «что такое OAuth» без привязки к конкретному документу, Contextual Retrieval может даже ухудшить результаты, потому что embedding обогащённого чанка дрифтует в сторону контекста (компания, продукт), а не в сторону самого предмета вопроса.

В этих случаях есть альтернативы: метаданные‑driven фильтрация перед поиском, hierarchical retrieval с двумя уровнями (сначала документ, потом чанк внутри), specialized retrievers для разных типов вопросов.

Что делать с обновлениями документов

Один из неочевидных аспектов Contextual Retrieval — что делать, когда документ обновляется. Контекст чанка генерировался на основе старой версии документа, и после редактирования может стать некорректным.

Несколько стратегий:

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

  2. Инкрементальное обновление по изменённым чанкам. Если diff‑инструмент определяет, какие чанки изменились, контекст пересчитывается только для них. Сложнее в реализации, дешевле в эксплуатации.

  3. Переиндексация только при значительных изменениях. Версионирование документа и переиндексация при major‑update, игнорирование minor‑edits. Компромисс: контекст немного устаревает, но индексация не идёт постоянно.

В большинстве случаев используется простая стратегия: полная переиндексация документа при любом изменении, плюс ночная задача, которая прогоняет переиндексацию всего корпуса раз в месяц для защиты от drift.

Базовая гигиена и чек‑лист внедрения

Минимальный план применения Contextual Retrieval в существующей RAG‑системе:

  1. Проверить evaluation baseline. Без replicable metric нет смысла внедрять что‑либо: непонятно, помогло или нет. Если evaluation pipeline отсутствует, сначала собрать датасет из 100–200 вопрос‑ответ‑документ пар.

  2. Реализовать генерацию контекстов через Claude Haiku 4.5 с prompt caching. На прототипе достаточно синхронной генерации, для production обязательно batch API.

  3. Переиндексировать корпус с обогащёнными чанками. Сохранить оригинальные чанки и контексты отдельно в метаданных.

  4. Прогнать evaluation на новой индексации. Сравнить Recall@K, MRR, NDCG со старой версией. Должно быть улучшение хотя бы на 20–30% по любой метрике; если меньше — что‑то не так с реализацией или корпусом.

  5. Если результаты хорошие, добавить hybrid retrieval (BM25 + dense) на обогащённых чанках. Это даёт дополнительный буст.

  6. Финальный шаг — добавить reranking через cross‑encoder. К этому моменту RAG‑система должна работать на 50–70% точнее, чем naive baseline.

  7. Постоянный мониторинг качества на production‑трафике: процент запросов, на которых пользователь не нашёл правильный ответ, метрики через faithfulness и context precision, регулярное human evaluation на выборке.

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


Если хотите разобрать RAG не только на уровне схемы, но и в прикладных сценариях, приходите на бесплатные уроки OTUS. Обсудим, как LLM-приложения работают с базами знаний, документами и разными типами данных — и где в таких системах начинаются реальные инженерные сложности.

  • 6 июля в 20:00 — «Как сделать LLM приложение, которое отвечает клиентам по базе знаний компании». Записаться
    Разберём, что такое LLM-app, как устроена база знаний для ИИ и как модель находит нужный ответ в документах.

  • 16 июля в 20:00 — «Мультимодальный RAG: как LLM работают с текстами, картинками и документами». Записаться
    Поговорим о Retrieval-Augmented Generation и его мультимодальной версии: как LLM работают не только с текстом, но и с изображениями и документами.

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

Что ещё почитать по теме RAG:

Автор: badcasedaily1

Источник