☰
Claude API调价后如何精准计算Token成本?Python用量分析脚本实战
2026/10/6 23:11:55 网站建设 项目流程

1. 这次调价到底动了谁的蛋糕

Claude API 的价格调整在开发者圈子里传得挺快,但很多人第一反应是“降了多少”,第二反应就卡住了——我自己的业务到底能省多少?这个问题不搞清楚,调价就只是一个新闻标题,跟你的账单没有半毛钱关系。我见过太多团队,模型换了、价格降了,月底一看账单反而涨了,原因就是从来没认真算过自己的 Token 消耗结构。

这篇文章要解决的就是这件事:把 Claude API 的 Token 计费逻辑拆开,用 Python 写一套能直接跑的用量分析脚本,让你清楚地知道每一分钱花在了哪里、调价之后能省下多少、以及怎么在不牺牲效果的前提下把成本压下去。适合正在用或者准备用 Claude API 做产品的开发者、做 AI 应用的成本敏感型团队,以及想搞清楚大模型计费门道的技术爱好者。脚本部分我会给完整代码,复制粘贴就能用,不需要你有很深的 Python 功底,能装环境、会改配置就行。

先说一个基本事实:大模型的计费从来不是“按次收费”这么简单。输入和输出是两个价格,缓存命中和未命中又是两个价格,不同模型版本之间的差价可能有好几倍。你如果只盯着“每次调用多少钱”这个粗粒度指标,永远算不清账。所以下面我会从计费模型讲起,再落到脚本实现,最后给一套成本优化的实操方法。

2. Claude API 的 Token 计费模型拆解

2.1 输入、输出、缓存三种 Token 的价格差异

Claude API 的计费核心是三个维度:输入 Token、输出 Token、缓存 Token。这三个的价格差距非常大,理解它们是算账的前提。

输入 Token 就是你发给模型的全部内容,包括系统提示词、对话历史、用户当前问题、以及你塞进去的上下文文档。输出 Token 是模型生成的内容。一般来说,输出 Token 的单价是输入的几倍,因为生成过程需要更多的计算资源。这个比例在不同模型上不太一样,但“输出比输入贵”是普遍规律。

缓存 Token 是很多人忽略的一块。Claude 支持Prompt Caching功能,如果你有一段很长的系统提示词或者固定的上下文,可以在第一次请求时把它标记为可缓存,后续请求命中缓存的部分价格会大幅降低。缓存写入的价格通常比普通输入略高,但缓存读取的价格会低很多。对于系统提示词很长、调用频率很高的场景,这个差距能直接决定你的成本结构。

我拿一个实际场景举例。假设你的应用每次请求都带一段 2000 Token 的系统提示词,一天调用 10000 次。如果不做缓存,这 2000 Token 每次都按输入价格计费,一天就是 2000 万 Token 的输入消耗。如果做了缓存,第一次写入之后,后续 9999 次都按缓存读取价格算,成本可能直接砍掉一大半。这个账不算不知道,一算吓一跳。

2.2 不同模型版本的定价梯度

Claude 系列有多个模型版本,从轻量到旗舰,价格梯度很明显。轻量模型适合做分类、提取、简单问答这类任务,旗舰模型适合复杂推理、长文生成、代码编写。很多团队的问题是一上来就用最贵的模型干所有事,结果成本居高不下。

合理的做法是按任务分层。比如用户意图识别用轻量模型,真正需要深度推理的环节才切到旗舰模型。这样整体成本能降下来,效果也不会差太多。我在实际项目里试过,一个客服问答系统,把意图分类和简单 FAQ 交给轻量模型,只有复杂投诉和需要多步推理的问题才走旗舰模型,整体成本降了大概六成,用户满意度几乎没有变化。

这里的关键是要有一套路由逻辑。你不能靠感觉去判断哪个请求该用哪个模型,得有一个可量化的规则。最简单的做法是根据输入长度和关键词做初筛,复杂一点可以用一个小模型先做意图分类,再决定路由。这个逻辑用 Python 写起来不复杂,后面脚本部分我会给一个简化版的实现思路。

2.3 调价之后,哪些场景受益最明显

调价不是所有场景都等比例受益。根据我的观察,以下几类场景的降本效果最明显:

第一类是长上下文、高频调用的场景。比如文档问答、代码助手、长对话客服。这类场景输入 Token 占比高,如果调价主要降的是输入价格,那受益就很大。如果再叠加 Prompt Caching,效果更明显。

