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

Hermes Agent использует двойную систему сжатия и кеширование подсказок (prompt caching) 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"    # default — built-in lossy summarization
  engine: "lcm"           # example — plugin providing lossless context

Движок отвечает за:

Выбор осуществляется через конфигурацию context.engine в config.yaml. Порядок разрешения:

  1. Проверка директории plugins/context_engine/<name>/

  2. Проверка общей системы плагинов (register_context_engine())

  3. Возврат к встроенному ContextCompressor

Движки-плагины никогда не активируются автоматически — пользователь должен явно установить context.engine в имя плагина. Значение по умолчанию "compressor" всегда использует встроенный движок.

Настройка через hermes plugins → Provider Plugins → Context Engine, или редактирование config.yaml напрямую.

Для создания плагина движка контекста см. Context Engine Plugins.

Двойная система сжатия

Hermes имеет два отдельных слоя сжатия, которые работают независимо:

                     ┌──────────────────────────┐
  Incoming message      Gateway Session Hygiene   Fires at 85% of context
  ─────────────────►    (pre-agent, rough est.)   Safety net for large sessions
                     └─────────────┬────────────┘
                                                                                           ┌──────────────────────────┐
                        Agent ContextCompressor   Fires at 50% of context (default)
                        (in-loop, real tokens)    Normal context management
                     └──────────────────────────┘

1. Гигиена сессий Gateway (порог 85%)

Находится в gateway/run.py (ищите Session hygiene: auto-compress). Это страховочная сеть, которая запускается перед обработкой сообщения агентом. Она предотвращает ошибки API, когда сессии становятся слишком большими между витками (например, при ночном накоплении в Telegram/Discord).

Порог гигиены Gateway intentionally выше, чем у компрессора агента. Установка его на уровне 50% (как у агента) приводила к преждевременному сжатию на каждом витке в длительных сессиях Gateway.

2. ContextCompressor агента (порог 50%, настраивается)

Находится в agent/context_compressor.py. Это основная система сжатия, которая работает внутри цикла инструментов агента с доступом к точным данным о количестве токенов из API.

Конфигурация

Все настройки сжатия читаются из config.yaml в разделе compression:

compression:
  enabled: true              # Enable/disable compression (default: true)
  threshold: 0.50            # Fraction of context window (default: 0.50 = 50%)
  target_ratio: 0.20         # How much of threshold to keep as tail (default: 0.20)
  protect_last_n: 20         # Minimum protected tail messages (default: 20)

# Summarization model/provider configured under auxiliary:
auxiliary:
  compression:
    model: null              # Override model for summaries (default: auto-detect)
    provider: auto           # Provider: "auto", "openrouter", "nous", "main", etc.
    base_url: null           # Custom OpenAI-compatible endpoint

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

Параметр По умолчанию Диапазон Описание
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 (жёстко задано) Системный промпт + первый обмен всегда сохраняются

