在 Python 中跟踪 AI agent token 成本:usage、预算、停止
记录每个请求的 usage 字段,按任务累加,并在美元预算处停止 agent。适用于 OpenAI 兼容 API 的可运行 Python 示例。

你的 agent 跑了十分钟,账单比任务本身还大。要弄清原因,请读取每个 API 响应中的 usage 对象,按任务累加,再乘以模型每 1M token 的价格。要限制它,就在每一步之后将累计总量与预算比较,一旦超出就停止循环。下面是一个实现此功能的小型 Python 类。
示例使用 OpenAI 兼容端点 https:/ 和模型 ID deepseek-(380K 上下文,每次回答最多 32,768 个输出 token)。如果你想在运行之前估算成本,那是另一回事,如何估算 AI agent 的成本 已涵盖。本文讨论的是在运行过程中进行测量和限制。
数字在哪里:usage 对象#
每个非流式聊天补全都会返回一个 usage 对象。DeepSeek 的 API 参考(2026 年 10 月 26 日检查)列出了以下字段:
prompt_tokens— 你这次发送的所有内容:系统提示、历史记录、工具结果。completion_— 模型写回的内容。tokens total_tokens— 两者相加。
DeepSeek 还记录了可选的额外字段:prompt_ 和 prompt_(它们相加等于 prompt_tokens),以及用于思考模型的 completion_。该页面没有显示发布日期,而且这些额外字段是 OpenAI 格式的扩展,因此下面的代码将它们视为可选,仅依赖前两个字段。
为什么要记录每一步而不只是最后一步?因为 agent 每次都会重新发送不断增长的上下文。第 1 步可能发送 2,000 个输入 token,第 30 步可能发送 9,000 个,而账单是所有这些的总和。原因在为什么长对话成本更高中解释,输入/输出拆分在输入与输出 token中说明。
一个能停止 agent 的计量器#
以下是全部内容:一个累加 token、每步打印一行并在超出预算时抛出错误的计量器,外加一个围绕它的简单 agent 循环。使用 pip install openai 安装 SDK,并将你的密钥放入 CHEAPTOKENS_。
import os
from openai import OpenAI
client = OpenAI(
base_url='https://api.cheaptokens.dev/v1',
api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'
# example prices, USD per 1M tokens: use the current ones from the pricing table
PRICE_IN = 0.10
PRICE_OUT = 0.40
class BudgetExceeded(Exception):
pass
class TaskMeter:
def __init__(self, budget_usd):
self.budget = budget_usd
self.steps = 0
self.tokens_in = 0
self.tokens_out = 0
self.reasoning = 0
@property
def cost(self):
return (self.tokens_in * PRICE_IN + self.tokens_out * PRICE_OUT) / 1_000_000
def add(self, usage):
self.steps += 1
self.tokens_in += usage.prompt_tokens
self.tokens_out += usage.completion_tokens
details = getattr(usage, 'completion_tokens_details', None)
self.reasoning += getattr(details, 'reasoning_tokens', 0) or 0
print(f'step {self.steps}: in={usage.prompt_tokens} '
f'out={usage.completion_tokens} task=${self.cost:.4f}')
if self.cost > self.budget:
raise BudgetExceeded(
f'budget ${self.budget:.2f} crossed at step {self.steps}: '
f'${self.cost:.4f}')
def run_task(goal, budget_usd=0.50, max_steps=40):
meter = TaskMeter(budget_usd)
messages = [
{'role': 'system', 'content': 'Work step by step. Reply DONE when finished.'},
{'role': 'user', 'content': goal},
]
for _ in range(max_steps):
resp = client.chat.completions.create(
model=MODEL, messages=messages, max_tokens=1024)
meter.add(resp.usage) # log first, then decide
reply = resp.choices[0].message.content or ''
messages.append({'role': 'assistant', 'content': reply})
if 'DONE' in reply:
break
messages.append({'role': 'user', 'content': 'Continue.'})
return meter
try:
meter = run_task('Summarise the main risks of a database migration.')
print(f'finished: {meter.steps} steps, ${meter.cost:.4f}')
except BudgetExceeded as err:
print('stopped:', err)其中有三个选择值得借鉴。
先记录再决定。 meter.add 先记录步骤,然后才检查预算,因此被停止的运行仍会留下完整的跟踪记录。检查发生在越线的那次调用之后,所以最多超出一个步骤。max_tokens=1024 限制最后一步的输出。
两个硬停止,而不是一个。 max_steps 捕获循环但花费不多的 agent;美元预算捕获每步花费很多的 agent。两者都需要。
价格集中在一处。 PRICE_IN 和 PRICE_OUT 是占位数字。从定价页面复制你模型的当前值;目前 DeepSeek-V4-Flash 是每 1M 输入 token $0.015,每 1M 输出 token $0.10。
40 步运行的总计#
假设你的 agent 执行 40 步,并且由于上下文增长,平均每步发送 6,000 个输入 token 并返回 400 个 token。那么该任务就是 40 × 6,000 = 240,000 个输入 token 和 40 × 400 = 16,000 个输出 token。

在 DeepSeek-V4-Flash 上,这花费 $0.0052。下表在其他模型上运行相同的工作负载,并将 OpenRouter 的参考价格与我们的价格并列:
| 模型 | 输入 $/1M | 输出 $/1M | 费用 | OpenRouter |
|---|---|---|---|---|
| GLM-5.3-Flash | $0.02 | $0.04 | $0.0054 | $0.0111 |
| DeepSeek-V4-Flash | $0.015 | $0.10 | $0.0052 | $0.0139 |
| MiniMax-M2.7 | $0.05 | $0.15 | $0.0144 | $0.0912 |
总计 240,000 个输入 token 和 16,000 个输出 token。每 100 万 token 的价格为当前价格,会随我们的资费一同变动;OpenRouter 列显示相同工作负载的参考价格。
总体而言,将输入和输出一起计算,对于此任务,DeepSeek-V4-Flash 比 OpenRouter 更便宜。还要注意 token 的分布:256,000 中有 240,000 是输入。你的 agent 拖带的上下文就是账单,这就是为什么日志中每步的 in= 数字值得关注。上面示例中每任务 $0.50 的预算是对失控的限制,而不是目标。
切换模型只需更改 MODEL。从 CheapTokens 文档中选择 ID:不同提供商的名称不同,价格标签中的短 ID 在 API 中无法使用。对于长 agent 循环,DeepSeek-V4-Flash 在一天中的任何时间都是同一价格,因此任务在中午和凌晨 3 点的成本相同。其限制见 DeepSeek 页面。
当模型在思考时#
如果回答的 token 数远多于你看到的文本,请查看 reasoning_tokens。思考 token 计为输出,因此推理密集的步骤可能产生比可见文本多得多的输出 token,并且它们按输出计费。当 API 返回该字段时,上面的计量器会将其收集到 meter.reasoning 中,不返回时保持为 0,因此它永远不会因缺少键而崩溃。如果你想查看输出中有多少是思考,请将其打印在 out= 旁边。推理努力与思考 token 解释了原因。
流式传输:usage 最后出现#
如果你的 agent 使用流式传输,普通块中没有 usage。在 OpenAI 格式中,你通过 stream_ 请求它。DeepSeek 的参考(2026 年 10 月 26 日检查)指出,该选项仅与 stream=True 一起使用(否则 API 返回 400),usage 在除一个块之外的每个块中都是 null,并且 data: [DONE] 之前的最后一个块携带整个请求的总计。因此,在流式循环中,在 usage 不为空的块上调用一次 meter.add。上面的非流式循环是最简单的起点;Python 中的流式传输 包含循环本身。
如果数字看起来不对#
usage为 None。 你在没有include_usage的情况下进行流式传输。添加它,或切换到非流式调用。- 总计低于余额下降。 检查每次调用是否都经过计量器:重试、摘要调用或在
run_task之外进行的工具选择调用很容易被遗漏。将client.包装在一个函数中,并让所有内容都通过它。chat. completions. create - 每步成本持续攀升。 对于将所有内容追加到
messages的循环来说,这是正常的。修剪旧的工具输出或摘要历史;工具输出脱离上下文 展示了这个思路。 BudgetExceeded在第 2 步触发。 你的预算小于一步。检查PRICE_IN是每 1M token 还是每 1K。
要存储这些数字,请将 meter.steps、meter.tokens_in、meter.tokens_out 和 meter.cost 写为每个任务一行 JSON。一周的这些行会告诉你 agent 每步的真实 token 数,这是之后每次估算都要使用的数字。模型、价格和第一个请求见文档;工具调用在 Python 中的函数调用 中介绍。



