ARTICLE DETAIL

资讯详情

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

大模型API Token成本计算与优化:Python脚本实战

大模型API Token成本计算与优化:Python脚本实战 1. 从一次账单异常说起为什么Token成本值得单独算一笔账上个月帮一个做知识库问答的朋友看账单他发现自己的应用每天调用大模型接口几千次月底账单比预期高出一大截。第一反应是是不是涨价了结果把调用日志拉出来一算问题根本不在单价上——他的提示词里塞了大量重复的系统指令每次请求都带着两千多Token的历史上下文真正有用的用户输入可能只有几十个Token。换句话说他花的钱里超过九成是在为废话和重复买单。这件事让我意识到Token成本计算这件事很多人是没做过的。大家习惯了看官方定价表上的每百万Token多少钱然后拍脑袋估一个数但真实用量和估算之间往往差着好几倍。尤其是当你的应用涉及多轮对话、长文档处理、批量任务时输入Token和输出Token的比例、缓存命中的比例、不同模型的单价差异这些因素叠加起来成本结构会变得相当复杂。这篇内容就是围绕这个场景展开的怎么把Token用量算清楚怎么用Python写一个能跑的脚本去分析API调用日志怎么从数据里找出可以优化的点。适合已经在用大模型API做开发、或者正准备接入、对成本比较敏感的开发者。不需要你是Python高手脚本部分我会把每一步都拆开讲能复制粘贴跑起来就行。先说清楚一个前提不同厂商、不同模型的计费方式不完全一样有的按输入输出分开计价有的对缓存读取有折扣有的对批量调用有优惠。我下面讲的方法论是通用的具体单价你替换成自己用的那家就行。核心思路是——把估算变成实测把感觉贵变成知道贵在哪。2. Token到底怎么算输入、输出、缓存三笔账要分开记2.1 为什么不能只看总Token数很多人算成本的方式是把请求和响应的Token加在一起乘以一个单价。这个算法在简单场景下勉强能用但只要你的应用稍微复杂一点就会失真。原因在于输入Token和输出Token的单价通常不一样而且差距可能不小。输出Token往往比输入Token贵好几倍因为生成过程需要逐Token解码计算量更大。举个例子假设某模型输入是每百万Token 3块输出是每百万Token 15块。你一次请求输入2000 Token、输出200 Token如果按总量2200 Token乘以一个平均单价去算误差会很大。正确做法是分开算输入2000除以一百万乘以3输出200除以一百万乘以15加起来才是真实成本。这个例子很极端但现实中输出占比高的场景比如内容生成、代码补全确实存在不算清楚就会低估。2.2 缓存Token是省钱的关键变量现在很多API都支持提示词缓存Prompt Caching。原理不复杂如果你多次请求的前缀部分是一样的比如固定的系统提示词、同一份长文档服务端可以把这部分缓存起来后续请求命中缓存时这部分Token按更低的单价计费有的甚至能便宜到十分之一。这个机制对成本的影响非常大。我见过一个客服机器人的案例系统提示词加产品知识库有八千多Token每次对话都带着。开启缓存之前每次请求光输入就八千Token开启之后命中缓存的部分按折扣价算成本直接降了六成以上。所以你在统计用量时必须把Token分成三类普通输入Token、缓存命中Token、输出Token。这三类的单价不同混在一起算就失去了优化的意义。2.3 一个容易被忽略的细节上下文累积多轮对话场景下每一轮请求通常会把之前的历史消息一起带上。这意味着第10轮对话的输入Token可能是第1轮的十倍。如果你按平均每次请求多少Token去估算会严重低估后期的成本。我建议在日志里记录每一轮请求的实际输入Token数然后画出随对话轮次变化的曲线。你会发现它是线性增长的如果不做截断或摘要的话。这个曲线能直接告诉你对话到第几轮时成本开始变得不可接受从而决定要不要做历史压缩、要不要限制对话轮数。3. 用Python把API调用日志变成成本报表3.1 日志里必须记录的字段想让脚本能算账前提是日志里得有足够的信息。如果你现在还没记录建议从今天开始加上这几个字段字段名含义为什么需要timestamp请求时间按天/小时聚合看趋势model模型名称不同模型单价不同input_tokens输入Token数计算输入成本output_tokens输出Token数计算输出成本cache_read_tokens缓存命中Token数计算折扣成本cache_write_tokens缓存写入Token数部分厂商写入也计费request_id请求唯一标识排查异常调用latency_ms响应耗时辅助判断是否重试导致重复计费大部分API的响应体里都会返回usage字段包含这些Token统计。你要做的就是在调用完成后把usage里的数据连同时间、模型一起写进日志文件或数据库。别嫌麻烦这一步做扎实了后面所有分析都是水到渠成。3.2 脚本的整体结构我写的这个脚本分三块读取日志、按模型和日期聚合、输出成本报表。日志格式假设是JSON Lines每行一个JSON对象这是最省事也最通用的格式。如果你的日志是CSV或者其他格式改一下读取部分就行。先定义单价配置。这部分我单独抽出来方便你替换成自己用的模型价格# pricing.py # 单价单位元 / 百万 Token PRICING { model-a: { input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75, }, model-b: { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25, }, }这里要说明一下cache_write的单价通常比普通input略高因为写入缓存本身有开销。有些厂商不单独收写入费那你就把它设成和input一样。单价一定要以你实际使用的厂商文档为准我这里的数字只是示例不要直接拿去用。3.3 核心计算逻辑接下来是计算部分。核心就是把每条日志的各类Token数乘以对应单价再除以一百万# cost_calculator.py import json from collections import defaultdict from pricing import PRICING def calc_cost(record): model record.get(model) price PRICING.get(model) if not price: return 0.0, f未知模型: {model} input_tokens record.get(input_tokens, 0) output_tokens record.get(output_tokens, 0) cache_read record.get(cache_read_tokens, 0) cache_write record.get(cache_write_tokens, 0) # 注意input_tokens 通常已包含 cache_read 部分 # 需要把缓存命中的部分单独拆出来避免重复计费 normal_input max(input_tokens - cache_read, 0) cost ( normal_input * price[input] output_tokens * price[output] cache_read * price[cache_read] cache_write * price[cache_write] ) / 1_000_000 return cost, None这里有个坑要重点提醒不同厂商对input_tokens的定义不一样。有的厂商返回的input_tokens是总输入里面已经包含了缓存命中的部分有的则是分开返回的。如果你不搞清楚这一点就会把缓存部分算两遍成本虚高。我的处理方式是先假设input_tokens包含cache_read用减法拆出普通输入。你接入新厂商时拿一条真实响应验证一下看数字对不对得上。3.4 按维度和时间聚合算完单条成本还不够你需要按模型、按天、按调用来源聚合才能看出问题在哪def aggregate(log_path): by_model defaultdict(float) by_date defaultdict(float) by_model_tokens defaultdict(lambda: {input: 0, output: 0, cache_read: 0}) errors [] with open(log_path, r, encodingutf-8) as f: for line_no, line in enumerate(f, 1): line line.strip() if not line: continue try: record json.loads(line) except json.JSONDecodeError: errors.append(f第{line_no}行解析失败) continue cost, err calc_cost(record) if err: errors.append(f第{line_no}行: {err}) continue model record.get(model, unknown) date record.get(timestamp, )[:10] by_model[model] cost by_date[date] cost by_model_tokens[model][input] record.get(input_tokens, 0) by_model_tokens[model][output] record.get(output_tokens, 0) by_model_tokens[model][cache_read] record.get(cache_read_tokens, 0) return by_model, by_date, by_model_tokens, errors聚合完之后打印成表格。我习惯用简单的文本表格不依赖第三方库跑在服务器上也不用装东西def print_report(by_model, by_date, by_model_tokens): print( * 60) print(按模型成本汇总) print( * 60) print(f{模型:20}{成本(元):15}{输入Token:15}{输出Token:15}) for model, cost in sorted(by_model.items(), keylambda x: -x[1]): t by_model_tokens[model] print(f{model:20}{cost:15.4f}{t[input]:15}{t[output]:15}) print() print( * 60) print(按日期成本汇总) print( * 60) for date, cost in sorted(by_date.items()): print(f{date} {cost:.4f} 元)跑起来之后你会得到一张清晰的报表。我第一次跑自己的日志时发现某个测试用的模型占了总成本的40%而它只处理了不到5%的请求——原因是我在调试时忘了关掉一个循环调用。这种问题不看报表根本发现不了。4. 从报表里读出优化空间几个真实的降本方向4.1 缓存命中率低先查提示词结构报表里如果cache_read_tokens占比很低说明缓存没起作用。最常见的原因是提示词前缀不稳定。比如系统提示词里带了当前时间戳、随机ID、或者每次都不一样的用户信息那缓存永远命中不了。正确的做法是把稳定不变的内容放在最前面把变化的内容放在后面。系统指令、角色设定、固定的知识库文档这些放前面用户的具体问题、当前会话的临时信息放后面。这样前缀部分才能被缓存复用。我调整过一次提示词顺序缓存命中率从不到10%提到了70%以上成本直接砍半。4.2 输出Token占比过高考虑限制生成长度如果报表显示输出Token的成本占了总成本的大头那你要看看是不是每次都在生成很长的内容。有些场景其实不需要那么长的输出比如分类任务、抽取任务答案可能就几个字但模型有时候会话痨输出一大段解释。解决办法是在提示词里明确要求简洁输出同时设置max_tokens上限。别小看这个上限它能防止个别请求失控生成超长内容把成本拉高。我一般会设一个比预期输出略大的值既不影响正常使用又能兜底。4.3 模型混用不是所有请求都需要最强模型报表按模型拆分之后你可能会发现某些简单任务也在用最贵的模型。比如意图识别、格式转换这种任务用小模型完全够用成本可能只有大模型的十分之一。我的做法是在应用层做一个路由根据任务类型选择模型。复杂的推理、创作类任务走大模型简单的分类、抽取、改写走小模型。这个改动不需要重构代码加一个判断逻辑就行。实测下来整体成本能降三到五成效果损失几乎感知不到。4.4 重复请求检测重试逻辑可能在偷偷烧钱日志里如果有大量相同或相似的请求可能是重试机制出了问题。比如网络超时后自动重试但第一次请求其实已经成功了只是响应没及时返回结果又发了一次。这种情况在报表上表现为请求数异常高但业务量没那么多。排查方法是按request_id或者请求内容做去重统计。如果发现重复率超过正常水平就要检查重试策略是不是重试次数设太多了是不是没有做幂等处理。我遇到过一次重试逻辑写成了无限循环一晚上跑掉了几百块第二天看报表才发现。5. 脚本落地时的几个实操细节5.1 日志量大了怎么办如果每天几十万条日志用Python逐行读JSON会有点慢。这时候可以按天切分日志文件只分析当天的或者用pandas批量读取速度会快很多。但我不建议一上来就上pandas先用标准库跑通逻辑确认报表准确了再考虑性能优化。过早优化容易把简单问题复杂化。另外日志文件建议按日期命名比如api_log_20250101.jsonl这样脚本可以接受日期参数只读指定文件。我一般会保留最近30天的日志更早的归档压缩既省空间又不影响分析。5.2 单价配置要能热更新单价是会变的今天这个价明天可能就调了。如果把单价硬编码在脚本里每次调价都要改代码重新部署很麻烦。我的做法是把单价配置放在一个独立的JSON文件里脚本启动时读取。调价时只改配置文件脚本不用动。{ model-a: {input: 3.0, output: 15.0, cache_read: 0.3, cache_write: 3.75}, model-b: {input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25} }读取的时候加个默认值兜底万一配置文件格式错了脚本还能用内置的默认单价跑不至于直接崩掉。5.3 时区问题别踩坑日志时间戳如果是UTC而你按本地日期聚合会出现跨天错位。比如北京时间早上8点的请求UTC是前一天晚上12点聚合到前一天去了。这个坑我在做日报的时候踩过数字对不上查了半天才发现是时区问题。解决办法很简单在聚合之前统一转成同一个时区。我一般统一用UTC存储展示的时候再转成本地时间。这样不管服务器在哪数据都是一致的。5.4 异常请求要单独标记有些请求的usage字段可能是空的或者Token数异常大比如几百万这些往往是异常情况。脚本里要对这些做标记不要直接算进总成本否则报表会被污染。我一般会设一个阈值比如单次请求超过10万Token就单独列出来人工检查。大部分情况下是提示词拼接出了bug把整个文档库都塞进去了。6. 把成本分析变成日常习惯脚本跑通只是第一步更重要的是把它变成日常动作。我现在习惯每天早上花五分钟看一眼昨天的成本报表重点看三个数总成本、缓存命中率、单次请求平均成本。这三个数只要有一个异常波动就能顺藤摸瓜找到原因。比如某天总成本突然翻倍先看是不是请求量涨了如果请求量没涨就看单次平均成本是不是高了如果平均成本高了再看是不是缓存命中率掉了或者某个模型被误用了。这套排查链路走下来基本十分钟内能定位到问题。还有一点成本分析的结果要反馈到开发流程里。比如新功能上线前先估算一下Token消耗设一个预算上限上线后对比实际消耗和估算偏差大的话复盘原因。这样慢慢就能建立起对成本的敏感度而不是等到账单来了才被动应对。最后分享一个我自己的小习惯我会把每个月的成本报表存一份年底拉出来看趋势。你会发现有些优化当时觉得效果一般但累积起来省下的钱相当可观。成本优化这件事单次看可能只是省几块钱但养成习惯之后它带来的收益是持续的。
返回列表