ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型API Token成本计算与用量分析:Python脚本实战

大模型API Token成本计算与用量分析:Python脚本实战 1. 从一次账单异常说起Token 计费到底怎么算上个月帮一个朋友看他项目的 API 账单他信誓旦旦跟我说我这个月调用量跟上月差不多怎么费用翻了一倍。我让他把两次调用的日志导出来对比了一下输入输出长度问题立刻就清楚了——他把系统提示词从 200 字扩到了 1500 字而且每次请求都带着完整的历史对话。调用次数没变但每次请求的 token 量涨了三四倍账单自然跟着涨。这件事让我意识到很多人用大模型 API 的时候脑子里只有调用次数这个概念对 token 这个真正的计费单位其实是模糊的。所以这篇就围绕 token 成本计算和 API 用量分析来展开把账算清楚再给一套能直接跑的 Python 脚本让你随时能算出自己的真实成本。先说清楚这篇适合谁看如果你已经在用或者准备用 Claude API 做应用但对每个月花多少钱心里没底或者想搞清楚为什么我的费用和预期对不上那这篇就是写给你的。如果你还没接触过 API只是想了解 token 是什么前半部分的基础解释也能帮到你。整篇不涉及任何特定网络环境的配置纯粹讲计费和用量分析这件事本身。Token 这个词在不同语境下含义不一样。在登录鉴权里它指的是身份凭证在自然语言处理里它指的是文本被切分后的最小单位。这篇讲的是后者。你可以把 token 粗略理解成模型眼里的字——它不按我们人类的字数来算而是按自己的切分规则。英文里一个常见单词往往就是一个 token中文里通常一个汉字要占一到两个 token具体取决于分词器。这个差异直接决定了中英文内容的成本不一样后面会细说。2. Token 计费的三个隐藏变量输入、输出、缓存大部分人算成本的时候只盯着我发了多少字但真实的计费模型比这复杂。Claude API 的计费至少涉及三个变量任何一个没考虑到算出来的数字都会偏。2.1 输入 token 和输出 token 的价格不是一回事这是最容易被忽略的一点。输出 token 的单价通常比输入 token 贵好几倍。原因也不难理解生成内容比读取内容计算量更大模型要逐个 token 地想出下一个词而读取输入基本是一次性的编码过程。所以如果你的应用是长输入、短输出型比如文档摘要、内容分类成本主要压在输入上如果是短输入、长输出型比如文案生成、代码补全那输出 token 才是大头。这两种场景的优化思路完全相反前者要压缩输入后者要控制输出长度。我见过有人为了省钱把输出限制得很死结果模型话说到一半被截断用户还得重新请求一次反而更贵。所以限制输出长度要谨慎得保证一次能给完整答案。2.2 系统提示词是每次请求都在重复付费系统提示词system prompt是很多人成本失控的元凶。它每次请求都会完整地作为输入发过去也就是说如果你写了一段 1000 token 的系统提示调用一万次那就是一千万 token 的输入成本哪怕用户实际只问了一句你好。我朋友那个案例就是这个问题。他为了让模型更懂业务往系统提示里塞了大量背景资料和规则说明结果每次请求都背着这一大坨。正确的做法是把固定不变的大段背景知识做成缓存或者干脆用检索的方式按需注入而不是无脑全塞进系统提示。2.3 缓存机制能省多少取决于命中率Claude API 提供了提示词缓存prompt caching能力简单说就是把重复出现的前缀内容缓存起来后续请求命中缓存的部分按更低的单价计费。这个机制对系统提示很长且固定的场景特别友好。但缓存不是白给的它有两个成本点一是写入缓存本身有额外费用二是缓存有存活时间过期了就得重新写。所以缓存划不划算取决于你的请求频率和前缀的稳定程度。如果前缀天天变或者请求稀疏到缓存总过期那缓存反而可能更贵。这个账得具体算不能想当然。下面这张表把三个变量的影响方向列清楚方便你对照自己的场景变量影响方向优化手段注意事项输入 token长输入场景成本主因压缩提示、按需注入、去重别把固定背景全塞系统提示输出 token长输出场景成本主因合理设上限、避免截断重试上限太低会导致重试反而更贵缓存高频固定前缀可显著降本稳定前缀、提高命中率低频或前缀多变时可能不划算理解了这三个变量你才算真正看懂了账单。接下来讲怎么把这些数字算出来。3. 手算一遍从文本到费用的完整推导光讲概念不够我们拿一个具体例子从头算到尾你跟着走一遍就明白了。假设你做了一个客服问答机器人系统提示词 800 个 token每次用户提问平均 50 个 token模型回答平均 300 个 token。一天处理 2000 次咨询。我们假设输入单价是每百万 token 3 美元输出单价是每百万 token 15 美元这里用假设值演示算法实际单价以官方最新公布为准。先算每天的输入 token 总量。每次请求的输入包括系统提示加用户提问也就是 800 50 850 token。2000 次就是 850 × 2000 1,700,000 token即 1.7 百万 token。输入成本 1.7 × 3 5.1 美元。再算输出。每次 300 token2000 次就是 600,000 token即 0.6 百万。输出成本 0.6 × 15 9 美元。一天总成本 5.1 9 14.1 美元。一个月按 30 天算就是 423 美元。现在你看出问题了吗系统提示那 800 token 贡献了 800 × 2000 1,600,000 token 的输入占了输入总量的 94%成本 4.8 美元。而用户真正问的那 50 token 只花了 0.3 美元。也就是说你 94% 的输入成本花在了那段固定不变的提示词上。如果这段系统提示能通过缓存命中假设缓存读取单价是正常输入的十分之一那这部分成本能从 4.8 美元降到 0.48 美元一天省 4.32 美元一个月省将近 130 美元。这就是为什么我说系统提示词是成本优化的第一战场。反过来如果你把输出上限从 300 压到 150看似省了一半输出成本但模型可能答不完整用户追问一次等于又花了一次输入加输出算下来未必省。所以优化要有全局视角不能只盯一个数字。这个手算过程看着简单但真到线上请求长度是波动的你得靠脚本去统计真实分布而不是拍脑袋取平均值。平均值会骗人——如果 90% 的请求很短10% 的请求超长平均值会被那 10% 拉高你按平均值做预算就会低估峰值成本。4. 用 Python 写一个能跑的 Token 成本分析脚本概念和手算都清楚了现在上工具。下面这个脚本做三件事读取你的调用日志、统计 token 分布、算出总成本和各项占比。我尽量写得能直接改改就用。4.1 脚本的整体设计思路为什么这么设计因为真实场景里你的调用记录通常散落在日志文件或者数据库里格式五花八门。所以脚本的第一步是把数据统一成标准结构第二步才是分析。这样即使你换了数据源只要改读取部分分析逻辑不用动。脚本不依赖任何第三方库纯标准库实现避免你为了跑个分析还要折腾环境。如果你日志是 JSON 格式用内置的 json 模块就够了如果是 CSV用 csv 模块。这样门槛最低。import json import csv from collections import defaultdict # 单价配置单位美元/百万 token # 实际使用时请替换为官方最新公布的价格 PRICE_INPUT 3.0 PRICE_OUTPUT 15.0 PRICE_CACHE_READ 0.3 PRICE_CACHE_WRITE 3.75 def load_from_json(path): 从 JSON 日志加载每行一个 JSON 对象 records [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue records.append(json.loads(line)) return records def load_from_csv(path): 从 CSV 加载要求包含 input_tokens/output_tokens 等列 records [] with open(path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: records.append({ input_tokens: int(row.get(input_tokens, 0)), output_tokens: int(row.get(output_tokens, 0)), cache_read_tokens: int(row.get(cache_read_tokens, 0)), cache_write_tokens: int(row.get(cache_write_tokens, 0)), }) return records这段是数据加载层。我特意把 JSON 和 CSV 两种都写了因为不同人的日志格式差别很大。JSON 适合结构化日志CSV 适合从表格导出的数据。你按自己的情况选一个用就行。4.2 核心统计逻辑与成本计算加载完数据接下来是统计。这里的关键是把总量和分布分开看。总量告诉你花了多少钱分布告诉你钱花在哪、有没有异常。def analyze(records): total_input 0 total_output 0 total_cache_read 0 total_cache_write 0 per_request_input [] for r in records: it r.get(input_tokens, 0) ot r.get(output_tokens, 0) cr r.get(cache_read_tokens, 0) cw r.get(cache_write_tokens, 0) total_input it total_output ot total_cache_read cr total_cache_write cw per_request_input.append(it) cost_input total_input / 1_000_000 * PRICE_INPUT cost_output total_output / 1_000_000 * PRICE_OUTPUT cost_cache_read total_cache_read / 1_000_000 * PRICE_CACHE_READ cost_cache_write total_cache_write / 1_000_000 * PRICE_CACHE_WRITE total_cost cost_input cost_output cost_cache_read cost_cache_write # 计算输入 token 的分布找出长尾 per_request_input.sort() n len(per_request_input) if n 0: p50 per_request_input[int(n * 0.5)] p90 per_request_input[int(n * 0.9)] p99 per_request_input[int(n * 0.99)] max_val per_request_input[-1] else: p50 p90 p99 max_val 0 return { 请求数: n, 输入token总量: total_input, 输出token总量: total_output, 缓存读取token: total_cache_read, 缓存写入token: total_cache_write, 输入成本: round(cost_input, 4), 输出成本: round(cost_output, 4), 缓存读取成本: round(cost_cache_read, 4), 缓存写入成本: round(cost_cache_write, 4), 总成本: round(total_cost, 4), 输入P50: p50, 输入P90: p90, 输入P99: p99, 输入最大值: max_val, }这段是核心。注意我加了 P50、P90、P99 这几个分位数。为什么因为平均值会掩盖长尾。如果你的 P99 是 P50 的十倍说明有少量超长请求在偷偷吃你的预算这时候你就该去查这些请求是怎么回事——是不是某类问题触发了超长上下文是不是有用户恶意灌入大量文本。4.3 把结果打印成人能看懂的样子算出来一堆数字得让人一眼看明白。下面这个输出函数把结果整理成表格形式还顺便算了一下各项成本的占比方便你判断优化重点。def report(stats): print( * 50) print(Token 成本分析报告) print( * 50) print(f总请求数: {stats[请求数]}) print(f输入 token 总量: {stats[输入token总量]:,}) print(f输出 token 总量: {stats[输出token总量]:,}) print(f缓存读取 token: {stats[缓存读取token]:,}) print(f缓存写入 token: {stats[缓存写入token]:,}) print(- * 50) print(f输入成本: ${stats[输入成本]}) print(f输出成本: ${stats[输出成本]}) print(f缓存读取成本: ${stats[缓存读取成本]}) print(f缓存写入成本: ${stats[缓存写入成本]}) print(f总成本: ${stats[总成本]}) print(- * 50) print(输入 token 分布:) print(f P50: {stats[输入P50]:,}) print(f P90: {stats[输入P90]:,}) print(f P99: {stats[输入P99]:,}) print(f 最大: {stats[输入最大值]:,}) total stats[总成本] if total 0: print(- * 50) print(成本占比:) print(f 输入: {stats[输入成本] / total * 100:.1f}%) print(f 输出: {stats[输出成本] / total * 100:.1f}%) print(f 缓存读取: {stats[缓存读取成本] / total * 100:.1f}%) print(f 缓存写入: {stats[缓存写入成本] / total * 100:.1f}%) if __name__ __main__: # 按你的实际情况改路径和加载方式 records load_from_json(api_logs.jsonl) stats analyze(records) report(stats)跑起来之后你会得到一份清晰的报告。我第一次用这个脚本分析自己的项目时发现输出成本占了总成本的 70%而我一直以为输入才是大头。这个反直觉的结果让我重新审视了输出策略把一些不必要的长回答场景做了约束成本降了不少。5. 分析结果怎么读几个真实场景的排查思路脚本跑出来只是第一步关键是看懂数字背后的故事。我拿几个典型场景说说怎么排查。5.1 输入成本占比过高先查系统提示如果你的报告显示输入成本占了七成以上八成是系统提示词太长或者历史对话没做截断。这时候去看 P50 和 P99 的差距。如果 P50 就已经很大说明每次请求的基础输入就重问题在系统提示如果 P50 正常但 P99 爆表说明是少数请求带了超长上下文问题在历史管理。系统提示的优化思路前面说过能缓存就缓存能精简就精简。历史对话的优化则是做滑动窗口或者摘要压缩别把整段对话历史每次都原样带上。5.2 输出成本高看是不是回答太啰嗦输出成本高通常有两个原因一是模型回答本身冗长二是输出上限设得太高导致模型话痨。前者可以在提示里明确要求简洁后者可以适当调低上限。但要注意调低上限有截断风险得配合监控看有没有因为截断导致的重复请求。我一般会统计输出 token 的分布如果大量请求都贴着上限说明上限设低了模型被憋着如果大部分请求远低于上限说明上限设高了可以往下调。5.3 缓存成本不降反升检查命中率缓存写入是有成本的如果命中率低写入的钱就白花了。判断方法很简单看缓存读取 token 和缓存写入 token 的比例。如果写入远大于读取说明缓存基本没被复用这时候要么提高前缀稳定性要么干脆关掉缓存。缓存适合前缀固定、请求频繁的场景。如果你的前缀每次都不一样或者请求间隔长到缓存总过期那缓存就是负优化。6. 几个我踩过的坑和对应的处理办法讲完方法论分享几个实操中真金白银换来的教训。第一个坑是用字符数估算 token 数。我早期图省事按一个汉字一个 token、四个英文字符一个 token粗略估算结果和实际账单差了 20% 以上。中文的 token 切分比想象中复杂标点、数字、混合文本都会影响。后来我改成用官方的 token 计数接口或者分词库来精确统计误差才降下来。所以别偷懒该精确统计就精确统计。第二个坑是忽略了重试带来的重复计费。网络抖动或者超时的时候如果代码里做了自动重试而重试前的那次请求其实已经产生了输出那你就为同一份内容付了两次钱。解决办法是给请求加幂等标识或者在重试逻辑里判断上次请求是否真的失败了。这个坑很隐蔽账单上不会单独列出来只能靠日志分析发现。第三个坑是把开发测试的调用混进了生产统计。我有一段时间发现成本莫名上涨查了半天才发现是测试环境用了同一个 API key测试脚本跑得比生产还勤。后来我给不同环境配了不同的 key统计的时候分开看问题立刻清晰了。这个习惯强烈建议你养成不然数据永远是浑的。第四个坑是只看总量不看趋势。成本分析不是算一次就完事得持续看。我现在的做法是每天跑一次脚本把结果存下来画个趋势图。有一次我发现输入 token 连续三天缓慢上涨追查下去发现是某个功能的提示词被同事悄悄改长了。如果没有趋势监控这种渐进式的成本上涨很难被察觉。7. 把成本分析变成日常习惯最后说点我个人的体会。成本分析这件事最怕的就是等账单来了再说。账单是滞后的等你看到数字的时候钱已经花出去了。真正有效的做法是把它变成日常的一部分每天或者每周跑一次脚本看趋势、看异常、看占比变化。我现在给自己定的规矩是任何涉及提示词改动的上线都要先跑一遍成本预估任何新功能上线后一周内都要重点看它的 token 消耗。这样成本就从一个月底才关心的数字变成了随时可控的指标。另外单价是会变的官方时不时会调整价格或者推出新的计费档位。所以脚本里的单价配置要定期核对别用着半年前的旧价格算那样算出来的数字没有参考价值。我一般会在脚本里加个注释标明价格更新日期提醒自己定期检查。这套脚本和分析思路我从最初的手算一路迭代到现在帮我和身边的团队省下了不少冤枉钱。你拿到之后不用照搬按自己的日志格式和业务场景改一改跑通第一次后面就是重复劳动了。真正难的不是写脚本而是养成看数据做决策的习惯——这一点比任何优化技巧都值钱。
返回列表