1. Почему «работает» ≠ «окупается»
У LLM-приложений есть важная особенность: помимо фиксированных затрат на хостинг у них часто появляются переменные затраты на выполнение некоторых запросов, связанные с вызовами моделей.
Важно различать два мира:
- когда модель работает на стороне ChatGPT (пользователь общается с вашим App в ChatGPT, а он вызывает mcp-tools) — за токены платит пользователь своей подпиской ChatGPT;
- когда ваш backend/MCP-сервер сам вызывает OpenAI API или другие LLM-сервисы — за эти токены платите уже вы.
Именно во втором случае у вас появляются классические переменные LLM-затраты, которые зависят от количества и «тяжести» (tokens_in/tokens_out) запросов.
Классический сценарий:
- Вы радостно выкатываете GiftGenius в прод, всё летает, пользователи счастливы.
- Через месяц приходит счёт за OpenAI + облако + Stripe комиссии, и внезапно выясняется, что «успешный рост» на самом деле значит «мы платим за каждый подарок больше, чем зарабатываем с продажи».
FinOps‑подход (FinOps) говорит: стоимость — это такая же метрика, как latency или error‑rate. Её надо логировать, агрегировать и по ней принимать решения, а не «угадывать в Excel».
Цель этой лекции — сделать так, чтобы вы могли ответить на вопросы уровня:
- «Сколько стоил вот этот конкретный подбор подарка для пользователя user42?»
- «Сколько денег за эту неделю сжёг инструмент suggest_gifts и сколько он при этом принёс заказов?»
И чтобы ответы брались не из воздуха, а из логов и метрик.
2. Структура затрат ChatGPT App
Начнём с карты расходов. Без неё всё остальное — хаотичный сбор чисел.
LLM‑затраты (переменные)
Это всё, что связано с вызовами моделей с вашего бэкенда:
- Вызовы OpenAI-моделей из MCP-сервера или агентов: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT и т.п.
- Дополнительные модели: reranking для поиска, эмбеддинги для рекомендаций, генерация изображений.
Важно помнить тонкий момент: когда вы строите интерфейс через Apps SDK и используете только встроенную модель ChatGPT, вы не платите за токены — платит пользователь (через свою ChatGPT‑подписку). Но как только ваш MCP‑сервер сам начинает вызывать OpenAI API (Agents, Responses API, embeddings и т.п.), токены идут уже на ваш счёт.
Базовая идея: стоимость таких вызовов пропорциональна tokens_in и tokens_out, умноженным на цену за токен.
Вызов MCP-tool сам по себе бесплатен для разработчика с точки зрения токенов; расходы появляются только там, где в его обработчике вы решаете вызвать OpenAI API или другой LLM.
Инфраструктура
Это всё железо и сервисы вокруг:
- MCP‑серверы: Vercel / AWS / GCP / bare metal.
- Агенты (если крутятся как отдельные сервисы).
- Базы данных: Postgres/MySQL, векторные БД, S3/объектные хранилища.
- Кэши: Redis/KeyDB.
- Очереди и воркеры: например, для фоновой генерации, пересчёта фидов и т.п.
Эти затраты чаще фиксированные по месяцу (или ступенчато‑фиксированные), поэтому их обычно считают по агрегированным данным из расходов на облачные сервисы, а не по каждому запросу.
Платёжные и внешние сервисы
У GiftGenius есть ACP/Stripe, а значит появляются:
- Комиссии за каждый успешный платёж (Stripe порядка нескольких процентов + фиксированная часть).
- Потери на фрод и chargeback’и.
- Стоимость внешних API: e‑mail / SMS / push‑уведомления, дополнительная аналитика и т.п.
На старте это копейки, но с масштабом начинают чувствоваться, поэтому хотя бы на уровне логов и отчётов их полезно выделять.
Небольшая табличка для памяти
| Категория | Примеры | Как считаем грубо |
|---|---|---|
| LLM | GPT‑5.1, GPT‑5‑mini, embeddings, rerank | |
| Инфраструктура | MCP, Agents, DB, Redis, очереди, CDN | Делим счёт провайдера на трафик/период |
| Платежи и сервисы | Stripe, e‑mail API, SMS, аналитика | Кол‑во событий × тариф/комиссию |
Наша цель: привязать эти категории к конкретным событиям в системе (tool‑вызовы, workflow, checkout), а не смотреть только финальные месячные суммы.
3. Где снимать usage‑данные: три слоя
Чтобы считать cost не «раз в месяц», а в реальном времени, нужно встроить инструментацию в код. Мест всего три.
MCP‑сервер: каждый вызов инструмента
MCP‑сервер — естественная точка, через которую ChatGPT вызывает ваши tools. Здесь мы можем:
- Поймать момент начала/конца вызова.
- Замерить duration_ms (или latency_ms).
- Собрать токены из ответа OpenAI (если MCP вызывает наша модель) или хотя бы оценить их.
- Поставить user_id, tenant_id, request_id/trace_id для связи логов.
Схематично лог‑событие tool_invocation для GiftGenius выглядит так:
{
"timestamp": "2025-11-20T12:34:56Z",
"level": "info",
"event": "tool_invocation",
"request_id": "abc123",
"user_id": "user42",
"service": "mcp-giftgenius",
"tool_name": "suggest_gifts",
"tokens_in": 120,
"tokens_out": 350,
"cost_estimate_usd": 0.045,
"latency_ms": 320
}
Теперь то же самое в виде TypeScript‑типа и куска кода.
// types/telemetry.ts
export interface ToolInvocationLog {
event: 'tool_invocation';
requestId: string;
userId?: string;
toolName: string;
tokensIn?: number;
tokensOut?: number;
costEstimateUsd?: number;
latencyMs: number;
}
// mcp/logger.ts
export function logToolInvocation(payload: ToolInvocationLog) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
...payload,
}));
}
А теперь обёртка вокруг обработчика MCP‑инструмента (условно suggest_gifts).
// mcp/tools/suggestGifts.ts
export async function handleSuggestGifts(ctx: Context, input: Input) {
const started = Date.now();
const llmResult = await callGiftModel(input); // тут зовём OpenAI
const duration = Date.now() - started;
const { prompt_tokens, completion_tokens } = llmResult.usage ?? {};
const costEstimate = estimateCost(prompt_tokens, completion_tokens);
logToolInvocation({
event: 'tool_invocation',
requestId: ctx.requestId,
userId: ctx.userId,
toolName: 'suggest_gifts',
tokensIn: prompt_tokens,
tokensOut: completion_tokens,
costEstimateUsd: costEstimate,
latencyMs: duration,
});
return llmResult.output;
}
Даже если токены посчитать «на глаз» через оценку длины текста, это уже лучше, чем ничего.
Уровень агента (Agents SDK): шаги workflow
Если вы используете Agents SDK, агент может сам вызывать несколько tools подряд. Здесь важно логировать контекст шага: какую задачу пытается решить агент.
Например, при каждом tool‑вызове агентного раннера можно добавлять поля workflow_name и step_name: «поиск идей», «фильтрация по бюджету», «подготовка checkout».
Это позволит потом строить отчёты не только по инструментам, но и по шагам сценария: вдруг 80% стоимости уходит на какой‑то бесполезный «дополнительный уточняющий шаг».
Пример небольшого «hook» вокруг агента:
// agents/logStep.ts
export function logAgentStep(data: {
requestId: string;
workflow: string;
step: string;
toolName: string;
}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
event: 'agent_step',
...data,
}));
}
И использовать это из раннера:
// agents/giftAgent.ts
logAgentStep({
requestId: run.requestId,
workflow: 'gift_selection',
step: 'rank_candidates',
toolName: 'rerank_gifts',
});
Commerce: checkout и деньги
В commerce‑слое нас интересуют события:
- checkout_started — начата покупка.
- checkout_success — оплата прошла.
- checkout_failed — ошибка с кодом/типом.
И к ним нужно приклеить:
- amount, currency.
- request_id той же сессии, что и tool_invocation.
Тогда мы сможем ответить на вопрос: «Эта покупка стоила нам N центов LLM‑затрат и принесла M долларов выручки».
Пример простого обработчика событий чекаута:
// api/commerce/logCheckout.ts
export function logCheckoutEvent(e: {
type: 'checkout_started' | 'checkout_success' | 'checkout_failed';
requestId: string;
userId?: string;
amountCents?: number;
currency?: string;
errorCode?: string;
}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level: 'info',
service: 'commerce',
...e,
}));
}
4. Структурированные логи для cost (связь с М17)
Ключевой момент: никаких «свободных» текстовых логов вида console.log("Tool suggest_gifts used 123 tokens"). Всё в JSON.
В модуле 17 мы уже договорились логировать запросы в виде JSON с базовыми полями вроде request_id, user_id, tool_name и т.п. Теперь поверх этого добавим cost‑поля.
Поля, которые обязательно должны быть в логах, связанных с затратами:
- timestamp, level.
- event (tool_invocation, agent_step, checkout_success и т.п.).
- request_id, trace_id — чтобы связать цепочку событий одного workflow.
- user_id, tenant_id — чтобы потом агрегировать по пользователям/компаниям.
- tool_name / service.
- tokens_in, tokens_out, cost_estimate_usd.
- latency_ms, success/error_code.
В примерах будем называть поле стоимости cost_estimate_usd (стоимость в долларах США) и придерживаться этого имени в коде и дашбордах.
Именно такая структура позволяет:
- Строить агрегаты: средний cost_estimate_usd по tool_name, по user_id, по workflow.
- Коррелировать «дорогие» запросы с повышенной латентностью или ошибками и решать, что оптимизировать первым.
Если вы уже в М17 сделали базовый logger.info({...}), добавление cost‑полей — это не новый фреймворк, а пара дополнительных свойств в объекте.
5. Как примерно считать LLM‑cost в коде
Формулы тут совсем не страшные. Нам нужен только примерный порядок величин, а не идеально сходящийся с биллингом до последнего цента.
Берём usage из OpenAI‑ответа
Когда ваш MCP‑сервер вызывает OpenAI Response API, он обычно получает объект usage:
{
"usage": {
"prompt_tokens": 120,
"completion_tokens": 350,
"total_tokens": 470
}
}
По нему удобно считать стоимость. Разные модели имеют разные цены за 1M токенов входа/выхода.
Простейшая функция‑оценщик на TypeScript:
// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };
const PRICING = {
inputPerMillion: 2.5, // доллара за 1M входных токенов, пример
outputPerMillion: 10.0, // и за выходные
};
export function estimateCost(
promptTokens?: number,
completionTokens?: number,
): number {
const inTokens = promptTokens ?? 0;
const outTokens = completionTokens ?? 0;
const inputCost = (inTokens / 1_000_000) * PRICING.inputPerMillion;
const outputCost = (outTokens / 1_000_000) * PRICING.outputPerMillion;
return Number((inputCost + outputCost).toFixed(6)); // округляем немного
}
Цены здесь примерные; реальные вы возьмёте из актуального прайсинга OpenAI и положите в конфиг. Важно, что эта функция вызывается при каждом tool‑вызове, а результат попадает в поле cost_estimate_usd в логе.
Если usage недоступен
Иногда вы используете сторонний LLM, который не присылает usage, или нужен предварительный контроль до реального вызова. Тогда можно:
- Оценивать токены с помощью библиотеки типа tiktoken или её аналога для нужной модели.
- Брать средние значения по историческим логам (median_tokens_in/median_tokens_out для инструмента) и умножать их на цену.
Код‑заглушка для оценки длины:
// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
// Грубая оценка: 1 токен ≈ 4 символа латиницы
return Math.ceil(text.length / 4);
}
Это не rocket science, но позволяет, например, не пускать в дешёвый тариф промпт на 200000 токенов.
6. Ключевые cost‑метрики
Собранные логи — сырьё. Теперь посмотрим, какие агрегаты из них жизненно полезны.
cost_per_tool_call
Что это: средняя стоимость одного вызова конкретного инструмента.
Зачем:
- Видно, какие инструменты особо дорогие.
- Можно искать «дорогие и бесполезные»: высокий avg_cost_per_call и низкая конверсия в успех сценария.
Как считать по логам:
- Берём логи event = "tool_invocation" за период.
- Группируем по tool_name.
- Для каждого считаем avg(cost_estimate_usd) и можно ещё p95 (95‑й перцентиль стоимости).
cost_per_successful_task (или cost_per_workflow)
Task/workflow — это завершённый сценарий уровня пользователя:
- В GiftGenius это может быть «подбор подарка + показ карточек + пользователь сохранил N идей» или «подбор → checkout → успешная покупка».
Что делаем:
- При завершении workflow пишем событие workflow_completed с request_id, workflow_name и флагом успешности.
- Через request_id «подтягиваем» все tool_invocation этого workflow и суммируем их cost_estimate_usd.
Таким образом получаем «сколько стоила одна успешная задача» — ключ к пониманию себестоимости сценария.
cost_per_user / cost_per_tenant
Для B2B‑сценариев часто важен вопрос: «Сколько стоит нам один пользователь/одна команда в месяц?»
Считаем:
- Группируем tool_invocation и другие cost‑события по user_id или tenant_id.
- Суммируем cost_estimate_usd за период (день, месяц).
Потом сравниваем с ценой подписки. Если cost_per_user сильно приближается к цене тарифа, пора либо поднимать цену, либо оптимизировать usage (об этом поговорим в следующей лекции про pricing и эксперименты «стоимость ↔ качество»).
7. Пример: формат tool_invocation и дашборд для GiftGenius
Теперь сделаем то, что было в упражнении плана: спроектируем лог‑событие и минимальный дашборд по tools.
Формат события tool_invocation для GiftGenius
Раньше мы смотрели на минимальный лог для MCP‑инструмента. Теперь давайте спроектируем более подробное событие tool_invocation, которое уже можно использовать в продакшене и дашбордах: идея та же, просто добавились поля для сервисов, ошибок и связки с моделями.
Сначала — тип в TypeScript:
// telemetry/events.ts
export interface ToolInvocationEvent {
timestamp: string;
level: 'info' | 'error';
event: 'tool_invocation';
service: 'mcp-giftgenius';
requestId: string;
traceId?: string;
userId?: string;
tenantId?: string;
toolName: string;
modelId?: string;
tokensIn?: number;
tokensOut?: number;
costEstimateUsd?: number;
latencyMs: number;
success: boolean;
errorCode?: string;
}
И удобный helper:
// telemetry/emitToolInvocation.ts
export function emitToolInvocation(e: ToolInvocationEvent) {
console.log(JSON.stringify(e));
// В реальной жизни: отправим в Logtail/Datadog/ELK и т.п.
}
Каждому инструменту (например, suggest_gifts, rerank_gifts, fetch_catalog) мы добавляем вызов emitToolInvocation в конце handler’а (или в finally-блоке, чтобы лог был даже при ошибке).
Простейший дашборд по инструментам
Минимальная таблица для дашборда (например, в Metabase / Grafana / любой BI):
| Колонка | Описание |
|---|---|
|
Имя инструмента (suggest_gifts, checkout_create_session, …) |
|
Доля всех tool_invocation, которые пришлись на этот инструмент |
|
Средняя стоимость одного вызова (из cost_estimate_usd) |
|
Процент событий с success = false |
|
Средняя латентность |
|
Средняя выручка, ассоциированная с этим инструментом (если есть) |
Визуально это обычно выглядит так: сверху таблица, снизу — пара графиков:
- Бар‑чарт: tool_name по оси X, avg_cost_per_call по оси Y.
- Scatter‑plot: X = avg_cost_per_call, Y = error_rate или conversion_to_checkout.
Такие графики помогают быстро найти кандидатов на оптимизацию: дорого, медленно и не даёт конверсии — сначала туда.
Связать cost с revenue помогает то, что мы логируем checkout_* вместе с request_id. Тогда мы можем посчитать avg_revenue_per_call как сумму выручки, поделённую на количество вызовов инструмента в сценариях, где произошёл checkout_success.
8. Учёт инфраструктурных затрат (без фанатизма)
С LLM‑затратами всё красиво: у каждого вызова есть токены, можно прямо в логе считать стоимость. Инфраструктура так просто не даётся: у вас есть месячный счёт за Vercel, базы, Redis и т.д.
На старте можно пойти простым путём:
- Берёте суммарный счёт за месяц по инфраструктуре (допустим, 200$).
- Делите его на количество workflow за месяц (workflow_completed) — получаете приблизительный infra_cost_per_task.
- Либо делите на количество активных пользователей — infra_cost_per_user.
Потом эти числа складываются с LLM‑cost (который мы посчитали детально по логам) — получаем приблизительную полную себестоимость сценария или пользователя.
Когда приложение вырастет, можно будет делать тоньше (раскидывать расходы по сервисам и инструментам), но для первых версий этого вполне достаточно, чтобы не идти вслепую.
9. Небольшой пример end‑to‑end для GiftGenius
Соберём всё в мини‑историю.
Пользователь описывает получателя подарка, ChatGPT предлагает включить GiftGenius. Дальше:
- Виджет запускает workflow "gift_selection".
- Ваш бэкенд решает использовать LLM-агента, чтобы интеллектуальнее подобрать подарки.
- Агент делает 3 шага:
- analyze_recipient (анализ описания с помощью LLM).
- suggest_gifts (наш MCP‑инструмент).
- rerank_gifts (доп. модель для улучшения списка).
- Пользователь видит карточки подарков, сохраняет несколько идей.
- Нажимает «Купить», запускается ACP и checkout_create_session.
- Успешный checkout_success с суммой 79.00 USD.
Что у нас остаётся в логах:
- Три tool_invocation (каждый со своими tokens_in/tokens_out, cost_estimate_usd, latencyMs).
- Несколько agent_step с workflow = "gift_selection", step_name.
- checkout_started и checkout_success с amount=7900, currency="USD".
По request_id мы связываем всё это и можем сказать:
- LLM‑стоимость сценария: сумма cost_estimate_usd трёх инструментов, допустим 0.19$.
- Доля инфраструктуры (из агрегатов) примерно 0.03$ на один workflow.
- Итого 0.22$ себестоимости.
- Выручка по транзакции — 79$ минус комиссия Stripe и прочее.
Это уже конкретная юнит‑экономика, а не «кажется, GPT‑4 это дорого».
10. Типичные ошибки при работе с cost‑инструментацией
Ошибка №1: считать только месячный счёт и не иметь гранулярности.
Очень соблазнительно смотреть только на общий счёт от OpenAI/облака. Но без связи с tool_name, user_id, workflow вы не знаете, где именно тратятся деньги. В итоге оптимизация превращается в «слепое понижение модели» вместо точечного улучшения дорогих сценариев.
Ошибка №2: писать cost‑данные в текстовые логи без структуры.
Линии вида "Tool suggest_gifts used 123 tokens" невозможно качественно агрегировать и фильтровать. В какой‑то момент вы поймёте, что надо мигрировать на JSON, и этот переезд будет болезненным. Сразу делайте структурированные логи с полями request_id, tool_name, tokens_in/tokens_out, cost_estimate_usd.
Ошибка №3: игнорировать связь cost ↔ commerce‑события.
Логировать checkout_success без request_id и связи с tool‑вызовами — значит добровольно отказаться от понимания, какие сценарии приносят прибыль, а какие только жрут токены. Не ленитесь протащить request_id через весь путь от виджета до ACP.
Ошибка №4: пытаться сделать «идеальный» биллинг вместо практичной оценки.
Некоторые команды закапываются в попытках идеально воспроизвести биллинг OpenAI до последнего токена. В реальности достаточно порядка величин: если сценарий стоит 0.02$ или 0.021$, это не принципиально. Важно, что не 2$. Не бойтесь использовать приблизительные оценки через usage или даже грубые эвристики.
Ошибка №5: смотреть только на cost и забывать про качество.
Иногда, увидев красивые цифры экономии, хочется везде переключиться на самую дешёвую модель. Так можно «оптимизировать» приложение до состояния, когда пользователи перестанут им пользоваться. Стоимость должна рассматриваться вместе с качеством ответов и конверсией — эта связка будет темой следующей лекции этого же модуля — про pricing и эксперименты «стоимость ↔ качество».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