Вычисленные значения (для модели с контекстом 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

Алгоритм сжатия

Метод ContextCompressor.compress() следует 4-фазному алгоритму:

Фаза 1: Обрезка старых результатов инструментов (дёшево, без вызова LLM)

Старые результаты инструментов (>200 символов) за пределами защищённого хвоста заменяются на:

[Old tool output cleared to save context space]

Это дёшевый предварительный проход, который экономит значительное количество токенов от многословных выводов инструментов (содержимое файлов, вывод терминала, результаты поиска).

Фаза 2: Определение границ

┌─────────────────────────────────────────────────────────────┐
│  Message list                                               │
│                                                             │
│  [0..2]  ← protect_first_n (system + first exchange)        │
│  [3..N]  ← middle turns → SUMMARIZED                        │
│  [N..end] ← tail (by token budget OR protect_last_n)        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Защита хвоста основана на бюджете токенов: проходит с конца в обратном направлении, накапливая токены до исчерпания бюджета. Возвращается к фиксированному числу protect_last_n, если бюджет защитил бы меньше сообщений.

Границы выравниваются, чтобы избежать разделения групп tool_call/tool_result. Метод _align_boundary_backward() проходит мимо последовательных результатов инструментов, чтобы найти родительское сообщение ассистента, сохраняя группы целыми.

Фаза 3: Генерация структурированного резюме

warning Длина контекста модели для резюмирования Модель для резюмирования должна иметь окно контекста не меньше, чем у основной модели агента. Весь средний раздел отправляется модели для резюмирования в одном вызове call_llm(task="compression"). Если контекст модели для резюмирования меньше, API возвращает ошибку длины контекста — _generate_summary() перехватывает её, записывает предупреждение и возвращает None. Затем компрессор отбрасывает средние витки без резюме, молча теряя контекст разговора. Это самая частая причина снижения качества уплотнения.

Средние витки резюмируются с использованием вспомогательной LLM со структурированным шаблоном:

## Goal
[What the user is trying to accomplish]

## Constraints & Preferences
[User preferences, coding style, constraints, important decisions]

## Progress
### Done
[Completed work  specific file paths, commands run, results]
### In Progress
[Work currently underway]
### Blocked
[Any blockers or issues encountered]

## Key Decisions
[Important technical decisions and why]

## Relevant Files
[Files read, modified, or created  with brief note on each]

## Next Steps
[What needs to happen next]

## Critical Context
[Specific values, error messages, configuration details]

Бюджет резюме масштабируется в зависимости от объёма сжимаемого контента:

Фаза 4: Сборка сжатых сообщений

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

  1. Головные сообщения (с примечанием, добавленным к системному промпту при первом сжатии)

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

  3. Хвостовые сообщения (без изменений)

Осиротевшие пары tool_call/tool_result очищаются с помощью _sanitize_tool_pairs():

Итеративное повторное сжатие

При последующих сжатиях предыдущее резюме передаётся LLM с инструкцией обновить его, а не резюмировать с нуля. Это сохраняет информацию при нескольких уплотнениях — пункты перемещаются из "В процессе" в "Готово", добавляется новый прогресс, а устаревшая информация удаляется.

Поле _previous_summary в экземпляре компрессора хранит последний текст резюме для этой цели.

Пример до/после

До сжатия (45 сообщений, ~95K токенов)

[0] system:    "You are a helpful assistant..." (system prompt)
[1] user:      "Help me set up a FastAPI project"
[2] assistant: <tool_call> terminal: mkdir project </tool_call>
[3] tool:      "directory created"
[4] assistant: <tool_call> write_file: main.py </tool_call>
[5] tool:      "file written (2.3KB)"
    ... 30 more turns of file editing, testing, debugging ...
[38] assistant: <tool_call> terminal: pytest </tool_call>
[39] tool:      "8 passed, 2 failed\n..."  (5KB output)
[40] user:      "Fix the failing tests"
[41] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[42] tool:      "import pytest\n..."  (3KB)
[43] assistant: "I see the issue with the test fixtures..."
[44] user:      "Great, also add error handling"

После сжатия (25 сообщений, ~45K токенов)

[0] system:    "You are a helpful assistant...
               [Note: Some earlier conversation turns have been compacted...]"
[1] user:      "Help me set up a FastAPI project"
[2] assistant: "[CONTEXT COMPACTION] Earlier turns were compacted...

               ## Goal
               Set up a FastAPI project with tests and error handling

               ## Progress
               ### Done
               - Created project structure: main.py, tests/, requirements.txt
               - Implemented 5 API endpoints in main.py
               - Wrote 10 test cases in tests/test_api.py
               - 8/10 tests passing

               ### In Progress
               - Fixing 2 failing tests (test_create_user, test_delete_user)

               ## Relevant Files
               - main.py  FastAPI app with 5 endpoints
               - tests/test_api.py  10 test cases
               - requirements.txt  fastapi, pytest, httpx

               ## Next Steps
               - Fix failing test fixtures
               - Add error handling"
[3] user:      "Fix the failing tests"
[4] assistant: <tool_call> read_file: tests/test_api.py </tool_call>
[5] tool:      "import pytest\n..."
[6] assistant: "I see the issue with the test fixtures..."
[7] user:      "Great, also add error handling"

Кеширование подсказок (Prompt Caching, Anthropic)

Исходный код: agent/prompt_caching.py

Снижает затраты на входные токены примерно на 75% в многошаговых разговорах за счёт кеширования префикса разговора. Использует точки останова cache_control от Anthropic.

Стратегия: system_and_3

Anthropic допускает максимум 4 точки останова cache_control на запрос. Hermes использует стратегию "system_and_3":

Breakpoint 1: System prompt           (stable across all turns)
Breakpoint 2: 3rd-to-last non-system message  ─┐
Breakpoint 3: 2nd-to-last non-system message   ├─ Rolling window
Breakpoint 4: Last non-system message          ─┘

Как это работает

apply_anthropic_cache_control() выполняет глубокое копирование сообщений и внедряет маркеры cache_control:

# Cache marker format
marker = {"type": "ephemeral"}
# Or for 1-hour TTL:
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 для длительных сессий, где пользователь делает перерывы между витками.

Включение кеширования подсказок

Кеширование подсказок автоматически включается, когда:

# config.yaml — TTL is configurable (must be "5m" or "1h")
prompt_caching:
  cache_ttl: "5m"

CLI показывает статус кеширования при запуске:

💾 Prompt caching: ENABLED (Claude via OpenRouter, 5m TTL)

Предупреждения о давлении контекста

Промежуточные предупреждения о давлении контекста были удалены (см. блок iteration-budget в run_agent.py, где отмечено: "No intermediate pressure warnings — they caused models to 'give up' prematurely on complex tasks"). Сжатие срабатывает, когда токены промпта достигают настроенного compression.threshold (по умолчанию 50%) без предварительного этапа предупреждения; гигиена сессий Gateway срабатывает как вторичная страховочная сеть на уровне 85% окна контекста модели.