第二类是输出密集型的场景。比如内容生成、摘要、翻译。这类场景输出 Token 占比高,如果调价覆盖了输出价格,降本也很直接。

第三类是批量处理的场景。比如离线数据标注、批量文档分析。这类场景对延迟不敏感,可以用更便宜的模型或者批量接口,调价之后单位成本进一步下降。

反过来,如果你的场景是短输入、短输出、低频调用,那调价对你的影响可能很小,不值得花大力气去重构。先把精力放在高频高消耗的环节上,这是成本优化的基本原则。

3. 用 Python 写一套 Token 用量分析脚本

3.1 环境准备与依赖安装

脚本用 Python 写,依赖不多,主要是anthropic官方 SDK 和tiktoken用来做 Token 估算。如果你还没装 Python,去官网下载 3.10 以上的版本,安装的时候记得勾选“Add Python to PATH”。装完之后打开终端,用 pip 装依赖:

pip install anthropic tiktoken pandas matplotlib

这里解释一下每个包的作用。anthropic是官方 SDK,用来发请求和拿用量数据。tiktoken是 OpenAI 开源的 Token 计算库,虽然它不是 Claude 的官方分词器,但用来做粗略估算足够,误差在可接受范围内。pandas用来做数据整理,matplotlib用来画图。如果你只是想在终端看结果,后两个可以不装。

注意:tiktoken的估算结果和 Claude 实际计费会有偏差,通常在 5% 到 15% 之间。如果你要做精确对账,还是以 API 返回的 usage 字段为准。脚本里我会同时展示估算值和实际值,方便你对比。

3.2 核心脚本结构设计

脚本的整体思路是:记录每次请求的用量,汇总分析,输出报告。我把它分成三个模块:

  • 请求模块:封装 Claude API 调用,每次调用后把 usage 数据写进日志。
  • 分析模块:读取日志,按模型、按日期、按请求类型汇总 Token 消耗和成本。
  • 报告模块:输出表格和图表,直观展示成本结构。

这样设计的好处是解耦。请求模块可以嵌入你现有的业务代码,分析模块可以独立运行,报告模块可以按需替换。你不需要一次性全用上,可以先从记录开始,跑一段时间有了数据再做分析。

日志格式我用 JSON Lines,每行一条记录,方便追加和解析。字段包括时间戳、模型名、输入 Token、输出 Token、缓存写入 Token、缓存读取 Token、请求类型标签。请求类型标签是你自己打的,比如“客服问答”“文档摘要”“代码生成”,用来做分类分析。

3.3 完整可运行代码

下面是完整脚本。我把它写成一个文件,你可以直接保存为token_analyzer.py,改一下 API Key 就能跑。

