У внутреннего помощника было окно на 128 тысяч токенов. Команда положила туда системную инструкцию, историю чата за неделю, регламент на 80 страниц и результаты поиска. Счёт за запрос вырос, а ответы стали хуже: модель находила старый тариф из переписки и не замечала актуальную таблицу в конце prompt.
Контекстное окно задаёт верхнюю границу, но не гарантирует внимания к каждому фрагменту. Вход конкурирует за место и влияние на ответ. Повтор инструкции тоже стоит токены, а длинная история приносит устаревшие факты. Поэтому контекст следует собирать как данные для конкретного решения, а не как архив на всякий случай.
Бюджет запроса
До вызова модели полезно посчитать явный бюджет:
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Собирать переменную часть лучше по приоритетам: текущий вопрос, обязательные правила, найденные источники, затем краткая история. Полную историю можно хранить отдельно и сворачивать в проверяемое состояние: выбранный продукт, номер договора, уже заданные вопросы. Резюме, которое сочинила модель, нельзя считать источником факта.
Механизм сборки
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 оказался внутри.
Что ломается
- Обрезка с конца удаляет вопрос или свежие источники. Обрезайте блоки осознанно.
- Резюме диалога закрепляет ошибку модели. Храните факты отдельно от свободного текста.
- Десять почти одинаковых чанков вытесняют разные источники. Делайте дедупликацию и ограничивайте число фрагментов на документ.
- Большой
max_output_tokensсъедает входной бюджет, даже когда обычный ответ занимает 300 токенов. - Секреты и PII попадают в prompt вместе с «полезной историей». Фильтрация должна происходить до сборки.
Правило главы
Каждый блок контекста должен отвечать на два вопроса: какое решение он меняет и откуда взят. Если ответов нет, блок не заслужил токены.
Практикум
Возьмите двадцать реальных запросов сервиса. Сохраните токены по категориям, версии источников и ответ. Затем установите бюджет на историю и лимит фрагментов с одного документа. Сравните стоимость, долю ответов со ссылкой на источник и качество на тех же двадцати запросах. Отдельно проверьте случай, где в истории лежит старое значение, а в базе знаний новое.