openai-agents-python 使用量追踪完全指南:从 Run 上下文到会话与检查点的 Token 计量
【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python
openai-agents-python(Agents SDK)为每一次 Agent 运行自动追踪 token 用量,开发者可通过运行上下文读取这些数据,用于成本监控、限额控制和数据分析。本文基于 docs/usage.md 及其韩语译本 docs/ko/usage.md 的系统讲解,并结合仓库源码 src/agents/usage.py、src/agents/run_context.py 与 src/agents/model_settings.py 的实现细节,带读者完整掌握"追踪什么、如何读取、如何精细化计量"这条主线,读完后可以立即在真实项目中落地 token 用量统计。
追踪了哪些指标
SDK 在每次运行中自动维护一个聚合的Usage对象,其核心字段如下:
| 字段 | 含义 |
|---|---|
requests | 发起的 LLM API 调用次数 |
input_tokens | 发送的总输入 token 数 |
output_tokens | 接收到的总输出 token 数 |
total_tokens | 输入 + 输出 |
request_usage_entries | 每次请求的用量明细列表(RequestUsage对象) |
details(嵌套字段) | input_tokens_details.cached_tokens、input_tokens_details.cache_write_tokens、output_tokens_details.reasoning_tokens |
其中details部分与 OpenAI Responses API 的 usage 细节结构对齐:cached_tokens表示命中缓存的输入 token,cache_write_tokens表示写入缓存的 token,reasoning_tokens表示推理模型用于思考过程的 token。从源码看,Usage的__post_init__会把缺失的可选细节字段统一规范化为0(而不是None),避免后续相加时出现TypeError;add()方法则负责把单次模型调用的用量累加到运行总量上,并自动在request_usage_entries中保留逐请求的明细。
requests的计数口径值得一提:适配器可以通过_mark_requests_completed_without_usage()(见 src/agents/usage.py)显式登记"完成了但没有用量"的物理请求次数,也就是说即使某些提供方不返回 usage,调用次数依然会被如实计入。
从一次运行中读取用量
Runner.run(...)执行完毕后,通过result.context_wrapper.usage即可访问本次运行聚合后的用量:
result = await Runner.run(agent, "What's the weather in Tokyo?") usage = result.context_wrapper.usage print("Requests:", usage.requests) print("Input tokens:", usage.input_tokens) print("Output tokens:", usage.output_tokens) print("Total tokens:", usage.total_tokens)这里的context_wrapper是RunContextWrapper,它除了携带你传入的context业务对象外,还维护一个usage: Usage字段,注释明确说明"这是到目前为止该 agent 运行的用量";对于流式响应,该值在流的最后一个 chunk 处理完成前是滞后的。
需要强调的聚合范围是:用量横跨运行期间发生的所有模型调用,包括触发工具调用(tool call)或交接(handoff)的那些模型调用。也就是说一个 Agent 内部多次请求 LLM、调用子 Agent、执行交接,最终拿到的usage都是这些请求的总和。
自动压缩会话的用量归并
当使用OpenAIResponsesCompactionSession且其在运行结束前自动压缩历史记录时,responses.compact请求上报的用量也会被加进同一运行的合计中。反过来,如果在运行之外手动调用run_compaction(),由于没有包裹它的运行上下文,它不会去更新之前运行返回的 usage 对象。更完整的说明见 OpenAI Responses 压缩会话。
第三方适配器下启用用量上报
用量上报行为因第三方适配器与提供方后端而异。当通过第三方适配器访问模型、又需要准确的result.context_wrapper.usage值时,注意以下两点:
- 使用
AnyLLMModel:只要上游提供方返回用量就会被自动透传;但通过 Chat Completions 后端流式响应时,可能需要ModelSettings(include_usage=True)才会产生用量 chunk。 - 使用
LitellmModel:部分提供方后端默认不上报用量,因此常常需要设置ModelSettings(include_usage=True)。
include_usage字段定义于ModelSettings,注释标明"仅适用于 Chat Completions API"——这是流式场景下能否拿到 usage chunk 的关键开关。具体部署前,建议对照 Models 指南中的第三方适配器一节,并在目标提供方后端上实测用量上报是否准确。
逐请求用量追踪
SDK 会自动把每一次 API 请求的用量记录在request_usage_entries中,这对精细化的成本计算和上下文窗口占用监控非常有价值:
result = await Runner.run(agent, "What's the weather in Tokyo?") for i, request in enumerate(result.context_wrapper.usage.request_usage_entries): print(f"Request {i + 1}: {request.input_tokens} in, {request.output_tokens} out")每个条目是一个RequestUsage对象,包含该单次请求的input_tokens、output_tokens、total_tokens以及各自的 details。聚合与明细的关系在源码 docstring 里有直观的例子:一次运行发起 3 次 API 调用、输入分别为 100K / 150K / 80K token,则聚合的input_tokens是 330K,而request_usage_entries保留[100K, 150K, 80K]的分解,便于逐项核算与上下文窗口管理。
add()的合并逻辑(src/agents/usage.py)保证了明细不被吞掉:如果被合并的Usage自带request_usage_entries,则深拷贝后追加;否则当它代表"单次请求且有 token"时,会现场合成一个RequestUsage条目再追加。
保留提供方原始用量载荷
SDK 默认会把各提供方的用量统一规范化为跨模型一致的Usage字段。当应用需要保留提供方特有的 usage 字段,或者需要区分"字段缺失"与"提供方上报为 0"时,把ModelSettings.preserve_raw_usage设为True:
from agents import Agent, ModelSettings, Runner agent = Agent( name="Assistant", model_settings=ModelSettings(preserve_raw_usage=True), ) result = await Runner.run(agent, "What's the weather in Tokyo?") for response in result.raw_responses: print(response.raw_usage)机制层面的要点如下:
- 每个
ModelResponse.raw_usage保存的是该次模型调用提供方载荷的一份分离的、JSON 兼容的快照(由_raw_usage_snapshot()在规范化之前抓取,见 src/agents/usage.py)。 - SDK不跨运行聚合
raw_usage。 - 当保存被禁用、提供方未返回用量载荷,或上游适配器已经丢弃了原始字段存在性信息时,该值保持为
None。 - 快照抓取失败(例如无法 JSON 序列化的适配器特有值)不会让一次本应成功的模型调用失败,而是静默返回
None——因为用量保留本质是诊断元数据。
需要特别注意的是,preserve_raw_usage只保留到达模型适配器的用量载荷,它本身并不会向提供方请求用量。因此当流式 Chat Completions 提供方要求显式请求用量时,还需要同时设置ModelSettings(include_usage=True)。
适配器差异:LitellmModel 的原始用量限制
当前LitellmModel在流式与非流式运行中都不会填充ModelResponse.raw_usage,所以对LitellmModel而言preserve_raw_usage=True无效。使用该适配器时,请继续使用规范化的Usage字段;若确需提供方特有的字段存在性信息,则应选择支持原始用量保留的适配器。
结合会话(Session)使用
使用Session(例如SQLiteSession)时,每次Runner.run(...)都会返回该次运行专属的用量。会话只为上下文保留对话历史,但各次运行的用量相互独立:
session = SQLiteSession("my_conversation") first = await Runner.run(agent, "Hi!", session=session) print(first.context_wrapper.usage.total_tokens) # Usage for first run second = await Runner.run(agent, "Can you elaborate?", session=session) print(second.context_wrapper.usage.total_tokens) # Usage for second run一个容易忽略的细节:会话虽然保留了运行之间的对话上下文,但历史消息会作为输入被重新喂给后续的每次运行,因此后续轮次的输入 token 数会随之增长——这也是多轮对话成本持续攀升的根源。
RunState 检查点中的用量
RunResult.to_state()会捕获截至当前已累计用量的一份独立快照。从该检查点恢复的运行以捕获的总量为起点,再累加自己模型调用的用量;恢复后的运行不会把新合计写回原RunResult,也不会写回由该结果派生的其他检查点:
first = await Runner.run(agent, "First request") checkpoint_a = first.to_state() checkpoint_b = first.to_state() resumed_a = await Runner.run(agent, checkpoint_a) resumed_b = await Runner.run(agent, checkpoint_b) assert resumed_a.context_wrapper.usage is not first.context_wrapper.usage assert resumed_b.context_wrapper.usage is not resumed_a.context_wrapper.usage这种隔离同样作用于Usage内部的request_usage_entries列表。唯一的例外是恢复后的嵌套Agent.as_tool()运行:其恢复后的模型用量会被有意地聚合进外部活跃运行的用量中,与其恢复前的模型调用行为保持一致——即嵌套运行始终并入最外层运行的独立记账。
在 Hook 中利用用量
使用RunHooks时,每个 hook 收到的context对象都带有usage字段,可在关键生命周期节点记录用量:
class MyHooks(RunHooks): async def on_agent_end(self, context: RunContextWrapper, agent: Agent, output: Any) -> None: u = context.usage print(f"{agent.name} → {u.requests} requests, {u.total_tokens} total tokens")这适用于所有传入RunContextWrapper的 hook 回调(例如 agent 结束、交接、工具调用等时机)。值得注意的是RunContextWrapper.usage本身就是整个运行过程中被持续累加的那个字段,因此 hook 里读到的就是"到目前为止"的真实用量,非常适合做单次运行的成本审计日志。
序列化与追踪集成
除了在代码中读取,Usage还提供了一组序列化辅助(src/agents/usage.py),供存储与追踪系统复用:
serialize_usage():把Usage转成 JSON 友好的字典,包含request_usage_entries完整明细;deserialize_usage():从序列化数据重建Usage,兼容历史快照(例如旧版本缺少cache_write_tokens字段)并做了容错兜底;model_usage_to_span_usage():为追踪 span 输出完整的逐模型调用用量;total_usage_to_span_metadata()/turn_usage_to_span_data()/task_usage_to_span_data():为追踪元数据输出聚合计数(含cached_input_tokens、cache_write_input_tokens)。
这意味着你可以把每次运行的用量落库做长期成本分析,也可以直接与 docs/tracing.md 描述的追踪链路打通,在 span 元数据中看到每次任务/轮次的 token 消耗。
API 参考速查
Usage— 用量追踪数据结构(聚合总量 + 逐请求明细 + token 细节)RequestUsage— 单次请求的用量详情RunContextWrapper— 从运行上下文访问用量RunHooks— 接入用量追踪生命周期(on_agent_end等回调)ModelSettings.preserve_raw_usage— 保留提供方原始用量载荷ModelResponse.raw_usage— 单次模型调用的原始用量快照
总结:用量追踪的五个实用心法
- 默认可用:
result.context_wrapper.usage开箱即得聚合用量,无需任何配置;request_usage_entries天然保留逐请求明细。 - 流式场景记得开
include_usage:Chat Completions 后端流式响应、以及LitellmModel下的多数提供方,都需要ModelSettings(include_usage=True)才会回报用量。 - 原始载荷按需开启:需要提供方特有字段或区分"缺失 vs 为 0"时用
preserve_raw_usage=True,但它不主动向提供方要数据,且对LitellmModel无效。 - 会话与检查点各自独立:会话只共享对话历史不共享用量;
to_state()产生的检查点携带用量快照,恢复后的运行从快照继续累加。 - hook 是轻量计费点:在
RunHooks回调里通过context.usage记录每个生命周期节点的用量,配合序列化辅助即可落地完整的成本分析链路。
【免费下载链接】openai-agents-pythonA lightweight, powerful framework for multi-agent workflows项目地址: https://gitcode.com/GitHub_Trending/op/openai-agents-python
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考