☰
OpenAI Agents SDK 工程笔记:Usage 用量统计、token 预算与成本闸门生产禁区
2026/10/14 2:49:57 网站建设 项目流程


千笔-AIWritePaper · https://www.aiwritepaper.com

Agent 能跑通,不等于成本账能平。生产里最常见的五类事故是:只抄最后一轮 token、流式还没消费完就入库、as_tool子 agent 用量漏计、用启发式估算代替账单、超预算后静默截断上下文还继续跑。它们都不会立刻抛异常,却会在月末账单和上下文完整性上一起爆。

本文在openai-agents 0.23.1下核对Usage/RunContextWrapper.usage字段与累计语义,并用假模型固定用量做离线 smoke:注入子 agent 漏计、流式中途记账、超预算仍继续三类故障,各留日志与 CSV(_w/usage-cost-smoke/)。不打真实计费 API。

图:上排为每次 run 可读字段;中排为累计路径与预算闸门;底部为五条生产禁区。

目标说明

读完你应能独立完成五件事。

  1. 从context.usage(或等价的 run 上下文)读出请求数、输入/输出/总 token,以及缓存与推理细分。
  2. 在多轮、handoff、as_tool子 agent 场景用Usage.add(或共享同一 usage 对象)做整次 run 累计。
  3. 流式场景等到最后一块消费完再记账;中途读数视为过时。
  4. 实现按 run 的 token 预算闸门:超限拒绝,禁止静默截断后续跑。
  5. 把五条失败样本写进禁区表与 CI。

适用与边界

适合:已在 Agents SDK 里跑多 agent / 工具子 agent,需要按次 run 做用量审计与成本上限。

不讲:tool 循环与超时、MCP、guardrails、lifecycle、ModelSettings、RunContext 业务字段、streaming 事件协议细节、handoffs 路由、tracing、ModelProvider、LocalShell、needs_approval、tool_use_behavior、SQLiteSession、AgentsException、托管 WebSearch/FileSearch(见既有工程笔记)。本篇专写用量统计与成本闸门。

失败含义:smoke 用固定Usage对象验证累计与闸门逻辑;真实账单以平台结算为准。类名与字段来自本机openai-agents 0.23.1。

机制:从哪里读、怎样累计、何时完整

字段含义工程注意
requests对本轮 LLM API 的请求次数多轮会累加
input_tokens/output_tokens/total_tokens跨请求合计账单主字段用累计值
input_tokens_details.cached_tokens缓存命中输入 token可为 0;勿当「免费」就忽略审计
input_tokens_details.cache_write_tokens写入缓存的 token与 cached 分开记账
output_tokens_details.reasoning_tokens推理类输出细分有的模型为 0
request_usage_entries每次请求的拆条目add()会保留,便于分请求成本

读数入口:RunContextWrapper.usage。文档写明,流式响应在最后一块处理完之前,usage 是过时的。因此「流式中途打印 token」不能当结账依据。

累计:同一 run 内多次模型调用应落到同一Usage上。Usage.add(other)会累加请求数与各类 token,并合并request_usage_entries。as_tool子 agent 在 SDK 里通过 fork 共享父级 usage 引用;你若在外层自己开子 Runner,必须显式把子结果 usageadd回父级,否则就是 F3。

fromagents.usageimportUsage,InputTokensDetails,OutputTokensDetails run_usage=Usage()run_usage.add(parent_turn_usage)run_usage.add(as_tool_child_usage)# 漏掉这一行 = 子 agent 漏计# 预算闸门:用累计 total_tokens,不用估算ifrun_usage.total_tokens>MAX_TOKENS_PER_RUN:raiseRuntimeError("token budget exceeded")

预算闸门建议钉在「整次 Runner.run / run_streamed 结束且流已耗尽」之后、对外交付之前。超限动作只能是拒绝或降级到人工,不能静默截断历史消息继续自动跑。

离线 smoke:5 条通过 + 5 条失败

