CodeGym /课程 /ChatGPT Apps /成本控制与 cost 仪表化

成本控制与 cost 仪表化

ChatGPT Apps
第 19 级 , 课程 0
可用

1. 为什么“能用” ≠ “能回本”

LLM 应用有一个重要特性:除了固定的托管成本,它们往往还会出现与模型调用相关的某些请求的可变执行成本

务必区分两种情形:

  • 当模型运行在ChatGPT 侧(用户在 ChatGPT 中与您的 App 交互,而它调用 mcp-tools)——token 费用由用户通过其 ChatGPT 订阅支付;
  • 当您的后端/MCP 服务器自行调用 OpenAI API 或其他 LLM 服务——这些 token 就由您来支付。

正是在第二种情况下,您会产生典型的 LLM 可变成本,它取决于请求的数量与“重量”(tokens_in/tokens_out)。

典型场景:

  1. 您兴冲冲把 GiftGenius 上线到生产,一切飞快,用户很开心。
  2. 一个月后收到账单:OpenAI + 云 + Stripe 手续费,突然发现,“成功增长”其实意味着“我们为每个礼物花的钱比卖出的收入还多”。

FinOps 方法(FinOps)认为:成本和延迟或错误率一样,都是指标。 要记录、聚合并据此决策,而不是“在 Excel 里拍脑袋”。

本讲的目标是让你能回答诸如:

  • “用户 user42 的这次礼物推荐具体花了多少钱?”
  • “这一周工具 suggest_gifts 花了多少钱,它带来了多少订单?”

而且答案不是凭空猜测,而是来自日志与指标

2. ChatGPT App 的成本结构

先画出成本地图。没有它,其他工作都只是混乱地收集数字。

LLM 成本(可变)

凡是由你的后端发起的模型调用都在此类:

  • 从 MCP 服务器或代理发起的 OpenAI 模型调用: GPT-5.1 / GPT-5-mini / embeddings / rerank / vision / TTS/STT 等等。
  • 附加模型:用于搜索的 reranking、用于推荐的 embedding、图像生成。

务必记住一个细微点:当你通过 Apps SDK 构建界面并仅使用内置的 ChatGPT 模型时,你不为 token 付费——由用户(通过其 ChatGPT 订阅)支付。 但一旦你的 MCP 服务器开始自行调用 OpenAI API(Agents、Responses API、embeddings 等),token 就计在你的账上

基本思路:这类调用的成本与 tokens_intokens_out 成正比,再乘以每个 token 的单价。

MCP 工具的调用本身从开发者的 token 角度看是免费的;只有在其处理器里你决定调用 OpenAI API 或其他 LLM 时才会产生费用。

基础设施

即所有的硬件与周边服务:

  • MCP 服务器: Vercel / AWS / GCP / 裸机。
  • 代理(Agents)(若以独立服务运行)。
  • 数据库: Postgres/MySQL、向量数据库、S3/对象存储。
  • 缓存: Redis/KeyDB。
  • 队列与 worker: 例如用于离线生成、重算 feed 等。

这类成本通常是按月固定(或阶梯式固定),因此一般按云账单的聚合数据来计算,而不是到每个请求的粒度。

支付与外部服务

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 调用你工具的天然入口。在这里我们可以:

  • 捕获调用的开始/结束时刻。
  • 测量 duration_ms(或 latency_ms)。
  • 从 OpenAI 的响应中收集 token(如果我们的模型由 MCP 调用),或至少估算它们。
  • 设置 user_idtenant_idrequest_id/trace_id 以便串联日志。

以 GiftGenius 的 tool_invocation 日志事件示意如下:

{
  "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;
}

即便只能按文本长度粗略估算 token,也比没有强。

代理层(Agents SDK):workflow 步骤

如果你使用 Agents SDK,代理可能会连续调用多个工具。这里要记录步骤上下文:代理要解决什么子任务。

例如,在每次工具调用处为代理 runner 加上 workflow_namestep_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,
  }));
}

在 runner 中这样使用:

// agents/giftAgent.ts
logAgentStep({
  requestId: run.requestId,
  workflow: 'gift_selection',
  step: 'rank_candidates',
  toolName: 'rerank_gifts',
});

