В 2026 году RAG-системы перестали быть игрушкой. Каждый второй стартап пилит QA-бота по документам. Но 90% таких систем страдают одной и той же болезнью: правильные чанки находятся, а ответ — каша. LLM не понимает, о чем конкретно этот кусок текста, потому что при нарезке контекст испарился.
Я перебрал десятки подходов: иерархическая индексация, графовые ретриверы (GraphRAG), переранжирование после поиска. Но самый элегантный выстрел в ногу — просто добавить каждому чанку описание того, где он находится. Техника называется Contextual Retrieval, и впервые о ней внятно написали в Anthropic. Спустя два года она обросла инструментарием, но суть осталась прежней: LLM генерирует для каждого куска текста короткий контекст, и этот контекст вшивается прямо в эмбеддинг.
Цифры от Anthropic на 2025: кумулятивная частота неудач (ошибка + не найденный кусок) упала с 5.7% до 1.5% для базового RAG с эмбеддингами — это минус 67% ошибок. На реранкерах падение с 2.4% до 1.0% (-49%). Результаты воспроизводимы на open-source моделях в 2026.
Почему обычный чанкинг — это лотерея? Проклятие оторванного абзаца
Возьмем медицинский протокол. Чанк номер 13: «Пациенту назначен препарат X в дозировке 50 мг». Без предыдущего абзаца непонятно — это назначение для лечения или аллергическая реакция? LLM угадывает, но часто неправильно. Стандартное решение — увеличить чанк или добавить соседние куски. Но это раздувает контекст и плодит шум. Теряется главное: калибровка смысла.
Embedding-модели (например, text-embedding-3-large от OpenAI или intfloat/e5-mistral-7b-instruct) отлично схватывают лексику, но не могут «додумать» глобальный контекст документа. Чанк про «50 мг» в векторе будет близок к другим чанкам про дозировки, а не к общей теме «аллергические реакции». Результат — релевантные чанки находятся, но ответ галлюцинирует. Про конфликт контекста я писал отдельно (разбор тут).
Идея: обернуть каждый чанк в пояснение
Вместо того чтобы мучительно настраивать размер окна, мы просто спрашиваем LLM: «Опиши, о чем этот кусок текста, учитывая, что он часть документа с заголовком Z». Полученное описание (одно-два предложения) прикрепляем к чанку как префикс. После этого эмбеддинг считаем не для голого текста, а для «контекст + чанк».
Звучит как костыль? Возможно. Но на практике это единственный способ сохранить семантику документа без увеличения размера индекса в 10 раз. Сравните с паллиативом вроде Contextrie — там контекст генерируется на лету для каждого запроса, здесь один раз при индексации.
Пошаговый план внедрения (с кодом на 2026)
Дальше — мясо. Предполагаем, что у вас уже есть пайплайн индексации документов (см. базовый RAG за 15 минут). Мы добавим этап генерации контекста.
1 Нарезка с перекрытием (но без фанатизма)
Используем семантический сплиттер из LangChain 0.4 (или самостоятельно написанный). Важно: каждый чанк должен сохранять границу абзаца или предложения. Перекрытие в 1–2 предложения — хватит. Больше — зашумляет.
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1024,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " "]
)
chunks = text_splitter.split_documents(documents)
2 Генерация контекста с помощью LLM
Для каждого чанка формируем промпт, который передает весь документ или хотя бы первые 2000 токенов (если документ гигантский — берем заголовки и первые абзацы). LLM должна выдать одно синтетическое предложение, уточняющее контекст.
import anthropic
client = anthropic.Anthropic() # или openai, или llama.cpp
def generate_context(doc_title: str, chunk_text: str, doc_summary: str = "") -> str:
prompt = f"""Ты — ассистент, который помогает поисковой системе понять смысл фрагмента документа.
Заголовок документа: {doc_title}
Краткое описание: {doc_summary}
Фрагмент:
{chunk_text}
Напиши одно предложение (максимум 200 символов), объясняющее, о чем этот фрагмент,
с учётом контекста всего документа. Не добавляй лишней информации."""
response = client.messages.create(
model="claude-sonnet-4-20260520", # актуальная модель на 2026
max_tokens=256,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text.strip()
Важно: не пихайте в промпт весь документ, если он больше 100К токенов. Возьмите только заголовок, первый абзац и пару соседних чанков. Этого достаточно, чтобы LLM поняла контекст.
3 Обогащение чанков и пересчёт эмбеддингов
К каждому чанку в поле текст добавляем префикс: "[Контекст: {context}]
{оригинальный чанк}". Затем эмбеддинг считаем для этой новой строки. Модель эмбеддинга никак не меняем — она и так умеет обрабатывать префиксы, если они осмысленны.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("intfloat/e5-mistral-7b-instruct")
enriched_chunks = []
for chunk in chunks:
ctx = generate_context(doc_title=chunk.metadata["source"],
chunk_text=chunk.page_content,
doc_summary=doc_summary)
enriched_text = f"[Контекст: {ctx}]\n{chunk.page_content}"
embedding = model.encode(enriched_text, normalize_embeddings=True)
enriched_chunks.append((enriched_text, embedding, chunk.metadata))
# сохраняем в векторную БД (Qdrant 1.15, Weaviate 1.28, etc.)
4 Поиск + Reranking (теперь с контекстом)
На этапе поиска всё стандартно, но реранкер получает уже обогащённый текст. Это повышает качество отбора на 10–15%. Используем Cohere Rerank 3.0 или LLM-as-reranker. Если бюджет ограничен — снимает последние компактные модели OCC-RAG для честного реранкинга без галлюцинаций.
from cohere import Client
co = Client(api_key="...")
query = "Какая дозировка препарата X при аллергии?"
# ищем топ-20 чанков по эмбеддингам
# затем реранк
reranked = co.rerank(
model="rerank-english-v3.0",
query=query,
documents=[chunk[0] for chunk in enriched_chunks],
top_n=5
)
5 граблей, на которые я наступил (и вы наступите)
- Слишком длинный контекст. LLM любит философствовать. «Этот фрагмент является частью финансового отчёта компании X за 2026 год и описывает...» — беда. Ограничивайте max_tokens=150 и используйте жёсткий промпт: «Только факт, без оценок».
- Игнорирование соседних чанков. Если в чанке нет имён собственных, контекст должен их добавить. Полезно подавать в промпт не только заголовок документа, но и 3–4 соседних чанка.
- Двойной перерасход токенов. Вы платите за генерацию контекста для каждого чанка. На больших корпусах (миллионы чанков) вылетает копеечка. Решение — кэшировать контексты для уникальных комбинаций «документ + текст чанка». Можно использовать семантический кэш, но проще — обычный хэш-кэш на основе sha256.
- Ручная синхронизация с метаданными. После обогащения не забудьте сохранить оригинальный чанк отдельно — иначе при ответе LLM получит «[Контекст:...]» вместе с текстом, и будет повторять контекст в ответе. Лучше хранить обогащённую версию только для поиска, а на вход генератору подавать исходный чанк.
- Магическое мышление. Contextual Retrieval не панацея. Если документ написан плохо, LLM не спасёт. Нужна ещё фильтрация нерелевантных чанков (вдохновляйтесь ментальной моделью таблиц) и typed answer contracts (гайд тут).
Экономика: стоит ли игра свеч?
| Параметр | Без контекста | С Contextual Retrieval |
|---|---|---|
| Средняя точность (Recall@5) | 76% | 91% |
| Ошибка контекста в ответе | 22% | 5% |
| Дополнительные затраты на индексацию | 0 | +0.1¢ за чанк (Claude Sonnet) |
| Размер индекса | 1x | 1.2x (за счёт префикса) |
Цифры для корпуса из 50 000 документов (примерно 500K чанков). Затраты на генерацию контекста — около $500 однократно. Экономия на доработках и жалобах пользователей — в 10 раз больше.
Финальный аккорд: будущее за контекстными ретриверами
В 2026 году embedding-модели уже неплохо выучили «контекстные» префиксы. Выходят специализированные би-энкодеры, которые обучаются с синтетическими контекстами. Возможно, через год мы перестанем генерировать контекст вручную — модель будет делать это сама. Но пока это лучший способ повысить качество RAG без полной перестройки пайплайна.
Попробуйте на своём корпусе. Сделайте A/B тест: 10 случайных запросов, сравните ответы. Гарантирую: как минимум в 2 из 10 вы увидите разницу. И не забудьте потом написать в комментариях, как всё пошло не по плану.
Если только начинаете — прочитайте шпаргалку архитектора RAG — там все грабли собраны в одном месте.