P1 PASS 假模型固定 Usage:请求数与 token/缓存/推理细分可读 -> req=2 in=1200 out=300 total=1500 cached=400 cache_write=50 reasoning=80 P2 PASS 多 agent/as_tool:父+子 Usage.add 累计且保留 request_usage_entries -> req=2 total=880 entries=2 cached=40 P3 PASS 流式:未消费完 usage 过时;最后一块后才完整 -> mid_total=0 complete=True final_total=1020 P4 PASS 按 run 的 token 预算闸门:未超限放行 -> allow=True billed=1000 max=2000 action=continue P5 PASS 账单以累计 Usage.total_tokens 为准,估算不得替代 -> estimated=5000 billed_usage=880 use_estimate_as_bill=False F1 FAIL 禁区:只看最后一轮用量 -> last_only=670 true_total=1120 undercount=450 F2 FAIL 禁区:流式未消费完就记账 -> booked_early=0 stream_complete=False stale=True F3 FAIL 禁区:as_tool 子 agent 用量漏计 -> parent_only_total=600 child_missed=280 true_should_be=880 F4 FAIL 禁区:把估算当账单 -> estimate_as_bill=5000 actual_usage=1500 mismatch=True F5 FAIL 禁区:超预算后静默截断上下文仍继续 -> over=True allow=True silent_truncate=True billed=3500 action=continue_truncated CSV rows: 10

逐条读法。

  • P1:固定假用量可读出 requests、in/out/total、cached、cache_write、reasoning。
  • P2:父 600 + 子 280,累计 total=880,entries=2,缓存细分一并相加。
  • P3:流式中途mid_total=0(过时);消费完后final_total=1020。
  • P4:预算 2000,实际 1000,放行。
  • P5:估算 5000 与真实累计 880 分离,禁止用估算代替账单。
  • F1:只看最后一轮 670,真实合计 1120,少计 450。
  • F2:只消费 1/4 块就记账,booked_early=0且stale=True。
  • F3:只计父 600,子 280 漏计。
  • F4:把估算 5000 当账单,与实际 Usage 1500 不一致。
  • F5:超预算 3500>2000 仍allow=True且silent_truncate=True。

生产禁区表

禁区失败含义正确动作
只看最后一轮用量前面轮次成本消失读整次 run 的累计usage
流式未消费完就记账入库数字过时或为 0等最后一块处理完
as_tool子用量漏计子调用「免费」幻觉共享 usage 或显式add
估算当账单财务与工程对数不上只认 Usage / 平台账单
超预算静默截断续跑上下文残缺仍对外答闸门拒绝或转人工

最小接入示例(读数 + 闸门)

把闸门挂在 Runner 返回之后:先取result.context_wrapper.usage(字段名以你使用的结果对象为准),再调用TokenBudgetGate.check。若你使用run_streamed,必须先异步迭代至流结束,再读 usage;否则会落入 F2。对含as_tool的图,在返回前断言usage.requests不少于「父模型调用次数 + 子模型调用次数」的下界,作为漏计的廉价探针。

本地联调可用假模型固定 Usage:每次响应带上已知的 input/output,跑完后检查累计是否等于各次之和,并故意丢掉子段add让 F3 复现。通过后再接真实模型,避免一上来就被缓存命中波动干扰断言。

验收清单

  • 每次 run 结束后持久化requests/input/output/total与缓存、推理细分。
  • 多 agent / handoff /as_tool场景核对累计是否等于各段之和。
  • 流式路径在complete之前禁止写账单表。
  • TokenBudgetGate超限返回 reject,测试里断言不存在continue_truncated。
  • CI 跑smoke.py,P1至P5 保持 PASS,F1至F5 保持 FAIL。

跑 smoke 前确认本机已安装openai-agents==0.23.1:脚本只依赖agents.usage的数据类,不发起网络请求。把smoke_output.txt与 CSV 一并归档,换小版本 SDK 时先看Usage字段是否仍含缓存与推理细分,再决定是否更新断言。

当天 30 分钟落地

  1. 5 分钟:在现有 Runner 回调里打印context.usage.total_tokens(非流或流结束后)。
  2. 10 分钟:给一次含as_tool的 run 做父/子分项与合计对照。
  3. 10 分钟:接入TokenBudgetGate,用固定假用量打超限用例。
  4. 5 分钟:把 F1至F5 写进 oncall 手册。

流式取消与中途快照

用户或网关取消流式生成时,已发出的 chunk 可能已经产生部分 output tokens,但RunContextWrapper.usage在最后一块处理完之前仍可能过时。正确做法是:取消路径也要走到「流关闭/耗尽」钩子,再读取一次 usage 作为不完整 run的审计记录,并打上cancelled=True。错误做法是:把取消前某次日志里的total_tokens=0或中间值写成正式账单,或因为取消就整行删除用量(平台侧仍可能计费)。

F2 用「只消费 1/4 块就记账」模拟这一类事故:booked_early=0且stream_complete=False。CI 里应断言:账单写入函数的前置条件包含stream_complete(或非流路径的run_done)。

缓存 token 要不要进预算