Commerce:结账与收入

在 commerce 层我们关注这些事件:

  • checkout_started — 开始购买。
  • checkout_success — 付款成功。
  • checkout_failed — 携带错误代码/类型的失败。

并为它们附带:

  • amountcurrency
  • tool_invocation 同一会话的 request_id

这样我们就能回答:“这笔购买花费了我们 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. 与成本相关的结构化日志(与 M17 的关联)

关键点:不要写任何“自由格式”的文本日志,比如 console.log("Tool suggest_gifts used 123 tokens")。 一切都用 JSON。

在模块 17 我们已经约定用 JSON 记录请求,包含 request_iduser_idtool_name 等基础字段。 现在在此之上加上 cost 字段。

与成本相关的日志里必须具备的字段:

  • timestamplevel
  • eventtool_invocationagent_stepcheckout_success 等)。
  • request_idtrace_id——用于串联同一 workflow 的事件链。
  • user_idtenant_id——之后可按用户/企业聚合。
  • tool_name / service
  • tokens_intokens_outcost_estimate_usd
  • latency_mssuccess/error_code

在示例中我们将成本字段命名为 cost_estimate_usd(以美元计),并在代码与仪表盘中统一使用该名称。

这样结构能让我们:

  • 做聚合:按 tool_nameuser_idworkflow 统计平均 cost_estimate_usd
  • 把“昂贵”的请求与更高的延迟或错误相关联,优先决定优化方向。

如果你已在 M17 做好了基础的 logger.info({...}), 那么加入 cost 字段并不是新框架,只是对象里多了几个属性。

5. 如何在代码中粗略计算 LLM 成本

公式一点也不可怕。我们需要的只是数量级的准确度,而非精确到最后一美分与账单完全吻合。

从 OpenAI 响应中获取 usage

当你的 MCP 服务器调用 OpenAI Response API 时,通常会拿到一个 usage 对象:

{
  "usage": {
    "prompt_tokens": 120,
    "completion_tokens": 350,
    "total_tokens": 470
  }
}

用它来计成本很方便。不同模型的输入/输出每百万 token 价格不同。

一个最简单的 TypeScript 估算函数:

// mcp/cost.ts
type Usage = { prompt_tokens?: number; completion_tokens?: number };

const PRICING = {
  inputPerMillion: 2.5,   // 每百万输入 token 的美元价(示例)
  outputPerMillion: 10.0, // 输出 token 的价格
};

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 的最新定价为准并放入配置中。 关键是该函数在每次工具调用时执行,结果写入日志的 cost_estimate_usd 字段。

如果拿不到 usage

有时你使用第三方 LLM,它不返回 usage,或者你需要在真正调用前做预估。此时可以:

  • 用诸如 tiktoken 或目标模型的等价库来估算 token。
  • 采用历史日志的均值/中位数(工具的 median_tokens_in/median_tokens_out)再乘以单价。

用于估算长度的占位代码:

// mcp/costEstimateFallback.ts
export function roughTokenEstimate(text: string): number {
  // 粗略估计:1 个 token ≈ 4 个拉丁字符
  return Math.ceil(text.length / 4);
}

这不是火箭科学,但可以避免把 200000 token 的 prompt 放进廉价套餐。

6. 关键成本指标

收集到的日志是“原料”。现在看看从中提炼出的哪些聚合最有价值。

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_idworkflow_name 和成功标志。
  • 通过 request_id 关联该 workflow 的所有 tool_invocation, 并汇总它们的 cost_estimate_usd

由此得到“完成一次成功任务的成本”——理解场景毛利/成本的关键。

cost_per_user / cost_per_tenant

在 B2B 场景中经常要回答:“每个用户/每个团队一个月要花我们多少钱?”

计算:

  • tool_invocation 以及其他成本相关事件按 user_idtenant_id 分组。
  • 在周期内(天、月)汇总 cost_estimate_usd

然后与订阅价格对比。如果 cost_per_user 逼近套餐价格, 是时候涨价或优化 usage 了(下一讲会谈定价与“成本 ↔ 质量”实验)。

7. 示例:GiftGenius 的 tool_invocation 格式与仪表盘

现在做计划中的练习:设计日志事件与最小化的工具仪表盘。

