У внутреннего помощника было окно на 128 тысяч токенов. Команда положила туда системную инструкцию, историю чата за неделю, регламент на 80 страниц и результаты поиска. Счёт за запрос вырос, а ответы стали хуже: модель находила старый тариф из переписки и не замечала актуальную таблицу в конце prompt.
Разговор после инцидента шёл по знакомому кругу: «мы же дали ей всю информацию». Дали. Ровно так же кладовщик даёт полный доступ к складу человеку, которого просили принести одну коробку с гвоздями. Формально всё выдано. На деле он вернётся с чем-нибудь другим и не сразу.
Контекстное окно задаёт верхнюю границу, но не гарантирует внимания к каждому фрагменту. Вход конкурирует за место и влияние на ответ. Повтор инструкции тоже стоит токены, а длинная история приносит устаревшие факты. Поэтому контекст следует собирать как данные для конкретного решения, а не как архив на всякий случай.
Место в окне — не то же самое, что внимание
Три вещи, которые ломают интуицию «больше контекста лучше».
Позиция важна. Начало и конец входа влияют на ответ сильнее середины. Двадцатая страница регламента, зажатая между историей чата и результатами поиска, лежит в самой невыгодной части окна. Она оплачена, она в контексте, и она почти не работает.
Конкуренция важнее полноты. Две версии одного тарифа в одном prompt модель не сверяет: она выберет одну. Какую именно, вы узнаете из тикета.
Токены стоят дважды. Первый раз — в счёте (глава 43), второй — во времени ответа. Регламент на 80 страниц в каждом запросе платится в каждом запросе, включая те тысячи, где вопрос был про пароль от вайфая.
Отсюда рабочее определение: контекст — это не всё, что мы знаем. Контекст — это то, что нужно для одного конкретного решения, собранное под лимит и с указанием происхождения.
Бюджет запроса
До вызова модели полезно посчитать явный бюджет:
window
- max_output_tokens
- system_and_schema
- safety_margin
= budget_for_user_history_and_sourcesЗапас нужен из-за различий токенизаторов и служебных сообщений провайдера. Если задача допускает 32 000 токенов входа, не стоит планировать ровно 32 000.
from dataclasses import dataclass
@dataclass(frozen=True)
class ContextBudget:
window: int
output: int
fixed: int
reserve: int = 1024
@property
def variable(self) -> int:
value = self.window - self.output - self.fixed - self.reserve
if value <= 0:
raise ValueError("fixed context exhausted the model window")
return valueИсключение здесь важнее, чем кажется. Оно ловит момент, когда системная инструкция и схема разрослись настолько, что на данные места не осталось, — и ловит его на деплое, а не ночью. Инструкция растёт незаметно: каждый инцидент добавляет в неё абзац «а ещё никогда не делай вот так», и через полгода системный промпт напоминает свод правил общежития, где половина пунктов написана из-за одного человека в 2019 году.
Собирать переменную часть лучше по приоритетам: текущий вопрос, обязательные правила, найденные источники, затем краткая история. Полную историю можно хранить отдельно и сворачивать в проверяемое состояние: выбранный продукт, номер договора, уже заданные вопросы. Резюме, которое сочинила модель, нельзя считать источником факта.
Механизм сборки
def build_context(question, rules, chunks, state, budget, count_tokens):
items = [question, rules, state]
used = sum(count_tokens(x) for x in items)
selected = []
for chunk in sorted(chunks, key=lambda x: x.score, reverse=True):
cost = count_tokens(chunk.text)
if used + cost > budget:
continue
selected.append(chunk)
used += cost
return {"question": question, "rules": rules,
"state": state, "sources": selected}В production коде у каждого фрагмента должны быть source_id, версия и причина включения. Тогда спорный ответ можно восстановить, а не гадать, какой PDF оказался внутри.
«Причина включения» звучит как бюрократия ровно до первого разбора. Строка included: top-1 by rerank, doc=refund-policy, v=2026-03 превращает часовое расследование в один взгляд в лог. Без неё вы будете доказывать, что модель что-то выдумала, а окажется, что вы сами положили ей это в контекст.
История диалога: состояние, а не стенограмма
Хранить последние N сообщений просто. Работает это редко. Стенограмма несёт устаревшие значения (клиент называл первый адрес, потом сменил), вежливый шум («спасибо», «ок, понял») и чужие формулировки, которые модель охотно повторит.
Работает другое разделение:
- Факты живут в структуре:
selected_product,contract_id,already_asked. Это данные вашей системы, их можно проверить и починить. - Свободный текст попадает в контекст в минимальном объёме: последний вопрос и, если нужно, предыдущий обмен репликами.
Резюме, сгенерированное моделью, годится для человека и не годится как источник факта. Один раз ошибившись в резюме, система будет пересказывать эту ошибку себе вечно, с каждым разом всё увереннее: испорченный телефон, где на обоих концах провода одна и та же модель.
Что ломается
- Обрезка с конца удаляет вопрос или свежие источники. Обрезайте блоки осознанно.
- Резюме диалога закрепляет ошибку модели. Храните факты отдельно от свободного текста.
- Десять почти одинаковых чанков вытесняют разные источники. Делайте дедупликацию и ограничивайте число фрагментов на документ.
- Большой
max_output_tokensсъедает входной бюджет, даже когда обычный ответ занимает 300 токенов. - Секреты и PII попадают в prompt вместе с «полезной историей». Фильтрация должна происходить до сборки.
- «Добавим ещё немного контекста» — самое дорогое предложение на планёрке. Оно не проходит ревью, потому что не выглядит как изменение кода, и попадает в прод через конфиг.
Как это измерять
Три цифры на дашборд с первого дня:
- Разбивка токенов по категориям: system, schema, rules, sources, history, question. Сумма скажет про счёт, пропорции — про здравый смысл. Если история занимает больше, чем источники, вы платите за архив.
- Доля бюджета, вытесненная дубликатами. Считается элементарно: сколько токенов ушло на чанки с высоким пересечением текста.
- Доля ответов, где сработал лимит (пришлось выкинуть источники). Рост этой доли — ранний признак, что корпус вырос, а бюджет остался прежним.
Каждую цифру пишите рядом с llm_call_log из главы 24: контекст без учёта — это счёт без расшифровки.
Правило главы
Каждый блок контекста должен отвечать на два вопроса: какое решение он меняет и откуда взят. Если ответов нет, блок не заслужил токены.
Чеклист главы
Практикум
Задача 18.1 — рентген контекста. ⭑⭑
Возьмите двадцать реальных запросов сервиса. Для каждого сохраните разбивку токенов по категориям (system, rules, sources, history, question), версии источников и итоговый ответ.
Критерии приёмки:
Подсказки:
- Считать токены по категориям проще, если сборщик контекста возвращает не строку, а список блоков с метаданными.
- «Ничего не меняет» проверяется грубо и честно: выкиньте блок и прогоните те же двадцать запросов.
Задача 18.2 — бюджет с приоритетами. ⭑⭑
Реализуйте ContextBudget и сборщик, который укладывается в бюджет по приоритетам и умеет объяснить каждое включение и каждое исключение.
Критерии приёмки:
Подсказки:
- Дедупликацию удобно делать по хешу нормализованного текста плюс порогу пересечения n-грамм; идеальная точность здесь не нужна.
- Приоритеты держите данными, а не
if-ами: список правил проще менять и объяснять на ревью.
Задача 18.3 — история как состояние. ⭑⭑⭑
Замените хранение последних N сообщений на структурированное состояние диалога: факты в полях, свободный текст в жёстко ограниченном объёме.
Критерии приёмки:
Подсказки:
- Начните с трёх-четырёх полей, которые чаще других фигурируют в тикетах. Полная модель диалога здесь не нужна и вредна.
- Хороший тест на инвалидацию: диалог, где пользователь дважды меняет решение, а на третьем шаге просит подтвердить итог.