Контекст - это новая валюта. И мы все банкроты
Вы загружаете 300-страничный технический отчет в GPT-4o. Модель с контекстом в 128К токенов должна справиться, верно? На практике она теряет связь между главой 2 и приложением G. Или генерирует ответ, основанный на первых 50 страницах, игнорируя ключевые детали в конце.
Предел контекста у LLM - это горизонт. Кажется, что вот-вот расширят, а ты все равно упираешься. 128К, 200К, 1M токенов - неважно. Всегда найдется документ, который нужно "скормить" целиком. И тогда рождается вопрос: как анализировать то, что не помещается в память модели?
Забудьте про SemanticZip. В нашей предыдущей статье мы разобрали, почему семантическое сжатие документов для LLM обречено на провал. Восстановленный текст - это всегда НОВЫЙ текст, а не точная копия оригинала.
Почему просто нарезать текст на куски - недостаточно
Самый очевидный подход: разбить документ на чанки по 1000 токенов, отправить их в модель по очереди. Звучит логично. Работает ужасно.
Представьте юридический договор. Пункт 3.2 ссылается на определение из пункта 1.1. Если эти пункты попадут в разные чанки, модель не увидит связи. Она будет интерпретировать "Сторона А" из пункта 3.2 как абстрактное понятие, а не конкретного участника, определенного в начале документа.
1 Умный chunking: нарезка с сохранением смысловых границ
Глупый чанкер режет по символам или словам. Умный - по смысловым границам. Разница в точности ответов достигает 40%.
Вот как НЕ надо делать:
# ПЛОХО: наивный чанкинг по символам
def naive_chunk(text, chunk_size=1000):
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
А вот рабочий подход с использованием актуальных на 2026 год библиотек:
# ХОРОШО: семантический чанкинг с сохранением структуры
from langchain_text_splitters import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter
)
# Для технической документации с заголовками
headers = [("#", "Заголовок 1"), ("##", "Заголовок 2"), ("###", "Заголовок 3")]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers)
# Для обычного текста с рекурсивным разделением по символам
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200, # Критически важно для контекста
separators=["\n\n", "\n", ". ", " ", ""], # Иерархия разделителей
length_function=len,
is_separator_regex=False
)
Ключевые параметры, которые большинство упускает:
- chunk_overlap=200: Не жадничайте. 10-20% перекрытия между чанками спасают от разрыва контекста. Предложение, которое начинается в одном чанке и заканчивается в другом, - гарантия ошибки.
- Иерархия разделителей: Сначала пробуем разбить по двойным переносам строк (абзацы), потом по одинарным, потом по точкам. Это сохраняет логические блоки.
- Учет структуры документа: Для PDF с четкой структурой используйте специализированные сплиттеры. В нашем гайде по Docling мы разбирали стратегии для 130+ страничных документов.
2 RAG 2.0: не просто поиск, а реконтекстуализация
Базовый RAG, который все знают: векторный поиск + LLM. Проблема в том, что найденные чанки все равно теряют связь с исходным документом.
Advanced RAG 2026 года - это трехэтапный процесс:
- Гибридный поиск: Векторная семантика + лексическое совпадение (BM25). Почему это must-have, мы подробно разбирали в полном руководстве по RAG.
- Рерайтинг (reranking): Модель-ранкер (например, BGE-reranker-v2.0) переоценивает релевантность найденных чанков конкретно для вашего вопроса.
- Контекстуализация: Самый важный и самый часто пропускаемый шаг.
Вот как выглядит контекстуализация в коде:
# Продвинутый RAG с контекстуализацией
from llama_index.core import (
VectorStoreIndex,
ServiceContext,
StorageContext
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.core.postprocessor import SentenceTransformerRerank
# Современная модель эмбеддингов (актуально на 2026)
embed_model = OpenAIEmbedding(
model="text-embedding-3-large-3072d", # 3072 размерность вместо старых 1536
dimensions=1536 # Можно уменьшить для экономии без большой потери качества
)
# Семантический сплиттер вместо простого
node_parser = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=embed_model
)
# Рерайтер для улучшения релевантности
rerank = SentenceTransformerRerank(
model="BAAI/bge-reranker-v2.0",
top_n=3 # Берем только 3 самых релевантных чанка после переранжирования
)
# Критический момент: добавляем мета-контекст к каждому чанку
def enrich_chunk_with_context(chunk, document_metadata, surrounding_chunks):
"""Добавляем к чанку информацию о его месте в документе"""
context_info = f"""
Документ: {document_metadata.get('title', 'Без названия')}
Раздел: {document_metadata.get('section', 'Основной текст')}
Предыдущий контекст: {surrounding_chunks['previous'][:200] if surrounding_chunks['previous'] else 'Начало документа'}
Следующий контекст: {surrounding_chunks['next'][:200] if surrounding_chunks['next'] else 'Конец документа'}
---
{chunk}
---
Этот фрагмент находится в середине документа о {document_metadata.get('topic', 'технической документации')}.
"""
return context_info
Используйте модели эмбеддингов с увеличенной размерностью. text-embedding-3-large-3072d от OpenAI (2024) и BGE-M3 от BAAI (2025) дают на 15-25% лучшее качество поиска на сложных документах по сравнению со старыми моделями в 768-1536 измерений.
3 Иерархическая агрегация: от деталей к общей картине
Map-Reduce - классика, но в 2026 она эволюционировала. Теперь это многоуровневая агрегация с промежуточными суммаризациями.
Как это работает на практике:
# Многоуровневая агрегация для анализа длинных документов
from typing import List, Dict
import asyncio
class HierarchicalDocumentAnalyzer:
def __init__(self, llm, chunk_size=2000):
self.llm = llm
self.chunk_size = chunk_size
async def analyze_large_document(self, document_text: str, question: str) -> str:
# Уровень 1: Разбиваем на крупные секции (главы)
sections = self._split_into_sections(document_text)
# Уровень 2: Параллельный анализ каждой секции
section_tasks = []
for section in sections:
task = self._analyze_section(section, question)
section_tasks.append(task)
section_summaries = await asyncio.gather(*section_tasks)
# Уровень 3: Агрегация результатов секций
if len(section_summaries) > 1:
# Если секций много, делаем промежуточную агрегацию
intermediate_groups = self._group_summaries(section_summaries, group_size=3)
intermediate_results = []
for group in intermediate_groups:
combined = "\n".join(group)
intermediate_analysis = await self._aggregate_analysis(combined, question)
intermediate_results.append(intermediate_analysis)
# Финальная агрегация
final_input = "\n---\n".join(intermediate_results)
else:
final_input = section_summaries[0]
# Финальный ответ с учетом всей иерархии
final_prompt = f"""
На основе анализа всего документа, ответьте на вопрос.
Контекст анализа:
{final_input}
Вопрос: {question}
Ответ должен учитывать информацию из всех разделов документа.
"""
return await self.llm.ainvoke(final_prompt)
async def _analyze_section(self, section_text: str, question: str) -> str:
"""Анализ отдельной секции с учетом вопроса"""
# Здесь может быть RAG внутри секции
prompt = f"""
Проанализируйте следующий раздел документа в контексте вопроса:
Вопрос: {question}
Раздел документа:
{section_text[:3000]}
Ключевые выводы из этого раздела, релевантные вопросу:
"""
return await self.llm.ainvoke(prompt)
Преимущество иерархического подхода: модель сначала понимает каждую часть документа в контексте вопроса, затем синтезирует общее понимание. Это дороже (больше вызовов LLM), но точнее на 30-50% для сложных аналитических задач.
Типичные ошибки, которые сведут на нет все ваши усилия
Я видел эти ошибки в десятках проектов. Они выглядят незначительными, но каждая убивает точность.
| Ошибка | Последствие | Как исправить |
|---|---|---|
| Чанкинг без перекрытия | Разрыв контекста на границах предложений | chunk_overlap=10-20% от размера чанка |
| Игнорирование структуры документа | Заголовки и подзаголовки теряются, иерархия нарушается | Использовать MarkdownHeaderTextSplitter или аналоги |
| Pure векторный поиск | Пропускает точные лексические совпадения | Гибридный поиск: векторы + BM25 |
| Отсутствие рерайтинга | В топ попадают семантически близкие, но нерелевантные чанки | Добавить этап reranking с BGE-reranker-v2.0 |
| Изоляция чанков | LLM не видит связи между частями документа | Добавлять мета-контекст (раздел, соседние чанки) |
Практический кейс: анализ юридического договора на 150 страниц
Давайте пройдем весь путь на реальном примере. У нас есть договор аренды коммерческой недвижимости. Нужно ответить на вопрос: "Какие штрафные санкции предусмотрены за досрочное расторжение договора арендатором?"
Шаг 1: Загружаем и структурируем PDF. Используем специализированные инструменты вроде Unstructured.io или Docling (про который мы писали здесь).
Шаг 2: Семантический чанкинг с учетом юридической структуры:
# Юридический документ имеет четкую структуру
legal_splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=240, # 20% перекрытие критично для ссылок между пунктами
separators=[
"\nСтатья ", # Юридические статьи
"\nПункт ", # Пункты статей
"\n\n", # Абзацы
"\n", # Строки
". ", # Предложения
" " # Слова
],
keep_separator=True # Сохраняем разделители как часть текста
)
Шаг 3: Векторизация с современной моделью. Для юридических текстов лучше использовать специализированные эмбеддинги или хотя бы text-embedding-3-large.
Шаг 4: Гибридный поиск. Запрос "штрафные санкции досрочное расторжение арендатор" должен найти не только семантически близкие чанки, но и точные упоминания "штраф", "неустойка", "расторжение".
Шаг 5: Контекстуализация найденных чанков. Если найден пункт 8.3 о штрафах, добавляем информацию: "Это пункт 8.3 Раздела VIII 'Ответственность сторон'. Предыдущий пункт 8.2 описывает форс-мажор, следующий 8.4 - возмещение убытков."
Шаг 6: Иерархическая агрегация, если вопрос сложный и требует анализа нескольких разделов.
Что будет дальше? Прогноз на 2027-2028
Текущие методы - это костыли. Элегантное решение появится, когда LLM научатся:
- Селективному чтению: Модель будет сама решать, какие части документа нужно прочитать внимательно, а какие можно пролистать. Как человек с книгой.
- Динамическому контексту: Вместо фиксированного окна - адаптивное. Сложные части документа получают больше "внимания", простые - меньше.
- Кросс-документному пониманию: Анализ не одного PDF, а сети связанных документов с сохранением ссылок между ними.
Пока этого нет, используйте гибридный подход: умный chunking + advanced RAG с контекстуализацией + иерархическая агрегация для сложных вопросов. Это не идеально, но это работает. Сегодня.
И последний совет: никогда не доверяйте LLM анализ критически важных документов без человеческой проверки. Особенно после того, как вы узнаете, как легко RAG-системы могут сливать персональные данные. Технологии - инструмент, а не замена эксперту.