import os import json import time from datetime import datetime from collections import defaultdict import anthropic import tiktoken # ============ 配置区 ============ API_KEY = os.environ.get("ANTHROPIC_API_KEY", "your-api-key-here") LOG_FILE = "token_usage.jsonl" MODEL_PRICING = { # 单位:美元 / 百万 Token # 这里的价格是示例,实际以官方最新定价为准 "claude-3-5-sonnet": {"input": 3.0, "output": 15.0, "cache_write": 3.75, "cache_read": 0.3}, "claude-3-5-haiku": {"input": 0.8, "output": 4.0, "cache_write": 1.0, "cache_read": 0.08}, "claude-3-opus": {"input": 15.0, "output": 75.0, "cache_write": 18.75, "cache_read": 1.5}, } client = anthropic.Anthropic(api_key=API_KEY) encoder = tiktoken.get_encoding("cl100k_base") def estimate_tokens(text): """粗略估算 Token 数,仅用于对比""" return len(encoder.encode(text)) def log_usage(record): """把用量记录追加到日志文件""" with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") def call_claude(prompt, model="claude-3-5-haiku", tag="default", system=None, max_tokens=1024): """封装一次 Claude API 调用,并记录用量""" messages = [{"role": "user", "content": prompt}] kwargs = { "model": model, "max_tokens": max_tokens, "messages": messages, } if system: kwargs["system"] = system start = time.time() response = client.messages.create(**kwargs) elapsed = time.time() - start usage = response.usage record = { "timestamp": datetime.now().isoformat(), "model": model, "tag": tag, "input_tokens": usage.input_tokens, "output_tokens": usage.output_tokens, "cache_write_tokens": getattr(usage, "cache_creation_input_tokens", 0) or 0, "cache_read_tokens": getattr(usage, "cache_read_input_tokens", 0) or 0, "estimated_input": estimate_tokens(prompt), "elapsed_seconds": round(elapsed, 3), } log_usage(record) return response def calc_cost(record): """根据记录计算单次请求成本(美元)""" model = record["model"] pricing = MODEL_PRICING.get(model) if not pricing: return 0.0 cost = ( record["input_tokens"] / 1_000_000 * pricing["input"] + record["output_tokens"] / 1_000_000 * pricing["output"] + record["cache_write_tokens"] / 1_000_000 * pricing["cache_write"] + record["cache_read_tokens"] / 1_000_000 * pricing["cache_read"] ) return cost def load_records(): """读取日志文件""" records = [] if not os.path.exists(LOG_FILE): return records with open(LOG_FILE, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line: records.append(json.loads(line)) return records def analyze(records): """按模型和标签汇总分析""" by_model = defaultdict(lambda: {"count": 0, "input": 0, "output": 0, "cache_write": 0, "cache_read": 0, "cost": 0.0}) by_tag = defaultdict(lambda: {"count": 0, "input": 0, "output": 0, "cache_write": 0, "cache_read": 0, "cost": 0.0}) for r in records: cost = calc_cost(r) for key, bucket in [(r["model"], by_model), (r["tag"], by_tag)]: bucket[key]["count"] += 1 bucket[key]["input"] += r["input_tokens"] bucket[key]["output"] += r["output_tokens"] bucket[key]["cache_write"] += r["cache_write_tokens"] bucket[key]["cache_read"] += r["cache_read_tokens"] bucket[key]["cost"] += cost return by_model, by_tag def print_report(by_model, by_tag): """打印分析报告""" print("=" * 70) print("按模型汇总") print("=" * 70) print(f"{'模型':<25}{'次数':>8}{'输入':>12}{'输出':>12}{'成本($)':>12}") for model, d in sorted(by_model.items(), key=lambda x: -x[1]["cost"]): print(f"{model:<25}{d['count']:>8}{d['input']:>12}{d['output']:>12}{d['cost']:>12.4f}") print() print("=" * 70) print("按请求类型汇总") print("=" * 70) print(f"{'类型':<25}{'次数':>8}{'输入':>12}{'输出':>12}{'成本($)':>12}") for tag, d in sorted(by_tag.items(), key=lambda x: -x[1]["cost"]): print(f"{tag:<25}{d['count']:>8}{d['input']:>12}{d['output']:>12}{d['cost']:>12.4f}") total_cost = sum(d["cost"] for d in by_model.values()) total_calls = sum(d["count"] for d in by_model.values()) print() print(f"总调用次数: {total_calls}") print(f"总成本: ${total_cost:.4f}") if total_calls > 0: print(f"平均单次成本: ${total_cost / total_calls:.6f}") if __name__ == "__main__": # 示例:跑几次调用,然后分析 # 实际使用时,把 call_claude 嵌入你的业务代码即可 call_claude("用一句话解释什么是 Token", model="claude-3-5-haiku", tag="测试") call_claude("写一段 200 字的商品描述", model="claude-3-5-sonnet", tag="内容生成") records = load_records() by_model, by_tag = analyze(records) print_report(by_model, by_tag)

这段代码的核心逻辑很直白:每次调用后把 usage 写进日志,分析时读日志做汇总。MODEL_PRICING里的价格你需要根据官方最新定价更新,我这里填的是示例值。calc_cost函数把四种 Token 分别乘以对应单价再求和,这就是单次请求的真实成本。

3.4 脚本运行与结果解读

跑起来之后,你会看到类似这样的输出:

====================================================================== 按模型汇总 ====================================================================== 模型 次数 输入 输出 成本($) claude-3-5-sonnet 1 15 180 0.002745 claude-3-5-haiku 1 12 25 0.000110 ====================================================================== 按请求类型汇总 ====================================================================== 类型 次数 输入 输出 成本($) 内容生成 1 15 180 0.002745 测试 1 12 25 0.000110 总调用次数: 2 总成本: $0.002855 平均单次成本: $0.001428

这个报告告诉你几件事:哪个模型花钱最多、哪类请求成本最高、平均单次成本是多少。有了这些数据,你就能做针对性的优化。比如发现“内容生成”这一类占了总成本的 90%,那优化重点就放在这里,看看能不能用更便宜的模型、能不能缩短输出、能不能做缓存。

实操心得:日志文件会越来越大,建议按天切分,比如token_usage_2024-01-15.jsonl。分析的时候可以指定日期范围,避免一次性加载太多数据。另外,estimated_input字段可以用来校验估算误差,如果偏差超过 20%,说明你的文本特征和tiktoken的假设差得比较远,需要调整估算策略。