cached_tokens与cache_write_tokens进入input_tokens_details,不一定与「向用户报价」的口径相同,但预算闸门默认应看total_tokens(或平台定义的计费 token),而不是只看非缓存输入。若产品政策允许缓存优惠,应在闸门外单独配置「计费口径」,并在 CSV 里同时留下 raw Usage 与计费口径两列,避免事后无法对账。smoke 的 P1 要求缓存与推理字段可读,不等于要求它们从预算中豁免。

与 oncall 手册的衔接

告警建议至少三条:单 runtotal_tokens超预算、同日累计超日预算、as_tool调用次数与requests增量明显不匹配。前两条直接绑TokenBudgetGate;第三条用 entries 条数或子调用计数做启发式,命中后人工抽查是否漏add。手册里写清:估算值只用于容量规划,不用于客户账单与拒绝决策。

踩坑

  • 用「大约几千 token」做额度申请,月末才发现 Usage 合计翻倍。
  • 取消流式生成后仍把中途快照当最终用量。
  • 子 agent 单独 Runner 跑完不回写父 usage。
  • 超限时删掉最早的 tool 结果再重试,还不记截断事件。
  • 只监控 output_tokens,忽略巨大的 input 与缓存写入。

假模型固定用量怎么对照账单字段

对照时先固定金标准:一次 run 结束后的Usage快照,含requests、input_tokens、output_tokens、total_tokens,以及cached_tokens、cache_write_tokens、reasoning_tokens。若启用了request_usage_entries,还应能按请求拆开复核。假模型侧每次调用返回写死的 Usage,便于断言加减;真实平台延迟与缓存命中率会变,但字段名与累计规则应保持一致。

实践上建议把成本终裁拆成三道门。第一道:流是否已耗尽(或非流路径已返回)。第二道:父 run 与所有as_tool/handoff 子段是否都已add。第三道:累计total_tokens是否小于本 run 预算。三道都过,才允许写入账单表或触发下游计费事件。任何一道失败,输出停在草稿区,禁止用「大概没超」作结案理由。

与「托管检索选型」「异常恢复」「会话持久化」分开记账:本文只验收用量对象与预算闸门。检索是否抽检、异常是否可恢复,各自用各自的 smoke,避免一篇工程笔记塞满所有 SDK 主题。

预算闸门伪代码与拒绝语义

@dataclassclassTokenBudgetGate:max_total_tokens:intdefcheck(self,usage:Usage)->str:ifusage.total_tokens>self.max_total_tokens:return"reject"# 禁止 continue_truncatedreturn"continue"

拒绝后的产品行为可以是:返回明确错误码、降级到更小模型的新run(新预算、新 usage)、或转人工。不允许的行为是:删掉上下文前缀使 total 表面下降,却继续同一 run 编号对外回答。F5 专拦后一种「看起来还能跑」的静默截断。

若业务必须做上下文压缩,应开新 run:旧 usage 冻结入库,新 run 使用压缩后的输入并重新累计。这样财务与审计仍能把两次 run 分开看,而不是在同一次自动循环里把截断藏掉。

多 agent 累计的两种合法写法

写法一:SDK 默认的共享引用。as_toolfork 让子调用直接往父usage上累加,父侧结束时读一次即可。写法二:子 Runner 独立 usage,结束后parent_usage.add(child_usage)。两种都可以;不合法的是「子跑完只把文本结果拼回去,usage 丢在子作用域」。handoff 换 agent 后同样要确认仍落在同一 run 上下文的 usage 上,而不是每个 agent 各记一份末轮快照却从不汇总。

排查漏计时,用request_usage_entries的条数与requests对照:若请求次数对得上但 total 偏小,多半是某次add被跳过;若 entries 偏少,说明有请求从未进入累计对象。

为何不能只监控 output

不少团队面板只画 output_tokens,因为「生成字数」好懂。实际成本与上下文风险往往由 input 主导:工具结果回灌、历史消息、缓存写入都会推高input_tokens。预算闸门与告警应以total_tokens(或平台计费口径)为主,output 仅作辅助序列。P1 把 in/out/total 与缓存、推理一并打印,就是为了避免单一指标盲区。

总结

用量统计的入口是每次 run 上下文上的Usage:请求数、输入/输出/总 token,以及缓存与推理细分;add与共享引用负责多 agent/as_tool累计;流式必须消费到最后一块才完整。成本闸门按累计total_tokens做硬拒绝。五条禁区:只看末轮、中途记账、子用量漏计、估算当账单、超预算静默截断:在离线 smoke 中全部 FAIL。把同一脚本挂进 CI,换模型时复跑,而不是凭感觉调额度。

用量与预算闸门日志可用千笔-AIWritePaper 整理归档;骨架与脚本不依赖产品名。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询