Contextual Retrieval: как сохранить контекст в RAG — это метод предобработки, который дописывает к каждому фрагменту короткую пояснительную приставку до построения индексов. В первичной работе Anthropic метод описан как сочетание Contextual Embeddings и Contextual BM25: приставка объясняет фрагмент через весь документ, поэтому поиск находит нужный кусок там, где раньше терял название сервиса, тариф или период.
Что теряется при разбиении документа на фрагменты
Классический RAG устроен так: корпус режут на куски обычно не длиннее нескольких сотен токенов, каждый кусок превращают в вектор эмбеддингом и складывают в векторную базу для поиска по семантической близости. На запросе база отдаёт самые близкие фрагменты, и они уходят в промпт генеративной модели. Эта схема описана в первичной работе Anthropic о Contextual Retrieval.
Слабое место появляется на шаге нарезки. Фрагмент живёт сам по себе, а документ вокруг него исчезает. В примере из той же работы вопрос звучит как рост выручки ACME Corp за второй квартал 2023 года, а релевантный кусок содержит фразу о росте выручки на 3% к предыдущему кварталу. В куске нет ни названия компании, ни периода, поэтому и найти его по запросу трудно, и использовать в ответе неудобно.
Тот же провал виден на учебном документе D-aster. Полный текст: «Сервис Aster. Регламент экспорта. Для тарифа Команда доступен экспорт CSV. Для тарифа Личный экспорт CSV не предусмотрен. Выгрузка содержит только активные задачи». Фрагмент C-export: «Выгрузка содержит только активные задачи». Запрос: «Попадут ли архивные задачи Aster в CSV?»
Фрагмент отвечает на вопрос по существу, но не называет ни сервис, ни тариф, ни то, что речь идёт о CSV. Слова «Aster», «CSV», «тариф» остались в соседних предложениях. Без названий Aster и CSV смысловая связь с запросом может оказаться слабее. В исходном фрагменте отсутствуют точные строки Aster и CSV, поэтому у BM25 нет этих лексических сигналов. Фактическое место фрагмента в выдаче проверяют на своём корпусе; поиск по этому учебному примеру редакцией не запускался.
Потери при нарезке бывают нескольких видов. Пропадает субъект: непонятно, о каком продукте, компании или человеке речь. Пропадает условие: тариф, роль, регион, версия. Пропадает время: квартал, год, дата вступления в силу. Пропадает тип документа: регламент, инструкция, договор, переписка. Каждый пропуск уменьшает шанс, что поиск вытащит именно этот кусок, и увеличивает шанс, что модель получит фрагмент без опоры.
Практический признак проблемы виден по вопросам, на которые система отвечает уклончиво при том, что ответ в корпусе есть. Читатель спрашивает про архивные задачи, поиск приносит куски про активные, модель отвечает общими словами. Причина в том, что запрос и фрагмент говорят на разных уровнях: запрос называет сущности, фрагмент их подразумевает.
Как устроены Contextual Embeddings и Contextual BM25
Contextual Retrieval решает эту проблему одной операцией: перед эмбеддингом и перед построением BM25-индекса к каждому куску приписывается пояснительная приставка, специфичная для этого куска. В работе Anthropic приведён пример: исходный кусок про рост выручки на 3% превращается в текст, который начинается словами о том, что это фрагмент отчёта SEC о результатах ACME Corp за второй квартал 2023 года и что выручка предыдущего квартала составила 314 миллионов долларов.
Дальше работают две ветки поиска. Contextual Embeddings — это векторный поиск по кускам, у которых приставка уже включена в текст перед эмбеддингом. Contextual BM25 — это лексический индекс, построенный по тому же расширенному тексту. BM25 опирается на идею TF-IDF: TF-IDF измеряет важность слова для документа в коллекции, а BM25 уточняет оценку с учётом длины документа и насыщения по частоте термина, чтобы частые слова не перевешивали результат. Для запросов с уникальными идентификаторами и техническими терминами лексическое совпадение даёт то, что семантический поиск пропускает.
Пример из работы Anthropic: запрос «Error code TS-999» в базе техподдержки. Эмбеддинг может найти общий материал про коды ошибок и пройти мимо точной строки, а BM25 ищет именно эту последовательность символов. Приставка усиливает обе ветки: она добавляет в текст куска названия сущностей, которые запрос называет явно.
Схема стандартного RAG с обеими ветками выглядит так. Корпус режется на куски. Для кусков считаются TF-IDF-кодировки и семантические эмбеддинги. BM25 находит верхние куски по точным совпадениям, эмбеддинги — по семантической близости. Результаты двух списков объединяются и дедуплицируются ранговым слиянием, верхние K кусков уходят в промпт. Contextual Retrieval встраивается в этот конвейер на этапе подготовки: приставка попадает и в эмбеддинг, и в BM25-индекс.
Приставку генерирует языковая модель. В работе Anthropic приведён промпт для Claude 3 Haiku: в документ подставляется весь текст, в кусок — конкретный фрагмент, и модель просят дать короткий контекст, который помещает кусок в общий документ для улучшения поиска. Результат обычно занимает 50–100 токенов и приписывается к куску перед эмбеддингом и перед созданием BM25-индекса.
Стоимость такой предобработки снижается кэшированием промптов. В работе Anthropic приведён расчёт: при кусках в 800 токенов, документах в 8 тысяч токенов, инструкции в 50 токенов и контексте в 100 токенов на кусок разовая стоимость генерации контекстных кусков составляет 1,02 доллара на миллион токенов документа. Документ загружается в кэш один раз, дальше куски ссылаются на закэшированный текст.
Как отделить оригинальный фрагмент от сгенерированного контекста
Приставка — это сгенерированный текст, и он может ошибаться. Поэтому в хранилище оригинал и приставка живут раздельно. Карточка куска держит как минимум четыре поля: идентификатор документа, идентификатор куска, оригинальный текст фрагмента и сгенерированную приставку. Индексируемый текст собирается на лету как приставка плюс неизменённый оригинал.
Раздельное хранение даёт три возможности. Первая — пересобрать индекс с новой приставкой, не трогая оригинал. Вторая — показать читателю или проверяющему исходный фрагмент без машинной надстройки. Третья — сверить приставку с полным документом и отклонить её, если она противоречит источнику.
В учебном примере карточка выглядит так: document_id D-aster, chunk_id C-export, оригинальный фрагмент «Выгрузка содержит только активные задачи» хранится отдельно от приставки «Фрагмент из регламента экспорта сервиса Aster; CSV-выгрузка относится к тарифу Команда». Индексируемый текст — приставка плюс неизменённый C-export. Оригинал остаётся дословным, приставка помечена как сгенерированная.
Такое разделение полезно и на этапе генерации ответа. В работе Anthropic отмечено, что качество ответа может улучшиться, если передавать модели контекстный кусок и явно различать, где контекст, а где сам фрагмент. Модель тогда понимает, что часть текста — пояснение, добавленное системой, а часть — цитата из источника.
Практический формат хранения удобно свести к таблице полей.
| Поле | Что содержит | Зачем нужно |
|---|---|---|
| document_id | Идентификатор исходного документа | Сверка приставки с полным текстом |
| chunk_id | Идентификатор фрагмента | Адресация куска при пересборке |
| original_text | Дословный фрагмент | Цитата в ответе, проверка |
| generated_context | Приставка от модели | Индексация, пояснение |
| indexed_text | Приставка плюс оригинал | То, что видит поиск |
Поле indexed_text пересобирается из отдельно сохранённых исходника и проверенной приставки. Если приставку забраковали, кусок возвращается к оригиналу без переиндексации всего корпуса.
Полный пример Aster: документ, фрагмент, приставка и ответ
Соберём учебный пример целиком. Это вымышленный регламент и образец ручной проверки.
Документ D-aster: «Сервис Aster. Регламент экспорта. Для тарифа Команда доступен экспорт CSV. Для тарифа Личный экспорт CSV не предусмотрен. Выгрузка содержит только активные задачи».
Фрагмент C-export: «Выгрузка содержит только активные задачи».
Запрос: «Попадут ли архивные задачи Aster в CSV?»
Приставка: «Фрагмент из регламента экспорта сервиса Aster; CSV-выгрузка относится к тарифу Команда».
Индексируемый текст: приставка плюс неизменённый C-export.
Ожидаемый обоснованный ответ: «По регламенту CSV содержит активные задачи; архивные задачи не включены».
Разберём, что даёт приставка. Запрос называет три сущности: Aster, архивные задачи, CSV. В оригинальном фрагменте есть только «активные задачи» и «выгрузка». Приставка добавляет «Aster» и «CSV», поэтому и векторный поиск, и BM25 получают совпадения по редким словам запроса. BM25 видит точные строки «Aster» и «CSV», эмбеддинг получает кусок, семантически близкий к вопросу про экспорт.
Ответ строится на оригинале. Приставка говорит, что фрагмент относится к CSV-выгрузке тарифа Команда, а сам оригинал утверждает, что выгрузка содержит только активные задачи. Вывод про архивные задачи делается из оригинала: раз включены только активные, архивные не попадают. Приставка помогает найти кусок, а факт берётся из дословного текста.
Здесь видно разделение ролей. Приставка — навигационный слой, который связывает кусок с запросом. Оригинал — источник утверждения. Если приставка и оригинал расходятся, приоритет у оригинала, а приставка идёт на проверку.
Промпт для генерации приставки к куску можно взять таким.
<document>
[полный текст документа]
</document>
Вот фрагмент, который нужно поместить в контекст всего документа
<chunk>
[текст фрагмента]
</chunk>
Дай короткий сжатый контекст, который помещает этот фрагмент в общий документ для улучшения поиска по фрагменту. Ответь только сжатым контекстом, без пояснений.
Под себя меняют содержимое тегов document и chunk. В document подставляется весь документ целиком, в chunk — конкретный фрагмент. Формулировка задачи в промпте остаётся той же: короткий контекст для улучшения поиска.
Вариации той же задачи под другие домены:
<document>
[регламент внутреннего сервиса]
</document>
<chunk>
[пункт регламента]
</chunk>
Дай короткий контекст: какой сервис, какой раздел, какие условия упомянуты в пункте. Ответь только контекстом.
<document>
[текст научной статьи]
</document>
<chunk>
[абзац статьи]
</chunk>
Дай короткий контекст: тема статьи, раздел, о каком эксперименте или выводе идёт речь. Ответь только контекстом.
<document>
[договор]
</document>
<chunk>
[пункт договора]
</chunk>
Дай короткий контекст: стороны договора, номер раздела, к какому обязательству относится пункт. Ответь только контекстом.
<document>
[переписка поддержки]
</document>
<chunk>
[сообщение]
</chunk>
Дай короткий контекст: кто пишет, по какому обращению, о каком продукте речь. Ответь только контекстом.
Как проверить ошибочную приставку и вопрос без ответа
Приставка генерируется моделью, поэтому её нужно проверять. Простейший контроль — сверка с полным документом. В учебном примере ошибочная приставка звучит как «CSV доступен всем тарифам». Полный D-aster говорит обратное: для тарифа Команда экспорт CSV доступен, для тарифа Личный не предусмотрен. Такая приставка отклоняется сверкой с D-aster, потому что она расширяет условие за пределы документа.
Ошибочные приставки бывают нескольких типов. Расширение условия: приставка обобщает тариф, роль или регион, которых в документе нет. Сужение: приставка приписывает условие одному тарифу, тогда как документ говорит о нескольких. Подмена сущности: приставка называет другой сервис или продукт. Добавление факта: приставка вносит число, срок или правило, которых в документе нет.
Проверка строится как ручной проход по выборке кусков. Для каждого куска сверяют приставку с полным документом и отвечают на три вопроса. Называет ли приставка сущности так же, как документ. Не добавляет ли она условий, которых в документе нет. Не противоречит ли она оригинальному фрагменту. Если сущность указана неверно, добавлено неподтверждённое условие или обнаружено противоречие, приставка идёт на пересборку.
Отдельный контроль — вопрос без ответа. В учебном примере контрольный вопрос «Каков срок хранения выгрузки?» получает ответ «В документе срок не указан». Это проверяет, что система не выдумывает факт из приставки. Приставка говорит про тариф и CSV, но ничего не говорит про срок хранения, и модель обязана это признать.
Порядок проверки удобно вести по шагам.
- Взять выборку кусков из корпуса, включая редкие и короткие.
- Для каждого куска открыть полный документ.
- Сверить сущности в приставке с сущностями документа.
- Проверить, не добавлены ли условия, числа или сроки.
- Задать контрольный вопрос, ответа на который в документе нет.
- Убедиться, что система сообщает об отсутствии сведений в документе.
- Забракованные приставки пересобрать и повторить сверку.
Ошибка в приставке опаснее ошибки в поиске. Плохой поиск вернёт не тот кусок, и это видно по ответу. Плохая приставка подмешивает в индекс утверждение, которого в источнике нет, и ответ выглядит уверенно. Поэтому сверка с полным документом входит в регулярный цикл.
Как сравнить с late chunking и reranking без обещания универсального выигрыша
Contextual Retrieval — один из способов вернуть контекст в кусок. Рядом стоят другие приёмы, и их полезно различать по тому, где именно они работают.
Late chunking, разбор которого даёт Jina AI в своей статье, относится к семейству методов, где контекст документа учитывается на этапе эмбеддинга. Jina — коммерчески заинтересованный источник, и её утверждения о дешевизне и скорости не принимаются как наш замер. Общая идея: эмбеддинг фрагмента учитывает контекст документа.
Reranking работает на другом конце конвейера. В работе Anthropic описаны шаги: сначала идёт первичный поиск верхних потенциально релевантных кусков, в их тесте — верхних 150. Затем верхние N кусков вместе с запросом проходят через модель реранкинга, которая даёт каждому куску оценку релевантности и важности, после чего выбираются верхние K кусков, в тесте — верхние 20. Эти куски уходят в модель как контекст для финального ответа.
Разница в точке приложения усилий. Contextual Retrieval меняет то, что попадает в индекс. Late chunking меняет то, как считается эмбеддинг. Reranking меняет порядок уже найденных кусков. Приёмы складываются: приставка улучшает первичный поиск, реранкер отсеивает лишнее из найденного.
Сравнение по критериям удобно свести в таблицу.
| Приём | Где работает | Что меняет | Что требует |
|---|---|---|---|
| Contextual Retrieval | Предобработка | Текст куска в индексе | Генерация приставок, кэш |
| Late chunking | Эмбеддинг | Способ расчёта вектора | Поддержка в модели эмбеддингов |
| Reranking | Рантайм | Порядок найденных кусков | Модель реранкинга, задержка |
В работе Anthropic приведены результаты их тестов: Contextual Embeddings снизили долю неудачных поисков в верхних 20 кусках на 35% (с 5,7% до 3,7%), сочетание Contextual Embeddings и Contextual BM25 — на 49% (с 5,7% до 2,9%), а с добавлением реранкинга — на 67% (с 5,7% до 1,9%). Это исторические результаты тестов Anthropic на их наборах данных и моделях того времени. Переносить эти проценты на другой корпус, другую модель или текущую редакцию нельзя.
Документация LlamaIndex по Contextual Retrieval прямо отмечает: улучшение метрик наблюдалось, однако результат сильно зависит от запросов, размера куска, перекрытия и других переменных, поэтому нужно экспериментировать, чтобы оптимизировать пользу от метода. Это честная рамка: универсального выигрыша никто не обещает.
Реранкинг добавляет задержку и стоимость. В работе Anthropic отмечено, что реранкинг вносит дополнительный шаг в рантайме и небольшую задержку, хотя оценки считаются параллельно, и есть компромисс между числом реранкируемых кусков и задержкой. Решение о внедрении принимается по результатам проверки на своём корпусе.
Как подготовить собственную проверочную выборку перед внедрением
Перед внедрением нужна своя выборка, на которой видно, помогает ли метод именно вашему корпусу. Чужие метрики для этого не годятся.
Начните с набора вопросов. Возьмите реальные формулировки, с которыми приходят пользователи, и добавьте вопросы, ответы на которые разбросаны по документу. В учебном примере это вопрос про архивные задачи в CSV сервиса Aster. Для каждого вопроса зафиксируйте, какой кусок считается правильным и в каком документе он лежит.
Соберите контрольную группу. В неё войдут вопросы, ответа на которые в корпусе нет, как вопрос про срок хранения выгрузки. Система должна сообщать об отсутствии сведений в корпусе. На этих контрольных вопросах отдельно считают долю ответов с утверждениями, которых нет в корпусе. Эта доля характеризует ошибки на выбранной проверочной выборке.
Зафиксируйте метрики до внедрения. Документация LlamaIndex использует набор метрик для оценки поиска: hit_rate, mrr, precision, recall, ap, ndcg. Эти метрики считаются на парах «вопрос — правильный кусок» и позволяют сравнить конфигурации на одном наборе. Тот же набор прогоняется с приставками и без, чтобы увидеть разницу на вашем корпусе.
Порядок подготовки выборки.
- Собрать 30–50 реальных вопросов пользователей.
- Для каждого отметить правильный кусок и документ.
- Добавить 5–10 вопросов без ответа в корпусе.
- Зафиксировать метрики поиска на текущей конфигурации.
- Прогнать тот же набор с контекстными приставками.
- Сравнить метрики и разобрать расхождения вручную.
- Проверить приставки на выборке кусков сверкой с документами.
Отдельно проверьте параметры нарезки. В работе Anthropic отмечено, что размер куска, границы и перекрытие влияют на качество поиска, а число кусков в промпте имеет предел: тесты с 5, 10 и 20 кусками показали лучший результат на 20, но авторы советуют экспериментировать под свой случай. Документация LlamaIndex в примере использует размер куска 1024 и перекрытие 200, а также верхние 3 куска для сравнения конфигураций.
Проверьте выбор модели эмбеддингов. В работе Anthropic сказано, что Contextual Retrieval улучшает результат на всех протестированных моделях, но некоторые выигрывают сильнее; среди протестированных особенно эффективными названы Gemini и Voyage. Это наблюдение их тестов, и на вашем корпусе порядок может отличаться.
Наконец, заложите пересборку приставок в регулярный цикл. Документы обновляются, тарифы меняются, и приставка, верная вчера, может противоречить сегодняшнему тексту. Сверка приставок с полными документами на выборке — та операция, которая удерживает индекс в согласии с источником.
Частые вопросы
Что такое contextual retrieval простыми словами?
Это предобработка корпуса перед индексацией: к каждому фрагменту дописывается короткая пояснительная приставка, которая связывает кусок с остальным документом. Приставка попадает и в векторный индекс, и в лексический индекс BM25. За счёт этого поиск находит куски, которые сами по себе не называют нужных сущностей. Метод описан в первичной работе Anthropic о Contextual Retrieval.
Чем Contextual Embeddings отличаются от обычных эмбеддингов?
Обычный эмбеддинг считается по тексту куска как он есть. Contextual Embeddings считаются по тексту, в который уже включена сгенерированная приставка. Вектор получает сведения о документе, тарифе или периоде, которых в исходном куске не было. Остальная механика векторного поиска сохраняется.
Зачем нужен Contextual BM25, если есть векторный поиск?
Векторный поиск хорошо ловит смысловую близость и может пропустить точное совпадение по редкому слову или идентификатору. BM25 ищет именно текстовые совпадения и опирается на TF-IDF с поправкой на длину документа и насыщение по частоте термина. В работе Anthropic приведён пример с кодом ошибки TS-999, который эмбеддинг может пропустить, а BM25 находит.
Можно ли обойтись без реранкинга?
Реранкинг — отдельный шаг, который переупорядочивает уже найденные куски и отсеивает лишнее. В тестах Anthropic он дал дополнительное снижение доли неудачных поисков поверх контекстных приставок. При этом он добавляет задержку и стоимость в рантайме. Решение зависит от требований к скорости и качеству на вашем корпусе.
Как понять, что приставка ошибочна?
Приставку сверяют с полным документом. Признаки ошибки: она называет сущность иначе, расширяет условие тарифа или роли, добавляет число или срок, которых в документе нет, либо противоречит оригинальному фрагменту. В учебном примере приставка «CSV доступен всем тарифам» отклоняется, потому что D-aster ограничивает экспорт тарифом Команда. Забракованные приставки пересобирают и сверяют заново.