4. 成本优化的实操方法

4.1 Prompt Caching 的正确用法

Prompt Caching 是降本利器,但用不对反而会增加成本。它的原理是:你把一段固定的内容标记为可缓存,第一次请求时系统会把它存起来,后续请求如果这段内容没变,就直接从缓存读取,价格大幅降低。但缓存有写入成本,而且缓存有有效期,过期之后需要重新写入。

所以适用的场景是:固定内容足够长、调用频率足够高、时间间隔足够短。三个条件缺一个,缓存就可能不划算。我一般会算一个简单的盈亏平衡点:缓存写入的额外成本,除以每次缓存读取省下的钱,得到需要多少次命中才能回本。如果预计命中次数低于这个值,就不做缓存。

具体到代码,Claude 的缓存是通过在消息内容里加cache_control标记实现的。你需要把固定部分(比如系统提示词、参考文档)单独放在一个 content block 里,加上标记。这部分内容一旦被缓存,后续请求只要前缀完全一致就能命中。

4.2 模型路由与降级策略

模型路由的核心思想是让合适的模型干合适的事。我通常会把请求分成三档:

  • 简单档:意图分类、关键词提取、格式转换。用最便宜的轻量模型。
  • 中等档:常规问答、摘要、简单生成。用中档模型。
  • 复杂档:多步推理、长文生成、代码编写。用旗舰模型。

路由逻辑可以很简单,比如根据输入长度和是否包含特定关键词来判断。也可以用一个轻量模型先做分类,再决定走哪条路。后者的成本略高,但准确率更好。我在实际项目里用的是混合策略:先用规则做初筛,规则覆盖不到的再用小模型分类。

降级策略是另一个维度。当旗舰模型调用失败或者超时,自动降级到中档模型,保证服务可用性。这个逻辑用 try-except 就能实现,但要注意降级后的结果质量可能下降,需要在业务层做兜底。

4.3 输出长度控制与截断技巧

输出 Token 通常是最贵的部分,控制输出长度能直接省钱。几个实用技巧:

第一,在提示词里明确要求简洁。比如“用不超过 100 字回答”“只输出 JSON,不要解释”。模型会遵循这些约束,输出长度明显缩短。

第二,设置max_tokens参数。这个参数是硬上限,超过会被截断。但要注意,截断可能导致输出不完整,需要业务层处理。我一般会把max_tokens设成预期长度的 1.5 倍,留一点余量。

第三,用结构化输出替代自然语言。比如让模型输出 JSON 而不是一段话,Token 数通常更少,而且解析更方便。这个技巧在数据提取类任务里特别有效。

第四,避免让模型重复你已经知道的信息。比如你问“这段代码有什么问题”,模型可能会先把代码复述一遍再分析。你可以在提示词里明确说“不要复述代码,直接指出问题”。

4.4 批量处理与异步调用

对于离线任务,批量处理能显著降低成本。Claude 提供了批量接口,价格比实时接口低不少。适合的场景包括:批量文档分析、数据集标注、离线内容生成。这些任务对延迟不敏感,用批量接口完全没问题。

异步调用是另一个思路。如果你有大量请求要发,用同步方式一个个发效率很低。用异步方式并发发送,能大幅缩短总耗时,虽然单价不变,但时间成本降下来了。Python 里可以用asyncio配合anthropic的异步客户端实现。

注意:并发不是越高越好。API 有速率限制,并发太高会触发限流,反而导致重试和额外消耗。我一般会从 5 到 10 的并发开始试,根据实际表现调整。另外,异步代码的异常处理要格外小心,一个请求失败不能影响其他请求。

5. 常见问题与排查技巧

5.1 Token 数对不上怎么办

这是最常见的问题。你估算的 Token 数和 API 返回的对不上,原因通常有几个:

一是分词器差异。tiktoken不是 Claude 的官方分词器,估算有偏差很正常。偏差大小取决于你的文本特征,中文、代码、特殊符号的偏差通常更大。

二是隐藏的 Token。API 返回的 usage 里,输入 Token 可能包含了一些你没显式发送的内容,比如系统自动添加的格式标记。这部分你控制不了,只能接受。

三是缓存的影响。如果用了缓存,input_tokens和cache_read_input_tokens是分开计的,你需要把它们加起来才是总输入。很多人只看input_tokens,漏掉了缓存部分,导致对不上。

排查方法:先确认你读的是哪些字段,再把估算值和实际值做对比,找出偏差最大的样本,分析文本特征。如果偏差稳定在某个范围,可以在估算公式里加一个修正系数。

5.2 成本突然飙升的排查思路

成本突然涨了,先别慌,按这个顺序排查:

第一步,看调用量有没有变化。是不是某个功能上线导致请求量增加,或者有异常重试导致重复调用。

第二步,看模型分布有没有变化。是不是某个环节从轻量模型切到了旗舰模型,或者路由逻辑出了问题,把简单请求也路由到了贵模型。

第三步,看输入输出长度有没有变化。是不是提示词变长了,或者用户输入变长了,或者输出没做限制导致模型话变多了。

第四步,看缓存命中率有没有下降。缓存过期、前缀变化、并发写入冲突都可能导致命中率下降。

我遇到过一次成本翻倍的情况,最后查出来是缓存前缀里带了一个时间戳,导致每次请求前缀都不一样,缓存完全没命中。这种问题不看日志根本发现不了。

5.3 缓存不生效的常见原因

缓存不生效,通常是这几个原因:

  • 前缀不一致:缓存是按前缀匹配的,只要开头有一点不同,后面全部失效。检查你的固定内容里有没有动态字段,比如时间戳、随机 ID、用户 ID。
  • 内容太短:缓存有最小长度要求,太短的内容不会被缓存。具体阈值看官方文档。
  • 调用间隔太长:缓存有有效期,超过有效期就失效了。如果你的调用频率很低,缓存可能来不及命中就过期了。
  • 标记位置不对:cache_control要加在正确的 content block 上,加错位置不会生效。

排查方法:在日志里记录每次请求的缓存命中情况,对比前缀内容,找出变化的部分。如果前缀里有动态字段,把它挪到缓存块外面。

5.4 常见问题速查表

问题现象可能原因排查方法解决思路
Token 数对不上分词器差异、隐藏 Token、缓存未计入对比估算值与实际值,检查 usage 字段加修正系数,确认读取字段完整
成本突然飙升调用量增加、模型切换、长度变化、缓存失效按模型和标签分组对比历史数据定位变化点,针对性修复
缓存不生效前缀不一致、内容太短、间隔太长、标记错误记录缓存命中情况,对比前缀移除动态字段,调整标记位置
输出被截断max_tokens 设置过小检查 finish_reason 字段调大 max_tokens 或优化提示词
请求超时输入过长、模型负载高、网络问题记录耗时,分析长尾请求拆分请求,加超时重试
并发触发限流并发数过高观察错误码和重试次数降低并发,加退避重试

这张表建议存下来,遇到问题先查表,能省不少时间。

6. 我踩过的坑和几条实在建议

先说一个我踩过的坑。早期做成本优化的时候,我一看旗舰模型贵,就把所有请求都切到了轻量模型,结果效果下降得厉害,用户投诉变多,最后又切回去,来回折腾浪费了不少时间。后来才明白,成本优化不是一刀切,而是分层。该用贵的地方不能省,不该用贵的地方一分钱都别多花。

第二个坑是缓存。我一开始以为只要把系统提示词标记一下就能缓存,结果发现命中率极低。查了半天才发现,提示词里有一个动态生成的日期字段,每次都不一样,前缀全变了。把日期挪到缓存块外面之后,命中率直接上去了。这个教训是:缓存块里绝对不能有动态内容。

第三个坑是日志。我一开始没做用量日志,出了问题只能靠猜。后来加了日志,才发现很多成本花在了意想不到的地方,比如某个内部测试接口被频繁调用,或者某个重试逻辑没有上限。没有数据就没有优化,这句话在成本管理上特别成立。

几条实在建议:

第一,先测量再优化。不要凭感觉判断哪里贵,用脚本跑一段时间,让数据说话。

第二,优化要抓大头。80% 的成本通常集中在 20% 的请求上,先把这部分搞定,收益最明显。

第三,留出缓冲。成本优化不要做到极致,留一点余量应对突发情况。把max_tokens卡得太死,输出被截断反而影响体验。

第四,定期复盘。业务在变,成本结构也在变。我一般每个月看一次用量报告,看看有没有新的异常点。

第五,别忽略小钱。单次请求省几分钱看起来不多,但乘以百万次调用就是一笔不小的数目。成本优化是个细活,积少成多。

这套脚本我现在还在用,每次调价或者换模型,跑一遍报告就能知道影响。你也可以根据自己的业务调整字段和标签,让它更贴合你的场景。工具是死的,思路是活的,关键是养成用数据做决策的习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询