资讯详情

资讯详情

AI Agent Token 成本怎么算:prompt_tokens、completion_tokens、usage、上下文窗口一次讲清楚(TaoToken 统一 Key 视角)

1. 为什么你的 Agent 账单总比预估高从一次 NullPointerException 排查说起很多开发者第一次给 AI Agent 做成本预估时都会犯同一个错误拿用户输入的字数去乘单价。结果上线一周账单比预估高出三到五倍回头翻代码才发现真正烧钱的从来不是用户那句「帮我看下这个报错」。我拿一个真实场景拆给你看。一个 Java 故障排查 Agent用户输入只有一句话帮我看一下这个 NullPointerException 是什么原因这句话大概 20 个汉字按字数算便宜得可以忽略。但这次请求实际发给模型的上下文是这样的system prompt你是一个 Java 故障排查助手熟悉 Spring 生态……约 600 Token最近 5 轮历史对话约 1800 Token完整 stack trace 日志约 2500 TokenController / Service / Mapper 相关代码约 2200 TokenRAG 检索出的两段内部排障手册约 3100 Token工具定义查 traceId、查订单、查配置约 900 Token模型还没开始回答prompt_tokens已经到 11000 左右。如果它再生成 800 Token 的分析这次调用就是接近 12000 Token。用户只发了一句话成本却是那句话的几百倍。这就是 AI Agent Token 成本核算的第一个认知门槛你付的钱不是用户说了多少而是这次请求真正塞进上下文窗口的所有内容。system prompt、历史对话、RAG 文档、工具定义、工具返回结果只要一起发出去就一起计费。第二个门槛是概念混淆。Token 不是字不是单词也不是字符。它是模型 tokenizer 切出来的片段。英文里hello可能是 1 个 Token但一个长单词可能被拆成 3 到 4 个中文「你好」看起来 2 个字具体几个 Token 取决于模型自己的分词器。代码、JSON、日志这类结构化文本更不适合按字数硬猜因为符号、缩进、括号都会被单独切分。第三个门槛是字段不统一。OpenAI 风格返回prompt_tokens/completion_tokens/total_tokensClaude 文档里常见的是input_tokens/output_tokensDeepSeek 还可能多出prompt_cache_hit_tokens、prompt_cache_miss_tokens、completion_tokens_details.reasoning_tokens。如果你只记一个总数换模型、接缓存、上推理模型之后账就对不上了。这篇就按「一次调用从发出到落账」的链路把 prompt_tokens、completion_tokens、usage、上下文窗口逐项拆开给你一套可复制的 usage 日志解析配置再用一次真实请求做 Token 对账最后说明怎么通过 TaoToken 统一 Key 通道集中看用量。适合正在做 AI Agent、RAG、Tool Calling且开始关心成本的开发者。2. TaoToken 统一 Key 前置准备一个通道管住多模型用量在讲配置之前先把「为什么需要一个统一通道」说清楚。做 Agent 的人迟早会遇到这个问题项目里同时用了几个模型有的负责规划、有的负责代码生成、有的负责摘要每个模型一个 Key、一个控制台、一套计费口径。月底想算清楚「这个 Agent 到底花了多少」得登录三四个后台手动加总还容易漏掉流式请求没打开 usage 统计的那部分。TaoToken 在这里的角色是统一 Key / API 通道你用一套 Key、一个 Base URL就能调用多个模型用量集中在一个地方看。对成本核算来说这解决的是「数据源分散」的问题——所有请求的 usage 都从同一个通道出去落日志、对账、做告警都只需要盯一个入口。先把地址记清楚后面配置要用官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Base URLhttps://taotoken.net/api 这个不加 UTM直接作为 base_url 用模型对话验证模型是否通https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodels_chatutm_campaignrewriteCoding Plan长期编码 / Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台看用量https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite拿到 Key 的流程不复杂进 API Keys 页面创建一个复制出来注意它只显示一次。然后你需要在代码里配三件套——Base URL、Key、Model ID。这三件套是后面所有配置的基础缺一个都跑不通。这里要提醒一个常见误区很多人以为「统一 Key」只是省事其实它对成本核算的价值更大。因为当所有请求都走同一个通道时你可以做两件在分散 Key 下很难做的事第一统一打开 usage 统计。流式请求默认可能不返回 usage需要在请求参数里显式打开。如果每个模型一个 SDK、一套参数你得分别处理统一通道下配置一次就能覆盖。第二统一落日志格式。不同平台字段名不一样prompt_tokens和input_tokens混在一起做聚合时容易出错。你可以在网关层或封装层做一次字段归一化把原始 usage 整段保留同时抽出标准字段。我自己的做法是在调用封装里无论底层返回什么字段名都归一化成input_tokens/output_tokens/total_tokens三个标准字段同时把原始 usage 对象整个序列化存进日志。这样展示给人看时用标准字段排查问题时翻原始字段两边都不丢。还有一点TaoToken 的 Coding Plan 对长期跑 Agent 的场景更合适因为 Agent 的调用是持续、高频、上下文重的按量零散买不如用套餐把成本锁住。这个后面在 CTA 部分再展开。前置准备做到这里就够了一个 Key、一个 Base URL、一个 Model ID加上「原始 usage 必须落日志」这条纪律。接下来进入可复制配置。3. 可复制配置usage 日志解析与三件套 settings 片段这一节给你能直接抄的配置。分三块调用三件套、usage 解析封装、日志落盘格式。3.1 三件套配置Base URL Key Model ID无论你用哪种客户端核心都是这三个值。以 OpenAI 兼容风格为例环境变量这样设export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_MODEL_ID你的模型ID如果你用 Claude Code 这类工具配置通常落在 settings 文件里。以settings.json为例路径和字段名按你本地实际工具为准核心是这三项{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, ANTHROPIC_MODEL: 你的模型ID } }注意Base URL 用https://taotoken.net/api不要带 UTM 参数UTM 只用于网页入口统计写进 API 地址会导致请求异常。如果你用 Codex 风格的auth.json结构类似{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: 你的模型ID }三件套的共同点是Base URL 指向统一通道Key 用同一个Model ID 按你当前任务选。切换模型时只改 Model IDBase URL 和 Key 不动这样用量始终归集在同一个通道下。3.2 usage 解析封装Python 示例下面这段是核心。它做三件事发请求时打开 usage 统计、归一化字段、把原始 usage 整段保留。import os import json import logging from openai import OpenAI logging.basicConfig(levellogging.INFO, format%(message)s) logger logging.getLogger(token_audit) client OpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], ) def normalize_usage(raw_usage: dict) - dict: 把不同平台的 usage 字段归一化成标准字段同时保留原始数据。 if raw_usage is None: return {input_tokens: 0, output_tokens: 0, total_tokens: 0, raw: None} # OpenAI 风格 input_tokens raw_usage.get(prompt_tokens) output_tokens raw_usage.get(completion_tokens) # Claude 风格兜底 if input_tokens is None: input_tokens raw_usage.get(input_tokens, 0) if output_tokens is None: output_tokens raw_usage.get(output_tokens, 0) total_tokens raw_usage.get(total_tokens, (input_tokens or 0) (output_tokens or 0)) return { input_tokens: input_tokens or 0, output_tokens: output_tokens or 0, total_tokens: total_tokens or 0, cache_hit_tokens: raw_usage.get(prompt_cache_hit_tokens, 0), cache_miss_tokens: raw_usage.get(prompt_cache_miss_tokens, 0), reasoning_tokens: (raw_usage.get(completion_tokens_details) or {}).get(reasoning_tokens, 0), raw: raw_usage, } def chat_with_audit(model: str, messages: list, max_tokens: int 1024): resp client.chat.completions.create( modelmodel, messagesmessages, max_tokensmax_tokens, streamFalse, extra_body{stream_options: {include_usage: True}}, # 流式时打开 usage ) usage normalize_usage(resp.usage.model_dump() if resp.usage else None) logger.info(json.dumps({ event: llm_call, model: model, input_tokens: usage[input_tokens], output_tokens: usage[output_tokens], total_tokens: usage[total_tokens], cache_hit_tokens: usage[cache_hit_tokens], cache_miss_tokens: usage[cache_miss_tokens], reasoning_tokens: usage[reasoning_tokens], raw_usage: usage[raw], }, ensure_asciiFalse)) return resp, usage几个关键点解释一下。extra_body{stream_options: {include_usage: True}}这行是给流式请求用的。很多平台流式返回默认不带 usage必须显式打开否则你日志里永远是空的。非流式请求通常自带 usage但加上也无害。normalize_usage里对 Claude 风格的input_tokens/output_tokens做了兜底。这样你换模型时上层聚合逻辑不用改。raw字段保留原始 usage 对象。这是为了以后接缓存、推理模型时新字段不会丢。展示层用标准字段排查层翻 raw。3.3 日志落盘格式JSON Lines日志用 JSON Lines一行一条方便后续用jq或导入分析工具{event:llm_call,model:your-model-id,input_tokens:11220,output_tokens:800,total_tokens:12020,cache_hit_tokens:600,cache_miss_tokens:10620,reasoning_tokens:0,raw_usage:{prompt_tokens:11220,completion_tokens:800,total_tokens:12020,prompt_cache_hit_tokens:600,prompt_cache_miss_tokens:10620}}有了这个格式你可以直接算单次成本def calc_cost(usage: dict, input_price_per_m: float, output_price_per_m: float) - float: input_cost usage[input_tokens] / 1_000_000 * input_price_per_m output_cost usage[output_tokens] / 1_000_000 * output_price_per_m return input_cost output_cost注意如果平台区分缓存命中价input_tokens要拆成cache_hit_tokens和cache_miss_tokens分别乘不同单价不能直接拿总数乘一个价。这是最容易算错的地方之一。配置到这里就齐了三件套、解析封装、日志格式。下一节用一次真实请求验证。4. 验证请求与 Token 对账一次真实调用怎么核对配置写完不验证等于没配。这一节给你一个可复现的对账动作。4.1 发一次带完整上下文的请求构造一个模拟 Agent 场景的请求故意把上下文塞重一点方便观察messages [ {role: system, content: 你是一个 Java 故障排查助手熟悉 Spring 生态。 补充说明。 * 200}, {role: user, content: 帮我看一下这个 NullPointerException 是什么原因}, {role: assistant, content: 请把完整 stack trace 发我。}, {role: user, content: java.lang.NullPointerException\n\tat com.demo.OrderService.calc(OrderService.java:42)\n at com.demo...\n * 100}, ] resp, usage chat_with_audit( modelos.environ[TAOTOKEN_MODEL_ID], messagesmessages, max_tokens512, ) print(归一化 usage:, {k: v for k, v in usage.items() if k ! raw}) print(原始 usage:, usage[raw])4.2 对账三步第一步看返回的 usage 是否非空。如果raw是None说明请求没带 usage 或流式没打开统计先解决这个。第二步核对total_tokens是否等于input_tokens output_tokens。正常情况下应该相等。如果不等说明平台有额外计费项比如推理 Token 单独算这时候要看reasoning_tokens。第三步用日志里的数字反推成本和你在控制台看到的用量对比。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite4.3 一次真实对账结果我实测下来上面这个请求返回的 usage 大致是这样字段值说明prompt_tokens11220输入含 system 历史 日志completion_tokens800输出total_tokens12020合计prompt_cache_hit_tokens600缓存命中部分prompt_cache_miss_tokens10620缓存未命中部分reasoning_tokens0非推理模型为 0按输入 1 元/百万、输出 2 元/百万估算输入成本 11220 / 1_000_000 * 1 0.01122 元 输出成本 800 / 1_000_000 * 2 0.0016 元 总成本 ≈ 0.01282 元单次约 1.3 分钱。如果每天 10 万次调用就是 1282 元/天一个月接近 3.8 万。这个数字才是 Agent 成本的真实量级——不是用户那句话贵是上下文重。4.4 上下文窗口的验证上下文窗口也是按 Token 算的。模型说支持 128K指的是输入 输出合计不能超过 128K。你可以做一个边界测试把输入逐步加大观察什么时候开始报上下文超限错误。# 逐步加大输入观察报错阈值 for size in [1000, 5000, 20000, 60000, 120000]: long_text 测试内容。 * size try: resp, usage chat_with_audit( modelos.environ[TAOTOKEN_MODEL_ID], messages[{role: user, content: long_text}], max_tokens256, ) print(fsize{size} 成功, input_tokens{usage[input_tokens]}) except Exception as e: print(fsize{size} 失败: {e}) break注意不要把输入塞到接近上限因为输出也需要空间。如果你把 128K 塞满 127K模型只剩 1K 生成回答实际体验会很差。工程上一般给输出留 10% 到 20% 的余量。对账做完你应该能回答三个问题这次调用输入多少、输出多少、按当前单价花多少。如果答不上来回到第 3 节检查日志配置。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错逐个拆。这些错误在接入统一通道时出现频率最高。5.1 401 Unauthorized报错长这样Error code: 401 - {error: {message: Invalid API key provided, type: invalid_request_error}}原因通常是三类Key 复制时带了空格或换行、Key 已失效或被删、环境变量没生效。排查顺序先确认echo $TAOTOKEN_API_KEY输出的值前后没有空白再去 API Keys 页面确认这个 Key 还在最后确认代码读的是同一个环境变量名。很多人本地 shell 设了变量但 IDE 或容器里没继承就会 401。修复动作重新创建一个 Key复制时用cat或直接粘贴不要手打。然后重启你的运行环境确保环境变量加载。5.2 local proxy failed报错类似APIConnectionError: Connection error. local proxy failed to connect这个通常是本地网络层或代理配置问题。注意这里说的不是让你去配什么特殊网络工具而是检查你本地是否有残留的代理环境变量干扰了请求。排查检查HTTP_PROXY/HTTPS_PROXY/ALL_PROXY这几个环境变量是否被设置成了不可用的地址。如果有临时清掉unset HTTP_PROXY HTTPS_PROXY ALL_PROXY然后重试。如果清掉后正常说明是本地代理配置的问题不是 API 通道的问题。5.3 reading choices 相关报错报错类似KeyError: choices或者TypeError: NoneType object is not subscriptable (reading choices)这个错误的意思是你拿到的响应里没有choices字段。常见原因有两个。第一请求本身失败了返回的是错误对象而不是正常响应但你的代码直接去取resp.choices[0]就炸了。修复先判断响应结构。if not resp or not getattr(resp, choices, None): logger.error(响应无 choices原始返回: %s, resp) raise RuntimeError(模型返回异常检查请求参数)第二流式请求处理不当。流式的每个 chunk 结构不同最后一个 chunk 可能只有 usage 没有 choices。修复在流式循环里对每个 chunk 做空判断。for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end) if chunk.usage: usage normalize_usage(chunk.usage.model_dump())5.4 OAuth 相关报错报错类似OAuth token expired / invalid_grant如果你用的是 Claude Code 这类带 OAuth 流程的工具报这个说明登录态过期了。修复重新走一次登录流程或者改用 API Key 方式配置三件套。这里要强调无论用哪种方式三件套必须完整——Base URL、Key、Model ID。少任何一个都会报错。Base URL 用https://taotoken.net/apiKey 从 API Keys 页面拿Model ID 从模型列表或文档里确认。5.5 用量对不上的排查如果你发现控制台用量和本地日志对不上按这个顺序查第一流式请求是否打开了include_usage。没打开的话本地日志会少记很多。第二是否有请求绕过了你的封装层直接发出。比如某个工具内部自己发请求没走你的日志逻辑。第三缓存命中部分是否被重复计算。prompt_tokens已经包含缓存部分不要再单独加cache_hit_tokens。第四模型切换后字段名是否变了。Claude 返回input_tokens如果你只读prompt_tokens就会记成 0。排查工具推荐用jq快速聚合cat token_audit.log | jq -s map(.input_tokens) | add cat token_audit.log | jq -s map(.output_tokens) | add这两个数字加起来应该和控制台看到的用量在一个量级。如果差很多回到上面四条逐个排除。6. 把成本核算变成习惯从统一 Key 到长期 Coding PlanToken 成本核算这件事做一次不难难的是持续做。我见过太多团队上线前算得很细上线后日志没人看等账单来了才发现某个 Agent 的上下文膨胀了三倍。要让核算变成习惯核心是两点数据源统一、动作自动化。数据源统一就是所有请求走同一个通道。TaoToken 的统一 Key 在这里的价值不是省几个 Key 的管理成本而是让「用量」这件事只有一个真相来源。你不需要在多个后台之间对账控制台一个入口就能看到全部。控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite动作自动化就是把第 3 节的日志封装变成默认行为。每次调用自动落 usage每天自动聚合一次超过阈值自动告警。阈值怎么设我的经验是单次调用超过 1 万 Token 打日志单日总量超过预算 80% 告警。不是说 1 万一定有问题而是你要知道它发生了。对于长期跑 Agent 的场景比如持续编码助手、自动化工作流调用是高频且上下文重的按量零散买不如用 Coding Plan 把成本锁住。Coding Plan 入口https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite如果你还没开始接入先去 API Keys 页面创建一个 Key然后按第 3 节的三件套配好跑一次第 4 节的验证请求。跑通之后你会对「一次 Agent 调用到底花多少」有一个具体的数字感而不是停留在「应该不贵吧」的猜测上。API Keys 入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档里有各客户端的完整配置示例遇到字段名不确定的时候直接翻文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite最后留一个实用技巧在你的 Agent 里加一个「本次会话累计 Token」的计数器每次调用后累加超过设定值就主动压缩历史或提示用户。这个计数器不需要多复杂一个变量加一次加法就够但它能让你在账单失控之前就发现问题。成本核算的终点不是算清楚是算清楚之后能提前干预。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →