1. 为什么“能用” ≠ “能回本”
LLM 应用有一个重要特性:除了固定的托管成本,它们往往还会出现与模型调用相关的某些请求的可变执行成本。
务必区分两种情形:
- 当模型运行在ChatGPT 侧(用户在 ChatGPT 中与您的 App 交互,而它调用 mcp-tools)——token 费用由用户通过其 ChatGPT 订阅支付;
- 当您的后端/MCP 服务器自行调用 OpenAI API 或其他 LLM 服务——这些 token 就由您来支付。
正是在第二种情况下,您会产生典型的 LLM 可变成本,它取决于请求的数量与“重量”(tokens_in/tokens_out)。
典型场景:
- 您兴冲冲把 GiftGenius 上线到生产,一切飞快,用户很开心。
- 一个月后收到账单: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_in 和 tokens_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 | |
| 基础设施 | 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_id、tenant_id、request_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_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,
}));
}
在 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 — 携带错误代码/类型的失败。
并为它们附带:
- amount、currency。
- 与 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_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(以美元计),并在代码与仪表盘中统一使用该名称。
这样结构能让我们:
- 做聚合:按 tool_name、user_id、workflow 统计平均 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_id、workflow_name 和成功标志。
- 通过 request_id 关联该 workflow 的所有 tool_invocation, 并汇总它们的 cost_estimate_usd。
由此得到“完成一次成功任务的成本”——理解场景毛利/成本的关键。
cost_per_user / cost_per_tenant
在 B2B 场景中经常要回答:“每个用户/每个团队一个月要花我们多少钱?”
计算:
- 把 tool_invocation 以及其他成本相关事件按 user_id 或 tenant_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_gifts、rerank_gifts、fetch_catalog), 我们在 handler 末尾添加一次 emitToolInvocation 调用(或放在 finally 块中,以确保出错时也有日志)。
最小化的工具仪表盘
用于仪表盘(如 Metabase / Grafana / 任意 BI)的最小表格:
| 列 | 说明 |
|---|---|
|
工具名(suggest_gifts、checkout_create_session 等) |
|
所有 tool_invocation 中落在该工具的占比 |
|
单次调用的平均成本(来自 cost_estimate_usd) |
|
success = false 的事件占比 |
|
平均延迟 |
|
与该工具相关联的平均收入(若有) |
可视化通常是:上方表格,下方两张图:
- 柱状图:X 为 tool_name,Y 为 avg_cost_per_call。
- 散点图:X = avg_cost_per_call,Y = error_rate 或 conversion_to_checkout。
这类图能快速定位优化候选:昂贵、缓慢且无转化——先优化它。
把成本与收入关联起来的关键是同时记录 checkout_* 与 request_id。 于是我们可以将发生 checkout_success 的场景里,与该工具关联的收入之和除以该工具的调用次数,得到 avg_revenue_per_call。
8. 基础设施成本的核算(不必过度精细)
LLM 成本很好算:每次调用都有 token,甚至能在日志里直接算出成本。 基础设施就没那么直接:你有 Vercel、数据库、Redis 等的月度账单。
起步阶段可以用简单方法:
- 取月度基础设施总账单(比如 200$)。
- 用当月的 workflow 数量(workflow_completed)去除,得到近似的 infra_cost_per_task。
- 或按活跃用户数去除——得到 infra_cost_per_user。
然后把这些数与 LLM 成本(我们已按日志细算)相加——得到近似的完整成本(按场景或按用户)。
当应用做大后可以更精细(按服务与工具分摊),但在早期这些已足够,避免“摸黑前进”。
9. 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 与工具调用的关联——等于主动放弃对“哪些场景赚钱、哪些只烧 token”的理解。别偷懒,把 request_id 从小组件一路传到 ACP。
错误 4:试图做“完美账单”而不是务实估算。
有些团队一头扎进“完全复现 OpenAI 计费到最后一个 token”的努力。现实中只需数量级正确:场景成本是 0.02$ 还是 0.021$ 并不重要。重要的是不是 2$。放心用 usage 的近似或甚至粗略启发式。
错误 5:只看成本,忘了质量。
看到漂亮的节省数字后,可能会想处处切到最便宜的模型。这样会把应用“优化”到用户不想用的地步。成本必须与回答质量和转化一起看——这会是本模块下一讲的主题:定价与“成本 ↔ 质量”实验。
GO TO FULL VERSION