Pydantic AI 团队实测延迟加载 20 个工具 schema,平均省 21% token 费用,但四分之一任务类型反而多耗 12.3%——均值掩盖了方差。
我们对一个拥有 20 个工具的 Agent 启用了渐进式披露(progressive disclosure)。将工具 schema 延迟加载后,输入 token 减少了约 30%,总成本减少了约 21%。不错的成果,发版,写篇博客。
然后我把数字按任务拆开看了,平均值背后藏着东西。四个任务类型中有三个节省了 21% 到 30%。第四个在一个传输协议上没省钱,在另一个上还多花了 12.3%。
这篇文章就是关于第四个任务的,以及为什么你在延迟工具 schema 时真正应该关心的数字不是平均值。
全部 80 次运行记录已公开。下面的每一条命令都是我撰写本文时真实跑过的,输出结果也是原样粘贴。你可以在本地离线复现整个过程,不花一分钱。
四个业务任务,每个恰好需要一个工具。20 个工具 schema 始终注册。两组对照:
always-on:每次请求都把全部 20 个 schema 序列化进去
deferred:全部 20 个包在 Pydantic AI 的 DeferredLoadingToolset 中,模型需要先搜索能力、加载它,然后才能调用
每个单元格 20 次运行,顺序执行,无重试,gpt-4o temperature 0,parallel_tool_calls=False。2026-08-06 运行,使用 pydantic-ai-slim[openai]==2.24.0。两种传输协议都测了,Chat Completions 和 Responses API,因为工具搜索在一个上是服务端执行、另一个上通过本地 fallback 执行。
整个基准测试花了 $0.279585。
平均值,看起来还不错
cell mean_in stdev min max
A-chat 1361.5 10.3 1350 1372
B-chat 945.2 190.8 780 1264
A-resp 1353.8 10.6 1342 1364
B-resp 1001.0 263.1 807 1477
A 是 always-on,B 是 deferred。Chat Completions 输入 token 减少 30.6%,Responses 减少 26.1%。成本分别降低 21.2% 和 16.8%。正确性 80/80 完全匹配,什么都没坏。
但看看标准差那一列。always-on 组每次运行都在 22 token 以内。deferred 组的波动幅度是 484 和 670 token。延迟加载让输入大小变得大约二十倍不稳定。
我最初假设是运行间的非确定性:模型每次写出略微不同的搜索查询,返回略微不同的 schema 集合,prompt 大小随之波动。这个假设是错的,而验证它的过程才是真正的发现。
分解方差
这就是验证方法。按任务在每个单元格内分组,而不是把单元格混在一起:
curl -sL -o bc025.jsonl https://raw.githubusercontent.com/benchclawio/harness/main/results/bc025-progressive-disclosure-2026-08-06/bc025-scored-raw-2026-08-06.jsonl
python3 -c "
import json, statistics as s, collections
rows=[json.loads(l) for l in open('bc025.jsonl')]
for c in ('A-resp','B-resp'):
print('==', c)
g=collections.defaultdict(list)
for r in rows:
if r['cell']==c: g[r['task_id']].append(r['metrics']['tokens_in'])
for t,v in sorted(g.items()):
print(f' {t:22s} n={len(v)} mean={s.fmean(v):7.1f} sd={s.stdev(v):6.1f}')
"
== A-resp
currency-conversion n=5 mean= 1342.0 sd= 0.0
defect-threshold n=5 mean= 1364.0 sd= 0.0
inventory-reorder n=5 mean= 1345.0 sd= 0.0
shipment-delay n=5 mean= 1364.0 sd= 0.0
== B-resp
currency-conversion n=5 mean= 930.2 sd= 2.7
defect-threshold n=5 mean= 1431.8 sd= 95.6
inventory-reorder n=5 mean= 807.0 sd= 0.0
shipment-delay n=5 mean= 835.0 sd= 0.0
从中得出两个结论。
运行间的方差在两组中都接近零。同一任务在 deferred 条件下重复五次,多数时候得到完全相同的 token 计数,标准差 0.0。我假设存在的非确定性并不存在,或者小到可以忽略。
差异在于任务之间,而非运行之间。在 always-on 下,四个任务都落在 22 token 以内,因为 20 个 schema 占了主导,任务文本对总量几乎没有影响。在 deferred 下这种平整化消失了,任务分散在 807 到 1432 token 之间。
这重新定义了整件事。延迟加载不会让你的成本变得不稳定。它让你的成本变得任务依赖。always-on 不论用户问什么收费都一样。Deferred 根据模型决定搜索和加载什么来收费,这意味着你的账单现在追踪的是你的流量构成。
那个亏钱的任务
一旦成本是任务的函数,有些任务就会吃亏。下面是每个任务的成本,两种传输协议:
python3 -c "
import json, statistics as s, collections
rows=[json.loads(l) for l in open('bc025.jsonl')]
g=collections.defaultdict(list)
for r in rows: g[(r['cell'],r['task_id'])].append(r['metrics']['cost_usd'])
hdr=f\"{'task':22} {'A-resp':>10} {'B-resp':>10} {'delta':>8} {'A-chat':>10} {'B-chat':>10} {'delta':>8}\"
print(hdr)
for t in ('currency-conversion','defect-threshold','inventory-reorder','shipment-delay'):
ar=s.fmean(g[('A-resp',t)]); br=s.fmean(g[('B-resp',t)])
ac=s.fmean(g[('A-chat',t)]); bc=s.fmean(g[('B-chat',t)])
print(f'{t:22} {ar:10.6f} {br:10.6f} {br/ar*100-100:+7.1f}% {ac:10.6f} {bc:10.6f} {bc/ac*100-100:+7.1f}%')
"
task A-resp B-resp delta A-chat B-chat delta
currency-conversion 0.003795 0.003001 -20.9% 0.003795 0.002791 -26.5%
defect-threshold 0.003944 0.004428 +12.3% 0.003840 0.003810 -0.8%
inventory-reorder 0.003873 0.002707 -30.1% 0.003828 0.002770 -27.6%
shipment-delay 0.003970 0.002828 -28.8% 0.003846 0.002691 -30.0%
defect-threshold 是异常值。在 Responses API 上,启用渐进式披露后成本增加了 12.3%。在 Chat Completions 上节省了 0.8%,四位小数点后这点差异是舍入误差,不算真正的节省。
它的 deferred prompt 进入了 1431.8 输入 token,而同一单元格中最便宜的任务只有 807.0 token。这是 625 个额外 token 的已加载 schema,而这个任务和套件中其他任务一样,只需要一个工具。
我想谨慎地说明这里的机制,因为记录里没有 bundle。它。每一次 deferred 运行都恰好做了 2 次工具搜索和 3 次模型请求,defect-threshold 也不例外,所以不是在做额外的往返。最可能的解释是,它的搜索匹配了 20 个能力中的更多个,比其他任务往 prompt 中拉回了更多 schema。但原始 JSONL 日志记录的是 token 计数和最终调用的工具,而不是搜索返回的 schema 集合,所以我无法用公开数据证明这一点。把它当作显而易见的推断,而不是测量结果。
分布长什么样
每次运行的成本分布比标准差更能说明问题:
python3 -c "
import json
rows=[json.loads(l) for l in open('bc025.jsonl')]
for c in ('B-chat','B-resp'):
v=sorted(r['metrics']['cost_usd'] for r in rows if r['cell']==c)
print(c, [f'{x:.6f}' for x in v]); print()
"
B-chat ['0.002470', '0.002587', '0.002717', '0.002717', '0.002717', '0.002717', '0.002740', '0.002800', '0.002800', '0.002800', '0.002815', '0.002845', '0.002845', '0.002845', '0.002845', '0.003810', '0.003810', '0.003810', '0.003810', '0.003810']
B-resp ['0.002707', '0.002707', '0.002707', '0.002707', '0.002707', '0.002828', '0.002828', '0.002828', '0.002828', '0.002828', '0.002992', '0.002992', '0.002992', '0.002992', '0.003037', '0.003922', '0.004497', '0.004573', '0.004573', '0.004573']
这不是一个带肥尾的钟形曲线。是一簇,然后是悬崖,悬崖是一种任务类型。
在 Responses API 上,20 次 deferred 运行中有 5 次成本高于 always-on 的平均运行($0.003895)。最贵的 deferred 运行花了我 $0.004573,比 always-on 均值高出 17.4%。那个平均节省 16.8% 的优化,对这四分之一的运行来说,根本不是优化。
我从中真正得到的
按任务类型计算你的节省,而不是按整个语料库。一个混合百分比是那个告诉你该不该发版的最无用的数字。如果你的流量 80% 都是 defect-threshold 形状,deferred 会让你亏钱而你的仪表盘还在报告节省。
节省上限由 schema 占 prompt 的比例决定。延迟加载移除的是工具 schema。它不移除你的系统 prompt、用户消息、对话历史或返回的工具结果。在我们的任务中 schema 大约占了请求的三分之一,所以移除几乎全部 schema 节省了大约三分之一的输入 token。如果你有五个工具和一个 4000 token 的系统 prompt,这套方案对你没价值。
将额外往返作为确定性成本来预算。always-on 完成了 2 次模型请求。Deferred 全部 40 次 deferred 运行都用了 3 次,两种传输协议都一样。不是带波动的平均值,是一个常量。如果你的延迟预算是按请求计而不是按 token 计,你是在用固定的 50% 请求增加来换取可变的 token 减少。
不要通过框架自己的工具视图来验证延迟加载。AgentInfo.function_tools 在延迟工具加载前和加载后都会列出它,本地 search_tools fallback 两种情况都存在。那个界面只告诉你 agent 知道什么,而不是序列化了什么给 provider。本文中每个数字都来自 provider 报告的请求 token 计数,这是唯一映射到账单的东西。
准确性不是那个被打破的东西。全部 80 次运行都产生了完全匹配的结果,每个单元格都是 20/20。我原本以为正确性是风险所在,结果并非如此,虽然我们的任务每个都恰好需要一个能力。需要多个加载的任务会增加往返次数,给模型更多选错的机会。我们没测那个。
完整 bundle 是 80 条原始运行记录、两个带 SHA-256 的 manifest、确定性任务套件生成器、worker、收集器和分析脚本:
https://github.com/benchclawio/harness/tree/main/results/bc025-progressive-disclosure-2026-08-06
python3 analyze_bc025.py 重新生成置信区间。python3 bc025_capabilities.py 逐字节重新生成任务套件。两者都是离线的且不花一分钱,因为它们读取的是记录好的运行而不是调用模型。
完整基准测试,包括 bootstrap 置信区间以及那句广泛引用的"节省 90% 到 98%"数字的来源,写在了 benchclaw.io。
关于版本的一个注意事项,因为它在我们运行时更新了:pydantic-ai-slim 2.25.0 在这些运行前几小时才上 PyPI。我们基准测试的是 2.24.0 然后 diff 了标签。_tool_search.py 和 toolsets/deferred_loading.py,即测试的整个机制,在两者之间没有变化。我们没有在 2.25.0 上重新运行。