Перейти к основному содержимому

Сжатие и кэширование контекста

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. Порядок разрешения:

  1. Проверка каталога plugins/context_engine/<name>/
  2. Проверка общей системы плагинов (register_context_engine())
  3. Возврат к встроенному 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

Детали параметров​

ПараметрПо умолчаниюДиапазонОписание
threshold0.500.0–1.0Сжатие срабатывает, когда токенов промпта ≥ threshold × context_length
target_ratio0.200.10–0.80Управляет бюджетом токенов защиты хвоста: threshold_tokens × target_ratio
protect_last_n20≥1Минимальное количество последних сообщений, всегда сохраняемых
protect_first_n3(жёстко задано)Системный промпт + первый обмен всегда сохраняются
codex_gpt55_autoraisetrueboolПоднять порог до 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: Сборка сжатых сообщений​

Список сжатых сообщений:

  1. Головные сообщения (с примечанием, добавленным к системному промпту при первом сжатии)
  2. Сообщение-суммаризация (роль выбирается так, чтобы избежать нарушений последовательности одинаковых ролей)
  3. Хвостовые сообщения (без изменений)

Осиротевшие пары 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. Стабильный системный промпт: Системный промпт — точка останова 1 и кэшируется во всех оборотах. Избегайте его изменения в середине диалога (сжатие добавляет примечание только при первом уплотнении).

  2. Порядок сообщений имеет значение: Попадания в кэш требуют совпадения префикса. Добавление или удаление сообщений в середине аннулирует кэш для всего, что идёт после.

  3. Взаимодействие сжатия и кэша: После сжатия кэш аннулируется для сжатой области, но кэш системного промпта сохраняется. Скользящее окно из 3 сообщений восстанавливает кэширование в течение 1–2 оборотов.

  4. Выбор 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% окна контекста модели.