刚开始调用大模型API时,很多人只会在后台充值,然后偶尔打开账单页面看一下。
测试阶段问题不大,但项目一旦开始自动运行,费用可能因为以下情况迅速增加:
定时任务重复执行;
程序异常后无限重试;
对话历史越来越长;
单次输出没有限制;
API Key被提交到公开仓库;
公共接口没有用户级额度;
Agent反复调用搜索、文件和代码工具。
真正实用的成本控制,不是只设置一个“月度预算”,而是建立四层保护:
平台支出提醒和硬限额↓请求前预估Token和费用↓请求后记录真实Token↓项目、用户和单次请求限额
本文以OpenAI API为主要示例,同时说明Claude和Gemini应该怎么处理。
一、只设置平台预算提醒够不够?
不够。
平台提醒适合发现整体费用变化,但它通常不能替代应用自身的成本控制。
以OpenAI为例,目前可以在组织或项目层级设置:
支出提醒;
月度支出限额;
强制执行的硬限额。
达到硬限额后,相关请求会返回429错误,但官方也明确说明,限额状态的同步并非瞬时完成,最终费用仍有可能略微超过设定金额。
因此,比较稳妥的方案是:
不要只依赖其中一层!
二、主流API平台,目前有哪些成本控制能力?
1. OpenAI API
OpenAI支持组织和项目级支出提醒,也可以启用硬限额。达到硬限额后,请求会因为organization_spend_limit_exceeded或project_spend_limit_exceeded而失败。
OpenAI还提供输入Token统计接口,可以在正式调用模型前,使用与Responses API相同的请求参数计算输入Token。相比“字符数除以4”之类的估算,它能处理消息结构、工具定义、图片和文件等内容。
请求完成后,可以从响应的usage字段读取:
input_tokens;output_tokens;total_tokens;缓存Token;
推理Token等明细。
需要注意,部分推理模型的输出Token不仅包括最终显示的文字,也可能包括推理Token和不可见的格式Token。因此,不能只根据页面上看到的回答长度估算费用。
2. Claude API
Claude Console可以按照模型、日期和API Key查看输入与输出Token,并支持导出CSV费用数据。Claude还支持组织级月度支出上限,以及低于平台上限的自定义支出限制。
Claude API目前主要采用预付额度计费。额度耗尽后,API将无法继续调用;如果开启自动充值,需要同时设置最低余额和自动充值金额,避免自动充值本身成为新的超支入口。
Claude也提供Token Counting接口,可在发送请求前计算输入Token。
3. Gemini API
Gemini API提供countTokens方法用于请求前统计输入Token。请求完成后,可以从usage_metadata读取输入、输出、思考、缓存和总Token数量。
如果Gemini项目关联了Google Cloud结算账号,还可以通过Cloud Billing设置预算和提醒,并将预算通知发送到Pub/Sub,再交给Cloud Run函数、Slack或其他通知系统处理。
不过,Google Cloud的预算数据属于估算值,第一条程序化通知也可能需要等待数小时,所以它更适合作为账单层预警,不适合作为应用请求前的实时拦截。
三、个人开发者最实用的四层成本保护
1. 第一层:平台设置50%、80%和95%提醒
假设每月能接受的API费用是20美元,可以设置:
不要只设置100%提醒!!!等收到100%提醒的时候,预算已经用完了,正在运行的任务也可能继续产生费用。
对于测试项目,可以直接把平台硬限额设置为预算金额。对于已经有真实用户的项目,应提前处理好限额触发后的降级逻辑,例如:
暂停长文本生成;
切换到成本更低的模型;
停止后台批处理;
保留少量额度给核心功能;
向用户返回“今日额度已用完”。
2. 第二层:每次请求都限制最大输出Token
生成式API的费用通常由输入和输出共同决定。
如果不设置最大输出长度,一次错误提示、Agent循环或异常生成,就可能消耗远超预期的输出Token。
OpenAI Responses API可以使用:
max_output_tokens=800不要习惯性地把它设置成模型允许的最大值。
对于常见任务,可以参考:
最大输出Token应该按照业务需要设置,而不是越大越好。
3. 第三层:请求前先预估最坏成本
请求发出前,可以先计算输入Token,再加上允许的最大输出Token,得到一次调用的最坏成本。
基本公式为:
预计最高费用=输入Token ÷ 1,000,000 × 输入单价+最大输出Token ÷ 1,000,000 × 输出单价
假设:
当前月已经使用19.6美元;
月度预算是20美元;
本次请求最坏可能消耗0.6美元。
这次请求就不应该继续发送。
这种判断比“调用完成后再记账”更重要,因为请求完成后,费用已经产生。
4. 第四层:每个项目和用户分别计算费用
不要让所有项目共用一个API Key,也不要只记录账号总费用。
至少应该记录以下字段:
项目用户IDAPI Key或服务账号模型输入Token输出Token单次费用请求时间请求是否成功
推荐的拆分方式:
开发环境单独一个项目;
测试环境单独一个项目;
生产环境单独一个项目;
定时任务使用单独的API Key;
公共演示页面使用低预算项目;
不同客户使用不同的内部用户ID。
这样出现异常时,才能快速判断费用来自哪里。
四、OpenAI后台应该怎么设置?
以个人项目为例,可以按照下面的顺序操作。
第一步:为每个应用创建独立项目
不要把所有应用都放在默认项目里,例如:
personal-chatbotblog-generatordocument-summarytest-scripts
每个项目使用自己的API Key,费用才能按项目拆分。
第二步:设置月度支出限额
进入对应项目的:
Project Settings→ Limits→ Spend→ Edit spend limit
填写月度支出限额。
测试项目建议启用硬限额。生产项目是否启用,需要结合业务能否接受服务中断来决定。OpenAI的硬限额不是绝对实时执行,仍需要代码侧保护。
第三步:增加多个提醒阈值
至少设置:
80%100%
不要等月底才手动查看用量。
第四步:定期导出费用数据
OpenAI用量后台可以按项目、模型、API Key等维度查看数据,也可以导出用量或成本CSV。平台时间范围以UTC为准,对账时要注意时区。
五、用Python实现本地成本预警
下面这个示例完成了几件事:
请求前计算输入Token;
根据最大输出Token预估最高费用;
超过月度预算时停止请求;
请求完成后记录Token和估算费用;
达到50%、80%和95%时发送提醒;
使用SQLite保存历史数据。
1. 安装依赖
pip install openai requests2. 设置环境变量
export OPENAI_API_KEY="你的API Key"export OPENAI_MODEL="gpt-5.6"export MONTHLY_BUDGET_USD="20"export INPUT_PRICE_PER_1M="填写当前模型输入价格"export OUTPUT_PRICE_PER_1M="填写当前模型输出价格"export MAX_OUTPUT_TOKENS="800"export ALERT_WEBHOOK_URL=""
模型价格不要永久写死在代码中。
API价格、缓存价格和工具调用费用都可能调整,建议从平台当前定价页面确认后,再更新环境变量。
3. 完整代码
from __future__ import annotationsimport osimport sqlite3from datetime import datetime, timezonefrom decimal import Decimalfrom typing import Anyimport requestsfrom openai import OpenAIDB_PATH = os.getenv("COST_DB_PATH", "api_cost.db")MODEL = os.getenv("OPENAI_MODEL", "gpt-5.6")MONTHLY_BUDGET = Decimal(os.getenv("MONTHLY_BUDGET_USD", "20"))INPUT_PRICE = Decimal(os.environ["INPUT_PRICE_PER_1M"])OUTPUT_PRICE = Decimal(os.environ["OUTPUT_PRICE_PER_1M"])MAX_OUTPUT_TOKENS = int(os.getenv("MAX_OUTPUT_TOKENS", "800"))ALERT_LEVELS = (Decimal("0.50"),Decimal("0.80"),Decimal("0.95"),)ALERT_WEBHOOK_URL = os.getenv("ALERT_WEBHOOK_URL", "")client = OpenAI()def init_db() -> None:with sqlite3.connect(DB_PATH) as conn:conn.execute("""CREATE TABLE IF NOT EXISTS api_usage (id INTEGER PRIMARY KEY AUTOINCREMENT,created_at TEXT NOT NULL,model TEXT NOT NULL,input_tokens INTEGER NOT NULL,output_tokens INTEGER NOT NULL,estimated_cost_usd TEXT NOT NULL)""")def current_month() -> str:return datetime.now(timezone.utc).strftime("%Y-%m")def get_monthly_spend() -> Decimal:month = current_month()with sqlite3.connect(DB_PATH) as conn:rows = conn.execute("""SELECT estimated_cost_usdFROM api_usageWHERE substr(created_at, 1, 7) = ?""",(month,),).fetchall()return sum((Decimal(row[0]) for row in rows),start=Decimal("0"),)def estimate_cost(input_tokens: int,output_tokens: int,) -> Decimal:cost = (Decimal(input_tokens) * INPUT_PRICE+ Decimal(output_tokens) * OUTPUT_PRICE) / Decimal("1000000")return cost.quantize(Decimal("0.000001"))def save_usage(input_tokens: int,output_tokens: int,cost: Decimal,) -> None:created_at = datetime.now(timezone.utc).isoformat()with sqlite3.connect(DB_PATH) as conn:conn.execute("""INSERT INTO api_usage (created_at,model,input_tokens,output_tokens,estimated_cost_usd)VALUES (?, ?, ?, ?, ?)""",(created_at,MODEL,input_tokens,output_tokens,str(cost),),)def send_alert(message: str) -> None:print(f"[API COST ALERT] {message}")if not ALERT_WEBHOOK_URL:returntry:# 不同通知平台的JSON格式可能不同,# 飞书、企业微信、Slack需要按各自格式修改。requests.post(ALERT_WEBHOOK_URL,json={"text": message},timeout=5,).raise_for_status()except requests.RequestException as exc:print(f"Webhook发送失败:{exc}")def check_alerts(old_spend: Decimal,new_spend: Decimal,) -> None:for level in ALERT_LEVELS:threshold = MONTHLY_BUDGET * levelif old_spend < threshold <= new_spend:percent = int(level * 100)send_alert(f"本月API费用已达到预算的{percent}%:"f"{MONTHLY_BUDGET}")def get_value(obj: Any,name: str,default: int = 0,) -> int:if obj is None:return defaultif isinstance(obj, dict):return int(obj.get(name, default) or default)return int(getattr(obj, name, default) or default)def guarded_generate(prompt: str) -> str:old_spend = get_monthly_spend()# 请求前准确计算输入Tokentoken_result = client.responses.input_tokens.count(model=MODEL,input=prompt,)input_tokens = token_result.input_tokens# 按最大输出Token计算本次请求的最坏费用maximum_request_cost = estimate_cost(input_tokens=input_tokens,output_tokens=MAX_OUTPUT_TOKENS,)if old_spend + maximum_request_cost > MONTHLY_BUDGET:raise RuntimeError("本次请求可能超过月度API预算,已停止调用。"f"当前费用:{maximum_request_cost},"f"预算:{get_monthly_spend()}")except Exception as exc:print(f"调用失败:{exc}")
六、这段代码解决了什么,没解决什么?
这段代码适合普通文本API调用,能够防止:
某次请求超过剩余预算;
输出长度没有控制;
本地程序完全不知道已花多少钱;
达到预算后仍继续调用。
但它不是平台账单的替代品,以下的费用可能需要额外处理:
缓存写入和缓存读取;
Web Search;
File Search;
图片生成;
音频输入和输出;
代码执行;
向量存储;
Batch任务;
第三方网关费用;
税费或其他计费项目。
因此,建议把本地记录作为实时预警数据,把服务商控制台中的成本数据作为最终对账数据。
七、Claude和Gemini怎么接入同一套逻辑?
不需要重写整个系统。
只要把不同平台返回的Token字段,转换成统一格式即可:
usage.input_tokens | usage.output_tokens | |
message.usage.input_tokens | message.usage.output_tokens | |
usage_metadata.prompt_token_count | usage_metadata.candidates_token_count |
如果使用了推理、缓存或工具,还要读取对应的详细字段。
建议应用内部统一保存成:
{"provider": "openai","model": "模型名称","input_tokens": 1000,"output_tokens": 300,"cost_usd": "0.004500","user_id": "user_123","project": "blog-generator"}
以后切换模型或同时使用多个平台时,统计逻辑不需要跟着重写。
八、还要设置每日和单用户限额
月度限额只能防止整个月花太多,不能防止某一天突然消耗全部预算。
个人项目建议同时设置:
月度预算:20美元每日预算:2美元单用户每日预算:0.20美元单次请求最高费用:0.05美元
数值要根据业务调整,重点是同时限制不同时间范围。
例如,一个公共AI工具即使月度预算还有很多,也不应该允许单个用户在几分钟内连续发起数百次长文本请求。
可以按照以下顺序检查:
单次请求是否超限↓用户今日是否超限↓项目今日是否超限↓项目本月是否超限↓发送API请求
九、最容易造成Token费用失控的几个问题
1. 重试代码没有最大次数
错误写法:
while True:try:call_api()breakexcept Exception:continue
如果API持续报错,程序可能无限重试,至少应该限制重试次数,并使用指数退避:
import timefor attempt in range(3):try:call_api()breakexcept Exception:if attempt == 2:raisetime.sleep(2 ** attempt)
2. 每次都发送完整对话历史
对话越长,每次请求重新提交的输入Token越多。
可以采用:
只保留最近几轮;
对历史对话做摘要;
删除无关工具返回内容;
使用缓存;
超过长度后开启新会话。
3. API Key放在前端代码中
API Key不应直接放在网页、APP或公开JavaScript中。
正确做法是:
浏览器或APP↓自己的后端服务↓验证用户和额度↓调用模型API
只有后端持有真正的API Key。
4. 所有任务都使用高成本模型
分类、关键词提取、格式整理等简单任务,通常没有必要全部使用最高规格模型。
可以先判断任务复杂度,再选择模型:
简单分类 → 低成本模型普通问答 → 常规模型复杂推理 → 高能力模型
5. Agent没有设置最大步骤数
Agent可能反复搜索、调用工具和自我修正。
建议限制:
最大循环次数;
最大工具调用次数;
单任务最大Token;
单任务最大费用;
总执行时间。
十、什么时候需要使用第三方监控工具?
如果只有一个小项目,本地SQLite加平台后台已经够用。
如果出现以下情况,可以考虑接入LLM可观测平台:
同时使用OpenAI、Claude和Gemini;
需要按用户统计成本;
需要查看完整调用链;
使用LangChain、LlamaIndex或Agent框架;
需要按照环境、功能和项目拆分费用;
希望在网关层统一设置预算。
目前常见方案包括:
Langfuse支持采集或推算不同模型的Token与费用;LiteLLM可以跨多个模型服务商跟踪Key、用户和团队支出;Helicone支持成本阈值告警。
个人项目没有必要为了“看起来专业”而一开始就部署整套观测系统。先把请求限额、Token记录和异常提醒做好,通常更重要。
十一、用虚拟卡能不能控制API费用?
虚拟卡可以作为支付层的补充控制预警,但不能代替API成本预警。
在目标平台支持相应卡片类型的前提下,可以按照项目或用途分配不同卡片,并设置较低余额或额度。例如:
OpenAI及模型API开发工具订阅测试环境正式项目其他SaaS软件
这样更方便区分支出,也能避免所有服务共用同一张付款卡。
对于同时订阅多个海外AI工具或SaaS服务的个人开发者,可以了解MXK8虚拟卡的项目分卡和线上订阅场景。
不过需要注意:
卡片限额不能代替平台预算;
余额不足可能直接导致线上服务中断;
开卡前应先确认平台、币种和自动扣款要求。
比较合理的顺序是:
代码侧预算拦截↓API平台硬限额↓付款卡额度控制
而不是等银行卡拒绝付款后,才发现项目已经产生了大量费用。
十二、汇总后的提醒!
个人开发者控制API费用,至少要做到下面五件事:
每个项目使用独立API Key;
设置50%、80%和95%支出提醒;
为测试项目设置平台硬限额;
请求前统计Token并判断剩余预算;
请求后记录用户、模型、Token和单次费用。
真正危险的通常不是一次正常调用,而是程序出现死循环、无限重试、Key泄露,或者公共接口没有用户限额。
平台后台适合看整体账单,本地代码适合实时拦截,付款卡额度则适合作为最后一道保护。
三层一起使用,才能让API成本真正可控。