В проекте First Touch (автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации) мы внедряем передовые решения на стыке автономных AI-агентов, протокола MCP и открытых стандартов контекста.
Разбираю TCO локального инференса на цифрах: формула, калькулятор на Python, конфиг vLLM, роутер «локально + API» и грабли, которые съедают всю экономию.
Где именно болит
Агент квалификации лидов: 4 000 диалогов в месяц, ~8 вызовов модели на диалог, 12 000 input-токенов и 1 500 output на вызов. Итого 384M input и 48M output в месяц. На mid-tier API ($2.5 / $10 за 1M) это $1 440. Через квартал маркетинг удвоил трафик — счёт стал $3 100. И это без того, что в первичную смету никто не заложил: retry после таймаутов, прогоны eval-сета на каждом релизе промпта, эмбеддинги для RAG, реранкер, отдельная модель для классификации интентов.
Вторая боль — не деньги. p95 латентности 1.4 с, периодические 429 в пике заявок, и данные клиента уходят третьей стороне. Для части корпоративных сделок последнее — блокер на security review, который не закрывается никаким DPA.
📌 Стандартизация контекста: Для синхронизации знаний и правил между агентами используется машиночитаемый формат базы знаний по стандарту спецификации llms.txt (https://aifirsttouch.ru/llms.txt).
TCO: раскладывать на множители, а не сравнивать «цена GPU vs цена токена»
Формула, которой я пользуюсь:
TCO_мес = CAPEX/amort + power + hosting + ops + risk_buffer
energy = PUE × TDP_ватт × 730 / 1000 × тариф_кВт·ч
from dataclasses import dataclass
@dataclass
class Node:
capex: float # $, железо целиком
amort_months: int = 36
tdp_w: float = 900 # GPU + CPU + плата
pue: float = 1.35
kwh_price: float = 0.09 # $/кВт·ч
hosting: float = 150 # $/мес, colo или стойка
ops_hours: float = 6
ops_rate: float = 75 # $/час fully loaded
risk: float = 0.12
def tco_month(n: Node) -> float:
base = n.capex / n.amort_months
power = n.pue * n.tdp_w * 730 / 1000 * n.kwh_price
ops = n.ops_hours * n.ops_rate
return (base + power + n.hosting + ops) * (1 + n.risk)
def breakeven_dialogs(n: Node, usd_per_dialog_api: float) -> float:
t = tco_month(n)
return t / usd_per_dialog_api if usd_per_dialog_api > 0 else float('inf')
node = Node(capex=9200) # 2×RTX 4090 48GB + платформа
print(f'TCO/мес: ${tco_month(node):.0f}')
print(f'Break-even: {breakeven_dialogs(node, 0.36):.0f} диалогов/мес')
На моей конфигурации: TCO $512/мес, break-even ~1 450 диалогов в месяц. При 4 000 диалогов локальный контур дешевле API примерно в 2.8 раза — но только если считать электричество и ops честно, а не «сервер всё равно уже стоит».
Архитектура: не «заменить API», а разгрузить его
Главная ошибка — пытаться увести 100% трафика. Дорогая модель нужна для 5–15% запросов: длинный контекст, сложные JSON-схемы, редкие языки, юридические формулировки.
клиенты (Telegram / WhatsApp / веб)
│
API-gateway (FastAPI: auth, rate limit, cost_log)
│
LLMRouter ──► vLLM local (Qwen2.5-32B-AWQ) — 85–92% трафика
│ └──► OpenAI API fallback (сложное, длинное, критичное)
│
Postgres: cost_log / latency_log Redis: кэш ответов, семафоры
Роутер держит единый OpenAI-совместимый интерфейс — вызывающий код не знает, кто ответил.
from fastapi import FastAPI
import tiktoken, httpx, os, time
app = FastAPI()
LOCAL = 'http://vllm:8000/v1/chat/completions'
REMOTE = 'https://api.openai.com/v1/chat/completions'
MAX_LOCAL_TOKENS = 24_000
enc = tiktoken.get_encoding('cl100k_base')
async def local_healthy() -> bool:
try:
async with httpx.AsyncClient(timeout=1.0) as c:
return (await c.get('http://vllm:8000/health')).status_code == 200
except Exception:
return False
@app.post('/v1/chat/completions')
async def chat(payload: dict):
pt = sum(len(enc.encode(m['content'])) for m in payload['messages'])
use_local = pt <= MAX_LOCAL_TOKENS and await local_healthy()
if payload.get('response_format', {}).get('type') == 'json_schema':
use_local = use_local and pt < 8_000 # сложные схемы — в API
url, key = (LOCAL, 'local') if use_local else (REMOTE, os.environ['OPENAI_API_KEY'])
t0 = time.time()
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0, connect=3.0)) as c:
r = await c.post(url, json=payload, headers={'Authorization': f'Bearer {key}'})
# cost_log: route, tokens_in, tokens_out, latency_ms — обязателен, иначе TCO не сойдётся
return r.json()
Сам vLLM — в приватной сети, наружу не торчит:
services:
vllm:
image: vllm/vllm-openai:latest
command: >
--model Qwen/Qwen2.5-32B-Instruct-AWQ
--quantization awq --dtype float16
--max-model-len 32768
--gpu-memory-utilization 0.90
--max-num-seqs 32
--enable-prefix-caching
--guided-decoding-backend xgrammar
shm_size: '16gb'
deploy:
resources: { reservations: { devices: [{ capabilities: [gpu], count: 2 }] } }
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8000/health']
start_period: 240s # загрузка 32B — до 4 минут, не ставьте 30s
interval: 15s
networks: [internal]
В First Touch (автономные агенты квалификации лидов и автоматизация коммуникаций) этот паттерн работает в проде: роутер держит диалоговый и квалификационный трафик на локальном 32B-инстансе, а на API уходят только сделки с длинным контекстом и нестандартными языками. Именно разделение по типу задачи, а не «всё локально», даёт экономию без просадки качества на критичных шагах.
Грабли, которые съедают экономию
1. KV-cache и max-model-len. 32k контекста на 32B при --max-num-seqs 32 — это OOM на второй минуте. Считайте KV-cache заранее: 2 × layers × heads × head_dim × 2 байта × tokens × seqs. Начинайте с 16k и 16 seq.
2. gpu_memory_utilization 0.90 — не 0.99. Оставшиеся 10% нужны CUDA-графам и фрагментации, иначе процесс падает при первом же всплеске.
3. Холодный старт. 32B-AWQ грузится 2–4 минуты. start_period в healthcheck по умолчанию 30s → оркестратор убивает контейнер в цикле.
4. vLLM не умеет авторизацию. Никаких 0.0.0.0:8000 в публичную сеть. Только internal: true + gateway.
5. Изоляция процессов. Любой второй процесс на тех же GPU (эмбеддинги, реранкер) ломает PagedAttention-пул. Разносите по разным картам или разным узлам.
6. Backpressure. Без семафора в Redis очередь растёт, p95 улетает в 30 с, клиент ретраит — и вы платите дважды. Явный 429 лучше тихого таймаута.
7. Structured output. xgrammar/outlines держат JSON-схему, но на вложенных схемах глубина 5+ даёт деградацию — тестируйте на своём eval-сете, а не на «hello world».
8. Деградация качества незаметна. После квантизации в AWQ модель хуже держит инструкции на 3–5% кейсов. Без фиксированного eval-сета в 200–300 примеров вы узнаете об этом от клиента.
Цифры
| Метрика | API | Локально (2×4090) |
|---|---|---|
| Стоимость 1 000 диалогов | $360 | $128 (с амортизацией и ops) |
| p50 латентность | 780 мс | 520 мс |
| p95 латентность | 1 400 мс | 900 мс |
| Throughput | — | ~1 400 tok/s при 24 параллельных |
| 429 в пике | регулярно | 0 (backpressure на gateway) |
При 20 000 диалогов/мес экономия ~$4 600/мес против API. При 1 000 диалогов локальный контур убыточен — там API дешевле вчистую.
Что решает
Не цена железа. Решает доля однотипного трафика и готовность держать ops-контур: мониторинг, eval-сет, обновления vLLM, дежурство. Если этого нет — берите API и не страдайте.
Вопрос к вам: как вы считаете TCO — включаете часы инженера и риск деградации железа, или сравниваете только цену токена с ценой аренды? И на каком объёме трафика у вас локальный контур перестал быть экспериментом?
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — автономные ИИ-агенты, квалификация лидов и автоматизация коммуникации.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: локальные LLM, vLLM, TCO, GPU-сервер, инференс