Сжатие и кэширование контекста
VibeOS использует двойную систему сжатия и кэширование промптов Anthropic для эффективного управления окном контекста в длинных диалогах.
Исходные файлы: agent/context_engine.py (ABC), agent/context_compressor.py (стандартный движок),
agent/prompt_caching.py, gateway/run.py (гигиена сессий), run_agent.py (поиск _compress_context)
Подключаемый движок контекста
Управление контекстом построено на ABC ContextEngine (agent/context_engine.py). Встроенный ContextCompressor — реализация по умолчанию, но плагины могут заменять его альтернативными движками (например, Lossless Context Management).
context:
engine: "compressor" # по умолчанию — встроенное сжатие с потерями
engine: "lcm" # пример — плагин, обеспечивающий сжатие без потерь
Движок отвечает за:
- Определение момента, когда нужно запустить уплотнение (
should_compress()) - Выполнение уплотнения (
compress()) - Опциональное предоставление инструментов, которые может вызывать агент (например,
lcm_grep) - Отслеживание использования токенов из ответов API
Выбор осуществляется через конфигурацию context.engine в config.yaml. Порядок разрешения:
- Проверка каталога
plugins/context_engine/<name>/ - Проверка общей системы плагинов (
register_context_engine()) - Возврат к встроенному
ContextCompressor
Плагины движков никогда не активируются автоматически — пользователь должен явно установить context.engine на имя плагина. Значение по умолчанию "compressor" всегда использует встроенный движок.
Настройка через vibeos plugins → Provider Plugins → Context Engine или прямым редактированием config.yaml.
Инструкции по созданию плагина движка контекста см. в разделе Плагины движка контекста.
Двойная система сжатия
VibeOS имеет два независимых уровня сжатия, работающих отдельно:
┌──────────────────────────┐
Входящее сообщение │ Гигиена сессии шлюза │ Срабатывает при 85% контекста
─────────────────► │ (до агента, грубая оценка)│ Предохранитель для больших сессий
└─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ ContextCompressor агента │ Срабатывает при 50% контекста (по умолчанию)
│ (в цикле, реальные токены)│ Обычное управление контекстом
└──────────────────────────┘
1. Гигиена сессии шлюза (порог 85%)
Находится в gateway/run.py (поиск «Session hygiene: auto-compress»). Это предохранитель, который
запускается до обработки сообщения агентом. Он предотвращает ошибки API, когда сессии
становятся слишком большими между оборотами (например, ночное накопление в Telegram/Discord).
- Порог: Фиксированные 85% длины контекста модели
- Источник токенов: Предпочитает фактические токены, сообщённые API в последнем обороте; при отсутствии использует
грубую оценку на основе символов (
estimate_messages_tokens_rough) - Срабатывает: Только когда
len(history) >= 4и сжатие включено - Назначение: Перехват сессий, которые избежали собственного компрессора агента
Порог гигиены шлюза намеренно выше, чем у компрессора агента. Установка на 50% (как у агента) приводила к преждевременному сжатию на каждом обороте в длинных сессиях шлюза.
2. ContextCompressor агента (порог 50%, настраивается)
Находится в agent/context_compressor.py. Это основная система сжатия,
которая работает внутри цикла инструментов агента с доступом к точным
количествам токенов, сообщённым API.
Конфигурация
Все настройки сжатия читаются из config.yaml в разделе compression:
compression:
enabled: true # Включение/отключение сжатия (по умолчанию: true)
threshold: 0.50 # Доля окна контекста (по умолчанию: 0.50 = 50%)
target_ratio: 0.20 # Какая часть порога остаётся в хвосте (по умолчанию: 0.20)
protect_last_n: 20 # Минимальное количество защищённых хвостовых сообщений (по умолчанию: 20)
codex_gpt55_autoraise: true # gpt-5.5 на маршруте OpenAI ChatGPT: поднять порог до 85% (по умолчанию: true)
# Модель/провайдер суммаризации настраиваются в auxiliary:
auxiliary:
compression:
model: null # Переопределение модели для суммаризации (по умолчанию: автоопределение)
provider: auto # Провайдер: «auto», «openrouter», «nous», «main» и т.д.
base_url: null # Пользовательский endpoint, совместимый с OpenAI
Детали параметров
| Параметр | По умолчанию | Диапазон | Описание |
|---|---|---|---|
threshold | 0.50 | 0.0–1.0 | Сжатие срабатывает, когда токенов промпта ≥ threshold × context_length |
target_ratio | 0.20 | 0.10–0.80 | Управляет бюджетом токенов защиты хвоста: threshold_tokens × target_ratio |
protect_last_n | 20 | ≥1 | Минимальное количество последних сообщений, всегда сохраняемых |
protect_first_n | 3 | (жёстко задано) | Системный промпт + первый обмен всегда сохраняются |
codex_gpt55_autoraise | true | bool | Поднять порог до 85% для gpt-5.5 на маршруте OpenAI ChatGPT (см. ниже). Установите false, чтобы оставить глобальный threshold |
Автоповышение порога для ChatGPT gpt-5.5
Маршрут OpenAI ChatGPT жёстко ограничивает gpt-5.5 окном контекста в 272K
(тот же слаг открывает 1.05M на прямом API OpenAI и OpenRouter, и 400K на
GitHub Copilot). При стандартном пороге 50% уплотнение срабатывало бы при ~136K —
половине окна, которое модель может реально использовать. Когда активный маршрут — OpenAI
ChatGPT (provider: chatgpt-oauth; старый openai-codex тоже работает) и модель gpt-5.5, VibeOS поднимает порог
до 85% (~231K) и выводит одноразовое уведомление с командой для отключения.
Затрагивается только этот конкретный маршрут; gpt-5.5 на любом другом провайдере сохраняет
глобальный threshold. Чтобы вернуться к глобальному значению:
vibeos config set compression.codex_gpt55_autoraise false
Вычисляемые значения (для модели с контекстом 200K при настройках по умолчанию)
context_length = 200 000
threshold_tokens = 200 000 × 0,50 = 100 000
tail_token_budget = 100 000 × 0,20 = 20 000
max_summary_tokens = min(200 000 × 0,05, 12 000) = 10 000
threshold_tokens всегда равно threshold × context_length, где context_length
— это окно контекста основной модели агента — никогда не вспомогательной/суммаризирующей
модели. Для модели с 262 144 токенами при стандартном 0,50 порог равен
262 144 × 0,50 = 131 072. То, что это число близко к распространённому «контексту 128K»,
является совпадением из-за процента, а не признаком того, что окно вспомогательной модели
является триггером. Окно контекста вспомогательной модели — это отдельная тема —
см. предупреждение «Длина контекста модели суммаризации» ниже о том, как это влияет на возможность
создания суммаризации, а не на момент срабатывания сжатия.
Алгоритм сжатия
Метод ContextCompressor.compress() следует 4-фазному алгоритму:
Фаза 1: Очистка старых результатов инструментов (дёшево, без вызова LLM)
Старые результаты инструментов (>200 символов) за пределами защищённого хвоста заменяются на:
[Старый вывод инструмента очищен для экономии места в контексте]
Это дешёвый предварительный проход, который экономит значительное количество токенов от многословных выводов инструментов (содержимое файлов, вывод терминала, результаты поиска).
Фаза 2: Определение границ
┌─────────────────────────────────────────────────────────────┐
│ Список сообщений │
│ │
│ [0..2] ← protect_first_n (система + первый обмен) │
│ [3..N] ← средние обороты → СУММАРИЗИРУЮТСЯ │
│ [N..end] ← хвост (по бюджету токенов ИЛИ protect_last_n) │
│ │
└─────────────────────────────────────────────────────────────┘
Защита хвоста основана на бюджете токенов: проход от конца назад,
накопление токенов до исчерпания бюджета. Возврат к фиксированному количеству
protect_last_n, если бюджет защитил бы меньше сообщений.
Границы выравниваются, чтобы не разрывать группы tool_call/tool_result.
Метод _align_boundary_backward() проходит мимо последовательных результатов инструментов,
чтобы найти родительское сообщение ассистента, сохраняя группы целыми.
Фаза 3: Генерация структурированной суммаризации
Модель суммаризации должна иметь окно контекста как минимум такое же, как у основной модели агента. Вся средняя секция отправляется модели суммаризации в одном вызове call_llm(task="compression"). Если контекст модели суммаризации меньше, API возвращает ошибку длины контекста — _generate_summary() перехватывает её, записывает предупреждение в лог и возвращает None. Затем компрессор отбрасывает средние обороты без суммаризации, молча теряя контекст диалога. Это самая частая причина ухудшения качества уплотнения.
Средние обороты суммаризируются с помощью вспомогательной LLM по структурированному шаблону:
## Цель
[Чего пользователь пытается достичь]
## Ограничения и предпочтения
[Предпочтения пользователя, стиль кодирования, ограничения, важные решения]
## Прогресс
### Сделано
[Завершённая работа — конкретные пути к файлам, выполненные команды, результаты]
### В процессе
[Текущая работа]
### Заблокировано
[Любые блокирующие факторы или возникшие проблемы]
## Ключевые решения
[Важные технические решения и их обоснование]
## Соответствующие файлы
[Файлы, которые были прочитаны, изменены или созданы — с кратким примечанием о каждом]
## Следующие шаги
[Что нужно сделать дальше]
## Критический контекст
[Конкретные значения, сообщения об ошибках, детали конфигурации]
Бюджет суммаризации масштабируется в зависимости от объёма сжимаемого содержимого:
- Формула:
content_tokens × 0,20(константа_SUMMARY_RATIO) - Минимум: 2 000 токенов
- Максимум:
min(context_length × 0,05, 12 000)токенов
Фаза 4: Сборка сжатых сообщений
Список сжатых сообщений:
- Головные сообщения (с примечанием, добавленным к системному промпту при первом сжатии)
- Сообщение-суммаризация (роль выбирается так, чтобы избежать нарушений последовательности одинаковых ролей)
- Хвостовые сообщения (без изменений)
Осиротевшие пары tool_call/tool_result очищаются методом _sanitize_tool_pairs():
- Результаты инструментов, ссылающиеся на удалённые вызовы → удаляются
- Вызовы инструментов, чьи результаты были удалены → вставляется заглушка результата
Итеративное повторное сжатие
При последующих сжатиях предыдущая суммаризация передаётся LLM с инструкцией обновить её, а не суммаризировать с нуля. Это сохраняет информацию при нескольких уплотнениях — элементы перемещаются из «В процессе» в «Сделано», добавляется новый прогресс, а устаревшая информация удаляется.
Поле _previous_summary в экземпляре компрессора хранит последний текст
суммаризации для этой цели.
Пример «до/после»
До сжатия (45 сообщений, ~95K токенов)
[0] system: «Вы — полезный ассистент...» (системный промпт)
[1] user: «Помоги мне настроить проект FastAPI»
[2] assistant: <tool_call> terminal: mkdir project </tool_call>
[3] tool: «каталог создан»
[4] assistant: <tool_call> write_file: main.py </tool_call>
[5] tool: «файл записан (2,3 КБ)»
... ещё 30 оборотов редактирования файлов, тестирования, отладки ...
[38] assistant: <tool_call> terminal: pytest </tool_call>
[39] tool: «8 пройдено, 2 не пройдено\n...» (5 КБ вывода)
[40] user: «Исправь падающие тесты»
[41] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[42] tool: «import pytest\n...» (3 КБ)
[43] assistant: «Я вижу проблему с тестовыми фикстурами...»
[44] user: «Отлично, ещё добавь обработку ошибок»
После сжатия (25 сообщений, ~45K токенов)
[0] system: «Вы — полезный ассистент...
[Примечание: Некоторые предыдущие обороты диалога были уплотнены...]»
[1] user: «Помоги мне настроить проект FastAPI»
[2] assistant: «[УПЛОТНЕНИЕ КОНТЕКСТА] Более ранние обороты были уплотнены...
## Цель
Настроить проект FastAPI с тестами и обработкой ошибок
## Прогресс
### Сделано
- Создана структура проекта: main.py, tests/, requirements.txt
- Реализовано 5 API-эндпоинтов в main.py
- Написано 10 тестовых случаев в tests/test_api.py
- 8/10 тестов проходят
### В процессе
- Исправление 2 падающих тестов (test_create_user, test_delete_user)
## Соответствующие файлы
- main.py — FastAPI-приложение с 5 эндпоинтами
- tests/test_api.py — 10 тестовых случаев
- requirements.txt — fastapi, pytest, httpx
## Следующие шаги
- Исправить тестовые фикстуры
- Добавить обработку ошибок»
[3] user: «Исправь падающие тесты»
[4] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[5] tool: «import pytest\n...»
[6] assistant: «Я вижу проблему с тестовыми фикстурами...»
[7] user: «Отлично, ещё добавь обработку ошибок»
Кэширование промптов (Anthropic)
Исходный код: agent/prompt_caching.py
Снижает затраты на входные токены примерно на 75% в многооборотных диалогах за счёт кэширования
префикса диалога. Использует точки останова cache_control от Anthropic.
Стратегия: system_and_3
Anthropic допускает максимум 4 точки останова cache_control на запрос. VibeOS
использует стратегию «system_and_3»:
Точка останова 1: Системный промпт (стабилен во всех оборотах)
Точка останова 2: 3-е с конца несистемное сообщение ─┐
Точка останова 3: 2-е с конца несистемное сообщение ├─ Скользящее окно
Точка останова 4: Последнее несистемное сообщение ─┘
Как это работает
apply_anthropic_cache_control() создаёт глубокую копию сообщений и вставляет
маркеры cache_control:
# Формат маркера кэша
marker = {"type": "ephemeral"}
# Или для TTL в 1 час:
marker = {"type": "ephemeral", "ttl": "1h"}
Маркер применяется по-разному в зависимости от типа содержимого:
| Тип содержимого | Куда помещается маркер |
|---|---|
| Строковое содержимое | Преобразуется в [{"type": "text", "text": ..., "cache_control": ...}] |
| Список содержимого | Добавляется в словарь последнего элемента |
| None/пусто | Добавляется как msg["cache_control"] |
| Сообщения инструментов | Добавляется как msg["cache_control"] (только нативный Anthropic) |
Шаблоны проектирования с учётом кэша
-
Стабильный системный промпт: Системный промпт — точка останова 1 и кэшируется во всех оборотах. Избегайте его изменения в середине диалога (сжатие добавляет примечание только при первом уплотнении).
-
Порядок сообщений имеет значение: Попадания в кэш требуют совпадения префикса. Добавление или удаление сообщений в середине аннулирует кэш для всего, что идёт после.
-
Взаимодействие сжатия и кэша: После сжатия кэш аннулируется для сжатой области, но кэш системного промпта сохраняется. Скользящее окно из 3 сообщений восстанавливает кэширование в течение 1–2 оборотов.
-
Выбор TTL: По умолчанию —
5m(5 минут). Используйте1hдля длительных сессий, где пользователь делает перерывы между оборотами.
Включение кэширования промптов
Кэширование промптов автоматически включается, когда:
- Модель является моделью Anthropic Claude (определяется по имени модели)
- Провайдер поддерживает
cache_control(нативный API Anthropic или OpenRouter)
# config.yaml — TTL настраивается (должен быть «5m» или «1h»)
prompt_caching:
cache_ttl: "5m"
CLI показывает статус кэширования при запуске:
💾 Кэширование промптов: ВКЛЮЧЕНО (Claude через OpenRouter, TTL 5m)
Предупреждения о давлении контекста
Промежуточные предупреждения о давлении контекста были удалены (см. блок итерационного бюджета в run_agent.py, где указано: «Нет промежуточных предупреждений о давлении — они заставляли модели «сдаваться» преждевременно при выполнении сложных задач»). Сжатие срабатывает, когда токены промпта достигают настроенного compression.threshold (по умолчанию 50%) без предварительного шага предупреждения; гигиена сессии шлюза срабатывает как вторичный предохранитель при 85% окна контекста модели.