Один timestamp, один round-robin, один плавающий список tools: 7 анти-паттернов, которые убивают префикс кэша LLM

Один timestamp, один round-robin, один плавающий список tools: 7 анти-паттернов, которые убивают префикс кэша LLM

При работе с кэшированием префиксов в LLM легко попасть в ловушку: кэш вроде бы включён, но reuse почти отсутствует. cached_tokens не растут, задержки высокие, а prefill каждый раз пересчитывается. Причина — не в самой модели, а в инженерных ошибках. Ниже разбор семи анти-паттернов, которые убивают эффективность prefix caching.

Три условия для работы кэша

Чтобы кэш сработал, нужно соблюсти три условия одновременно:

  • Совпадение начала запроса — не просто похожее, а буквально идентичное начало по токенам.
  • Попадание на нужную реплику — запрос должен прийти туда, где кэш уже прогрет.
  • Выживание кэша — KV-cache должен сохраниться до следующего запроса, не быть вытесненным.

Если нарушено хотя бы одно — кэш не сработает. Всё остальное — детали реализации.

Синтетический кейс: как всё ломается

Представим корпоративного AI-ассистента. В каждом запросе — длинный system prompt, 14 tool definitions и короткая история чата. Идеальный кейс для кэширования: общая часть длинная, повторов много.

До релиза:

  • prompt_tokens p50 — стабильны
  • cached_tokens p50 — высокие
  • роутинг — sticky / prefix-aware

После трёх «безобидных» изменений:

  1. В system prompt добавили timestamp и user.company.
  2. Порядок tools стал динамическим.
  3. Перешли на round-robin между 4 репликами.

Результат:

  • prompt_tokens p50 — почти те же
  • cached_tokens p50 — почти ноль
  • роутинг — round-robin

Все три условия нарушены: начало не совпадает, запросы разлетаются по репликам, кэш не доживает.

1. Волатильные данные в начале запроса

Самый частый анти-паттерн. Добавление timestamp, user.name, session_id в начало делает каждый запрос уникальным с первых токенов.

Решение: статичный контент — в начало, динамичный — в конец или в отдельные метаданные. Это сохраняет общую, самую дорогую часть запроса.

2. Невидимый дрейф шаблона

Когда один и тот же запрос собирается по-разному: разное количество пробелов, переводов строк, detail="low" vs detail="high", разная сериализация в SDK.

Глазами — одинаково. Для кэша — разные начала.

Решение: нормализация сборки запроса: пробелы, переводы строк, параметры изображений, сериализация ссылок. Должен быть один канонический способ.

3. Tools, schema и response_format — это тоже начало

API-обвязка часто ломает кэш. Динамические поля в json_schema, случайный requestId в response_format — и тяжёлый общий кусок перестаёт быть общим.

Решение: телеметрия (request_id, trace_id) — в отдельные поля, не в кэшируемую часть. Tools и schema — детерминированные и стабильные.

4. Переписывание истории вместо append-only

Кэш любит, когда диалог растёт в конец. Если вы переписываете начало — через суммаризацию, сжатие или обрезание — кэш сбрасывается.

Summarization — это trade-off, а не бесплатная оптимизация. Вы экономите на длине контекста, но теряете reuse.

5. Round-robin размазывает кэш по репликам

Даже при стабильном начале запроса — если он попадает на разные реплики, кэш не переиспользуется.

Решение: sticky routing, prompt_cache_key или prefix-aware routing. Без этого одинаковые запросы будут независимо прогревать кэш на каждой машине.

Минус: возможны hot spots. Но выигрыш в reuse (до 70% экономии вычислений в vLLM) перевешивает.

6. Параллельный запуск до прогрева

Контринтуитивный баг: отправка 10 одинаковых запросов одновременно не прогревает кэш — все начинают с нуля.

Решение: сначала прогреть общий префикс одним запросом, потом запускать параллельные вызовы.

7. Кэш не доживает до следующего запроса

Даже при идеальном запросе и роутинге — кэш может исчезнуть.

Причины:

  • Редкий трафик — кэш остывает.
  • Недостаток памяти — KV-cache переполняется, полезные блоки вытесняются.

В managed API — короткое окно жизни кэша. В своей инфраструктуре — нужно смотреть настройки движка и объём памяти.

Что мониторить

cached_tokens — метрика с задержкой. Лучше смотреть:

  • Долю трафика, пригодного для кэширования.
  • Время до первого токена по группам похожих запросов.
  • Изменения после деплоев, A/B-тестов, смены SDK.
  • Частоту повторного прогрева одних и тех же префиксов.

Чеклист перед деплоем

Начало запроса

  • Только статичный и общий контент в начале.
  • Персонализация, время, id — в конец или metadata.
  • Один канонический способ рендеринга system prompt.

API-обвязка

  • tools — стабильны по содержимому и порядку.
  • JSON/schema — детерминированная сериализация.
  • response_format — без динамических полей.
  • Режимы — без мутации system prompt.

Диалог

  • Рост в конец (append-only).
  • Summarization — осознанный trade-off.
  • Обрезание не ломает общий префикс.

Роутинг

  • Stickiness: prompt_cache_key, affinity, prefix-aware routing.
  • Параллельные вызовы — только после прогрева.

Время жизни кэша

  • KV budget соответствует рабочему набору, а не паспортному максимуму.
  • Есть мониторинг вытеснения и повторного прогрева.

Главный вывод: плохой hit rate — не абстракция. Это конкретная инженерная поломка. Один timestamp, один плавающий список tools, один round-robin — и весь потенциал кэша теряется.

Читать оригинал