
1. 从一条计费通知说起为什么我要自己算 Token 账上周三凌晨我照例跑完当天的批量文本处理任务顺手打开控制台看了一眼账单发现同样的任务量费用比前一天少了将近四成。第一反应是脚本出 bug 了检查日志才发现是上游模型服务调整了计费策略。这件事让我意识到一个很现实的问题把成本控制完全交给平台账单等于把自己的钱包交给别人管。这篇文章要聊的就是这件事——Claude API 的 Token 成本计算与用量分析。我会从 Token 的计费原理讲起带你搞清楚输入 Token、输出 Token、缓存命中这几项到底怎么算钱然后给出一套可以直接跑的 Python 脚本帮你统计自己项目里每一次调用的真实开销。不管你是刚接触大模型 API 的新手还是已经在生产环境跑了几十万次调用的老手这套方法都能用得上。适合谁看三类人一是刚开始用 Claude API 做个人项目的开发者想知道钱花在哪二是团队里负责成本核算的同学需要一份可复现的用量报表三是像我这样习惯自己掌控数据的人不满足于平台给的那张汇总账单。需要提前说明的是模型服务的定价策略会不定期调整本文涉及的具体单价仅作为计算方法的演示实际计费请以官方最新公告为准。方法本身是通用的换个模型、换个单价脚本改一行配置就能继续用。2. Token 计费的核心逻辑拆解2.1 Token 到底是什么为什么按它收费很多人第一次接触 API 计费时会困惑为什么不是按字数、按请求次数收费而是按 Token这里得先把这个概念说清楚。Token 是模型处理文本的最小单位它不是字符也不是单词。英文里一个常见单词通常是 1 个 Token长单词会被拆成 2 到 3 个中文里一个汉字大约对应 1 到 2 个 Token具体取决于分词器的实现。你可以把它理解成模型眼里的文字颗粒度——模型读进去的是 Token 序列吐出来的也是 Token 序列算力消耗自然就按 Token 数量来计量。提示不同模型家族的分词规则不一样同样一段中文在 A 模型里是 800 Token在 B 模型里可能是 1100 Token。做成本对比时一定要用目标模型自己的分词器去估算不能拿字数直接换算。为什么按 Token 而不是按字数因为模型的注意力计算复杂度跟序列长度强相关处理 1000 个 Token 和处理 2000 个 Token算力开销不是线性翻倍那么简单。按 Token 计费本质上是在按真实算力消耗收费这对平台和用户都更公平。2.2 输入、输出、缓存三笔账要分开算Claude API 的计费不是单一价格而是分成了几个维度这是很多人算错账的根源。我把它拆成三块计费项说明单价特点输入 Token你发给模型的提示词、上下文、文档内容基准价通常最低输出 Token模型生成的回复内容通常是输入价的数倍缓存写入 Token把长上下文写入缓存供后续复用略高于普通输入缓存读取 Token命中缓存后读取的内容显著低于普通输入这里最关键的一点是输出 Token 的单价普遍比输入贵好几倍。这意味着如果你的任务输出很长成本大头在输出侧如果输入是超长文档那输入侧和缓存策略就成了优化重点。我见过不少人的账单里输出费用占了七成以上原因就是让模型生成了大量冗余内容。控制输出长度往往比压缩输入更省钱。2.3 缓存机制省钱的关键杠杆缓存Prompt Caching是这套计费体系里最值得研究的部分。原理不复杂如果你有一段很长的固定上下文比如系统提示词、知识库文档、代码库说明每次请求都重复发送那这部分 Token 每次都要按输入价计费非常浪费。缓存机制允许你把这段内容存起来第一次写入时付一次缓存写入费后续请求命中缓存时这部分只按缓存读取价计费而缓存读取价通常只有普通输入价的十分之一左右。注意缓存有有效期通常是几分钟到一小时不等超时后需要重新写入。所以缓存策略适合短时间内高频重复使用同一段长上下文的场景比如连续多轮对话、批量处理同一份文档。如果你的请求间隔很长缓存可能还没命中就过期了反而多付了写入费。算一笔账假设你有 10000 Token 的固定上下文普通输入价是每百万 Token 3 美元缓存读取价是每百万 Token 0.3 美元。不做缓存100 次请求要花 100 × 10000 ÷ 1000000 × 3 3 美元做了缓存写入一次 10000 Token 按 3.75 美元每百万算约 0.0375 美元剩下 99 次读取花 99 × 10000 ÷ 1000000 × 0.3 ≈ 0.297 美元合计约 0.33 美元。差了将近十倍。这个杠杆不用白不用。3. 用量分析脚本的设计思路3.1 为什么要自己写统计脚本平台控制台不是有账单吗为什么还要自己写脚本三个理由。第一账单是滞后的、聚合的。控制台通常按天或按小时汇总你看到的是总数不知道哪次调用、哪个功能、哪个用户消耗了多少。做成本优化时你需要的是细粒度数据。第二账单不区分业务维度。一个项目里可能有摘要任务、翻译任务、问答任务它们的 Token 消耗结构完全不同。只有自己在调用层埋点才能按业务标签拆分成本。第三实时性。等账单出来再发现超支往往已经晚了。自己统计可以做到每次调用后立即累计设置阈值告警把成本控制在事前。所以我的做法是在 API 调用封装层加一个轻量的统计模块每次请求记录 Token 用量和费用写入本地文件或数据库随时可以出报表。3.2 脚本整体架构脚本不复杂核心就三块计费配置模块把各模型的单价写成配置字典方便随时更新。用量记录模块每次调用后从 API 返回的 usage 字段里提取 Token 数按配置算出费用追加写入记录。报表分析模块读取记录文件按时间、按任务类型、按模型聚合输出汇总表和趋势。我特意把计费配置独立出来是因为单价会变。把单价硬编码在计算逻辑里改价时就得翻代码独立成配置改一个数字就行。这是踩过坑之后的经验——之前有一次平台调价我花半小时才找到散落在三个文件里的单价常量。3.3 关键设计决策与取舍有几个设计点值得说明因为它们直接影响脚本好不好用。用 JSON Lines 而不是 CSV 存记录。JSON Lines 每行一个独立 JSON 对象追加写入不会破坏已有数据字段可以灵活增减读取时逐行解析也不占内存。CSV 虽然 Excel 能直接打开但字段一变就要改表头追加时还容易出格式问题。我选 JSON Lines报表阶段再转成需要的格式。费用计算保留高精度。Token 单价很小比如每百万 Token 3 美元单次调用可能只花 0.0001 美元。如果用浮点数累加误差会累积。我用 Python 的decimal.Decimal做费用计算避免精度问题。这个细节很多人忽略但做成本核算时几分钱的误差累积起来也够呛。记录原始 usage 字段。除了算出的费用我把 API 返回的原始 usage 对象也存下来。这样万一计费规则理解错了可以回溯原始数据重新计算不用重新跑一遍任务。4. 可运行脚本的完整实现4.1 环境准备与依赖脚本只依赖 Python 标准库不需要额外装包这是刻意的选择——统计脚本应该轻量、好部署不该因为依赖问题跑不起来。Python 3.8 以上都能用。如果你还没装 Python去官网下载安装包安装时记得勾选Add Python to PATH。装完在命令行敲python --version能看到版本号就说明好了。这一步是很多新手卡住的地方Windows 上如果提示不是内部或外部命令基本就是 PATH 没配好重新装一遍勾上那个选项即可。4.2 计费配置模块先定义计费配置。单价用每百万 Token 多少美元表示这是行业惯例也方便阅读。# pricing_config.py # 单价单位美元 / 每百万 Token # 注意以下数值仅作演示实际请以官方最新公告为准 PRICING { claude-3-5-sonnet: { input: 3.00, output: 15.00, cache_write: 3.75, cache_read: 0.30, }, claude-3-5-haiku: { input: 0.80, output: 4.00, cache_write: 1.00, cache_read: 0.08, }, claude-3-opus: { input: 15.00, output: 75.00, cache_write: 18.75, cache_read: 1.50, }, } def get_price(model: str, token_type: str) - float: 获取指定模型指定类型的单价找不到时抛错而不是静默返回 0 if model not in PRICING: raise KeyError(f未知模型: {model}请在 PRICING 中补充配置) if token_type not in PRICING[model]: raise KeyError(f模型 {model} 不支持计费类型: {token_type}) return PRICING[model][token_type]这里有个细节get_price遇到未知模型直接抛错而不是返回 0。为什么因为静默返回 0 会让费用统计悄悄失真你以为是免费的其实只是没配价格。宁可报错让你发现也不要算出一份错误的报表。4.3 用量记录与费用计算接下来是核心的记录模块。它接收一次调用的 usage 数据算出费用追加写入文件。# usage_tracker.py import json import os from datetime import datetime, timezone from decimal import Decimal from pricing_config import get_price RECORD_FILE usage_records.jsonl def calculate_cost(model: str, usage: dict) - Decimal: 根据 usage 计算单次调用费用。 usage 字段命名参考常见 API 返回结构实际以你的接口为准。 cost Decimal(0) # 普通输入 Token input_tokens usage.get(input_tokens, 0) cost Decimal(str(input_tokens)) / Decimal(1000000) * Decimal(str(get_price(model, input))) # 输出 Token output_tokens usage.get(output_tokens, 0) cost Decimal(str(output_tokens)) / Decimal(1000000) * Decimal(str(get_price(model, output))) # 缓存写入 cache_write usage.get(cache_creation_input_tokens, 0) cost Decimal(str(cache_write)) / Decimal(1000000) * Decimal(str(get_price(model, cache_write))) # 缓存读取 cache_read usage.get(cache_read_input_tokens, 0) cost Decimal(str(cache_read)) / Decimal(1000000) * Decimal(str(get_price(model, cache_read))) return cost def record_usage(model: str, usage: dict, task_tag: str default): 记录一次调用追加写入 JSON Lines 文件 cost calculate_cost(model, usage) record { timestamp: datetime.now(timezone.utc).isoformat(), model: model, task_tag: task_tag, usage: usage, cost_usd: str(cost), # Decimal 转字符串存储避免精度丢失 } with open(RECORD_FILE, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) return cost几个关键点解释一下。usage里的字段名我用了常见命名比如input_tokens、output_tokens、cache_creation_input_tokens、cache_read_input_tokens。不同接口的字段名可能略有差异你对接时按实际返回结构调整即可逻辑是一样的。费用用Decimal计算最后转成字符串存储。为什么不直接存浮点数因为 JSON 里的浮点数在解析时可能丢精度存字符串最稳妥报表阶段再转回Decimal累加。task_tag参数是给业务打标签用的。你可以在调用时传入summary、translate、qa之类的标签后面就能按标签拆分成本。4.4 报表生成与聚合分析记录有了接下来出报表。这个模块读取 JSON Lines 文件按不同维度聚合。# report.py import json from collections import defaultdict from decimal import Decimal from datetime import datetime RECORD_FILE usage_records.jsonl def load_records(): records [] if not os.path.exists(RECORD_FILE): return records with open(RECORD_FILE, r, encodingutf-8) as f: for line in f: line line.strip() if line: records.append(json.loads(line)) return records def summarize_by(records, key_func): 通用聚合按 key_func 分组累加费用和各类 Token summary defaultdict(lambda: { calls: 0, cost: Decimal(0), input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0, }) for r in records: k key_func(r) s summary[k] s[calls] 1 s[cost] Decimal(r[cost_usd]) u r[usage] s[input_tokens] u.get(input_tokens, 0) s[output_tokens] u.get(output_tokens, 0) s[cache_read_tokens] u.get(cache_read_input_tokens, 0) s[cache_write_tokens] u.get(cache_creation_input_tokens, 0) return summary def print_report(records): if not records: print(暂无记录) return total_cost sum(Decimal(r[cost_usd]) for r in records) print(f总调用次数: {len(records)}) print(f总费用: ${total_cost:.6f}) print() print( 按模型拆分 ) by_model summarize_by(records, lambda r: r[model]) for model, s in sorted(by_model.items(), keylambda x: -x[1][cost]): print(f{model}: {s[calls]} 次, ${s[cost]:.6f}, f输入 {s[input_tokens]} / 输出 {s[output_tokens]} / f缓存读 {s[cache_read_tokens]} / 缓存写 {s[cache_write_tokens]}) print() print( 按任务标签拆分 ) by_tag summarize_by(records, lambda r: r.get(task_tag, default)) for tag, s in sorted(by_tag.items(), keylambda x: -x[1][cost]): print(f{tag}: {s[calls]} 次, ${s[cost]:.6f}) print() print( 按日期拆分 ) by_date summarize_by(records, lambda r: r[timestamp][:10]) for date, s in sorted(by_date.items()): print(f{date}: {s[calls]} 次, ${s[cost]:.6f})这个报表输出三个维度按模型、按任务标签、按日期。按模型看能发现是不是某个贵模型用多了按标签看能定位哪个业务最烧钱按日期看能观察成本趋势及时发现异常增长。4.5 接入实际调用的示例把上面几块拼起来看一个完整调用示例。这里用伪代码展示接入方式实际对接时替换成你的 API 客户端即可。# main.py from usage_tracker import record_usage from report import load_records, print_report def call_model(client, model: str, messages: list, task_tag: str): 封装一次模型调用自动记录用量 response client.messages.create( modelmodel, messagesmessages, max_tokens1024, ) # 从响应中提取 usage 并记录 usage { input_tokens: response.usage.input_tokens, output_tokens: response.usage.output_tokens, cache_creation_input_tokens: getattr(response.usage, cache_creation_input_tokens, 0), cache_read_input_tokens: getattr(response.usage, cache_read_input_tokens, 0), } cost record_usage(model, usage, task_tagtask_tag) print(f本次调用费用: ${cost:.6f}) return response if __name__ __main__: # 跑完任务后出报表 records load_records() print_report(records)getattr那两行是为了兼容不支持缓存的接口——如果响应里没有缓存字段就默认 0不会报错。这种防御性写法在对接多个模型时特别有用因为不同模型的 usage 结构不完全一致。5. 成本优化的实操经验5.1 从报表里能看出什么脚本跑起来之后报表会告诉你很多反直觉的事实。我拿自己的项目举例。第一次出报表时我发现文档摘要任务的成本占了总成本的 65%远超预期。细看数据问题出在输出侧摘要任务的平均输出 Token 是输入的 1.5 倍。按理说摘要应该比原文短为什么输出这么长检查提示词才发现我写的指令是请总结以下内容模型倾向于生成结构完整、面面俱到的摘要结果比原文还啰嗦。改成用不超过 100 字概括核心结论之后输出 Token 直接降了六成成本跟着降。这就是报表的价值——它把模糊的感觉贵变成了精确的哪里贵、贵多少。5.2 三个立竿见影的省钱手段根据我的实操按见效速度排序三个手段最值得先做。第一压缩输出。在提示词里明确限制输出长度比如用三句话回答、只输出 JSON不要解释。输出 Token 单价高压输出立竿见影。我一般会在系统提示词里加一句回答尽量简洁避免重复用户的问题这一句能省不少。第二用好缓存。如果你的任务有固定的长上下文务必启用缓存。判断标准很简单同一段长文本在短时间内被重复发送超过 2 次缓存就划算。我有个知识库问答项目把 8000 Token 的知识库说明做了缓存单次成本从 0.024 美元降到 0.003 美元。第三分级用模型。不是所有任务都需要最强的模型。分类、抽取、格式转换这类任务用便宜的小模型完全够用只有需要复杂推理的任务才上大模型。我在脚本里加了任务标签就是为了方便做这种分级——报表一看哪个标签用了贵模型但效果没差别就换掉。5.3 容易被忽略的隐性成本有几个成本项账单上不一定单独列但实实在在花钱。重试成本。网络抖动导致的失败重试每次重试都是一次完整计费。如果你的重试逻辑没做好幂等可能同一份输入被计费好几次。我的做法是在记录里标记重试次数报表里单独看这块占比超过 5% 就要查网络或限流问题。超长上下文。有些接口对超长输入有特殊计费或者会触发截断。如果你的输入经常接近上下文上限要留意是不是有冗余内容可以裁掉。我见过有人把整个日志文件塞进提示词其实只需要相关的那几行。测试流量。开发调试阶段的调用也是要花钱的。建议给测试流量单独打标签和生产流量分开统计这样能清楚知道为了开发这个功能我花了多少调试成本。6. 常见问题与排查技巧6.1 费用对不上账单怎么办这是最常被问到的问题。自己算的费用和平台账单有出入先别慌按这个顺序排查。现象可能原因排查方法自算偏低单价配置过期核对官方最新单价自算偏低漏记了某类 Token检查 usage 字段是否完整提取自算偏高重复记录检查是否有重试导致的重复写入差异很小精度或四舍五入属正常关注大额差异差异很大计费口径不同确认是否含缓存、是否含失败请求我的经验是差异在 1% 以内属于正常可能是精度或统计口径的细微差别。超过 5% 就要认真查通常是单价配置过期或者漏记了某类 Token。有一次我漏了缓存写入 Token 的统计导致自算比账单低了 8%补上之后就对齐了。6.2 脚本运行报错的典型场景报错一KeyError: 未知模型。说明你用的模型没在PRICING里配置。解决办法是补上配置别图省事改成默认值否则费用会算错。报错二usage字段取不到。不同接口返回结构不同有的把 usage 放在response.usage有的放在response.meta.usage。打印一下完整响应结构找到正确路径。报错三JSON 解析失败。通常是记录文件被并发写入破坏了。如果多进程同时写需要加文件锁或者改成每个进程写独立文件报表阶段再合并。报错四Decimal转换异常。检查传入的是不是字符串或数字Decimal不接受浮点数直接构造会有精度警告统一先转字符串再转Decimal。6.3 让统计更准的几个细节最后分享几个让统计更准的实操细节都是踩坑换来的。第一记录时间用 UTC。本地时间有时区、有夏令时跨时区协作时容易乱。统一用 UTC 存储展示时再转本地时间。第二费用字段存字符串。前面说过JSON 浮点数会丢精度存字符串最稳。第三定期归档。记录文件会越来越大建议按月归档老数据压缩存起来报表只读当月数据速度快很多。第四加个阈值告警。在记录模块里判断如果单日累计费用超过设定值打印醒目提示或发通知。这个功能救过我好几次——有次脚本死循环疯狂调用告警及时触发避免了更大的损失。提示统计脚本本身不产生费用但它的价值在于让你对费用有掌控感。建议从项目第一天就接入别等到账单吓人一跳才想起来做统计。这套方法我在几个项目里都跑过从个人小工具到日调用量几万次的服务逻辑是一样的。核心就一句话把成本数据握在自己手里你才有优化的依据。脚本不长但每一行都是为了解决一个具体问题而写的你可以直接拿去改改用。