跑通 DeepSeek API 之后,你多半会在返回 JSON 里看到一段 usage,里面有三个数字:prompt_tokens、completion_tokens、total_tokens。可真要按天、按模型、按请求把 token 用量统计起来,问题就来了——流式响应里的 usage 经常是空的,非流式响应里几个字段又容易混着用。这篇不重讲接入,只围绕 token 用量统计,从一次最小调用开始,把非流式与流式的取值、累计和排错走一遍。读完你会拿到一个能直接落盘的统计脚本,也知道哪些报错对应哪个填错的位置。

1. 场景与前置条件

这篇解决的是:你已经能调用 deepseek-v3.2,但想把每次请求消耗的 token 数记录、累计、按天聚合。只靠聊天窗口里看回复,看不到精确 token 用量。要看 API 返回体里的 usage 对象,并且把三个字段都留下。

前置条件有四个。第一,Python 3.8 或更高版本,示例使用 openai Python SDK 1.x。第二,已有一个 sk_live_ 开头的 API Key,并且它在控制台里已经绑定到项目。第三,本机能正常访问 OpenAI 兼容端点的 /chat/completions 路径。第四,模型调用名要从模型广场复制 stable alias,示例里用的是 deepseek-v3.2,不要根据展示名自行拼接。

这篇文章的侧重点不是通用接入,而是 token 用量统计里最容易被忽略的两个细节。一个是非流式响应里只存 total_tokens,之后想按 prompt 和 completion 拆分时发现原始数据不够。另一个是流式响应中间 chunk 的 usage 为 None,末尾才出现一个 choices 为空但 usage 有值的 chunk。很多统计脚本就是在这里漏算、甚至直接报错。

2. 环境准备

先在项目里建好虚拟环境,再安装两个包:

python -m venv .venv
source .venv/bin/activate
# Windows PowerShell 执行 .venv\Scripts\Activate.ps1
pip install openai python-dotenv

openai 要用 1.x,python-dotenv 用来读本地 .env 文件。装包完成之后,把 API Key 放进项目根目录的 .env 文件里。密钥变量名与脚本里 os.environ 读取的名称保持一致,别改成自己的项目缩写,否则代码照抄后会拿到 None。

为什么要用环境变量而不是硬编码?硬编码的密钥一旦进入 git 历史,很难删干净。环境变量还可以在 CI、容器和同事本机用不同值注入。.env 文件必须加进 .gitignore,因为本地文件很容易被误提交。加载 .env 可以在脚本最前面调用 python-dotenv,也可以直接在当前终端 export。跑示例前,先确认当前终端能读到这个 key。

3. 最小可跑示例

下面是一次非流式调用的完整脚本,跑通后终端会打印三类 token 数:

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ['SILVAMUX_API_KEY'],
    base_url='https://www.silvamux.com/api/v1',
)

completion = client.chat.completions.create(
    model='deepseek-v3.2',
    messages=[
        {'role': 'system', 'content': '你只做简短回答。'},
        {'role': 'user', 'content': '用一句话解释 API 中的 token。'},
    ],
    temperature=0.2,
)

usage = completion.usage
print('prompt_tokens:', usage.prompt_tokens)
print('completion_tokens:', usage.completion_tokens)
print('total_tokens:', usage.total_tokens)
print('model:', completion.model)
print('finish_reason:', completion.choices[0].finish_reason)

API Key 从环境变量读取,base_url 这一行必须保持原样,尤其不要删掉 www 前缀,也不要把域名改成 api 开头的子域。那些地址不存在,脚本会直接报连接错误。模型名 deepseek-v3.2 是 stable alias,如果写成 deepseek-chat 或自行拼一个版本号,会得到 404。

三个 usage 字段要分开看。prompt_tokens 是 system 加 user 输入的总 token,completion_tokens 是模型生成的 token,total_tokens 是两者之和。做 token 用量统计时,不能只保存 total_tokens,因为后续想按输入和输出拆分成本,或定位某次请求为什么消耗高,都需要前两个字段。finish_reason 也要记,一旦出现 length,说明输出被截断,这次 completion_tokens 只是部分结果。

4. 逐步扩展

大多数实时交互会开流式。流式下 usage 的取值逻辑不一样,不能照搬非流式写法。下面把流式统计和 JSONL 落盘放在同一个脚本里:

import os
import json
from datetime import datetime, timezone
from openai import OpenAI

client = OpenAI(
    api_key=os.environ['SILVAMUX_API_KEY'],
    base_url='https://www.silvamux.com/api/v1',
)

