返回博客

API 与 SDK指南

在 Python 中跟踪 AI agent token 成本:usage、预算、停止

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

作者:CheapTokens Team发布于 5 分钟阅读

终端风格代码卡片:每步 usage 日志和停止 AI agent 的预算检查
本页内容
  1. 数字在哪里:usage 对象
  2. 一个能停止 agent 的计量器
  3. 40 步运行的总计
  4. 当模型在思考时
  5. 流式传输:usage 最后出现
  6. 如果数字看起来不对

你的 agent 跑了十分钟,账单比任务本身还大。要弄清原因,请读取每个 API 响应中的 usage 对象,按任务累加,再乘以模型每 1M token 的价格。要限制它,就在每一步之后将累计总量与预算比较,一旦超出就停止循环。下面是一个实现此功能的小型 Python 类。

示例使用 OpenAI 兼容端点 https://api.cheaptokens.dev/v1 和模型 ID deepseek-ai/DeepSeek-V4-Flash-0731(380K 上下文,每次回答最多 32,768 个输出 token)。如果你想在运行之前估算成本,那是另一回事,如何估算 AI agent 的成本 已涵盖。本文讨论的是在运行过程中进行测量和限制。

数字在哪里:usage 对象#

每个非流式聊天补全都会返回一个 usage 对象。DeepSeek 的 API 参考(2026 年 10 月 26 日检查)列出了以下字段:

  • prompt_tokens — 你这次发送的所有内容:系统提示、历史记录、工具结果。
  • completion_tokens — 模型写回的内容。
  • total_tokens — 两者相加。

DeepSeek 还记录了可选的额外字段:prompt_cache_hit_tokens 和 prompt_cache_miss_tokens(它们相加等于 prompt_tokens),以及用于思考模型的 completion_tokens_details.reasoning_tokens。该页面没有显示发布日期,而且这些额外字段是 OpenAI 格式的扩展,因此下面的代码将它们视为可选,仅依赖前两个字段。

为什么要记录每一步而不只是最后一步?因为 agent 每次都会重新发送不断增长的上下文。第 1 步可能发送 2,000 个输入 token,第 30 步可能发送 9,000 个,而账单是所有这些的总和。原因在为什么长对话成本更高中解释,输入/输出拆分在输入与输出 token中说明。

一个能停止 agent 的计量器#

以下是全部内容:一个累加 token、每步打印一行并在超出预算时抛出错误的计量器,外加一个围绕它的简单 agent 循环。使用 pip install openai 安装 SDK,并将你的密钥放入 CHEAPTOKENS_API_KEY。

python
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。

第 1 步发送 2,000 个输入 token,第 30 步发送 9,000 个;任务总量是所有步骤的总和
Illustration with the article's assumptions, not a run result

在 DeepSeek-V4-Flash 上,这花费 $0.0052。下表在其他模型上运行相同的工作负载,并将 OpenRouter 的参考价格与我们的价格并列:

模型 费用 OpenRouter
GLM-5.3-Flash $0.0054 $0.0111
DeepSeek-V4-Flash $0.0052 $0.0139
MiniMax-M2.7 $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_options={'include_usage': True} 请求它。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 中的函数调用 中介绍。