JavaRush /Курсы /ChatGPT Apps /Контроль затрат и cost-инструментация

Контроль затрат и cost-инструментация

ChatGPT Apps
19 уровень , 0 лекция
Открыта

1. Почему «работает» ≠ «окупается»

У LLM-приложений есть важная особенность: помимо фиксированных затрат на хостинг у них часто появляются переменные затраты на выполнение некоторых запросов, связанные с вызовами моделей.

Важно различать два мира:

  • когда модель работает на стороне ChatGPT (пользователь общается с вашим App в ChatGPT, а он вызывает mcp-tools) — за токены платит пользователь своей подпиской ChatGPT;
  • когда ваш backend/MCP-сервер сам вызывает OpenAI API или другие LLM-сервисы — за эти токены платите уже вы.

Именно во втором случае у вас появляются классические переменные LLM-затраты, которые зависят от количества и «тяжести» (tokens_in/tokens_out) запросов.

Классический сценарий:

  1. Вы радостно выкатываете GiftGenius в прод, всё летает, пользователи счастливы.
  2. Через месяц приходит счёт за 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
tokens_in/out × price_per_token
Инфраструктура 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):

Колонка Описание
tool_name
Имя инструмента (suggest_gifts, checkout_create_session, …)
% трафика
Доля всех tool_invocation, которые пришлись на этот инструмент
avg_cost_per_call
Средняя стоимость одного вызова (из cost_estimate_usd)
error_rate
Процент событий с success = false
avg_latency_ms
Средняя латентность
avg_revenue_per_call
Средняя выручка, ассоциированная с этим инструментом (если есть)

Визуально это обычно выглядит так: сверху таблица, снизу — пара графиков:

  • Бар‑чарт: 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 и т.д.

На старте можно пойти простым путём:

  1. Берёте суммарный счёт за месяц по инфраструктуре (допустим, 200$).
  2. Делите его на количество workflow за месяц (workflow_completed) — получаете приблизительный infra_cost_per_task.
  3. Либо делите на количество активных пользователей — infra_cost_per_user.

Потом эти числа складываются с LLM‑cost (который мы посчитали детально по логам) — получаем приблизительную полную себестоимость сценария или пользователя.

Когда приложение вырастет, можно будет делать тоньше (раскидывать расходы по сервисам и инструментам), но для первых версий этого вполне достаточно, чтобы не идти вслепую.

9. Небольшой пример end‑to‑end для GiftGenius

Соберём всё в мини‑историю.

Пользователь описывает получателя подарка, ChatGPT предлагает включить GiftGenius. Дальше:

  1. Виджет запускает workflow "gift_selection".
  2. Ваш бэкенд решает использовать LLM-агента, чтобы интеллектуальнее подобрать подарки.
  3. Агент делает 3 шага:
  • analyze_recipient (анализ описания с помощью LLM).
  • suggest_gifts (наш MCP‑инструмент).
  • rerank_gifts (доп. модель для улучшения списка).
  1. Пользователь видит карточки подарков, сохраняет несколько идей.
  2. Нажимает «Купить», запускается ACP и checkout_create_session.
  3. Успешный 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 и эксперименты «стоимость ↔ качество».

1
Задача
ChatGPT Apps, 19 уровень, 0 лекция
Недоступна
API-оценка стоимости LLM-вызова (tokens → USD)
API-оценка стоимости LLM-вызова (tokens → USD)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