stream = client.chat.completions.create(
    model='deepseek-v3.2',
    messages=[
        {'role': 'user', 'content': '数一下:苹果、香蕉、苹果、梨,苹果出现了几次?'},
    ],
    stream=True,
)

content_parts = []
usage = None
for chunk in stream:
    if chunk.choices:
        delta = chunk.choices[0].delta
        if delta and getattr(delta, 'content', None):
            content_parts.append(delta.content)
    if chunk.usage is not None:
        usage = chunk.usage

print('reply:', ''.join(content_parts))

if usage:
    print('prompt_tokens:', usage.prompt_tokens)
    print('completion_tokens:', usage.completion_tokens)
    print('total_tokens:', usage.total_tokens)

    record = {
        'ts': datetime.now(timezone.utc).isoformat(),
        'model': 'deepseek-v3.2',
        'prompt_tokens': usage.prompt_tokens,
        'completion_tokens': usage.completion_tokens,
        'total_tokens': usage.total_tokens,
    }
    with open('token_usage.jsonl', 'a', encoding='utf-8') as f:
        f.write(json.dumps(record, ensure_ascii=False) + '\n')
else:
    print('no usage chunk received')

这里的关键是内容累计和 usage 保存分成两个判断。流式中间的 chunk 通常 choices 有内容但 usage 为 None,末尾可能出现一个 choices 为空但 usage 非空的块。不要只靠 data: [DONE] 结束判断,那样容易把最后的用量块丢掉。统计流式 token 用量时,保存最后一个非空 usage 即可。

落盘前后的差别是:原来只在终端打印,现在每次请求追加一条 JSONL 记录。时间戳用 UTC,不要用本地时间,这样服务器、CI 和本地统计不会因为时区错位。按天聚合 token 用量统计时,ts 和 model 都保留,排查问题会容易很多。

5. 排错

排错一:openai.AuthenticationError: Error code: 401 - ...

直接原因是当前进程没有读到有效的 API Key。可能是 .env 文件放错目录、变量名不一致、复制 key 时少了 sk_live_ 前缀,或者改了 .env 后没有重启脚本。修法是先用代码打印环境变量确认非空,再重新加载 .env 并重启。不要只在网页里测试通过,就假定本地一定读到了 key。

排错二:openai.NotFoundError: Error code: 404 - model not found

这个错误几乎都来自模型名填错。常见写法是 deepseek-chat 或 deepseek-v3,但平台调用名与上游官方命名不一定一致。直接原因是模型调用名不在可用列表里。修法是到模型广场详情页复制 stable alias,别根据展示名自行拼版本。

排错三:AttributeError: 'NoneType' object has no attribute 'total_tokens'

这是流式 token 用量统计里最常见的错误。中间 chunk 的 usage 为 None,代码却直接访问 chunk.usage.total_tokens。修法是把 usage 读取放到 if chunk.usage is not None 之后,或者循环结束后取最后一个非空 usage。不要假设每个 chunk 都带完整用量。

排错四:openai.RateLimitError: Error code: 429 - ...

批量统计用法时,如果一次开太多并发,会触发限流。直接原因是短时间请求太密。修法是做指数退避重试,间隔 1 秒、2 秒、4 秒这样递增;无间隔重试只会让限流更久。批量回放不算实时任务,降低并发更稳。

常见问题

问:DeepSeek API 统计 token 时,系统提示词算不算用量?

答:算。system 和 user 消息都会进入 prompt_tokens,工具调用传的 JSON 也会算。统计时如果只看用户输入长度,会明显低估 prompt 消耗。所以不能把对话界面里的字符数当成 API 的 token 数。

问:流式响应的 usage 为什么有时在 choices 为空的 chunk 里?

答:服务端要等生成结束后才能算清 completion token,因此把它放在流的末尾。末尾 chunk 可能 choices 为空,只有 usage,客户端应以流结束为准,不要只等 data: [DONE]。保存最后一个非空 usage 就是正确做法。

问:想按项目或功能统计 token 用量,应该在哪做标记?

答:可以在请求头中添加 X-Request-Id,平台会沿用这个 ID,排查问题时也用它。本地统计时把 request_id 和 usage 一起落盘,后续按项目聚合就不会混。不要用本地时间戳当唯一标记,并发下很容易重复。

以上示例在千木SilvaMux的 OpenAI 兼容端点上验证,模型调用名为 deepseek-v3.2。模型调用名等参数可在 开发者文档 查询。
模型广场

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