В сервисе разбора обращений один и тот же справочник тарифов занимал 18 тысяч входных токенов. Его отправляли заново с каждым тикетом. Ночная переиндексация шла синхронными запросами, а оператор ждал полный ответ, хотя первые слова были готовы через секунду. Команда собиралась обучать свою модель. Сначала она перестала оплачивать собственную небрежность.
Это стандартный порядок событий. Fine-tuning обсуждают на второй неделе, кэш префикса включают на восьмой, а разница в счёте между этими двумя решениями отличается примерно на порядок — в пользу того, которое делается за день.
Пять разных инструментов
Кэширование экономит повторный ввод. Оно работает, когда стабильный префикс запроса байт-в-байт одинаков: system prompt и справочники ставят первыми, пользовательские данные последними. Кэш ответа уместен только для детерминированных, обезличенных запросов; ключ обязан включать tenant, версии prompt, модели и корпуса.
Batch API снижает цену фоновой работы ценой задержки. Для ночной классификации каталога это хороший обмен, для чата поддержки нет. Streaming почти не меняет стоимость и полное время выполнения, зато уменьшает time to first token. Пользователь видит начало ответа, но сервер всё равно должен пережить обрыв соединения и сохранить итог.
Distillation переносит поведение дорогой модели в более дешёвую на размеченном наборе. Fine-tuning закрепляет формат, стиль или узкую классификацию. Ни один из них не добавляет свежих фактов: для этого нужен retrieval. Обучать модель потому, что prompt неудобно поддерживать, довольно дорогой способ не чинить prompt. К тому же после дообучения промпт никуда не денется: он просто станет невидимым, зашитым в веса, и его нельзя будет отревьюить.
Практический каркас
from hashlib import sha256
import json
def cache_key(*, tenant: str, prompt_version: str, model: str, payload: dict) -> str:
body = json.dumps(payload, sort_keys=True, ensure_ascii=False)
raw = f"{tenant}:{prompt_version}:{model}:{body}"
return sha256(raw.encode()).hexdigest()
async def answer(req):
key = cache_key(tenant=req.tenant_id, prompt_version="support-v12",
model=settings.STRONG_MODEL, payload=req.model_dump())
if cached := await redis.get(key):
return cached
result = await generate(req)
await redis.set(key, result, ex=900)
return resultСхема выбора проста:
повторяется вход? -> prompt cache
ответ можно переиспользовать безопасно? -> response cache
результат терпит часы? -> batch
важно быстро показать начало? -> streaming
много размеченных примеров и стабильная задача? -> distillation/fine-tuningПорядок, в котором это стоит делать
Механизмы дают очень разную отдачу на вложенный день работы. Порядок такой:
- Перестать отправлять лишнее. Разбивка токенов по категориям (глава 18) показывает, что треть входа не нужна. Это бесплатно и не требует ничего, кроме внимания.
- Кэш стабильного префикса. День работы: переставить блоки так, чтобы неизменная часть шла первой, и включить кэширование у провайдера.
- Роутинг моделей. Дешёвая модель на простых случаях (глава 45).
- Batch для фоновых задач. Пара дней, включая переделку джоб на асинхронный забор результатов.
- Streaming. Не про деньги, а про ощущение скорости.
- Distillation и fine-tuning. Недели работы, отдельный набор данных, отдельный процесс обновления и повторные замеры качества после каждой смены модели.
Начинать с шестого пункта — распространённая ошибка, потому что он самый интересный. Инженерное удовольствие и экономический эффект здесь расположены в противоположных концах списка.
Что ломается
Кэш без версии возвращает ответ старого prompt. Общий ключ показывает данные соседнего tenant. Streaming создаёт иллюзию успеха, хотя генерация оборвалась после первого абзаца. Batch-задачи теряются без собственного job id и сверки результатов. Fine-tuning деградирует после смены входных документов, а команда спорит с моделью вместо проверки drift.
Правило: сначала измерьте повтор, допустимую задержку и стоимость ошибки. Выбирайте механизм по найденному ограничению, а не по моде.
Чеклист главы
Практикум
Задача 46.1 — что именно вы оплачиваете дважды. ⭑⭑
Возьмите сто реальных вызовов и найдите, за что вы платите повторно.
Критерии приёмки:
Подсказки:
- Долю общего префикса считайте по фактическим строкам запроса, а не по шаблону: разница в мелочах вроде подстановки даты в начало.
- Один механизм за итерацию: иначе вы не узнаете, что именно сработало.
Задача 46.2 — batch для ночной работы. ⭑⭑⭑
Переведите одну фоновую задачу на batch-обработку.
Критерии приёмки:
Подсказки:
- Сверка по числу записей обязательна: тихая потеря части пачки — самый незаметный отказ.
- Если задача внезапно перестала терпеть часы, это продуктовое изменение, а не техническое: зафиксируйте его явно.