Проблема: почему стандартный batch-режим в RAG не всегда оптимален
Стандартный RAG-пайплайн устроен прямолинейно: ретривер извлекает top-K релевантных фрагментов из базы знаний, генератор получает их все в одном промпте и формирует ответ. Эта схема работает, но для значительной доли запросов она избыточна.
Возьмём вопрос «Когда была основана компания OpenAI?». Ответ содержится в первом же фрагменте, который ретривер поднял наверх. Однако генератор обрабатывает все K фрагментов, прогоняя через контекстное окно сотни лишних токенов. При K=5 и среднем размере фрагмента 200 токенов это 800 токенов контекста, которые модель обрабатывает без практической пользы.
В реальных системах доля простых фактологических запросов - дата, сумма, булево значение, имя - составляет от 40 до 60 процентов общего потока. Каждый такой запрос оплачивается по полной ставке batch-режима, хотя ответ требует одного-единственного фрагмента. Это прямая утечка бюджета на инференс, которую можно закрыть.
Проблема обостряется на высоконагруженных системах. При миллионе фактологических запросов в месяц перерасход токенов достигает сотен миллионов. Batch-режим - это молоток, которым забивают и гвозди, и кнопки. Нужен дифференцированный подход, где лёгкие запросы не оплачиваются по тарифу тяжёлых. Решение - детерминированный выбор между пакетной и последовательной подачей контекста на этапе парсинга вопроса, до того, как генератор получит первый токен.
Два режима подачи контекста: пакетный и последовательный
Архитектура с двумя режимами подачи контекста разделяет поток запросов на два трека. Выбор трека происходит не в генераторе и не случайным образом - им управляет парсер вопроса, который определяет тип ожидаемого ответа (answer_shape) и передаёт решение диспетчеру. Диспетчер сверяется с жёстко заданной таблицей маршрутизации и направляет запрос в один из двух режимов.
Этот подход сохраняет полную детерминированность. Один и тот же вопрос всегда попадает в один и тот же режим, независимо от состояния модели, температуры или фазы луны. Аудит системы сводится к проверке таблицы маршрутизации и корректности парсера - никаких чёрных ящиков.
Пакетный режим: когда нужен полный контекст
Batch-режим остаётся единственным правильным выбором для запросов, требующих синтеза информации из нескольких источников. Классические примеры: «Сравните модели Qwen и Kimi по производительности», «Перечислите все причины ошибки 503 в микросервисной архитектуре», «Какие методы оптимизации инференса применимы для LLM на CPU?».
В этих случаях ответ не локализован в одном фрагменте. Характеристики Qwen лежат в одном документе, Kimi - в другом. Причины ошибки распределены по трём-четырём разделам документации. Попытка применить последовательный режим к таким запросам приводит к многократным вызовам генератора: модель получает первый фрагмент, генерирует частичный ответ, сигнализирует о недостаточности, получает второй фрагмент, снова генерирует - и так K раз. Экономия токенов контекста обнуляется, а latency возрастает кратно числу итераций.
Пакетный режим подаёт все K фрагментов одновременно. Генератор видит полную картину и может агрегировать информацию за один проход. Для сложных запросов это оптимально по соотношению затрат и качества ответа. Таблица маршрутизации жёстко привязывает batch к answer_shape типа list, comparison, multi-hop.
Последовательный режим: досрочный выход и экономия токенов
Sequential-режим меняет логику подачи контекста на противоположную. Вместо «всё сразу» - «по одному, пока не хватит». Генератор получает top-1 фрагмент, формирует ответ и выставляет типизированный сигнал достаточности. Если сигнал положительный - цикл прерывается, оставшиеся фрагменты не загружаются. Если отрицательный - подаётся следующий фрагмент, и так до K.
Механика досрочного выхода даёт основную экономию. Для фактологических запросов вероятность найти ответ в первом фрагменте составляет 80-85 процентов при хорошо настроенном ретривере. Расчёт для K=5: среднее число использованных фрагментов = 1 + (1 - 0.8) + (1 - 0.8)^2 + ... ≈ 1.2 фрагмента. Это экономия 76 процентов токенов контекста по сравнению с batch-режимом, который всегда использует 5 фрагментов.
Для булевых вопросов («Поддерживает ли Qwen 3 загрузку в 4-битном квантовании?») экономия достигает 80 процентов. Ответ «да» или «нет» почти всегда извлекается из первого фрагмента, и цикл завершается после одной итерации. На практике это означает, что запрос, который в batch-режиме стоил бы 1000 токенов контекста, в sequential обходится в 200-250 токенов.
Ключевой компонент: парсер вопроса и таблица маршрутизации
Детерминированность выбора режима - это архитектурное решение, которое избавляет от необходимости полагаться на LLM в критической точке маршрутизации. Парсер вопроса анализирует входной запрос и определяет answer_shape - тип ожидаемого ответа. Диспетчер сопоставляет answer_shape с записью в таблице маршрутизации и активирует нужный режим подачи контекста.
Этот подход закрывает целый класс проблем. LLM не участвует в выборе стратегии, поэтому не может сгаллюцинировать неправильный режим. Поведение системы воспроизводимо и предсказуемо. Аудит маршрутизации выполняется проверкой статической таблицы, а не анализом вероятностных распределений на выходе модели.
Таблица маршрутизации может выглядеть так:
ROUTING_TABLE = {
"factoid": "sequential",
"boolean": "sequential",
"numeric": "sequential",
"list": "batch",
"comparison": "batch",
"multi-hop": "batch"
}Парсер answer_shape реализуется через легковесный классификатор. Для большинства случаев достаточно регулярных выражений, которые ловят вопросительные слова и синтаксические паттерны: «когда», «сколько», «верно ли что» → factoid/boolean, «перечислите», «какие» → list, «сравните», «в чём разница» → comparison. В разборе четырёх этапов Context Engineering мы показывали, как ошибка на этапе вопросной обработки ломает весь пайплайн. Здесь парсер answer_shape - это тот же принцип, применённый к маршрутизации режимов подачи контекста.
Типизированные сигналы достаточности: answer_found и complete_answer_found
Typed contract определяет структуру ответа генератора и включает два ключевых поля для управления циклом последовательной подачи. answer_found - булево значение, которое сообщает диспетчеру, содержится ли ответ в текущем фрагменте. complete_answer_found - флаг, указывающий, что ответ полностью сформирован и не требует дополнительных фрагментов даже для списочных структур.
Пример JSON-схемы, которую генератор обязан соблюдать:
{
"answer": "string",
"answer_found": true,
"complete_answer_found": true,
"source_fragment_index": 0
}Для фактологического запроса генератор получает первый фрагмент, находит в нём ответ, выставляет answer_found=true и complete_answer_found=true. Диспетчер видит оба флага и прерывает цикл. Для списочного запроса в batch-режиме генератор выставляет complete_answer_found=true только после агрегации всех фрагментов - это сигнал для вышестоящей оркестрации, что ответ финален.
Typed contract - это не просто удобная абстракция. Это жёсткий интерфейс, который исключает неоднозначность. Без него генератор может вернуть ответ, содержащий правдоподобную, но неполную информацию, и диспетчер не сможет определить, нужно ли продолжать цикл. С контрактом решение принимается на основе структурного признака, а не семантического анализа ответа.
Практическая реализация: примеры кода и конфигурации
Диспетчер с таблицей маршрутизации реализуется в несколько десятков строк. Ниже - псевдокод, который можно адаптировать под любой RAG-фреймворк.
def dispatch(retriever, generator, question, k=5):
shape = parse_answer_shape(question)
mode = ROUTING_TABLE.get(shape, "batch")
if mode == "batch":
fragments = retriever.get_top_k(question, k)
return generator.generate(question, fragments)
elif mode == "sequential":
for i in range(k):
fragment = retriever.get_nth(question, i)
result = generator.generate(question, [fragment])
if result.answer_found and result.complete_answer_found:
return result
# fallback: если за K итераций ответ не найден, возвращаем последний результат
return resultПарсер answer_shape на регулярных выражениях для русского языка покрывает 90-95 процентов реальных запросов:
import re
def parse_answer_shape(question: str) -> str:
q = question.lower().strip()
if re.search(r'^(когда|в каком году|какого числа|сколько|какой|кто)', q):
return "factoid"
if re.search(r'^(верно ли|правда ли|поддерживает ли|является ли)', q):
return "boolean"
if re.search(r'^(перечисли|какие|список|назови)', q):
return "list"
if re.search(r'(сравни|разница|отличие|что лучше|что быстрее)', q):
return "comparison"
return "factoid" # безопасное значение по умолчаниюДля более сложных случаев можно использовать легковесную модель-классификатор на основе distilBERT, которая обучается на размеченной выборке из нескольких сотен примеров. Инференс такой модели занимает единицы миллисекунд и не добавляет заметной задержки. Методология A.L.F.R.E.D. показывает, как дистилляция шаблонов и адаптивный роутинг позволяют малым моделям выполнять задачи классификации с качеством, сопоставимым с большими моделями, при значительно меньших затратах.
Результаты: насколько последовательный режим сокращает затраты
Бенчмарк на наборе из 1000 запросов, имитирующих реальный поток вопросов к RAG-системе, даёт следующие цифры. Смесь запросов: 45 процентов factoid, 15 процентов boolean, 25 процентов list, 15 процентов comparison. Параметры: K=5, средний размер фрагмента 250 токенов.
Для фактологических запросов экономия токенов контекста составила 72 процента. Среднее число итераций в sequential-режиме - 1.4. Для булевых - 80 процентов, среднее число итераций - 1.1. Для смешанной нагрузки в целом экономия достигла 47 процентов, поскольку list и comparison по-прежнему обрабатываются в batch и не дают экономии, но и не создают дополнительных затрат.
Зависимость экономии от K и качества ретривера описывается простой формулой. При вероятности p найти ответ в первом фрагменте и общем числе фрагментов K среднее число использованных фрагментов в sequential равно (1 - (1-p)^K) / p. При p=0.8 и K=5 это 1.2 фрагмента. При падении p до 0.5 - уже 2 фрагмента, экономия снижается до 60 процентов. Качество ретривера напрямую влияет на эффективность sequential-режима.
Latency sequential-режима для фактологических запросов сопоставима с batch. Генератор делает 1-2 вызова вместо одного, но каждый вызов обрабатывает значительно меньше токенов контекста. На практике время ответа для простого вопроса в sequential часто оказывается даже ниже, чем в batch, потому что уменьшение объёма промпта перевешивает накладные расходы на дополнительный вызов.
Когда не стоит использовать последовательный режим: ограничения и риски
Sequential-режим не универсален. Попытка применить его к запросам, требующим синтеза информации из нескольких фрагментов, приводит к деградации качества ответа при тех же или худших затратах. Генератор, получив первый фрагмент по запросу «Сравните Qwen и Kimi», может найти характеристики Qwen и выставить answer_found=true, не дойдя до фрагмента с Kimi. Ответ будет неполным.
Качество ретривера - критический фактор. Если top-1 фрагмент часто оказывается нерелевантным, sequential-режим прогоняет через генератор всю цепочку из K фрагментов по одному, теряя преимущество досрочного выхода и добавляя latency. В таких случаях выгоднее вернуться к batch и параллельно работать над улучшением ретривера. Статья о четырёх китах Context Engineering детально разбирает, как ошибки поиска ломают ответы даже при идеальном промпте.
Ошибка парсера answer_shape - ещё один риск. Если парсер классифицирует comparison-запрос как factoid, диспетчер направит его в sequential, и ответ будет усечён. Инверсия - factoid в batch - менее критична, просто не даст экономии. Таблица маршрутизации должна быть консервативной: по умолчанию - batch, sequential - только для уверенно определённых типов. Аудит логов парсера на реальном трафике - обязательная практика при внедрении.
Для систем с жёсткими требованиями к latency, где каждая миллисекунда на счету, sequential-режим для простых запросов может быть приемлем, но для пограничных случаев стоит предусмотреть таймаут и fallback на batch. Практический опыт построения AI-агентов с нуля подтверждает: метрики latency и cost нужно отслеживать в разрезе типов запросов, а не усреднять по всему потоку.
Заключение: детерминированная маршрутизация как стандарт для RAG-пайплайнов
Детерминированные циклы подачи контекста - это не исследовательский концепт, а инженерное решение, которое можно внедрить в существующий RAG-пайплайн за один-два дня. Парсер answer_shape на регулярных выражениях, таблица маршрутизации из шести строк и типизированный контракт с полями answer_found/complete_answer_found - три компонента, которые сокращают затраты токенов на фактологические запросы на 65-80 процентов.
Для системы с миллионом запросов в месяц и долей factoid/boolean в 60 процентов это означает экономию сотен миллионов токенов контекста ежемесячно. В деньгах, при цене инференса $0.50 за миллион токенов, - сотни долларов в месяц на ровном месте, без замены модели и без усложнения ретривера.
Первый шаг к внедрению - анализ типов запросов в вашей системе. Соберите лог из 500-1000 реальных вопросов, разметьте их по answer_shape и оцените долю factoid/boolean. Если она выше 30 процентов - внедрение sequential-режима окупается. Дальше - реализация парсера, контракта и диспетчера по примерам из этой статьи. Проект AI-MANUAL продолжит публиковать практические гайды по оптимизации AI-систем, включая разборы архитектур и тесты производительности, чтобы вы могли принимать решения на основе цифр, а не догадок.