Почему ваша LLM тупеет на больших документах и как ее починить

Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
Когда Google анонсировал 2M контекста, а Anthropic подкинул еще 100K, наверняка многие подумали, что RAG теперь можно выкинуть на свалку истории.
Зачем париться с этими чанками, эмбеддингами и всякой прочей ерундой, если можно просто кинуть в модель весь дамп википедии и спросить ее что угодно?
Однако, в реальности возникает эффект, который тихо убивает вашу точность, когда модель читает ваш гигантский промпт как студент перед экзаменом. То есть, первые 5 страниц учит, последние 5 страниц учит, а середину... ну, там что‑то было про котиков, наверное.
И в итоге контекст 2M, а вопрос по документу на 100 страниц модель отвечает хуже, чем GPT-3.5 с контекстом 4K. И да, это не баг, это фича архитектуры внимания.
В этой статье мы поговорим о том, как перестать кормить LLM модели тоннами текста и начать использовать маленькую модель как умного секретаря, повысив точность с 34% до 68%, одновременно сократив расходы в 8 раз.
Пытаем LLM документацией
Давайте для чистоты эксперимента возьмем реальный датасет — 500 страниц технической документации по сетевому оборудованию (наши любимые логи, IP‑адреса, версии прошивок). Разобьем его на 5 сегментов по 100 страниц: A, B, C, D, E.

Далее нам нужно будет задать 10 вопросов, ответы на которые лежат строго в сегменте C (золотая середина) и посмотреть, что из этого выйдет.
Стратегия № 1: Full‑Context (кормим всё целиком)
Начнем с самого простого варианта: запихнуть все 500 страниц в контекст GPT-4o. Но результат оказался не слишком интересным, так как мы получили точность всего 34%, и при этом первый токен появился только через 14 секунд (согласитесь, многовато).
Ну и конечно не обошлось без галлюцинаций: модель начала комбинировать IP‑адреса из сегментов A и E, создавая несуществующие маршруты.
В реальности такой вариант практически бесполезен.
Стратегия № 2: Naive RAG (векторный поиск, топ-5 чанков)
Теперь давайте немного усложним реализацию, нарезав данные на чанки. Здесь мы получаем точность 42%, то есть лучше, но не намного. При этом, ответ пришел через 1.1 секунды, быстро, но зачем скорость, если ответ неправильный?
Здесь суть проблемы заключается в том, что мы теряем контекст между чанками, и из‑за этого модель не понимает общей картины.
В итоге мы приходим к неутешительному выводу о том, что оба подхода неприменимы, так как Full‑Context дорогой и тупой, а Naive RAG быстрый, но тоже недалекий.
RAPTOR, Self‑Refine и прочие модные штуки
Прежде чем перейти к реально работающему решению, давайте разберем, почему некоторые существующие паттерны не работают в 2026 году.
RAPTOR (Recursive Abstractive Processing)

Этот паттерн строит деревья кластеров, суммаризируя их на каждом уровне. Звучит вроде бы мощно, однако, на практике он отлично подходит для пересказа «в целом», но убивает точность.
Например, спросите у RAPTOR версию драйвера на странице 245 из нашей документации, и он выдаст вам среднюю температуру по больнице. Потому что суммаризация усредняет факты, что хорошо подходит для аналитики, но для техподдержки и других задач, требующих точности это смерть.
Self‑Refine / ReAct (итеративные уточнения)

Эта модель что называется, ходит кругами, то есть перечитывает контекст и постоянно уточняет.
Она как‑бы работает, но требует 5–7 запросов к дорогой LLM и стоимость одной сессии может достигать $0.5. Теперь умножьте на 1000 пользователей в день и получите достаточно серьезные затраты.
Первым решением, которое может прийти на ум в такой ситуации, это увеличение чанков.
Если модель теряется в середине 500-страничного документа, давайте сделаем чанки по 10K токенов. Но, после такого изменения модель теряется в середине каждого чанка и становится только хуже.
Таким образом, мы приходим к неутешительному выводу о необходимости нового подхода, обеспечивающего точность, скорость и по приемлемой цене.
Семантический привратник
Представьте, что у вас в компании есть дорогой и талантливый юрист (наша большая LLM). Вы не будете скидывать ему 500 страниц договора и говорить «разберись». Вместо этого, вы сначала дадите договор младшему ассистенту, который вытащит оттуда ключевые факты (суммы, даты, стороны), оценит, какая часть договора вообще относится к вашему вопросу. В итоге ассистент подаст юристу только самое важное на одной странице. Именно так и будет работать наш «Семантический привратник».
Роль ассистента будет выполнять Mistral-7B‑Instruct в 4-битной квантизации. Он стоит копейки (в смысле денег и ресурсов), но умеет читать и анализировать.
Давайте посмотрим, как все это работает. В начале, мы делаем грубый векторный поиск по базе знаний и достаем не 5 чанков, а 50, чтобы наверняка не потерять релевантный кусок. Дальше каждый из этих 50 чанков прогоняем через Mistral-7B с простой задачей: «Оцени релевантность чанка вопросу от 0 до 1. Вытащи оттуда 3 ключевых факта. Ответь в формате JSON.»
При этом мы запрещаем модели фантазировать, то есть, она не генерирует связный текст, а только извлекает факты из исходного чанка. Это снижает вероятность галлюцинаций до минимума.
Вот пример ответа Mistral:

Далее мы используем буфер динамического приоритета, то есть сортируем 50 чанков по значению score и всё, что ниже 0.6, выкидываем. Из оставшихся берем ТОП-10 по релевантности.
Здесь важным моментом является то, что мы не кладем эти чанки в том порядке, в котором они были в документе. Вместо этого, мы сортируем их по убыванию важности и самый релевантный становится первым.
Ранее мы говорили о том, что модель забывает середину. Давайте попробуем ее обмануть, и для этого в начало промпта мы положим сжатые синопсисы всех 10 чанков (это примерно 2000 токенов), а в самом конце промпта продублируем наиболее релевантный чанк целиком, без сжатия.
Теперь самые важные факты оказались и в начале (как краткое резюме), и в конце (как развернутый оригинал). Модель видит их в обеих зонах высокой концентрации внимания и увернуться от фактов ей невозможно.
Как это выглядит на практике
Для инференса Mistral-7B мы используем vLLM — он расходует меньше памяти и работает быстрее трансформеров из коробки. Вот рабочий код нашего пайплайна на Python:
from vllm import LLM, SamplingParams
import json
# Загружаем Mistral-7B в 4-битной квантизации
# На A100 40GB влезает без проблем
llm = LLM(
model="mistralai/Mistral-7B-Instruct-v0.3",
quantization="AWQ", # 4-bit квантизация
dtype="half",
max_model_len=4096
)
def gatekeeper_filter(query, chunks, top_k=10):
"""
query: вопрос пользователя
chunks: список из 50 чанков (текст)
top_k: сколько лучших оставить
"""
prompt_template = """
[INST]
Твоя задача — проанализировать фрагмент документа.
Вопрос пользователя: {query}
Текст фрагмента:
{chunk}
Ответь строго в формате JSON:
- "score": число от 0 до 1 (насколько чанк релевантен вопросу)
- "facts": массив из максимум 3 ключевых фактов (короткие предложения из текста)
Пример: {{"score": 0.85, "facts": ["Сервер перезагружен в 14:32", "Ошибка E531"]}}
[/INST]
"""
# Готовим батч промптов для 50 чанков
batch_prompts = [
prompt_template.format(query=query, chunk=chunk)
for chunk in chunks
]
# Запускаем инференс с низкой температурой (детерминизм)
sampling_params = SamplingParams(
temperature=0.0, # Никакой креативности, только факты
max_tokens=200,
stop=["\n\n"] # Чтобы не генерировала лишнего
)
outputs = llm.generate(batch_prompts, sampling_params)
# Парсим JSON из ответов
parsed_results = []
for i, output in enumerate(outputs):
raw_text = output.outputs[0].text
try:
# Ищем { ... } в ответе
start = raw_text.find('{')
end = raw_text.rfind('}') + 1
json_str = raw_text[start:end]
data = json.loads(json_str)
parsed_results.append({
"score": float(data.get("score", 0.0)),
"facts": data.get("facts", [])[:3]
})
except (json.JSONDecodeError, ValueError):
# Если модель накосячила — ставим низкий скор
parsed_results.append({"score": 0.0, "facts": []})
# Сортируем по убыванию скора
scored_chunks = list(zip(chunks, parsed_results))
scored_chunks.sort(key=lambda x: x[1]['score'], reverse=True)
# Берем топ-K
top_chunks = scored_chunks[:top_k]
# Компонент В: формируем сжатый контекст из фактов
compressed_facts = []
for chunk_data, meta in top_chunks:
compressed_facts.extend(meta['facts'])
# Ограничиваем длину, чтобы не переполнить контекст
compressed_context = " ".join(compressed_facts[:40])
# Дублируем лучший чанк целиком в конец
best_chunk_raw = top_chunks[0][0] if top_chunks else ""
return {
"compressed_context": compressed_context,
"anchor_chunk": best_chunk_raw
}
# Пример использования
chunks = [...] # ваши 50 чанков из векторной БД
query = "Какая версия прошивки была на сервере 10.0.4.22 во время сбоя?"
result = gatekeeper_filter(query, chunks)
# Формируем финальный промпт для GPT-4
final_prompt = f"""
Контекст (ключевые факты):
{result['compressed_context']}
Подробный фрагмент документа (самый важный):
{result['anchor_chunk']}
Вопрос пользователя: {query}
Ответь на вопрос, используя только предоставленную информацию.
"""В итоге, результат будет иметь следующий вид:

Что мы получили в итоге
В заключении давайте посмотрим, какие результаты мы получили. Мы прогнали наш пайплайн на 1000 запросов из реальной техподдержки и сравнили с тремя другими подходами.
Метод | Токенов в промпте | Точность (точное совпадение) | Задержка | Стоимость за 1000 запросов |
Full‑Context | ~180 000 | 34.2% | 14.5 сек | $180 |
Naive RAG (Top-5) | ~8 000 | 41.1% | 1.4 сек | $15 |
RAPTOR | ~12 000 | 49.7% | 5.2 сек | $40 |
Semantic Gatekeeper (наш) | ~9 000 | 68.4% | 3.8 сек | $22 |
Если говорить о точности, то мы обогнали Full‑Context почти вдвое (68% против 34%), благодаря тому, что LLM перестала отвлекаться на шум. Мы дали ей только релевантные факты, а не 500 страниц воды.
Также, мы потратили 2.4 секунды на прогон через Mistral-7B, но сэкономили 10 секунд на GPT-4 (потому что промпт стал в 20 раз короче). И наконец, по деньгам, мы потратили $22 против $180, то есть экономия в 8 раз. На загрузке в десятки тысяч запросов вы можете сэкономить значительные суммы.
Таким образом, мы смогли починить наш RAG и получить хороший результат при обработке большого объема запросов.
ЧТО ЕЩЕ ПОЧИТАТЬ ПО ТЕМЕ:

Если вы уже работаете с LLM или только начинаете строить свои AI‑системы, наверняка сталкивались с проблемой: модель знает много, но при работе с большими объёмами данных начинает терять важные детали и выдавать неточные ответы. Разобраться, как управлять качеством работы LLM и строить надёжные сценарии взаимодействия с моделями, — следующий шаг после знакомства с базовыми возможностями ИИ.
На бесплатных уроках разберём, как устроены современные подходы к работе с LLM:
22 сентября в 18:00. «Ландшафт современного NLP: от эмбеддингов и классических ML‑методов до современных LLM». Записаться.
23 сентября в 18:00. «Structured Outputs: как заставить LLM всегда возвращать то, что вам нужно». Записаться.
1 октября в 20:00. «Проверка ответов LLM и борьба с галлюцинациями через промпт‑инжиниринг». Записаться.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.