Оптимизация затрат для систем LLM: куда на самом деле уходит деньги
Тратьте токены там, где они действительно важны.
Стоимость использования больших языковых моделей (LLM) растет линейно в зависимости от объема запросов. Система, обрабатывающая 10 000 запросов в день по цене $0,01 за запрос, обходится в $100 ежедневно — это $365 в год. В корпоративном масштабе эта сумма превышает $10 000.
Оптимизация затрат — это не про упрощение за счет качества. Это про расходование токенов там, где это действительно важно.
Каждый потраченный впустую токен — это токен, который мог бы быть использован для получения более качественного ответа.

Бюджетирование токенов
Самый простой способ контролировать расходы — установить лимиты. На сессию, на задачу или на день.
Стратегия 1: Бюджет на сессию
Бюджет на сессию прост и понятен:
class SessionBudget:
def __init__(self, budget_tokens: int = 10000):
self.budget = budget_tokens
self.used = 0
def allocate(self, tokens: int) -> bool:
if self.used + tokens <= self.budget:
self.used += tokens
return True
return False
def remaining(self) -> int:
return self.budget - self.used
Стратегия 2: Бюджет на задачу
Бюджетирование на задачу более полезно. Разным задачам требуется разное количество контекста:
task_budgets:
classify:
max_tokens: 100
model: qwen3-1.7b
summarize:
max_tokens: 500
model: qwen3-8b
code_review:
max_tokens: 2000
model: qwen2.5-coder-7b
reason:
max_tokens: 4000
model: qwen3-32b
Стратегия 3: Адаптивные бюджеты
Адаптивные бюджеты корректируются на основе реальных данных. Если задачи классификации стабильно используют 80 токенов, нет смысла выделять 100:
class AdaptiveBudget:
def __init__(self):
self.task_history = {}
def allocate(self, task_type: str) -> int:
if task_type in self.task_history:
return int(self.task_history[task_type] * 1.5)
return 1000
def record(self, task_type: str, tokens_used: int):
if task_type not in self.task_history:
self.task_history[task_type] = tokens_used
else:
self.task_history[task_type] = (
0.9 * self.task_history[task_type] + 0.1 * tokens_used
)
Использование экспоненциальной скользящей средней (с весом 0.9) означает, что недавнее использование имеет большее значение, чем история. Корректируйте вес в зависимости от волатильности ваших рабочих нагрузок.
API против локального вывода
Локальный вывод дешевле при масштабировании. Точка безубыточности зависит от вашего оборудования и тарифов API.
| Модель | API ($/M токенов) | Локальная стоимость/час | Точка безубыточности |
|---|---|---|---|
| GPT-4o | $2,50 / $10,00 | — | Н/Д |
| Claude Sonnet 4 | $3,00 / $15,00 | — | Н/Д |
| Qwen2.5-72B | $0,50 / $2,00 | ~$0,50 | ~4 часа/день |
| qwen3-32b | $0,30 / $1,20 | ~$0,20 | ~2 часа/день |
| qwen3-8b | $0,10 / $0,40 | ~$0,05 | ~1 час/день |
Математика для оборудования:
| Оборудование | Первоначальные затраты | Ежемесячное электричество | Точка безубыточности vs API |
|---|---|---|---|
| RTX 3090 (б/у) | $600 | $15 | ~4 месяца |
| RTX 4090 | $1,500 | $20 | ~6 месяцев |
| RTX 5080 | $1,000 | $18 | ~5 месяцев |
| DGX Spark | $2,000 | $30 | ~8 месяцев |
При умеренном использовании — час или более в день — локальный вывод окупается. При высоких нагрузках экономия становится существенной. Подводный камень — первоначальные капитальные затраты. RTX 5080 стоит $1,000. Счет за API можно приостановить. Оборудование — нет.
Стратегии резервного переключения
Когда ваша предпочтительная модель слишком дорога или слишком медленна, переключайтесь на более дешевую альтернативу. Ключевой момент — знать, когда качество является «достаточно хорошим».
Стратегия 1: Переключение на основе качества
Переключение на основе качества пробует модели до тех пор, пока вывод не достигнет заданного порога:
class QualityFallback:
def __init__(self, quality_threshold: float = 0.8):
self.threshold = quality_threshold
self.models = [
{"model": "claude-sonnet-4", "cost": 0.015},
{"model": "qwen2.5-72b", "cost": 0.002},
{"model": "qwen3-32b", "cost": 0.001},
{"model": "qwen3-8b", "cost": 0.0004},
]
def route(self, prompt: str) -> str:
for model_config in self.models:
result = self.call_model(model_config["model"], prompt)
if self.evaluate_quality(result) >= self.threshold:
return result
return self.call_model(self.models[0]["model"], prompt)
Проблема заключается в самом процессе оценки. Как измерить качество, не вызывая другую модель? Некоторые системы используют небольшой классификатор. Другие применяют эвристические проверки — длину, структуру, наличие ключевых слов. Ни один из этих методов не идеален.
Стратегия 2: Переключение на основе задержки
Переключение на основе задержки проще. Направляйте запросы к самой быстрой модели, которая укладывается в ваш временной бюджет:
class LatencyFallback:
def __init__(self, max_latency: float = 5.0):
self.max_latency = max_latency
self.models = [
{"model": "qwen3-1.7b", "latency": 0.5},
{"model": "qwen3-8b", "latency": 2.0},
{"model": "qwen3-32b", "latency": 10.0},
{"model": "claude-sonnet-4", "latency": 5.0},
]
def route(self, prompt: str) -> str:
for model_config in sorted(self.models, key=lambda x: x["latency"]):
if model_config["latency"] <= self.max_latency:
return self.call_model(model_config["model"], prompt)
return self.call_model(self.models[0]["model"], prompt)
Кэширование
Кэширование — это самая недооцененная стратегия оптимизации затрат. Идентичные промпты встречаются чаще, чем вы думаете — запросы на классификацию, вопросы в стиле FAQ, повторяющиеся вызовы инструментов.
Стратегия 1: Кэширование промптов
Точное кэширование промптов просто:
import hashlib
class PromptCache:
def __init__(self, max_size: int = 1000):
self.cache = {}
self.max_size = max_size
def get(self, prompt: str) -> str | None:
key = hashlib.sha256(prompt.encode()).hexdigest()
return self.cache.get(key)
def set(self, prompt: str, response: str):
key = hashlib.sha256(prompt.encode()).hexdigest()
if len(self.cache) >= self.max_size:
self.cache.pop(next(iter(self.cache)))
self.cache[key] = response
Стратегия 2: Семантическое кэширование
Семантическое кэширование более полезно. Оно улавливает промпты, которые отличаются, но означают одно и то же:
from sentence_transformers import SentenceTransformer
class SemanticCache:
def __init__(self, similarity_threshold: float = 0.95):
self.model = SentenceTransformer('all-MiniLM-L6-v2')
self.cache = {}
self.threshold = similarity_threshold
def get(self, prompt: str) -> str | None:
prompt_embedding = self.model.encode([prompt])[0]
for cached_prompt, cached_response in self.cache.items():
cached_embedding = self.model.encode([cached_prompt])[0]
similarity = self.cosine_similarity(
prompt_embedding, cached_embedding
)
if similarity >= self.threshold:
return cached_response
return None
def set(self, prompt: str, response: str):
self.cache[prompt] = response
Порог имеет значение. 0.95 — агрессивный порог, совпадают только очень похожие промпты. 0.85 — более мягкий, но есть риск возврата неверных ответов. Измеряйте частоту промахов (miss rate) и корректируйте порог.
Стоит также кэшировать ответы на распространенные запросы. Если пользователи неоднократно спрашивают «какая погода» или «которое время», кэшируйте шаблон, а не только точный промпт:
class ResponseCache:
def __init__(self):
self.common_queries = {
"what is the weather": "Check weather API",
"what is the time": "Check system time",
"who is the president": "Check current president",
}
def get(self, query: str) -> str | None:
query_lower = query.lower()
for common_query, response in self.common_queries.items():
if common_query in query_lower:
return response
return None
Это не сложно, но эффективно. Распространенные запросы распространены не просто так.
Когда оптимизация помогает
Оптимизация важна, когда вы обрабатываете большие объемы данных, запускаете смешанные рабочие нагрузки или платите за API, стоимость которых накапливается.
Она не имеет значения, когда вы прототипируете, используете одну модель или обрабатываете небольшие объемы. Сложность бюджетирования, резервного переключения и кэширования не оправдана для системы, совершающей 100 запросов в день.
Сначала настройте базовый рабочий процесс. Добавьте оптимизацию, когда придет счет.
Компромиссы
| Стратегия | Стоимость | Качество | Сложность |
|---|---|---|---|
| Без оптимизации | Самая высокая | Постоянное | Самая низкая |
| Бюджетирование токенов | Умеренная | Переменное | Средняя |
| Резервные модели | Низкая-Средняя | Переменное | Средняя |
| Кэширование | Самая низкая | Высокое (при попадании в кэш) | Средняя |
| Гибридная | Оптимизированная | Оптимизированная | Самая высокая |
Продакшн-системы обычно работают по гибридной схеме. Бюджетирование на сессию, резервное переключение на основе качества или задержки, кэширование всего, что возможно. Сложность реальна, но экономия тоже.
Связанные материалы
- Стратегии маршрутизации моделей — маршрутизация на основе возможностей, стоимости и задержки
- Ограничители LLM на практике — валидация входных данных, фильтрация выходных данных, безопасность
- Проектирование многомоделевых систем — архитектура для нескольких моделей
- Архитектура LLM — столп системы: маршрутизация, стоимость, ограничители и оркестрация