GiftGenius 的 tool_invocation 事件格式

之前我们看了 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_giftsrerank_giftsfetch_catalog), 我们在 handler 末尾添加一次 emitToolInvocation 调用(或放在 finally 块中,以确保出错时也有日志)。

最小化的工具仪表盘

用于仪表盘(如 Metabase / Grafana / 任意 BI)的最小表格:

说明
tool_name
工具名(suggest_giftscheckout_create_session 等)
% traffic
所有 tool_invocation 中落在该工具的占比
avg_cost_per_call
单次调用的平均成本(来自 cost_estimate_usd
error_rate
success = false 的事件占比
avg_latency_ms
平均延迟
avg_revenue_per_call
与该工具相关联的平均收入(若有)

可视化通常是:上方表格,下方两张图:

  • 柱状图:X 为 tool_name,Y 为 avg_cost_per_call
  • 散点图:X = avg_cost_per_call,Y = error_rateconversion_to_checkout

这类图能快速定位优化候选:昂贵、缓慢且无转化——先优化它。

把成本与收入关联起来的关键是同时记录 checkout_*request_id。 于是我们可以将发生 checkout_success 的场景里,与该工具关联的收入之和除以该工具的调用次数,得到 avg_revenue_per_call

8. 基础设施成本的核算(不必过度精细)

LLM 成本很好算:每次调用都有 token,甚至能在日志里直接算出成本。 基础设施就没那么直接:你有 Vercel、数据库、Redis 等的月度账单。

起步阶段可以用简单方法:

  1. 取月度基础设施总账单(比如 200$)。
  2. 当月的 workflow 数量workflow_completed)去除,得到近似的 infra_cost_per_task
  3. 或按活跃用户数去除——得到 infra_cost_per_user

然后把这些数与 LLM 成本(我们已按日志细算)相加——得到近似的完整成本(按场景或按用户)。

当应用做大后可以更精细(按服务与工具分摊),但在早期这些已足够,避免“摸黑前进”。

9. 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_outcost_estimate_usdlatencyMs)。
  • 若干 agent_stepworkflow = "gift_selection",含 step_name
  • checkout_startedcheckout_success,包含 amount=7900currency="USD"

通过 request_id 我们把这些串起来,可以得出:

  • 该场景的 LLM 成本:三次工具 cost_estimate_usd 之和,假设为 0.19$。
  • 基础设施分摊(来自聚合)约 0.03$/次 workflow。
  • 合计 0.22$ 的成本。
  • 交易收入——79$,再减去 Stripe 手续费等。

这已经是具体的单元经济学,而不是“感觉 GPT‑4 很贵”。

10. 进行 cost 仪表化时的常见错误

错误 1:只看月账单,没有粒度。
只盯 OpenAI/云的总账单很诱人。但若不与 tool_nameuser_idworkflow 关联,你就不知道钱具体花在何处。最终优化会退化为“盲目降模型”,而不是精准改造昂贵场景。

错误 2:把 cost 数据写成无结构的文本日志。
"Tool suggest_gifts used 123 tokens" 这样的行难以高质量地聚合与筛选。某个时刻你会意识到该迁移到 JSON,而这会很痛苦。一开始就用结构化日志,带上 request_idtool_nametokens_in/tokens_outcost_estimate_usd 等字段。

错误 3:忽视 cost ↔ commerce 事件的关联。
记录 checkout_success 却没有 request_id 与工具调用的关联——等于主动放弃对“哪些场景赚钱、哪些只烧 token”的理解。别偷懒,把 request_id 从小组件一路传到 ACP。

错误 4:试图做“完美账单”而不是务实估算。
有些团队一头扎进“完全复现 OpenAI 计费到最后一个 token”的努力。现实中只需数量级正确:场景成本是 0.02$ 还是 0.021$ 并不重要。重要的是不是 2$。放心用 usage 的近似或甚至粗略启发式。

错误 5:只看成本,忘了质量。
看到漂亮的节省数字后,可能会想处处切到最便宜的模型。这样会把应用“优化”到用户不想用的地步。成本必须与回答质量和转化一起看——这会是本模块下一讲的主题:定价与“成本 ↔ 质量”实验。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION