ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
# Agent End-to-End Cost Analysis / Agent 任务端到端成本分析(实验 7-9)
## English
This project performs a full cost decomposition for a multi-turn agent workflow (refund handling), including input/cache/output tokens, latency, and cost distribution. It enables practical measurement of where costs come from and how optimization strategies affect total spend.
### What it does
The benchmark runs a fixed 8-turn customer refund scenario and records every LLM call with a lightweight tracing layer:
- token usage (prompt, cached prompt, output)
- latency
- model cost by pricing table
It then reports:
- per-step cost breakdown
- cost component breakdown (non-cached input / cached input / output)
- p50/p95/p99 for per-step cost
- full 2×2 A/B comparison between optimization levers
The two levers are:
- **KV-cache friendliness**: keep a stable prefix to maximize cache hits
- **Context compression**: summarize long tool outputs for earlier turns
### Default model and API behavior
- Default model: `gpt-5.6-luna`.
- Preferred credentials: `OPENAI_API_KEY`, fallback to `OPENROUTER_API_KEY` (`gpt-*` remapped to `openai/*`).
- If OpenRouter keys exist for `gpt-5.x`, it is preferred due to authentication requirements.
- Offline mode is supported via `sample_trace.json` so all tables can be recomputed without API calls.
### Files
| File | Purpose |
|---|---|
| `config.py` | pricing model definitions and pricing presets |
| `tracer.py` | tracing helper and cost decomposition/aggregation |
| `agent.py` | 8-turn refund agent with `run_scenario(kv_cache, compress)` |
| `demo.py` | CLI entry for online/offline runs |
| `sample_trace.json` | captured 2×2 scenario token records for offline recomputation |
| `tests/` | offline pytest regressions for trace parsing and usage accounting |
| `requirements.txt` / `env.example` | dependencies and environment templates |
### Run
```bash
# From the repository root: use the shared Chapter 6 environment
uv sync --locked --python 3.12 --extra ch6
# Activate it before changing directories:
# macOS/Linux:
source .venv/bin/activate
# Windows PowerShell: .\.venv\Scripts\Activate.ps1
# Windows cmd: .venv\Scripts\activate.bat
# pip fallback when uv is not installed:
# python -m pip install -e ".[ch6]"
cd chapter7/agent-cost-analysis
# Single-project compatibility path, still supported during migration:
# python -m pip install -r requirements.txt
export OPENAI_API_KEY=your-openai-api-key # or OPENROUTER_API_KEY=your-openrouter-api-key
python demo.py
python demo.py --offline --scenario all
```
### Tests
Automated tests are offline and do not require API keys.
```bash
# From the repository root, include the dev extra for pytest:
uv sync --locked --python 3.12 --extra ch6 --extra dev
# pip testing fallback:
# python -m pip install -e ".[ch6,dev]"
cd chapter7/agent-cost-analysis
python -m pytest tests
```
### CLI options
| Argument | Meaning |
|---|---|
| `--live` / `--offline` | call real model (default) / recompute from trace |
| `--scenario` | `ab` (naive+both), `all` (four scenarios), or subset list |
| `--trace` | trace file for offline mode |
| `--save-trace` | persist observed token usage from online runs |
| `--model` | model name for price preset |
| `--price-input` / `--price-cached` / `--price-output` | override per-million-token prices |
| `--no-warmup` | disable prefix warmup for KV-cache scenario |
| `--output` | export full result JSON |
### A/B scenarios (2×2)
| Scenario | KV-cache | Compression | Context design |
|---|---|---|---|
| `naive` | no | no | random session header + full tool returns |
| `kv` | yes | no | stable long prefix |
| `compress` | no | yes | only keep last 2 turns full; older turns summarized |
| `both` | yes | yes | stable prefix + compressed history |
The task logic is identical across scenarios so differences isolate optimization effects.
### Interpretation
Empirical results show:
- KV-cache can produce large improvements when prefixes are stable.
- Compression lowers prompt growth while keeping functional behavior.
- Joint optimization usually gives best total cost, though cache gains and compression gains are not simply additive.
### Offline recomputation
Offline mode reads `sample_trace.json` and re-runs only cost arithmetic, enabling:
- quick replication without keys
- quick “what-if” with different model prices
### Notes
- Observed numbers can vary due to real API behavior and cache timing.
- Prompt cache is best-effort and may miss in some turns.
- Tool-return token estimate uses tokenizer counts against current model encoder.
- Key precedence: prefer `OPENAI_API_KEY`; fallback is automatic via OpenRouter for supported paths.
---
## 中文
# 实验 7-9:Agent 任务的端到端成本分析
配套《深入理解 AI Agent》第 6 章「实验 7-9 ★:Agent 任务的端到端成本分析」。
对一个典型的多轮 Agent 任务(客服退款)做**全链路成本拆解**,用**自建的轻量 tracing / 可观测系统**记录每次 LLM 调用的输入/输出/缓存 token、时延与成本:按步骤聚合出「哪一步最贵」,按**成本构成**拆出「未缓存输入 / 缓存输入 / 输出各占多少、工具返回注入了多少 token」,并给出**单步成本分布(p50/p95/p99**;再做完整 **2×2 A/B 对比**,量化 **KV-cache 复用****上下文压缩** 两个杠杆各自及叠加后的真实成本差异。
- 默认模型 **`gpt-5.6-luna`**(当前廉价旗舰),通过 openai Python SDK 调用。首选 `OPENAI_API_KEY`;未设置时**自动回退到 `OPENROUTER_API_KEY`**(走 OpenRouter 兼容端点,`gpt-*` 映射为 `openai/*`)。由于 `gpt-5.x` 直连 OpenAI 需组织实名认证,只要存在 `OPENROUTER_API_KEY` 就优先走 OpenRouter。
- KV-cache 的节省是**真实**的:利用 OpenAI 的自动 prompt caching(前缀 ≥ 1024 token 且命中近期相同前缀时,`usage.prompt_tokens_details.cached_tokens > 0`,这部分输入按缓存价 5 折计费)。
- 提供**离线模式**:不打模型,读入一份此前真实运行录下的 token 用量(`sample_trace.json`canned token counts),用可配置单价**重新计算**成本/成本构成/A/B 对比表——无需 API key 即可复现全部表格,也可一键换算到其它模型定价。
## 文件
| 文件 | 说明 |
|------|------|
| `config.py` | 模型与价格:`Pricing` 单价对象 + 常见 OpenAI 模型单价预设,token→成本换算 |
| `tracer.py` | 自建轻量 tracing:包裹每次 LLM 调用记录 token/缓存/时延/成本;成本构成拆解、单步成本分布、按步骤拆解表;支持从录制用量离线复算(`from_records`|
| `agent.py` | 多轮客服退款 Agent 任务;`run_scenario(kv_cache, compress)` 把两个开关正交组合成 2×2 场景,并用 tiktoken 估算「工具返回注入」token |
| `demo.py` | 命令行入口(argparse):在线跑真实模型 / 离线复算;选择 A/B 场景、模型单价、输出文件 |
| `sample_trace.json` | 一次真实运行录下的四个场景逐步 token 用量(离线模式的输入,成本按当前单价重算)|
| `tests/` | 离线 pytest 回归测试:trace 解析与 usage 计费容错 |
| `requirements.txt` / `env.example` | 依赖与环境变量示例 |
## 运行
```bash
# 在仓库根目录使用统一的第 6 章环境
uv sync --locked --python 3.12 --extra ch6
# 切换目录前先激活环境:
# macOS/Linux
source .venv/bin/activate
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# Windows cmd.venv\Scripts\activate.bat
# 未安装 uv 时可用 pip 兜底:
# python -m pip install -e ".[ch6]"
cd chapter7/agent-cost-analysis
# 迁移期间仍支持单项目兼容路径:
# python -m pip install -r requirements.txt
# 在线(真实调用模型,需要 key):默认跑 A(朴素)+B(优化) 两组
export OPENAI_API_KEY=your-openai-api-key # 或 export OPENROUTER_API_KEY=your-openrouter-api-key(自动回退)
python demo.py
# 离线(无需 key):用内置 canned trace 复算全部表格
python demo.py --offline --scenario all
```
在线模式会真实调用 OpenAI`--scenario all` 约几十次 chat completion,运行一两分钟。
## 测试
自动化测试均为离线回归测试,不需要 API Key。
```bash
# 在仓库根目录安装 pytest 所需的 dev extra
uv sync --locked --python 3.12 --extra ch6 --extra dev
# pip 测试兜底路径:
# python -m pip install -e ".[ch6,dev]"
cd chapter7/agent-cost-analysis
python -m pytest tests
```
### 命令行参数(`python demo.py --help`
| 参数 | 说明 |
|------|------|
| `--live` / `--offline` | 在线真实调用(默认)/ 离线从 trace 文件复算(无需 key)|
| `--scenario NAME` | `ab`(默认=naive+both) / `all`(2×2 四组) / 逗号分隔子集 `naive,kv,compress,both` |
| `--trace FILE` | 离线读取的 canned trace,默认 `sample_trace.json` |
| `--save-trace FILE` | 在线跑时把真实 token 用量落盘,供之后 `--offline` 复算 |
| `--model NAME` | 模型名(决定默认单价预设:`gpt-4o-mini`/`gpt-4o`/`gpt-4.1-mini`/`gpt-4.1`|
| `--price-input/-cached/-output` | 覆盖三档单价(每百万 token 美元)|
| `--no-warmup` | 关闭 KV-cache 组的前缀预热(默认预热以稳定命中缓存)|
| `--output FILE` | 把成本拆解结果(含成本构成/分布/逐步用量)写成 JSON |
> **不改任何参数直接 `python demo.py`,行为与之前一致**:在线跑 A(朴素) 与 B(优化) 两组并打印拆解 + A/B 对比表。
## A/B 四种策略(完整 2×2)
同一个 8 轮客服退款任务(查订单 → 查物流 → 查退款政策 → 查知识库 → 风控 → 发起退款 → 通知 → 关单),四组做的是**同样的逻辑工作**,只在上下文构造上不同——因此成本差异纯粹来自「是否 KV-cache 友好」与「是否压缩上下文」两个正交开关:
| 场景 | KV-cache | 压缩 | 上下文构造 |
|------|:--:|:--:|------|
| `naive` A 朴素 | ✗ | ✗ | 每轮 system 前塞随机 session 头(破坏前缀)+ 历史工具返回原样带全 |
| `kv` 仅缓存 | ✓ | ✗ | 稳定长前缀(system 逐字节不变)+ 历史不压缩 |
| `compress` 仅压缩 | ✗ | ✓ | 前缀不稳定 + 仅最近 2 轮保留完整工具返回、更早压成一句话摘要 |
| `both` B 优化 | ✓ | ✓ | 稳定长前缀 + 上下文压缩(两个杠杆叠加)|
> 为聚焦「输入侧」两个杠杆,四组都用 `temperature=0` 且限制输出长度(`max_tokens=160`),让输出 token 成本近似为四组相等的固定项,避免模型生成长度的随机波动干扰对比。
>
> 工具环境是「受控」的(工具返回内容预设,真实系统里来自订单/物流/知识库后端),但**每一次 LLM 调用、每一份 token 用量、每一分成本都是真实打到 OpenAI 得到的**,保证可复现。
## 真实运行输出(gpt-4o-mini
以下为一次真实运行(`python demo.py --scenario all`)的输出,`sample_trace.json` 即由该次运行落盘、供 `--offline` 复现。
### (a) 单次任务成本拆解:按步骤 + 按成本构成 + 分布
```
===== 成本拆解: A 朴素(无缓存/无压缩)(单次任务全链路拆解) =====
步骤 工具/动作 输入tok 缓存tok 工具tok 输出tok 时延(s) 成本($)
---------------------------------------------------------------------------------------
turn-1 query_order 1113 0 276 104 3.15 0.000229
turn-2 query_logistics 1807 0 829 99 2.09 0.000330
turn-3 check_refund_policy 2154 0 1046 139 2.69 0.000406
turn-4 query_knowledge_base 2564 0 1287 160 2.92 0.000481
turn-5 query_user_history 2863 0 1389 136 2.69 0.000511
turn-6 issue_refund 3123 0 1490 160 3.07 0.000564
turn-7 send_notification 3408 0 1579 160 3.09 0.000607
turn-8 close_ticket 3668 0 1648 160 2.50 0.000646
---------------------------------------------------------------------------------------
合计 20700 0 9544 1118 22.20 0.003776
最贵的一步 → turn-8 / close_ticket: $0.000646(占总成本 17.1%
成本构成:
未缓存输入 20700 tok $0.003105 (82.2%)
缓存输入 0 tok $0.000000 (0.0%)
输出 1118 tok $0.000671 (17.8%)
其中「工具返回注入」累计输入 9544 tok (同一份工具返回在后续每轮被反复计费)
单步成本分布(n=8): 均值 $0.000472 p50 $0.000481 p95 $0.000646 p99 $0.000646
===== 成本拆解: B 优化(KV缓存+压缩)(单次任务全链路拆解) =====
步骤 工具/动作 输入tok 缓存tok 工具tok 输出tok 时延(s) 成本($)
---------------------------------------------------------------------------------------
turn-1 query_order 1056 1024 276 139 2.41 0.000165
turn-2 query_logistics 1781 0 829 112 2.18 0.000334
turn-3 check_refund_policy 2143 1024 1046 151 2.56 0.000335
turn-4 query_knowledge_base 2310 0 1052 160 3.03 0.000442
turn-5 query_user_history 2060 1024 635 160 2.48 0.000328
turn-6 issue_refund 2143 1024 551 160 2.71 0.000341
turn-7 send_notification 2188 1024 430 160 2.79 0.000347
turn-8 close_ticket 2354 1024 429 122 2.38 0.000349
---------------------------------------------------------------------------------------
合计 16035 6144 5248 1164 20.54 0.002643
最贵的一步 → turn-4 / query_knowledge_base: $0.000442(占总成本 16.7%
成本构成:
未缓存输入 9891 tok $0.001484 (56.1%)
缓存输入 6144 tok $0.000461 (17.4%)
输出 1164 tok $0.000698 (26.4%)
其中「工具返回注入」累计输入 5248 tok (同一份工具返回在后续每轮被反复计费)
单步成本分布(n=8): 均值 $0.000330 p50 $0.000335 p95 $0.000442 p99 $0.000442
```
可以看到:朴素组 A 的输入 token 随轮次从 1113 一路涨到 3668(上下文累积效应,最后一步最贵),「工具返回注入」累计吃掉 9544 输入 token;优化组 B 的输入 token 被压缩策略压住(末轮 2354 而非 3668,工具注入累计降到 5248),且多数轮次持续命中 1024 缓存 token,缓存输入把整块费用打了 5 折。
### (b) 完整 2×2 A/B 对比
```
===== A/B 成本对比(同一个 8 轮客服退款任务)=====
方案 总输入tok 缓存tok 缓存率 输出tok 总成本($) vs基线
------------------------------------------------------------------------------------------
A 朴素(无缓存/无压缩) 20700 0 0.0% 1118 0.003776 基线
KV 仅缓存(稳定前缀/不压缩) 20386 13568 66.6% 1112 0.002707 -28.3%
仅压缩(前缀不稳定/摘要) 16177 0 0.0% 1147 0.003115 -17.5%
B 优化(KV缓存+压缩) 16035 6144 38.3% 1164 0.002643 -30.0%
------------------------------------------------------------------------------------------
重点对比:A 朴素(无缓存/无压缩) → B 优化(KV缓存+压缩)
总 token: A=21818 → B=17199 减少 4619 (21.2%)
缓存 token: A=0 → B=6144 B 靠稳定前缀命中缓存)
总成本: A=$0.003776 → B=$0.002643 降低 $0.001133 (30.0%)
成本倍率: A 是 B 的 1.43 倍
```
结论(读出两个杠杆各自与叠加的贡献):
- **仅 KV-cache**:不改上下文长度,只靠稳定前缀让重复的系统提示/工具定义/历史轮次按缓存价计费,缓存率冲到 66.6%,端到端成本就降了 **28.3%**——本例中它是单个最有效的杠杆。
- **仅压缩**:把旧轮次工具返回压成摘要,总输入 token 从 20700 降到 16177(约 −22%),端到端成本降 **17.5%**;但因为前缀不稳定,缓存率为 0。
- **两者叠加(B 优化)**:输入 token 最少、又能命中缓存,端到端总成本降 **30.0%**(A 是 B 的 1.43 倍)。单看输入侧成本,降幅落在书中「KV Cache 可降低 30%-60% 输入 token 成本」的经验区间内。
> 注意 KV 与压缩并非简单相加:压缩缩短了历史,可被缓存的「历史轮次」也随之变少,所以 B 的缓存 token(6144)反而少于「仅 KV」(13568)。这正是评估的价值——两个优化叠加时要实测协同效应,而不是把各自的收益直接相加。
## 离线复算(无需 API key
`sample_trace.json` 存的是上面这次真实运行的**逐步 token 用量(实测值)**;离线模式只做「按单价重算成本」这一步纯离线数学,因此无需 key 即可复现全部表格,还能一键换算到其它模型定价:
```bash
python demo.py --offline --scenario all # 用 gpt-4o-mini 单价复算
python demo.py --offline --scenario all --model gpt-4o # 同一份 token 用量,换 gpt-4o 单价重算
python demo.py --offline --price-input 0.20 --price-cached 0.10 --price-output 0.80
```
`gpt-4o` 单价后,同一批 token 的四组总成本等比放大(结论与占比不变,A=$0.062930 → B=$0.044047,仍是 30.0%)——这说明**成本优化的相对收益由 token 结构决定,与绝对单价无关**。
## 注意事项
- 具体数字每次在线运行会有小幅波动:OpenAI 的 prompt cache 是**尽力而为**的(按约 128 token 的块缓存、约 5–10 分钟过期、偶尔未命中),因此某些轮次可能出现 `cached_tokens=0`(如上面 B 组的 turn-2/turn-4)。`demo.py` 在正式计量前会先对 KV-cache 组跑一次「预热」把稳定前缀写入缓存,让命中更稳定;`--no-warmup` 可关闭。
- 价格写在 `config.py``PRICING_PRESETS`),默认是 gpt-4o-mini 的公开单价(输入 \$0.15 / 缓存 \$0.075 / 输出 \$0.60 每百万 token)。**换模型**:用 `--model`(如 `--model gpt-4o`)或 `--price-*` 直接覆盖单价即可;缓存命中要求稳定前缀 ≥ 1024 token,换更强模型不影响该机制。
- 「工具返回注入」token 用 tiktoken 按当前模型的编码器离线估算(统计每轮输入里工具返回文本占多少 token),用于回答书中「一次工具返回可能占 2000-5000 token,且在后续每轮被反复计费」这一放大因素。
- 凭据:首选 `OPENAI_API_KEY`;未设置时自动回退到 `OPENROUTER_API_KEY`(走 OpenRouter`gpt-*` 映射为 `openai/*`)。`gpt-5.x` 直连需组织实名认证,故有 `OPENROUTER_API_KEY` 时优先走 OpenRouter。离线复算(`--offline`)无需任何 key。
+273
View File
@@ -0,0 +1,273 @@
"""
一个多轮「客服退款 Agent」任务,用于成本分析(对应书 6.x 表6-4 的客服退款示例)。
为了让实验可复现、不依赖模型工具调用的随机性,这里用「受控工具环境」:
每一轮我们把上一步工具的返回结果喂给模型,由模型(真实 LLM 调用)决定下一步怎么做。
工具返回内容是预设好的(真实 API 里会是订单系统/物流系统的返回),
但每一次 LLM 调用、每一份 token 用量、每一分成本都是真实的。
本文件把「是否 KV-cache 友好」和「是否压缩上下文」两个开关正交拆开,
可组合出完整的 2×2 A/B(对应书中「对比启用/禁用 KV Cache、启用/禁用上下文压缩」):
run_scenario(kv_cache=False, compress=False) —— A 朴素(前缀不稳定 + 不压缩)
run_scenario(kv_cache=True, compress=False) —— 仅 KV-cache(稳定长前缀,历史不压缩)
run_scenario(kv_cache=False, compress=True) —— 仅压缩(前缀不稳定,旧轮次摘要)
run_scenario(kv_cache=True, compress=True) —— B 优化(稳定前缀 + 压缩,两个杠杆叠加)
兼容旧接口:run_naive == (False, False)run_optimized == (True, True)。
"""
import uuid
from functools import lru_cache
from config import MODEL, Pricing
from tracer import Tracer
# 最近保留几轮完整工具返回(更早的压成一句话摘要)。压缩关闭时视为无穷大。
KEEP_VERBOSE = 2
# 限制每轮输出长度:本实验聚焦「输入侧」的 KV-cache 与压缩两个杠杆,
# 把输出 token 控制在相近水平,可避免模型生成长度的随机波动干扰 A/B 成本对比。
MAX_OUTPUT_TOKENS = 160
# ---------------------------------------------------------------------------
# 一个「足够长且稳定」的系统提示 + 工具定义(> 1024 token),
# 这是 KV-cache 命中的关键:稳定的长前缀才会被 OpenAI 自动缓存。
# 内容是一个真实感的客服退款 Agent 的系统规范与工具手册。
# ---------------------------------------------------------------------------
STABLE_SYSTEM_PROMPT = """你是「云购商城」的高级客服 Agent,专门处理售后与退款事务。你必须严格遵循以下工作规范。
# 角色与目标
你的目标是在保障平台规则的前提下,高效、礼貌地帮助用户完成退款、退货、换货、物流查询等售后诉求。
你要主动澄清诉求、核对订单状态、判断是否符合退款政策,并在权限范围内执行操作。
# 可用工具手册(tool manual
1. query_order(order_id): 查询订单详情。返回字段包括:order_id, status, item_name, sku, price,
quantity, pay_time, pay_channel, buyer_note, seller_note, is_prepaid, warehouse, promotion_tags。
2. query_logistics(order_id): 查询物流轨迹。返回字段:carrier, tracking_no, current_status,
last_scan_time, last_scan_location, estimated_delivery, full_trace(数组,含每个扫描节点)。
3. check_refund_policy(sku, reason): 查询该 SKU 在给定退款原因下的退款政策。返回字段:
refundable, need_return, restocking_fee_rate, refund_window_days, special_notes, approval_required。
4. query_user_history(user_id): 查询用户历史行为,用于风控。返回:total_orders, refund_count_90d,
dispute_count, risk_level, vip_tier, register_days。
5. issue_refund(order_id, amount, reason): 发起退款。返回:refund_id, status, expected_arrival,
channel, operator。仅当政策允许且金额不超过订单实付金额时才可调用。
6. send_notification(user_id, channel, template, params): 给用户发通知(sms/app/email)。
# 决策规范
- 先核对订单是否存在、状态是否允许退款(已发货未签收、已签收 7 天内、未发货均有不同处理路径)。
- 未发货:可直接全额退款,无需退货。
- 已发货未签收:需拦截物流或等待退回,退款在退货签收后发起。
- 已签收 7 天内且商品无质量问题:适用 7 天无理由,可能收取一定比例的手续费(restocking_fee_rate)。
- 商品质量问题:全额退款且不收手续费,需用户提供凭证。
- 涉及大额退款(> 500 元)或高风险用户(risk_level=high)需要人工审批(approval_required=true)。
- 每一步都要给出简短的中文推理,说明你「基于什么信息、决定下一步调用哪个工具或给出什么结论」。
# 输出要求
- 保持专业、简洁、有同理心。
- 每轮只推进一步,不要臆造工具尚未返回的数据。
- 最终解决时,明确告知用户退款金额、到账时间与后续动作。
# 合规与风控
- 不得泄露其它用户信息;不得承诺超出政策的赔付;金额与政策以工具返回为准。
- 对疑似欺诈(短期高频退款、异常物流轨迹)要保持谨慎并触发人工审批。
请始终遵守以上全部规范。"""
# ---------------------------------------------------------------------------
# 预设的多轮剧本:用户诉求 + 每一步工具返回(真实 API 里来自后端系统)。
# 工具返回故意写得比较「啰嗦」(大 JSON),以体现工具结果注入上下文的 token 成本。
# ---------------------------------------------------------------------------
USER_REQUEST = (
"你好,我上周买的蓝牙耳机(订单号 ORD20240517001)到货后一直连不上,"
"试了各种办法都没用,我想退货退款,怎么处理?"
)
# 每一轮:(逻辑步骤名, 关联工具, 该工具的"啰嗦"返回文本)
# 工具返回都写得比较大(真实的订单/物流/知识库返回往往几百到上千 token),
# 以体现「工具结果注入上下文后在后续每一轮被反复计费」这一放大因素。
_LOGISTICS_TRACE = ",".join(
'{"time":"2024-05-%02dT%02d:%02d","loc":"%s","desc":"%s","operator":"SF%04d","scan_type":"auto"}'
% (17 + i // 6, 6 + i, (i * 7) % 60, loc, desc, 1000 + i)
for i, (loc, desc) in enumerate([
("华东1仓", "包裹已揽收,称重0.42kg"), ("华东1仓分拣中心", "已分拣,发往上海转运"),
("上海转运中心", "到达转运中心"), ("上海转运中心", "已发出,运输中"),
("苏州中转场", "途经中转"), ("上海浦东集散点", "到达派送网点"),
("上海浦东集散点", "安排派送"), ("浦东xx营业点", "派送中,联系收件人"),
("浦东xx营业点", "首次派送未接通"), ("浦东xx营业点", "二次派送"),
("浦东xx营业点", "已签收,签收人:本人"),
])
)
TOOL_RESULTS = [
("turn-1", "query_order",
'{"order_id":"ORD20240517001","status":"SIGNED","item_name":"Acme 主动降噪蓝牙耳机 Pro",'
'"sku":"SKU-BT-9981","price":499.00,"quantity":1,"pay_time":"2024-05-17T10:22:31",'
'"pay_channel":"wechat_pay","buyer_note":"希望尽快发货,送人用","seller_note":"已核对库存",'
'"is_prepaid":true,"warehouse":"华东1仓","promotion_tags":["满300减30","会员日","新客礼"],'
'"actual_paid":469.00,"coupon_used":"CPN-30","points_earned":469,"invoice_requested":false,'
'"sign_time":"2024-05-19T14:03:11","after_sale_window_end":"2024-05-26T23:59:59",'
'"sub_items":[{"sku":"SKU-BT-9981","name":"耳机主体","qty":1},{"sku":"SKU-BT-9981-CASE","name":"充电盒","qty":1},{"sku":"SKU-BT-9981-TIP","name":"耳塞套装","qty":1}],'
'"address_hash":"a1b2c3d4","channel":"app","device":"iOS","order_source":"首页推荐位"}'),
("turn-2", "query_logistics",
'{"carrier":"顺丰速运","tracking_no":"SF1234567890123","current_status":"已签收",'
'"last_scan_time":"2024-05-19T14:03:11","last_scan_location":"上海市浦东新区xx营业点",'
'"estimated_delivery":"2024-05-19","weight_kg":0.42,"volume":"20x15x8cm","insured":true,'
'"full_trace":[' + _LOGISTICS_TRACE + ']}'),
("turn-3", "check_refund_policy",
'{"sku":"SKU-BT-9981","reason":"quality_issue_cannot_connect","refundable":true,'
'"need_return":true,"restocking_fee_rate":0.0,"refund_window_days":7,'
'"special_notes":"质量问题类退款免手续费;需用户回寄并由质检确认是否为质量问题;'
'若质检判定非质量问题(如人为损坏、私自拆修),将按原路退回商品且不予退款;'
'回寄运费由平台承担,用户需在系统中申请电子面单;退款在质检通过后 1 个工作日内发起;'
'3C 电子类目已激活/绑定账号的商品,需先解绑再回寄,否则质检不予通过。",'
'"approval_required":false,"category":"3C-电子","quality_claim_supported":true,'
'"return_label_provided":true,"qc_sla_days":2,"related_policy_ids":["P-3C-01","P-3C-07","P-QC-12"]}'),
("turn-4", "query_knowledge_base",
'{"query":"蓝牙耳机无法连接 排查","hits":['
'{"kb_id":"KB-1001","title":"蓝牙耳机无法连接的常见原因","content":"1.未进入配对模式;'
'2.手机蓝牙缓存异常需忘记设备重连;3.固件版本过低;4.电量过低;5.多设备抢占连接。"},'
'{"kb_id":"KB-1002","title":"Acme Pro 系列重置方法","content":"长按充电盒按键15秒至指示灯红白交替闪烁即完成重置,'
'随后在手机端删除旧配对记录重新搜索。若重置后仍无法搜索到设备,多为硬件故障,建议走质量问题退换。"},'
'{"kb_id":"KB-1003","title":"质量问题判定标准","content":"重置无效 + 换设备仍无法连接 + 无进液/外观损伤,'
'通常判定为质量问题,支持免费退换。"}],"suggested_action":"引导用户重置;若无效则判定质量问题走退款流程"}'),
("turn-5", "query_user_history",
'{"user_id":"U-88123","total_orders":37,"refund_count_90d":1,"dispute_count":0,'
'"risk_level":"low","vip_tier":"gold","register_days":1180,"payment_disputes":0,'
'"avg_order_value":312.5,"last_refund_reason":"尺码不合适","chargeback_count":0,'
'"complaint_count":0,"account_status":"normal","fraud_flags":[],"lifetime_value":11562.5}'),
("turn-6", "issue_refund",
'{"refund_id":"RF20240520777","status":"APPROVED","amount":469.00,'
'"expected_arrival":"1-3 个工作日","channel":"原路退回-微信","operator":"agent-bot",'
'"return_shipping":"平台承担","return_address":"华东1仓退货组","return_label":"SF-RET-998877",'
'"qc_required":true,"qc_deadline":"2024-05-27","refund_flow":"pending_return->qc->refund"}'),
("turn-7", "send_notification",
'{"user_id":"U-88123","channel":"app","template":"refund_approved",'
'"delivered":true,"message_id":"MSG-556677","sent_time":"2024-05-20T15:20:03",'
'"params":{"refund_id":"RF20240520777","amount":469.00,"return_label":"SF-RET-998877"},'
'"read_receipt":false,"fallback_sms_scheduled":true}'),
("turn-8", "close_ticket",
'{"ticket_id":"TK-20240520-3345","status":"resolved","resolution":"refund_after_return",'
'"csat_survey_sent":true,"handle_time_s":184,"escalated":false,"agent":"agent-bot",'
'"summary_logged":true,"tags":["退款","质量问题","3C","已闭环"]}'),
]
# 供「上下文压缩」策略使用的旧轮次一句话摘要(把啰嗦的工具返回压成要点)
TOOL_SUMMARIES = {
"turn-1": "[摘要] 订单 ORD20240517001Acme降噪耳机Pro,实付469元,已于5/19签收,售后窗口至5/26。",
"turn-2": "[摘要] 物流:顺丰已签收(5/19 14:03,本人签收),11 个轨迹节点均正常无异常。",
"turn-3": "[摘要] 退款政策:质量问题可退、免手续费,需回寄质检,回寄运费平台承担,无需人工审批。",
"turn-4": "[摘要] 知识库:先引导重置耳机;重置无效即判定质量问题,支持免费退换。",
"turn-5": "[摘要] 用户风控:37单/90天仅1次退款/low风险/gold会员,信誉良好,无欺诈标记。",
"turn-6": "[摘要] 已发起退款 RF20240520777:469元原路退微信,需回寄质检,平台承担回寄运费。",
"turn-7": "[摘要] 已通过 app 通知用户退款已批准,附回寄面单。",
}
def _next_user_msg(tool_name: str, tool_result: str) -> str:
"""把工具返回包装成喂给模型的下一条 user 消息。"""
return (
f"[工具 {tool_name} 返回结果]\n{tool_result}\n\n"
f"请基于以上结果给出你的推理,并决定下一步动作。"
)
@lru_cache(maxsize=1)
def _encoder():
"""按当前模型取 tiktoken 编码器(离线可用),用于估算「工具返回注入」的 token。"""
import tiktoken
try:
return tiktoken.encoding_for_model(MODEL)
except Exception:
return tiktoken.get_encoding("cl100k_base")
def _ntok(text: str) -> int:
return len(_encoder().encode(text))
# ---------------------------------------------------------------------------
# 四种 A/B 场景的登记表:名字 + 两个开关。
# ---------------------------------------------------------------------------
SCENARIOS = {
"naive": ("A 朴素(无缓存/无压缩)", False, False),
"kv": ("KV 仅缓存(稳定前缀/不压缩)", True, False),
"compress": ("仅压缩(前缀不稳定/摘要)", False, True),
"both": ("B 优化(KV缓存+压缩)", True, True),
}
def build_messages(idx, step, tool, result, turns, kv_cache, compress):
"""构造第 idx 轮要发给模型的 messages,并返回本轮输入里「工具返回注入」的累计 token。
kv_cache=True → system 用逐字节稳定的长前缀(可被 OpenAI 自动缓存);
kv_cache=False → 每轮在 system 最前面塞随机 session 头,破坏前缀一致性。
compress=True → 仅最近 KEEP_VERBOSE 轮保留完整工具返回,更早轮次压成一句话摘要。
"""
if kv_cache:
system = {"role": "system", "content": STABLE_SYSTEM_PROMPT}
else:
volatile_head = f"[会话追踪] session={uuid.uuid4()} 请求序号={uuid.uuid4()}\n\n"
system = {"role": "system", "content": volatile_head + STABLE_SYSTEM_PROMPT}
history = [{"role": "user", "content": USER_REQUEST}]
tool_ctx_tokens = 0
for j, (p_step, p_assistant, p_tool, p_result) in enumerate(turns):
history.append({"role": "assistant", "content": p_assistant})
if compress and idx - j > KEEP_VERBOSE:
compact = TOOL_SUMMARIES.get(p_step, f"[摘要] {p_tool} 已完成。")
history.append({"role": "user", "content": compact})
tool_ctx_tokens += _ntok(compact)
else:
history.append({"role": "user", "content": _next_user_msg(p_tool, p_result)})
tool_ctx_tokens += _ntok(p_result)
messages = [system] + history + [
{"role": "user", "content": _next_user_msg(tool, result)}
]
tool_ctx_tokens += _ntok(result) # 本轮新注入的工具返回
return messages, tool_ctx_tokens
def run_scenario(client, kv_cache: bool, compress: bool, name: str = None,
pricing: Pricing = None) -> Tracer:
"""跑一遍 8 轮客服退款任务,两个开关正交组合出 2×2 中的一格。
两组做的是同样的逻辑工作,只在上下文构造上不同,因此成本差异纯粹来自
KV-cache 复用与上下文压缩这两个输入侧杠杆。
"""
tracer = Tracer(client, name=name or f"kv={kv_cache},compress={compress}",
pricing=pricing)
turns = []
for idx, (step, tool, result) in enumerate(TOOL_RESULTS):
messages, tool_ctx = build_messages(
idx, step, tool, result, turns, kv_cache, compress)
model_name = MODEL.lower()
if model_name.startswith("kimi-k2.5"):
temperature = 0.6
elif any(tag in model_name for tag in ("kimi-k3", "gpt-5")):
temperature = 1
else:
temperature = 0
request = {
"model": MODEL,
"messages": messages,
"temperature": temperature,
"max_tokens": MAX_OUTPUT_TOKENS,
}
if MODEL.lower().startswith("kimi-k2.5"):
request["extra_body"] = {"thinking": {"type": "disabled"}}
resp = tracer.chat(step=step, tool=tool, tool_ctx_tokens=tool_ctx, **request)
assistant_text = resp.choices[0].message.content or ""
turns.append((step, assistant_text, tool, result))
return tracer
def run_naive(client, pricing: Pricing = None) -> Tracer:
"""(a) 朴素做法:前缀不稳定 + 不压缩历史(KV-cache 命中不了、上下文疯长)。"""
return run_scenario(client, kv_cache=False, compress=False,
name=SCENARIOS["naive"][0], pricing=pricing)
def run_optimized(client, pricing: Pricing = None) -> Tracer:
"""(b) KV-cache 友好 + 上下文压缩:稳定长前缀命中缓存 + 旧轮次摘要。"""
return run_scenario(client, kv_cache=True, compress=True,
name=SCENARIOS["both"][0], pricing=pricing)
+129
View File
@@ -0,0 +1,129 @@
"""
全局配置:模型与价格。
价格换算成本时使用「每百万 token 单价(美元)」。
默认值取自 OpenAI gpt-4o-mini 的公开定价(2024-2025):
- 输入 : $0.15 / 1M tokens
- 缓存命中输入 : $0.075 / 1M tokens (命中 prompt cache 的输入按 5 折计费)
- 输出 : $0.60 / 1M tokens
注意:
1. 默认模型为 gpt-5.6-luna(当前廉价旗舰)。首选凭据是 OPENAI_API_KEY;若未设置,
自动回退到 OPENROUTER_API_KEY 并把模型名映射成 OpenRouter idgpt-* -> openai/*)。
由于 gpt-5.x 直连 OpenAI 需要组织实名认证,只要 OPENROUTER_API_KEY 存在就优先走
OpenRouter(见 make_client_and_model)。仍可用 COST_DEMO_MODEL / --model 切换任意模型。
2. OpenAI 的 prompt caching 是「自动」的:当请求前缀 >= 1024 token 且与近期请求
命中相同前缀时,usage.prompt_tokens_details.cached_tokens 会大于 0
这部分 token 按缓存价(更便宜)计费。本项目正是用它来真实体现 KV-cache 的节省。
OpenRouter 转发 OpenAI 时同样在 prompt_tokens_details.cached_tokens 回传缓存命中。)
"""
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
# 使用的模型(默认当前廉价旗舰 gpt-5.6-luna;可用 COST_DEMO_MODEL / --model 覆盖)
MODEL = os.environ.get("COST_DEMO_MODEL", "gpt-5.6-luna")
# OpenRouter 回退:无 OPENAI_API_KEY 时用 OPENROUTER_API_KEY 走 OpenAI 兼容端点。
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _to_openrouter_model(model: str) -> str:
"""把模型名映射成 OpenRouter id:含 '/' 视为原生 idgpt-* -> openai/*
claude-* -> anthropic/claude-opus-4.8;其余回退到 openai/gpt-5.6-luna。"""
if "/" in model:
return model
if model.startswith("gpt-"):
return "openai/" + model
if model.startswith("claude-"):
return "anthropic/claude-opus-4.8"
return "openai/gpt-5.6-luna"
def make_client_and_model(model: str):
"""构造 OpenAI 兼容 client 并返回 (client, 实际调用的模型名)。
回退策略(universal OpenRouter fallback):
- gpt-5.x 且存在 OPENROUTER_API_KEY -> 优先走 OpenRouter(直连需组织实名认证);
- 否则有 OPENAI_API_KEY -> 直连 OpenAI,模型名不变;
- 否则有 OPENROUTER_API_KEY -> 走 OpenRouter,模型名按 _to_openrouter_model 映射;
- 两者皆无 -> 抛出清晰错误。
"""
from openai import OpenAI
primary = os.environ.get("OPENAI_API_KEY", "").strip()
orkey = os.environ.get("OPENROUTER_API_KEY", "").strip()
prefer_openrouter = bool(orkey) and model.startswith("gpt-5")
if not prefer_openrouter and primary:
return OpenAI(timeout=60.0, max_retries=2), model
if orkey:
return (
OpenAI(base_url=OPENROUTER_BASE_URL, api_key=orkey,
timeout=60.0, max_retries=2),
_to_openrouter_model(model),
)
if primary:
return OpenAI(timeout=60.0, max_retries=2), model
raise RuntimeError(
"缺少可用凭据:请设置 OPENAI_API_KEY(直连 OpenAI),或设置 "
"OPENROUTER_API_KEY(自动回退到 OpenRouter);或改用 --offline 离线复算(无需 key)。"
)
# 每百万 token 的美元单价(默认 gpt-4o-mini
PRICE_INPUT_PER_M = 0.15 # 普通输入
PRICE_CACHED_PER_M = 0.075 # 命中缓存的输入(gpt-4o-mini 缓存读取为输入价的 50%)
PRICE_OUTPUT_PER_M = 0.60 # 输出
@dataclass(frozen=True)
class Pricing:
"""一组每百万 token 的美元单价。"""
input_per_m: float
cached_per_m: float
output_per_m: float
def cost_usd(self, prompt_tokens: int, cached_tokens: int,
completion_tokens: int) -> float:
"""按 token 用量换算成本(美元)。
prompt_tokens : usage.prompt_tokens,包含了缓存命中的部分
cached_tokens : usage.prompt_tokens_details.cached_tokens,命中缓存的输入 token
completion_tokens: usage.completion_tokens
未命中缓存的输入 = prompt_tokens - cached_tokens,按普通输入价计费;
命中缓存的输入按缓存价计费。
"""
uncached_input = max(prompt_tokens - cached_tokens, 0)
return (
uncached_input / 1_000_000 * self.input_per_m
+ cached_tokens / 1_000_000 * self.cached_per_m
+ completion_tokens / 1_000_000 * self.output_per_m
)
# 常见 OpenAI 模型的公开单价预设(每百万 token,美元),方便 CLI 用 --model 一键切换。
# 换更强的模型不影响 KV-cache 机制(仍要求稳定前缀 >= 1024 token)。
PRICING_PRESETS = {
"gpt-4o-mini": Pricing(0.15, 0.075, 0.60),
"gpt-4o": Pricing(2.50, 1.25, 10.00),
"gpt-4.1-mini": Pricing(0.40, 0.10, 1.60),
"gpt-4.1": Pricing(2.00, 0.50, 8.00),
}
def default_pricing() -> Pricing:
"""返回默认模型(config 中 MODEL)的单价;未知模型回退到模块级 PRICE_* 默认值。"""
return PRICING_PRESETS.get(
MODEL, Pricing(PRICE_INPUT_PER_M, PRICE_CACHED_PER_M, PRICE_OUTPUT_PER_M)
)
def cost_usd(prompt_tokens: int, cached_tokens: int, completion_tokens: int,
pricing: "Pricing | None" = None) -> float:
"""按 token 用量换算成本(美元)。默认用模块级单价,可传入自定义 Pricing。"""
p = pricing or Pricing(PRICE_INPUT_PER_M, PRICE_CACHED_PER_M, PRICE_OUTPUT_PER_M)
return p.cost_usd(prompt_tokens, cached_tokens, completion_tokens)
@@ -0,0 +1,452 @@
"""
Agent trajectory cost-efficiency analyzer (实验 7-9 成本效率分析).
Builds on the span/trace model from ``tracer.py``: an agent task is a sequence
of turns, each turn carrying token usage (prompt / cached / completion), tool
context tokens, and latency. This module turns a recorded trajectory into an
:class:`EfficiencyReport` — per-turn metrics, a single efficiency score, and
actionable recommendations (wasteful turns, compression opportunities, cache
miss patterns).
It is fully offline: it never calls a model. Pricing is configured per million
tokens (same convention as ``config.Pricing``) and defaults to gpt-4o-mini.
Two trajectory shapes are accepted:
1. A bare list of turn dicts (the spans of one scenario).
2. A trace dict as written by the tracer — ``{"turns": [...]}``,
``{"spans": [...]}``, or ``{"scenarios": [{"spans": [...]}, ...]}`` (the
first scenario with spans is analyzed). A top-level ``"pricing"`` key is
honoured when no explicit pricing was given to the constructor.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import Any
# --------------------------------------------------------------------------- #
# Data shapes
# --------------------------------------------------------------------------- #
@dataclass
class TurnMetrics:
"""Per-turn cost-efficiency metrics."""
turn_id: int
input_tokens: int
output_tokens: int
cache_hit_ratio: float
cost_usd: float
latency_ms: float
tool_calls: int
classification: str # productive / wasteful / cached / expensive
@property
def total_tokens(self) -> int:
return self.input_tokens + self.output_tokens
@dataclass
class EfficiencyReport:
"""Aggregate cost-efficiency report for a whole trajectory."""
total_turns: int
total_cost_usd: float
total_tokens: int
efficiency_score: float
turn_metrics: list[TurnMetrics]
recommendations: list[str]
# Derived aggregate metrics (computed by analyze_trajectory).
cumulative_costs: list[float] = field(default_factory=list)
tokens_per_tool_call: float = 0.0
latency_per_turn: float = 0.0
# --------------------------------------------------------------------------- #
# Analyzer
# --------------------------------------------------------------------------- #
_STEP_RE = re.compile(r"turn[-_ ]]?(\d+)", re.IGNORECASE)
class CostEfficiencyAnalyzer:
"""Analyze the cost-efficiency of a recorded agent trajectory.
Parameters
----------
pricing:
Per-million-token USD prices with keys ``input``, ``output`` and
``cached``. ``None`` falls back to :meth:`default_pricing` (and to a
``pricing`` block embedded in the trajectory, if present).
wasteful_token_threshold:
A turn with no tool calls and at least this many total tokens is
classified ``wasteful``.
expensive_cost_threshold:
Per-turn cost (USD) above which a turn is ``expensive``. ``None`` means
relative: a turn is expensive when its cost exceeds 1.5x the mean
per-turn cost of the trajectory (computed in :meth:`analyze_trajectory`;
:meth:`analyze_turn` alone treats ``None`` as "never expensive").
cached_ratio_threshold:
Cache hit ratio at or above which a turn is ``cached``.
"""
def __init__(
self,
pricing: dict[str, float] | None = None,
*,
wasteful_token_threshold: int = 1000,
expensive_cost_threshold: float | None = None,
cached_ratio_threshold: float = 0.5,
) -> None:
self._pricing_explicit = pricing is not None
self.pricing: dict[str, float] = pricing or self.default_pricing()
self.wasteful_token_threshold = wasteful_token_threshold
self.expensive_cost_threshold = expensive_cost_threshold
self.cached_ratio_threshold = cached_ratio_threshold
# ---------- pricing ---------- #
@staticmethod
def default_pricing() -> dict[str, float]:
"""Default per-million-token USD prices (gpt-4o-mini)."""
return {"input": 0.15, "cached": 0.075, "output": 0.60}
def _cost_usd(
self, input_tokens: int, cached_tokens: int, output_tokens: int
) -> float:
"""USD cost for one turn given per-million-token pricing."""
uncached = max(input_tokens - cached_tokens, 0)
per_m = 1_000_000.0
return (
uncached / per_m * self.pricing.get("input", 0.0)
+ cached_tokens / per_m * self.pricing.get("cached", 0.0)
+ output_tokens / per_m * self.pricing.get("output", 0.0)
)
# ---------- turn normalization ---------- #
@staticmethod
def _parse_turn_id(turn: dict[str, Any], index: int) -> int:
raw = turn.get("turn_id")
if isinstance(raw, (int, float)):
return int(raw)
step = turn.get("step") or turn.get("turn") or ""
if isinstance(step, str):
m = _STEP_RE.search(step)
if m:
return int(m.group(1))
return index + 1
@staticmethod
def _coerce_int(value: Any) -> int:
"""Coerce nullable/numeric JSON values to int (None -> 0)."""
if value is None:
return 0
try:
return int(value)
except (TypeError, ValueError):
return 0
@staticmethod
def _coerce_float(value: Any) -> float:
if value is None:
return 0.0
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def _normalize_turn(self, turn: dict[str, Any]) -> dict[str, Any]:
"""Map a raw turn/span dict onto the analyzer's canonical fields."""
input_tokens = self._coerce_int(
turn.get("prompt_tokens", turn.get("input_tokens"))
)
output_tokens = self._coerce_int(
turn.get("completion_tokens", turn.get("output_tokens"))
)
cached_tokens = self._coerce_int(turn.get("cached_tokens"))
explicit_ratio = turn.get("cache_hit_ratio")
if cached_tokens == 0 and explicit_ratio is not None:
cached_tokens = round(self._coerce_float(explicit_ratio) * input_tokens)
if input_tokens > 0:
cache_hit_ratio = cached_tokens / input_tokens
elif explicit_ratio is not None:
cache_hit_ratio = self._coerce_float(explicit_ratio)
else:
cache_hit_ratio = 0.0
cache_hit_ratio = max(0.0, min(1.0, cache_hit_ratio))
latency_ms: float
if turn.get("latency_ms") is not None:
latency_ms = self._coerce_float(turn.get("latency_ms"))
elif turn.get("latency_s") is not None:
latency_ms = self._coerce_float(turn.get("latency_s")) * 1000.0
else:
latency_ms = 0.0
tool_calls = turn.get("tool_calls")
if tool_calls is None:
tool = turn.get("tool")
tool_calls = 1 if (isinstance(tool, str) and tool) else 0
else:
tool_calls = self._coerce_int(tool_calls)
return {
"turn_id": self._parse_turn_id(turn, -1),
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"cached_tokens": cached_tokens,
"cache_hit_ratio": cache_hit_ratio,
"latency_ms": latency_ms,
"tool_calls": tool_calls,
"tool_ctx_tokens": self._coerce_int(turn.get("tool_ctx_tokens", -1)),
}
# ---------- classification ---------- #
def _classify(
self,
total_tokens: int,
tool_calls: int,
cache_hit_ratio: float,
cost_usd: float,
expensive_threshold: float,
) -> str:
if tool_calls == 0 and total_tokens >= self.wasteful_token_threshold:
return "wasteful"
if cost_usd >= expensive_threshold and expensive_threshold > 0:
return "expensive"
if cache_hit_ratio >= self.cached_ratio_threshold:
return "cached"
return "productive"
# ---------- public API ---------- #
def analyze_turn(self, turn: dict[str, Any]) -> TurnMetrics:
"""Analyze a single turn dict into :class:`TurnMetrics`.
Uses the absolute ``expensive_cost_threshold`` configured on the
analyzer; when it is ``None`` the turn is never classified expensive
here (a relative threshold is only available to :meth:`analyze_trajectory`,
which sees the whole distribution).
"""
n = self._normalize_turn(turn)
cost = self._cost_usd(n["input_tokens"], n["cached_tokens"], n["output_tokens"])
threshold = self.expensive_cost_threshold
if threshold is None:
threshold = float("inf")
classification = self._classify(
n["input_tokens"] + n["output_tokens"],
n["tool_calls"],
n["cache_hit_ratio"],
cost,
threshold,
)
return TurnMetrics(
turn_id=n["turn_id"],
input_tokens=n["input_tokens"],
output_tokens=n["output_tokens"],
cache_hit_ratio=n["cache_hit_ratio"],
cost_usd=cost,
latency_ms=n["latency_ms"],
tool_calls=n["tool_calls"],
classification=classification,
)
def _extract_turns(self, trajectory: dict[str, Any] | list[dict]) -> list[dict]:
"""Pull the list of turn dicts out of any supported trajectory shape."""
if isinstance(trajectory, list):
return list(trajectory)
if not isinstance(trajectory, dict):
raise TypeError(
"trajectory must be a list of turn dicts or a trace dict, "
f"got {type(trajectory).__name__}"
)
if "turns" in trajectory:
return list(trajectory["turns"] or [])
if "spans" in trajectory:
return list(trajectory["spans"] or [])
if "scenarios" in trajectory:
for scenario in trajectory["scenarios"] or []:
spans = scenario.get("spans") or []
if spans:
return list(spans)
return []
# A bare single-turn dict is treated as one turn.
if {"prompt_tokens", "input_tokens", "step", "tool"} & trajectory.keys():
return [trajectory]
return []
def analyze_trajectory(
self, trajectory: dict[str, Any] | list[dict]
) -> EfficiencyReport:
"""Analyze a full trajectory into an :class:`EfficiencyReport`."""
# Honour embedded pricing when no explicit pricing was configured.
if (
not self._pricing_explicit
and isinstance(trajectory, dict)
and isinstance(trajectory.get("pricing"), dict)
):
self.pricing = {**self.pricing, **trajectory["pricing"]}
turns = self._extract_turns(trajectory)
metrics = [self.analyze_turn(t) for t in turns]
total_turns = len(metrics)
total_cost = sum(m.cost_usd for m in metrics)
total_tokens = sum(m.total_tokens for m in metrics)
# Relative expensive threshold: 1.5x mean per-turn cost.
# When mean cost is zero (e.g. a fully cached or zero-token
# trajectory), every turn costs $0 and none should be flagged
# expensive — a zero threshold would mark all of them. Skip the
# relative reclassification in that case.
if self.expensive_cost_threshold is None and total_turns > 0:
mean_cost = total_cost / total_turns
rel_threshold = mean_cost * 1.5
if rel_threshold > 0:
for m in metrics:
if m.classification == "productive" and m.cost_usd >= rel_threshold:
m.classification = "expensive"
elif self.expensive_cost_threshold is None:
rel_threshold = float("inf")
else:
rel_threshold = self.expensive_cost_threshold
# Cumulative cost per turn (running sum).
cumulative: list[float] = []
running = 0.0
for m in metrics:
running += m.cost_usd
cumulative.append(running)
total_tool_calls = sum(m.tool_calls for m in metrics)
tokens_per_tool_call = (
total_tokens / total_tool_calls if total_tool_calls > 0 else 0.0
)
latency_per_turn = (
sum(m.latency_ms for m in metrics) / total_turns if total_turns > 0 else 0.0
)
# Efficiency score: productive-turn ratio weighted by token efficiency
# (fraction of tokens NOT spent on wasteful turns).
productive_turns = sum(1 for m in metrics if m.classification == "productive")
wasteful_tokens = sum(
m.total_tokens for m in metrics if m.classification == "wasteful"
)
if total_turns == 0:
efficiency_score = 0.0
else:
productive_ratio = productive_turns / total_turns
token_efficiency = (
1.0 - wasteful_tokens / total_tokens if total_tokens > 0 else 1.0
)
efficiency_score = max(0.0, min(1.0, productive_ratio * token_efficiency))
recommendations = self._recommendations(
metrics, efficiency_score, total_cost, total_tokens, rel_threshold
)
return EfficiencyReport(
total_turns=total_turns,
total_cost_usd=total_cost,
total_tokens=total_tokens,
efficiency_score=efficiency_score,
turn_metrics=metrics,
recommendations=recommendations,
cumulative_costs=cumulative,
tokens_per_tool_call=tokens_per_tool_call,
latency_per_turn=latency_per_turn,
)
# ---------- recommendations ---------- #
def _recommendations(
self,
metrics: list[TurnMetrics],
efficiency_score: float,
total_cost: float,
total_tokens: int,
expensive_threshold: float,
) -> list[str]:
recs: list[str] = []
# Wasteful turns: high tokens, no tool calls.
for m in metrics:
if m.classification == "wasteful":
recs.append(
f"Turn {m.turn_id} is wasteful: {m.total_tokens} tokens with "
f"no tool calls — consider context compression or early stopping."
)
# Expensive turns.
for m in metrics:
if m.classification == "expensive":
recs.append(
f"Turn {m.turn_id} is expensive: ${m.cost_usd:.6f} exceeds the "
f"${expensive_threshold:.6f}/turn threshold — review its prompt size."
)
# Cache miss pattern: high input tokens but low cache hit ratio overall.
if metrics:
high_input_turns = [m for m in metrics if m.input_tokens >= 1024]
if high_input_turns:
mean_ratio = sum(m.cache_hit_ratio for m in high_input_turns) / len(
high_input_turns
)
if mean_ratio < self.cached_ratio_threshold:
recs.append(
f"Cache miss pattern: mean cache hit ratio is {mean_ratio:.2%} "
f"across {len(high_input_turns)} turns with >=1024 input tokens "
f"— stabilize the prompt prefix to benefit from KV-cache."
)
# Context compression opportunity: tool_ctx_tokens growing across turns.
ctx_growth = self._max_tool_ctx_growth(metrics)
if ctx_growth > 0:
recs.append(
f"Context compression opportunity: tool context tokens grow by "
f"{ctx_growth} across the trajectory — summarize prior tool results "
f"to avoid re-billing them every turn."
)
# Overall efficiency verdict.
if metrics:
if efficiency_score < 0.5:
recs.append(
f"Low efficiency score ({efficiency_score:.2f}): fewer than half "
f"of turns are productive — review the trajectory structure."
)
elif efficiency_score >= 0.8:
recs.append(
f"High efficiency score ({efficiency_score:.2f}): trajectory is "
f"cost-efficient."
)
return recs
@staticmethod
def _max_tool_ctx_growth(metrics: list[TurnMetrics]) -> int:
"""Largest per-step increase in tool context tokens (0 if unknown)."""
# tool_ctx_tokens is not stored on TurnMetrics; recompute from the
# fact that input_tokens tend to grow as context accumulates. We use
# the raw input-token delta as a proxy when tool_ctx is unavailable.
if len(metrics) < 2:
return 0
growth = 0
prev = metrics[0].input_tokens
for m in metrics[1:]:
delta = m.input_tokens - prev
if delta > growth:
growth = delta
prev = m.input_tokens
return growth
if __name__ == "__main__": # pragma: no cover - manual smoke
import json
import pathlib
here = pathlib.Path(__file__).resolve().parent
trace = json.loads((here / "sample_trace.json").read_text(encoding="utf-8"))
analyzer = CostEfficiencyAnalyzer()
report = analyzer.analyze_trajectory(trace)
print(f"turns={report.total_turns} cost=${report.total_cost_usd:.6f} "
f"score={report.efficiency_score:.3f}")
for r in report.recommendations:
print(" -", r)
+291
View File
@@ -0,0 +1,291 @@
"""
实验 7-9:Agent 任务的端到端成本分析(可运行 demo + CLI)。
两种运行方式:
1) 在线(--live,默认):真实调用模型(默认 gpt-5.6-luna),token 与 cached_tokens
取自 API 返回的 usage,成本按单价换算。需要 OPENAI_API_KEY 或 OPENROUTER_API_KEY
(无 OpenAI key 时自动回退到 OpenRoutergpt-5.x 只要有 OpenRouter key 就优先走它)。
2) 离线(--offline):不打模型,读入一份此前真实运行录下的 tracecanned token
counts),用可配置的单价重新计算成本、成本构成与 A/B 对比表。无需 API key。
无论哪种方式,都会产出两份交付:
(a) 单次任务的「按步骤 + 按成本构成」拆解(哪一步最贵、输入/缓存/输出各占多少)。
(b) A/B 对比表:朴素 vs 仅 KV-cache vs 仅压缩 vs 两者叠加(完整 2×2),
量化 总 token / 缓存 token / 缓存率 / 成本 / 相对基线的节省。
示例:
python demo.py # 在线,默认跑 A(朴素)+B(优化) 两组
python demo.py --scenario all # 在线,跑完整 2×2 四组
python demo.py --live --save-trace out.json # 在线跑并把真实用量落盘
python demo.py --offline # 离线,用内置 sample_trace.json 重算
python demo.py --offline --model gpt-4o # 离线,换 gpt-4o 单价重算同一份用量
python demo.py --offline --price-input 0.20 --price-cached 0.10 --price-output 0.80
"""
import argparse
import json
import os
import sys
import config
from config import PRICING_PRESETS, Pricing
DEFAULT_TRACE = os.path.join(os.path.dirname(__file__), "sample_trace.json")
SCENARIO_KEYS = ["naive", "kv", "compress", "both"]
def _pct(saved: float, base: float) -> str:
if base == 0:
return "0.0%"
return f"{saved / base * 100:.1f}%"
def build_pricing(args) -> Pricing:
"""根据 --model 预设 + --price-* 覆盖,构造本次计费用的单价。"""
base = PRICING_PRESETS.get(args.model)
if base is None:
base = config.default_pricing()
return Pricing(
input_per_m=args.price_input if args.price_input is not None else base.input_per_m,
cached_per_m=args.price_cached if args.price_cached is not None else base.cached_per_m,
output_per_m=args.price_output if args.price_output is not None else base.output_per_m,
)
def resolve_scenarios(arg: str):
"""把 --scenario 解析成有序去重的场景 key 列表。"""
if arg == "all":
return list(SCENARIO_KEYS)
if arg == "ab":
return ["naive", "both"]
keys, seen = [], set()
for k in arg.split(","):
k = k.strip()
if k and k not in seen:
keys.append(k)
seen.add(k)
return keys
# ---------------------------------------------------------------------------
# 采集:在线跑真实模型,或离线从 trace 文件读回
# ---------------------------------------------------------------------------
def collect_live(keys, pricing, warmup: bool):
import agent
if not (os.environ.get("OPENAI_API_KEY") or os.environ.get("OPENROUTER_API_KEY")):
print("未检测到 OPENAI_API_KEY 或 OPENROUTER_API_KEY,请先 export 其一 "
"(无 OpenAI key 时会自动回退到 OpenRouter),或改用 --offline(离线复算,无需 key)。",
file=sys.stderr)
sys.exit(1)
# 构造 client 并解析实际模型名(可能被回退映射成 OpenRouter id)。
client, resolved = config.make_client_and_model(config.MODEL)
if resolved != config.MODEL:
print(f">>> 已回退到 OpenRouter:模型 {config.MODEL} -> {resolved}")
config.MODEL = resolved
agent.MODEL = resolved
try:
agent._encoder.cache_clear()
except Exception:
pass
tracers = []
for k in keys:
name, kv, compress = agent.SCENARIOS[k]
# KV-cache 组先跑一次「预热」,把稳定前缀写入 OpenAI 的 prompt cache
# 让正式计量时更稳定地命中 cached_tokens(真实系统里前缀早已是热的)。
if kv and warmup:
print(f">>> 预热 [{name}] 的稳定前缀(写入 prompt cache...")
agent.run_scenario(client, kv, compress, name=name, pricing=pricing)
print(f">>> 正在运行 [{name}] {'(在线计量)' if kv else ''}...")
tr = agent.run_scenario(client, kv, compress, name=name, pricing=pricing)
tracers.append((k, tr))
return tracers
def collect_offline(keys, pricing, trace_path):
from tracer import Tracer
if not os.path.exists(trace_path):
print(f"找不到 trace 文件:{trace_path}", file=sys.stderr)
sys.exit(1)
with open(trace_path, "r", encoding="utf-8") as f:
data = json.load(f)
by_key = {s.get("key", s.get("name")): s for s in data.get("scenarios", [])}
print(f"离线模式:读入 {trace_path}")
print(f" 该 trace 采集自模型 = {data.get('model', '?')}"
f"{len(by_key)} 个场景的真实录制用量(token 数为实测,成本按当前单价重算)。")
tracers = []
for k in keys:
sc = by_key.get(k)
if sc is None:
print(f" [跳过] trace 中没有场景 '{k}'(可用在线模式 --save-trace 补录)",
file=sys.stderr)
continue
spans = sc.get("spans")
if not spans:
print(f" [跳过] trace 中场景 '{k}' 缺少 spans 数据"
f"(可用在线模式 --save-trace 补录)", file=sys.stderr)
continue
tr = Tracer.from_records(spans, name=sc.get("name", k), pricing=pricing)
tracers.append((k, tr))
if not tracers:
print("trace 里没有任何被选中的场景,退出。", file=sys.stderr)
sys.exit(1)
return tracers
# ---------------------------------------------------------------------------
# 交付 (b)A/B 对比表
# ---------------------------------------------------------------------------
def print_ab_table(tracers):
print("\n\n===== A/B 成本对比(同一个 8 轮客服退款任务)=====")
header = (f"{'方案':<26} {'总输入tok':>10} {'缓存tok':>10} {'缓存率':>8} "
f"{'输出tok':>8} {'总成本($)':>12} {'vs基线':>10}")
print(header)
print("-" * len(header))
base_cost = tracers[0][1].total_cost()
for _, tr in tracers:
pin = tr.total_prompt_tokens()
cac = tr.total_cached_tokens()
rate = f"{(cac / pin * 100):.1f}%" if pin else "0.0%"
cost = tr.total_cost()
vs = "基线" if abs(cost - base_cost) < 1e-12 else f"-{_pct(base_cost - cost, base_cost)}"
print(f"{tr.name:<26} {pin:>10} {cac:>10} {rate:>8} "
f"{tr.total_completion_tokens():>8} {cost:>12.6f} {vs:>10}")
print("-" * len(header))
# 用第一个(基线)和最后一个(通常是 both 优化)做重点量化
base_k, base = tracers[0]
best_k, best = tracers[-1]
if base_k != best_k:
tok_a = base.total_prompt_tokens() + base.total_completion_tokens()
tok_b = best.total_prompt_tokens() + best.total_completion_tokens()
cost_a, cost_b = base.total_cost(), best.total_cost()
print(f"\n重点对比:{base.name}{best.name}")
print(f" 总 token: A={tok_a} → B={tok_b} "
f"减少 {tok_a - tok_b} ({_pct(tok_a - tok_b, tok_a)})")
print(f" 缓存 token: A={base.total_cached_tokens()}"
f"B={best.total_cached_tokens()} B 靠稳定前缀命中缓存)")
print(f" 总成本: A=${cost_a:.6f} → B=${cost_b:.6f} "
f"降低 ${cost_a - cost_b:.6f} ({_pct(cost_a - cost_b, cost_a)})")
if cost_b > 0:
print(f" 成本倍率: A 是 B 的 {cost_a / cost_b:.2f}")
print("\n结论: 稳定长前缀让重复的系统提示/工具定义/历史轮次按缓存价计费,")
print(" 叠加上下文压缩控制上下文增长,二者共同显著降低了端到端成本。")
def dump_output(path, tracers, pricing, model):
out = {
"model": model,
"pricing": {"input": pricing.input_per_m, "cached": pricing.cached_per_m,
"output": pricing.output_per_m},
"scenarios": [],
}
import agent
for k, tr in tracers:
name = agent.SCENARIOS[k][0] if k in agent.SCENARIOS else tr.name
out["scenarios"].append({
"key": k, "name": name,
"total_cost": tr.total_cost(),
"component_costs": tr.component_costs(),
"cost_distribution": tr.cost_distribution(),
"spans": tr.to_records(),
})
with open(path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"\n已写出结果到 {path}")
def build_parser():
p = argparse.ArgumentParser(
prog="demo.py",
description="实验 7-9:Agent 任务端到端成本分析——对客服退款 Agent 做全链路成本拆解,"
"并对比 KV-cache / 上下文压缩两个杠杆的成本差异(完整 2×2 A/B)。",
epilog="示例:\n"
" python demo.py # 在线,默认跑 A(朴素)+B(优化)\n"
" python demo.py --scenario all # 在线,跑完整 2×2 四组\n"
" python demo.py --offline # 离线,用内置 canned trace 重算(无需 key\n"
" python demo.py --offline --model gpt-4o # 换单价离线重算\n"
" python demo.py --live --save-trace out.json # 在线跑并落盘真实用量\n",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
mode = p.add_mutually_exclusive_group()
mode.add_argument("--live", action="store_true",
help="在线模式(默认):真实调用 OpenAI,需要 OPENAI_API_KEY。")
mode.add_argument("--offline", action="store_true",
help="离线模式:不打模型,从 trace 文件读真实录制的 token 用量并按单价重算成本。")
p.add_argument("--trace", metavar="FILE", default=DEFAULT_TRACE,
help=f"离线模式读取的 tracecanned token counts)文件,默认 {os.path.basename(DEFAULT_TRACE)}")
p.add_argument("--save-trace", metavar="FILE", default=None,
help="在线模式下把本次真实 token 用量落盘为 trace 文件(供之后 --offline 复算)。")
p.add_argument("--scenario", metavar="NAME", default="ab",
help="选择要跑的 A/B 场景:ab(默认,=naive+both) / all(2×2 四组) / "
"或逗号分隔的子集 naive,kv,compress,both。")
p.add_argument("--model", metavar="NAME", default=config.MODEL,
help=f"模型名(决定默认单价预设,可选 {', '.join(PRICING_PRESETS)}),"
f"默认 {config.MODEL}")
p.add_argument("--price-input", type=float, default=None,
help="覆盖输入单价(每百万 token 美元)。")
p.add_argument("--price-cached", type=float, default=None,
help="覆盖缓存命中输入单价(每百万 token 美元)。")
p.add_argument("--price-output", type=float, default=None,
help="覆盖输出单价(每百万 token 美元)。")
p.add_argument("--no-warmup", action="store_true",
help="在线模式下关闭 KV-cache 组的前缀预热(默认预热以稳定命中缓存)。")
p.add_argument("--output", metavar="FILE", default=None,
help="把成本拆解结果(含成本构成/分布/逐步用量)写成 JSON 文件。")
return p
def main():
args = build_parser().parse_args()
# 让 agent / tracer 使用选定模型
config.MODEL = args.model
try:
import agent
agent.MODEL = args.model
agent._encoder.cache_clear()
except Exception:
pass
pricing = build_pricing(args)
keys = resolve_scenarios(args.scenario)
bad = [k for k in keys if k not in SCENARIO_KEYS]
if bad:
print(f"未知场景 {bad},可选:{SCENARIO_KEYS} / all / ab", file=sys.stderr)
sys.exit(2)
print(f"模型: {args.model}")
print(f"单价(每百万token): 输入 ${pricing.input_per_m} / 缓存输入 ${pricing.cached_per_m} "
f"/ 输出 ${pricing.output_per_m}")
if args.offline:
tracers = collect_offline(keys, pricing, args.trace)
else:
print("说明: OpenAI prompt caching 自动生效(前缀>=1024token 且近期命中相同前缀),")
print(" 命中的输入 token 出现在 usage.prompt_tokens_details.cached_tokens。")
tracers = collect_live(keys, pricing, warmup=not args.no_warmup)
if args.save_trace:
dump_output(args.save_trace, tracers, pricing, args.model)
# 交付 (a):逐场景成本拆解
for _, tr in tracers:
tr.print_breakdown(title=f"{tr.name}(单次任务全链路拆解)")
# 交付 (b)A/B 对比表
if len(tracers) >= 2:
print_ab_table(tracers)
if args.output:
dump_output(args.output, tracers, pricing, args.model)
if __name__ == "__main__":
main()
+10
View File
@@ -0,0 +1,10 @@
# 复制为 .env 或直接 export。默认模型为 gpt-5.6-luna(当前廉价旗舰)。
# 首选 OPENAI_API_KEY;若未设置则自动回退到 OPENROUTER_API_KEY(走 OpenRouter 兼容端点)。
# 提示:gpt-5.x 直连 OpenAI 需组织实名认证,只要设置了 OPENROUTER_API_KEY 就会优先走它。
OPENAI_API_KEY=your-openai-api-key
# OpenRouter 回退 key(无 OPENAI_API_KEY 时使用;模型名自动映射 gpt-* -> openai/*
# OPENROUTER_API_KEY=your-openrouter-api-key
# 可选:覆盖默认模型(默认 gpt-5.6-luna
# COST_DEMO_MODEL=gpt-5.6-luna
@@ -0,0 +1,3 @@
openai>=1.40.0
python-dotenv>=1.0.0
tiktoken>=0.7.0
@@ -0,0 +1,426 @@
{
"model": "kimi-k2.5",
"pricing": {
"input": 0.5911688312,
"cached": 0.1034545455,
"output": 3.1036363636
},
"scenarios": [
{
"key": "naive",
"name": "A 朴素(无缓存/无压缩)",
"total_cost": 0.0152468353252392,
"component_costs": {
"uncached_input_cost": 0.0115224716889192,
"cached_input_cost": 0.0,
"output_cost": 0.00372436363632,
"uncached_input_tokens": 19491,
"cached_input_tokens": 0,
"output_tokens": 1200,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0019058544156549,
"p50": 0.0019094753247440002,
"p95": 0.0025698109091944,
"p99": 0.0025698109091944,
"max": 0.0025698109091944
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1013,
"cached_tokens": 0,
"completion_tokens": 120,
"tool_ctx_tokens": 300,
"latency_s": 2.287337064743042
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1670,
"cached_tokens": 0,
"completion_tokens": 120,
"tool_ctx_tokens": 899,
"latency_s": 3.4959402084350586
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2008,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 5.752177000045776
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2390,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1482,
"latency_s": 3.8040597438812256
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2682,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1585,
"latency_s": 6.000391006469727
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2970,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1693,
"latency_s": 5.413041353225708
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3251,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1779,
"latency_s": 5.660830974578857
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3507,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1847,
"latency_s": 5.281260013580322
}
]
},
{
"key": "kv",
"name": "KV 仅缓存(稳定前缀/不压缩)",
"total_cost": 0.0079609602864777,
"component_costs": {
"uncached_input_cost": 0.0029623470131432,
"cached_input_cost": 0.0014759860006485,
"output_cost": 0.003522627272686,
"uncached_input_tokens": 5011,
"cached_input_tokens": 14267,
"output_tokens": 1135,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0009951200358097126,
"p50": 0.001072025558548,
"p95": 0.0011735883637072,
"p99": 0.0011735883637072,
"max": 0.0011735883637072
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 129,
"tool_ctx_tokens": 300,
"latency_s": 5.202546119689941
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1620,
"cached_tokens": 512,
"completion_tokens": 145,
"tool_ctx_tokens": 899,
"latency_s": 4.699679136276245
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 1990,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 4.436970949172974
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2375,
"cached_tokens": 1536,
"completion_tokens": 160,
"tool_ctx_tokens": 1482,
"latency_s": 5.637408018112183
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2663,
"cached_tokens": 2048,
"completion_tokens": 160,
"tool_ctx_tokens": 1585,
"latency_s": 4.351202964782715
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2953,
"cached_tokens": 2304,
"completion_tokens": 160,
"tool_ctx_tokens": 1693,
"latency_s": 5.36053204536438
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3233,
"cached_tokens": 2816,
"completion_tokens": 160,
"tool_ctx_tokens": 1779,
"latency_s": 4.889960765838623
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3489,
"cached_tokens": 3072,
"completion_tokens": 61,
"tool_ctx_tokens": 1847,
"latency_s": 1.9250061511993408
}
]
},
{
"key": "compress",
"name": "仅压缩(前缀不稳定/摘要)",
"total_cost": 0.0132817901303144,
"component_costs": {
"uncached_input_cost": 0.009309135584906399,
"cached_input_cost": 0.0,
"output_cost": 0.003972654545408001,
"uncached_input_tokens": 15747,
"cached_input_tokens": 0,
"output_tokens": 1280,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0016602237662893,
"p50": 0.0017338981818776,
"p95": 0.00189765194812,
"p99": 0.00189765194812,
"max": 0.00189765194812
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1012,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 300,
"latency_s": 5.470736980438232
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1710,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 899,
"latency_s": 4.1132972240448
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2093,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 3.5907950401306152
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2227,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1231,
"latency_s": 3.2491540908813477
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2021,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 780,
"latency_s": 7.461982250213623
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2114,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 677,
"latency_s": 4.925306081771851
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2200,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 486,
"latency_s": 5.416359186172485
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2370,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 499,
"latency_s": 4.96558403968811
}
]
},
{
"key": "both",
"name": "B 优化(KV缓存+压缩)",
"total_cost": 0.0086632688576729,
"component_costs": {
"uncached_input_cost": 0.0041033028573592,
"cached_input_cost": 0.0008138769094485001,
"output_cost": 0.0037460890908652,
"uncached_input_tokens": 6941,
"cached_input_tokens": 7867,
"output_tokens": 1207,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0010829086072091125,
"p50": 0.0011068749610916,
"p95": 0.0013616982857848,
"p99": 0.0013616982857848,
"max": 0.0013616982857848
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 116,
"tool_ctx_tokens": 300,
"latency_s": 2.2258410453796387
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1607,
"cached_tokens": 512,
"completion_tokens": 131,
"tool_ctx_tokens": 899,
"latency_s": 4.5919647216796875
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 1963,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 7.6671669483184814
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2097,
"cached_tokens": 768,
"completion_tokens": 160,
"tool_ctx_tokens": 1231,
"latency_s": 5.431225061416626
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1887,
"cached_tokens": 768,
"completion_tokens": 160,
"tool_ctx_tokens": 780,
"latency_s": 5.2519941329956055
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 1988,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 677,
"latency_s": 4.7071919441223145
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2073,
"cached_tokens": 1280,
"completion_tokens": 160,
"tool_ctx_tokens": 486,
"latency_s": 5.377019882202148
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2238,
"cached_tokens": 1536,
"completion_tokens": 160,
"tool_ctx_tokens": 499,
"latency_s": 4.964300155639648
}
]
}
]
}
@@ -0,0 +1,426 @@
{
"model": "kimi-k2.5",
"pricing": {
"input": 0.5911688312,
"cached": 0.1034545455,
"output": 3.1036363636
},
"scenarios": [
{
"key": "naive",
"name": "A 朴素(无缓存/无压缩)",
"total_cost": 0.0152468353252392,
"component_costs": {
"uncached_input_cost": 0.0115224716889192,
"cached_input_cost": 0.0,
"output_cost": 0.00372436363632,
"uncached_input_tokens": 19491,
"cached_input_tokens": 0,
"output_tokens": 1200,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0019058544156549,
"p50": 0.0019094753247440002,
"p95": 0.0025698109091944,
"p99": 0.0025698109091944,
"max": 0.0025698109091944
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1013,
"cached_tokens": 0,
"completion_tokens": 120,
"tool_ctx_tokens": 300,
"latency_s": 2.287337064743042
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1670,
"cached_tokens": 0,
"completion_tokens": 120,
"tool_ctx_tokens": 899,
"latency_s": 3.4959402084350586
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2008,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 5.752177000045776
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2390,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1482,
"latency_s": 3.8040597438812256
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2682,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1585,
"latency_s": 6.000391006469727
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2970,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1693,
"latency_s": 5.413041353225708
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3251,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1779,
"latency_s": 5.660830974578857
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3507,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1847,
"latency_s": 5.281260013580322
}
]
},
{
"key": "kv",
"name": "KV 仅缓存(稳定前缀/不压缩)",
"total_cost": 0.0079609602864777,
"component_costs": {
"uncached_input_cost": 0.0029623470131432,
"cached_input_cost": 0.0014759860006485,
"output_cost": 0.003522627272686,
"uncached_input_tokens": 5011,
"cached_input_tokens": 14267,
"output_tokens": 1135,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0009951200358097126,
"p50": 0.001072025558548,
"p95": 0.0011735883637072,
"p99": 0.0011735883637072,
"max": 0.0011735883637072
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 129,
"tool_ctx_tokens": 300,
"latency_s": 5.202546119689941
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1620,
"cached_tokens": 512,
"completion_tokens": 145,
"tool_ctx_tokens": 899,
"latency_s": 4.699679136276245
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 1990,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 4.436970949172974
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2375,
"cached_tokens": 1536,
"completion_tokens": 160,
"tool_ctx_tokens": 1482,
"latency_s": 5.637408018112183
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2663,
"cached_tokens": 2048,
"completion_tokens": 160,
"tool_ctx_tokens": 1585,
"latency_s": 4.351202964782715
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2953,
"cached_tokens": 2304,
"completion_tokens": 160,
"tool_ctx_tokens": 1693,
"latency_s": 5.36053204536438
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3233,
"cached_tokens": 2816,
"completion_tokens": 160,
"tool_ctx_tokens": 1779,
"latency_s": 4.889960765838623
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3489,
"cached_tokens": 3072,
"completion_tokens": 61,
"tool_ctx_tokens": 1847,
"latency_s": 1.9250061511993408
}
]
},
{
"key": "compress",
"name": "仅压缩(前缀不稳定/摘要)",
"total_cost": 0.0132817901303144,
"component_costs": {
"uncached_input_cost": 0.009309135584906399,
"cached_input_cost": 0.0,
"output_cost": 0.003972654545408001,
"uncached_input_tokens": 15747,
"cached_input_tokens": 0,
"output_tokens": 1280,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0016602237662893,
"p50": 0.0017338981818776,
"p95": 0.00189765194812,
"p99": 0.00189765194812,
"max": 0.00189765194812
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1012,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 300,
"latency_s": 5.470736980438232
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1710,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 899,
"latency_s": 4.1132972240448
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2093,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 3.5907950401306152
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2227,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1231,
"latency_s": 3.2491540908813477
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2021,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 780,
"latency_s": 7.461982250213623
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2114,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 677,
"latency_s": 4.925306081771851
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2200,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 486,
"latency_s": 5.416359186172485
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2370,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 499,
"latency_s": 4.96558403968811
}
]
},
{
"key": "both",
"name": "B 优化(KV缓存+压缩)",
"total_cost": 0.0086632688576729,
"component_costs": {
"uncached_input_cost": 0.0041033028573592,
"cached_input_cost": 0.0008138769094485001,
"output_cost": 0.0037460890908652,
"uncached_input_tokens": 6941,
"cached_input_tokens": 7867,
"output_tokens": 1207,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0010829086072091125,
"p50": 0.0011068749610916,
"p95": 0.0013616982857848,
"p99": 0.0013616982857848,
"max": 0.0013616982857848
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 116,
"tool_ctx_tokens": 300,
"latency_s": 2.2258410453796387
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1607,
"cached_tokens": 512,
"completion_tokens": 131,
"tool_ctx_tokens": 899,
"latency_s": 4.5919647216796875
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 1963,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 1164,
"latency_s": 7.6671669483184814
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2097,
"cached_tokens": 768,
"completion_tokens": 160,
"tool_ctx_tokens": 1231,
"latency_s": 5.431225061416626
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1887,
"cached_tokens": 768,
"completion_tokens": 160,
"tool_ctx_tokens": 780,
"latency_s": 5.2519941329956055
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 1988,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 677,
"latency_s": 4.7071919441223145
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2073,
"cached_tokens": 1280,
"completion_tokens": 160,
"tool_ctx_tokens": 486,
"latency_s": 5.377019882202148
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2238,
"cached_tokens": 1536,
"completion_tokens": 160,
"tool_ctx_tokens": 499,
"latency_s": 4.964300155639648
}
]
}
]
}
@@ -0,0 +1,54 @@
{
"schema_version": 1,
"experiment": "7-9",
"status": "incomplete",
"generated_at_utc": "2026-07-29T22:31:57.870199+00:00",
"run_dir": "chapter7/agent-cost-analysis/runs/exp7-9-kimi25-20260730-v2",
"git_commit": "4a7f37cf278bd15948c409f14533017c4c7fbc29",
"command": "python demo.py --live --scenario all --model kimi-k2.5 --save-trace TRACE --output REPORT",
"provider_receipt_count": 32,
"status_reasons": [
"The real 2x2 run covers one eight-turn refund workflow; the manuscript asks for several representative task types and aggregate task-level p50/p95/p99.",
"Reasoning-token usage and provider response IDs are recorded, but success-quality equivalence across optimizations is not independently judged.",
"The 32 measured calls have unique provider response IDs; 16 cache-warmup calls were intentionally excluded from measured traces and do not have retained receipts."
],
"inputs": [
{
"path": "chapter7/agent-cost-analysis/agent.py",
"bytes": 17109,
"sha256": "30866b34ead7ff2897964aaf1e07867691102982971903f8a2a4172c48bfd718"
},
{
"path": "chapter7/agent-cost-analysis/tracer.py",
"bytes": 12309,
"sha256": "fb673ac8f10ca037b6df3822e00e6cbb21b8381b731736fb1b2a30748e7226fe"
},
{
"path": "chapter7/agent-cost-analysis/demo.py",
"bytes": 13477,
"sha256": "75dc27633829fe2a730c5654a38f6af90fd58eceee630dfa985e386fa536c104"
},
{
"path": "chapter7/agent-cost-analysis/config.py",
"bytes": 5701,
"sha256": "0a92de4849994e74b28e3b5ad6c308cc154a2b984d8827f859b7f15d9fbbb463"
},
{
"path": "chapter7/model-benchmark/campaign_config.json",
"bytes": 8103,
"sha256": "9a839ad907852798b35af5b4623814c343943ae1596a78d650d1a1affc93b17a"
}
],
"artifacts": [
{
"path": "chapter7/agent-cost-analysis/runs/exp7-9-kimi25-20260730-v2/report.json",
"bytes": 17810,
"sha256": "92b0ee29c7df3dd28778d625fe699662dee56a5401141631ead450cd2ca37ace"
},
{
"path": "chapter7/agent-cost-analysis/runs/exp7-9-kimi25-20260730-v2/trace.json",
"bytes": 17810,
"sha256": "92b0ee29c7df3dd28778d625fe699662dee56a5401141631ead450cd2ca37ace"
}
]
}
@@ -0,0 +1,554 @@
{
"model": "kimi-k2.5",
"pricing": {
"input": 0.5911688312,
"cached": 0.1034545455,
"output": 3.1036363636
},
"scenarios": [
{
"key": "naive",
"name": "A 朴素(无缓存/无压缩)",
"total_cost": 0.01574031350707,
"component_costs": {
"uncached_input_cost": 0.0118080062343888,
"cached_input_cost": 0.0,
"output_cost": 0.0039323072726812,
"uncached_input_tokens": 19974,
"cached_input_tokens": 0,
"output_tokens": 1267,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.00196753918838375,
"p50": 0.0019544041559152,
"p95": 0.0026135574027031996,
"p99": 0.0026135574027031996,
"max": 0.0026135574027031996
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1015,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 5.299084663391113,
"response_id": "chatcmpl-6a6a7cb80695ae5455eab84a",
"response_model": "kimi-k2.5",
"response_created": 1785363640
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1706,
"cached_tokens": 0,
"completion_tokens": 147,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 5.079176902770996,
"response_id": "chatcmpl-6a6a7cbd80925af8edb8bb5e",
"response_model": "kimi-k2.5",
"response_created": 1785363646
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2081,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 3.0797362327575684,
"response_id": "chatcmpl-6a6a7cc24511d264b10f37dd",
"response_model": "kimi-k2.5",
"response_created": 1785363651
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2466,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1482,
"latency_s": 3.4901974201202393,
"response_id": "chatcmpl-6a6a7cc5edbcdbb3613f3353",
"response_model": "kimi-k2.5",
"response_created": 1785363654
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2753,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1585,
"latency_s": 6.93122410774231,
"response_id": "chatcmpl-6a6a7cc97f6a7380b55ebedb",
"response_model": "kimi-k2.5",
"response_created": 1785363658
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 3048,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1693,
"latency_s": 5.43744421005249,
"response_id": "chatcmpl-6a6a7cd0d7374244555b9001",
"response_model": "kimi-k2.5",
"response_created": 1785363665
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3324,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1779,
"latency_s": 5.579896926879883,
"response_id": "chatcmpl-6a6a7cd56f58f5f62fa2981a",
"response_model": "kimi-k2.5",
"response_created": 1785363670
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3581,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1847,
"latency_s": 5.334557056427002,
"response_id": "chatcmpl-6a6a7cdb2e6a18d37452de21",
"response_model": "kimi-k2.5",
"response_created": 1785363676
}
]
},
{
"key": "kv",
"name": "KV 仅缓存(稳定前缀/不压缩)",
"total_cost": 0.0082537957670069,
"component_costs": {
"uncached_input_cost": 0.0027956374027448,
"cached_input_cost": 0.0015289547279445,
"output_cost": 0.0039292036363176,
"uncached_input_tokens": 4729,
"cached_input_tokens": 14779,
"output_tokens": 1266,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0010317244708758625,
"p50": 0.001053935792344,
"p95": 0.0011930969351368,
"p99": 0.0011930969351368,
"max": 0.0011930969351368
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 2.981578826904297,
"response_id": "chatcmpl-6a6a7d043905097c262cd2d8",
"response_model": "kimi-k2.5",
"response_created": 1785363716
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1652,
"cached_tokens": 512,
"completion_tokens": 146,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 8.04120397567749,
"response_id": "chatcmpl-6a6a7d07d5eb4229617017c7",
"response_model": "kimi-k2.5",
"response_created": 1785363719
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2023,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 6.327085256576538,
"response_id": "chatcmpl-6a6a7d0f3905097c262cd2f5",
"response_model": "kimi-k2.5",
"response_created": 1785363727
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2408,
"cached_tokens": 1792,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1482,
"latency_s": 4.883957862854004,
"response_id": "chatcmpl-6a6a7d15bb40cba1f65cc486",
"response_model": "kimi-k2.5",
"response_created": 1785363733
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2696,
"cached_tokens": 2048,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1585,
"latency_s": 5.0474817752838135,
"response_id": "chatcmpl-6a6a7d1adc87cefde7ebcca2",
"response_model": "kimi-k2.5",
"response_created": 1785363738
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2986,
"cached_tokens": 2560,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1693,
"latency_s": 3.6142687797546387,
"response_id": "chatcmpl-6a6a7d1f3905097c262cd332",
"response_model": "kimi-k2.5",
"response_created": 1785363743
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3266,
"cached_tokens": 2816,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1779,
"latency_s": 3.981088161468506,
"response_id": "chatcmpl-6a6a7d233905097c262cd33e",
"response_model": "kimi-k2.5",
"response_created": 1785363747
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3522,
"cached_tokens": 3072,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1847,
"latency_s": 5.233341217041016,
"response_id": "chatcmpl-6a6a7d27d5eb42296170182d",
"response_model": "kimi-k2.5",
"response_created": 1785363751
}
]
},
{
"key": "compress",
"name": "仅压缩(前缀不稳定/摘要)",
"total_cost": 0.0125982511692596,
"component_costs": {
"uncached_input_cost": 0.0089390638965752,
"cached_input_cost": 0.0,
"output_cost": 0.0036591872726844,
"uncached_input_tokens": 15121,
"cached_input_tokens": 0,
"output_tokens": 1179,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.00157478139615745,
"p50": 0.0016801018182384,
"p95": 0.0018338057143504,
"p99": 0.0018338057143504,
"max": 0.0018338057143504
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1010,
"cached_tokens": 0,
"completion_tokens": 121,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 3.843111276626587,
"response_id": "chatcmpl-6a6a7d2c96a5dde81c3844dd",
"response_model": "kimi-k2.5",
"response_created": 1785363756
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1671,
"cached_tokens": 0,
"completion_tokens": 110,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 3.103135108947754,
"response_id": "chatcmpl-6a6a7d3006fe5c9e72518409",
"response_model": "kimi-k2.5",
"response_created": 1785363760
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2002,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 5.465490102767944,
"response_id": "chatcmpl-6a6a7d3384ead76d6951b7bb",
"response_model": "kimi-k2.5",
"response_created": 1785363763
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2136,
"cached_tokens": 0,
"completion_tokens": 149,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1231,
"latency_s": 3.5718801021575928,
"response_id": "chatcmpl-6a6a7d3880925af8edb8bcdd",
"response_model": "kimi-k2.5",
"response_created": 1785363769
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1916,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 780,
"latency_s": 3.5103132724761963,
"response_id": "chatcmpl-6a6a7d3cfa52c1b646ff684d",
"response_model": "kimi-k2.5",
"response_created": 1785363772
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2022,
"cached_tokens": 0,
"completion_tokens": 159,
"reasoning_tokens": 1,
"tool_ctx_tokens": 677,
"latency_s": 3.597494125366211,
"response_id": "chatcmpl-6a6a7d3f8a024a47783f03bf",
"response_model": "kimi-k2.5",
"response_created": 1785363776
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2102,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 486,
"latency_s": 9.166883945465088,
"response_id": "chatcmpl-6a6a7d43503e5013c8fe6ef8",
"response_model": "kimi-k2.5",
"response_created": 1785363779
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2262,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 499,
"latency_s": 4.3432776927948,
"response_id": "chatcmpl-6a6a7d4c687ea3db1a5a4d6c",
"response_model": "kimi-k2.5",
"response_created": 1785363789
}
]
},
{
"key": "both",
"name": "B 优化(KV缓存+压缩)",
"total_cost": 0.009057608026520501,
"component_costs": {
"uncached_input_cost": 0.0042445922080159995,
"cached_input_cost": 0.0008403612730964999,
"output_cost": 0.003972654545408001,
"uncached_input_tokens": 7180,
"cached_input_tokens": 8123,
"output_tokens": 1280,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0011322010033150626,
"p50": 0.0011570356364336,
"p95": 0.0014060359481248,
"p99": 0.0014060359481248,
"max": 0.0014060359481248
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 3.2073097229003906,
"response_id": "chatcmpl-6a6a7d7658a6e8702ecebaee",
"response_model": "kimi-k2.5",
"response_created": 1785363830
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1652,
"cached_tokens": 512,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 7.249981880187988,
"response_id": "chatcmpl-6a6a7d792d20c6aa1053113f",
"response_model": "kimi-k2.5",
"response_created": 1785363833
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2038,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 4.833154916763306,
"response_id": "chatcmpl-6a6a7d80f9aa90a3ebfd7fe5",
"response_model": "kimi-k2.5",
"response_created": 1785363841
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2172,
"cached_tokens": 768,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1231,
"latency_s": 5.391065835952759,
"response_id": "chatcmpl-6a6a7d852d20c6aa10531154",
"response_model": "kimi-k2.5",
"response_created": 1785363846
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1962,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 780,
"latency_s": 5.175268888473511,
"response_id": "chatcmpl-6a6a7d8b2d20c6aa10531162",
"response_model": "kimi-k2.5",
"response_created": 1785363851
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2063,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 677,
"latency_s": 5.356127977371216,
"response_id": "chatcmpl-6a6a7d90a183ecb32232c19d",
"response_model": "kimi-k2.5",
"response_created": 1785363856
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2148,
"cached_tokens": 1280,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 486,
"latency_s": 7.17538595199585,
"response_id": "chatcmpl-6a6a7d953905097c262cd43c",
"response_model": "kimi-k2.5",
"response_created": 1785363862
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2313,
"cached_tokens": 1536,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 499,
"latency_s": 5.300028085708618,
"response_id": "chatcmpl-6a6a7d9d56e2191f9ae786d6",
"response_model": "kimi-k2.5",
"response_created": 1785363869
}
]
}
]
}
@@ -0,0 +1,554 @@
{
"model": "kimi-k2.5",
"pricing": {
"input": 0.5911688312,
"cached": 0.1034545455,
"output": 3.1036363636
},
"scenarios": [
{
"key": "naive",
"name": "A 朴素(无缓存/无压缩)",
"total_cost": 0.01574031350707,
"component_costs": {
"uncached_input_cost": 0.0118080062343888,
"cached_input_cost": 0.0,
"output_cost": 0.0039323072726812,
"uncached_input_tokens": 19974,
"cached_input_tokens": 0,
"output_tokens": 1267,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.00196753918838375,
"p50": 0.0019544041559152,
"p95": 0.0026135574027031996,
"p99": 0.0026135574027031996,
"max": 0.0026135574027031996
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1015,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 5.299084663391113,
"response_id": "chatcmpl-6a6a7cb80695ae5455eab84a",
"response_model": "kimi-k2.5",
"response_created": 1785363640
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1706,
"cached_tokens": 0,
"completion_tokens": 147,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 5.079176902770996,
"response_id": "chatcmpl-6a6a7cbd80925af8edb8bb5e",
"response_model": "kimi-k2.5",
"response_created": 1785363646
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2081,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 3.0797362327575684,
"response_id": "chatcmpl-6a6a7cc24511d264b10f37dd",
"response_model": "kimi-k2.5",
"response_created": 1785363651
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2466,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1482,
"latency_s": 3.4901974201202393,
"response_id": "chatcmpl-6a6a7cc5edbcdbb3613f3353",
"response_model": "kimi-k2.5",
"response_created": 1785363654
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2753,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1585,
"latency_s": 6.93122410774231,
"response_id": "chatcmpl-6a6a7cc97f6a7380b55ebedb",
"response_model": "kimi-k2.5",
"response_created": 1785363658
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 3048,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1693,
"latency_s": 5.43744421005249,
"response_id": "chatcmpl-6a6a7cd0d7374244555b9001",
"response_model": "kimi-k2.5",
"response_created": 1785363665
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3324,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1779,
"latency_s": 5.579896926879883,
"response_id": "chatcmpl-6a6a7cd56f58f5f62fa2981a",
"response_model": "kimi-k2.5",
"response_created": 1785363670
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3581,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1847,
"latency_s": 5.334557056427002,
"response_id": "chatcmpl-6a6a7cdb2e6a18d37452de21",
"response_model": "kimi-k2.5",
"response_created": 1785363676
}
]
},
{
"key": "kv",
"name": "KV 仅缓存(稳定前缀/不压缩)",
"total_cost": 0.0082537957670069,
"component_costs": {
"uncached_input_cost": 0.0027956374027448,
"cached_input_cost": 0.0015289547279445,
"output_cost": 0.0039292036363176,
"uncached_input_tokens": 4729,
"cached_input_tokens": 14779,
"output_tokens": 1266,
"tool_ctx_tokens": 10749
},
"cost_distribution": {
"n": 8,
"mean": 0.0010317244708758625,
"p50": 0.001053935792344,
"p95": 0.0011930969351368,
"p99": 0.0011930969351368,
"max": 0.0011930969351368
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 2.981578826904297,
"response_id": "chatcmpl-6a6a7d043905097c262cd2d8",
"response_model": "kimi-k2.5",
"response_created": 1785363716
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1652,
"cached_tokens": 512,
"completion_tokens": 146,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 8.04120397567749,
"response_id": "chatcmpl-6a6a7d07d5eb4229617017c7",
"response_model": "kimi-k2.5",
"response_created": 1785363719
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2023,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 6.327085256576538,
"response_id": "chatcmpl-6a6a7d0f3905097c262cd2f5",
"response_model": "kimi-k2.5",
"response_created": 1785363727
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2408,
"cached_tokens": 1792,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1482,
"latency_s": 4.883957862854004,
"response_id": "chatcmpl-6a6a7d15bb40cba1f65cc486",
"response_model": "kimi-k2.5",
"response_created": 1785363733
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2696,
"cached_tokens": 2048,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1585,
"latency_s": 5.0474817752838135,
"response_id": "chatcmpl-6a6a7d1adc87cefde7ebcca2",
"response_model": "kimi-k2.5",
"response_created": 1785363738
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2986,
"cached_tokens": 2560,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1693,
"latency_s": 3.6142687797546387,
"response_id": "chatcmpl-6a6a7d1f3905097c262cd332",
"response_model": "kimi-k2.5",
"response_created": 1785363743
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3266,
"cached_tokens": 2816,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1779,
"latency_s": 3.981088161468506,
"response_id": "chatcmpl-6a6a7d233905097c262cd33e",
"response_model": "kimi-k2.5",
"response_created": 1785363747
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3522,
"cached_tokens": 3072,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1847,
"latency_s": 5.233341217041016,
"response_id": "chatcmpl-6a6a7d27d5eb42296170182d",
"response_model": "kimi-k2.5",
"response_created": 1785363751
}
]
},
{
"key": "compress",
"name": "仅压缩(前缀不稳定/摘要)",
"total_cost": 0.0125982511692596,
"component_costs": {
"uncached_input_cost": 0.0089390638965752,
"cached_input_cost": 0.0,
"output_cost": 0.0036591872726844,
"uncached_input_tokens": 15121,
"cached_input_tokens": 0,
"output_tokens": 1179,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.00157478139615745,
"p50": 0.0016801018182384,
"p95": 0.0018338057143504,
"p99": 0.0018338057143504,
"max": 0.0018338057143504
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1010,
"cached_tokens": 0,
"completion_tokens": 121,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 3.843111276626587,
"response_id": "chatcmpl-6a6a7d2c96a5dde81c3844dd",
"response_model": "kimi-k2.5",
"response_created": 1785363756
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1671,
"cached_tokens": 0,
"completion_tokens": 110,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 3.103135108947754,
"response_id": "chatcmpl-6a6a7d3006fe5c9e72518409",
"response_model": "kimi-k2.5",
"response_created": 1785363760
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2002,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 5.465490102767944,
"response_id": "chatcmpl-6a6a7d3384ead76d6951b7bb",
"response_model": "kimi-k2.5",
"response_created": 1785363763
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2136,
"cached_tokens": 0,
"completion_tokens": 149,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1231,
"latency_s": 3.5718801021575928,
"response_id": "chatcmpl-6a6a7d3880925af8edb8bcdd",
"response_model": "kimi-k2.5",
"response_created": 1785363769
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1916,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 780,
"latency_s": 3.5103132724761963,
"response_id": "chatcmpl-6a6a7d3cfa52c1b646ff684d",
"response_model": "kimi-k2.5",
"response_created": 1785363772
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2022,
"cached_tokens": 0,
"completion_tokens": 159,
"reasoning_tokens": 1,
"tool_ctx_tokens": 677,
"latency_s": 3.597494125366211,
"response_id": "chatcmpl-6a6a7d3f8a024a47783f03bf",
"response_model": "kimi-k2.5",
"response_created": 1785363776
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2102,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 486,
"latency_s": 9.166883945465088,
"response_id": "chatcmpl-6a6a7d43503e5013c8fe6ef8",
"response_model": "kimi-k2.5",
"response_created": 1785363779
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2262,
"cached_tokens": 0,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 499,
"latency_s": 4.3432776927948,
"response_id": "chatcmpl-6a6a7d4c687ea3db1a5a4d6c",
"response_model": "kimi-k2.5",
"response_created": 1785363789
}
]
},
{
"key": "both",
"name": "B 优化(KV缓存+压缩)",
"total_cost": 0.009057608026520501,
"component_costs": {
"uncached_input_cost": 0.0042445922080159995,
"cached_input_cost": 0.0008403612730964999,
"output_cost": 0.003972654545408001,
"uncached_input_tokens": 7180,
"cached_input_tokens": 8123,
"output_tokens": 1280,
"tool_ctx_tokens": 6036
},
"cost_distribution": {
"n": 8,
"mean": 0.0011322010033150626,
"p50": 0.0011570356364336,
"p95": 0.0014060359481248,
"p99": 0.0014060359481248,
"max": 0.0014060359481248
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 955,
"cached_tokens": 955,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 300,
"latency_s": 3.2073097229003906,
"response_id": "chatcmpl-6a6a7d7658a6e8702ecebaee",
"response_model": "kimi-k2.5",
"response_created": 1785363830
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1652,
"cached_tokens": 512,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 899,
"latency_s": 7.249981880187988,
"response_id": "chatcmpl-6a6a7d792d20c6aa1053113f",
"response_model": "kimi-k2.5",
"response_created": 1785363833
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2038,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1164,
"latency_s": 4.833154916763306,
"response_id": "chatcmpl-6a6a7d80f9aa90a3ebfd7fe5",
"response_model": "kimi-k2.5",
"response_created": 1785363841
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2172,
"cached_tokens": 768,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 1231,
"latency_s": 5.391065835952759,
"response_id": "chatcmpl-6a6a7d852d20c6aa10531154",
"response_model": "kimi-k2.5",
"response_created": 1785363846
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 1962,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 780,
"latency_s": 5.175268888473511,
"response_id": "chatcmpl-6a6a7d8b2d20c6aa10531162",
"response_model": "kimi-k2.5",
"response_created": 1785363851
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2063,
"cached_tokens": 1024,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 677,
"latency_s": 5.356127977371216,
"response_id": "chatcmpl-6a6a7d90a183ecb32232c19d",
"response_model": "kimi-k2.5",
"response_created": 1785363856
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2148,
"cached_tokens": 1280,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 486,
"latency_s": 7.17538595199585,
"response_id": "chatcmpl-6a6a7d953905097c262cd43c",
"response_model": "kimi-k2.5",
"response_created": 1785363862
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2313,
"cached_tokens": 1536,
"completion_tokens": 160,
"reasoning_tokens": 1,
"tool_ctx_tokens": 499,
"latency_s": 5.300028085708618,
"response_id": "chatcmpl-6a6a7d9d56e2191f9ae786d6",
"response_model": "kimi-k2.5",
"response_created": 1785363869
}
]
}
]
}
@@ -0,0 +1,426 @@
{
"model": "gpt-4o-mini",
"pricing": {
"input": 0.15,
"cached": 0.075,
"output": 0.6
},
"scenarios": [
{
"key": "naive",
"name": "A 朴素(无缓存/无压缩)",
"total_cost": 0.0037758,
"component_costs": {
"uncached_input_cost": 0.0031049999999999997,
"cached_input_cost": 0.0,
"output_cost": 0.0006708,
"uncached_input_tokens": 20700,
"cached_input_tokens": 0,
"output_tokens": 1118,
"tool_ctx_tokens": 9544
},
"cost_distribution": {
"n": 8,
"mean": 0.000471975,
"p50": 0.00048059999999999997,
"p95": 0.0006462,
"p99": 0.0006462,
"max": 0.0006462
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1113,
"cached_tokens": 0,
"completion_tokens": 104,
"tool_ctx_tokens": 276,
"latency_s": 3.1535720825195312
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1807,
"cached_tokens": 0,
"completion_tokens": 99,
"tool_ctx_tokens": 829,
"latency_s": 2.0884652137756348
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2154,
"cached_tokens": 0,
"completion_tokens": 139,
"tool_ctx_tokens": 1046,
"latency_s": 2.685513973236084
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2564,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1287,
"latency_s": 2.922238826751709
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2863,
"cached_tokens": 0,
"completion_tokens": 136,
"tool_ctx_tokens": 1389,
"latency_s": 2.6890718936920166
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 3123,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1490,
"latency_s": 3.073489189147949
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3408,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1579,
"latency_s": 3.0861823558807373
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3668,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1648,
"latency_s": 2.504854917526245
}
]
},
{
"key": "kv",
"name": "KV 仅缓存(稳定前缀/不压缩)",
"total_cost": 0.0027075,
"component_costs": {
"uncached_input_cost": 0.0010227,
"cached_input_cost": 0.0010176,
"output_cost": 0.0006672,
"uncached_input_tokens": 6818,
"cached_input_tokens": 13568,
"output_tokens": 1112,
"tool_ctx_tokens": 9544
},
"cost_distribution": {
"n": 8,
"mean": 0.0003384375,
"p50": 0.00033465000000000003,
"p95": 0.00040815,
"p99": 0.00040815,
"max": 0.00040815
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1056,
"cached_tokens": 1024,
"completion_tokens": 121,
"tool_ctx_tokens": 276,
"latency_s": 2.1304330825805664
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1763,
"cached_tokens": 0,
"completion_tokens": 117,
"tool_ctx_tokens": 829,
"latency_s": 2.102344036102295
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2130,
"cached_tokens": 1024,
"completion_tokens": 136,
"tool_ctx_tokens": 1046,
"latency_s": 3.537381172180176
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2540,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 1287,
"latency_s": 3.393212080001831
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2834,
"cached_tokens": 2176,
"completion_tokens": 114,
"tool_ctx_tokens": 1389,
"latency_s": 1.906968116760254
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 3081,
"cached_tokens": 2560,
"completion_tokens": 160,
"tool_ctx_tokens": 1490,
"latency_s": 2.7878849506378174
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 3361,
"cached_tokens": 2560,
"completion_tokens": 160,
"tool_ctx_tokens": 1579,
"latency_s": 2.825930118560791
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 3621,
"cached_tokens": 3200,
"completion_tokens": 144,
"tool_ctx_tokens": 1648,
"latency_s": 2.7222893238067627
}
]
},
{
"key": "compress",
"name": "仅压缩(前缀不稳定/摘要)",
"total_cost": 0.00311475,
"component_costs": {
"uncached_input_cost": 0.00242655,
"cached_input_cost": 0.0,
"output_cost": 0.0006882,
"uncached_input_tokens": 16177,
"cached_input_tokens": 0,
"output_tokens": 1147,
"tool_ctx_tokens": 5248
},
"cost_distribution": {
"n": 8,
"mean": 0.00038934375,
"p50": 0.0004059,
"p95": 0.0004497,
"p99": 0.0004497,
"max": 0.0004497
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1112,
"cached_tokens": 0,
"completion_tokens": 127,
"tool_ctx_tokens": 276,
"latency_s": 2.223604917526245
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1829,
"cached_tokens": 0,
"completion_tokens": 91,
"tool_ctx_tokens": 829,
"latency_s": 2.916440963745117
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2164,
"cached_tokens": 0,
"completion_tokens": 129,
"tool_ctx_tokens": 1046,
"latency_s": 2.543590784072876
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2310,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1052,
"latency_s": 2.999290943145752
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2066,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 635,
"latency_s": 5.8334801197052
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2146,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 551,
"latency_s": 2.787147045135498
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2192,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 430,
"latency_s": 3.470608949661255
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2358,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 429,
"latency_s": 2.5834107398986816
}
]
},
{
"key": "both",
"name": "B 优化(KV缓存+压缩)",
"total_cost": 0.00264285,
"component_costs": {
"uncached_input_cost": 0.00148365,
"cached_input_cost": 0.0004608,
"output_cost": 0.0006984000000000001,
"uncached_input_tokens": 9891,
"cached_input_tokens": 6144,
"output_tokens": 1164,
"tool_ctx_tokens": 5248
},
"cost_distribution": {
"n": 8,
"mean": 0.00033035625,
"p50": 0.00033525000000000004,
"p95": 0.00044249999999999997,
"p99": 0.00044249999999999997,
"max": 0.00044249999999999997
},
"spans": [
{
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 1056,
"cached_tokens": 1024,
"completion_tokens": 139,
"tool_ctx_tokens": 276,
"latency_s": 2.405411958694458
},
{
"step": "turn-2",
"tool": "query_logistics",
"kind": "llm",
"prompt_tokens": 1781,
"cached_tokens": 0,
"completion_tokens": 112,
"tool_ctx_tokens": 829,
"latency_s": 2.1826939582824707
},
{
"step": "turn-3",
"tool": "check_refund_policy",
"kind": "llm",
"prompt_tokens": 2143,
"cached_tokens": 1024,
"completion_tokens": 151,
"tool_ctx_tokens": 1046,
"latency_s": 2.5647878646850586
},
{
"step": "turn-4",
"tool": "query_knowledge_base",
"kind": "llm",
"prompt_tokens": 2310,
"cached_tokens": 0,
"completion_tokens": 160,
"tool_ctx_tokens": 1052,
"latency_s": 3.0318493843078613
},
{
"step": "turn-5",
"tool": "query_user_history",
"kind": "llm",
"prompt_tokens": 2060,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 635,
"latency_s": 2.475983142852783
},
{
"step": "turn-6",
"tool": "issue_refund",
"kind": "llm",
"prompt_tokens": 2143,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 551,
"latency_s": 2.7105050086975098
},
{
"step": "turn-7",
"tool": "send_notification",
"kind": "llm",
"prompt_tokens": 2188,
"cached_tokens": 1024,
"completion_tokens": 160,
"tool_ctx_tokens": 430,
"latency_s": 2.7884562015533447
},
{
"step": "turn-8",
"tool": "close_ticket",
"kind": "llm",
"prompt_tokens": 2354,
"cached_tokens": 1024,
"completion_tokens": 122,
"tool_ctx_tokens": 429,
"latency_s": 2.3781700134277344
}
]
}
]
}
@@ -0,0 +1,9 @@
"""Test import bootstrap for the agent-cost-analysis experiment."""
from pathlib import Path
import sys
EXPERIMENT_ROOT = Path(__file__).resolve().parents[1]
if str(EXPERIMENT_ROOT) not in sys.path:
sys.path.insert(0, str(EXPERIMENT_ROOT))
@@ -0,0 +1,44 @@
"""Tracer.chat must tolerate response.usage == None (OpenAI-compatible providers)."""
from types import SimpleNamespace
import config
from tracer import Tracer
class _FakeClient:
def __init__(self, usage):
self.chat = SimpleNamespace(
completions=SimpleNamespace(create=self._create)
)
self._usage = usage
def _create(self, **_kwargs):
return SimpleNamespace(usage=self._usage)
def test_chat_tolerates_null_usage():
tr = Tracer(_FakeClient(None), pricing=config.default_pricing())
resp = tr.chat(step="turn-1", tool="query_order", model="m", messages=[])
assert resp.usage is None
assert len(tr.spans) == 1
s = tr.spans[0]
assert s.prompt_tokens == 0
assert s.completion_tokens == 0
assert s.cost_usd == 0.0
assert s.latency_s >= 0.0
def test_chat_keeps_real_usage():
usage = SimpleNamespace(
prompt_tokens=100,
completion_tokens=20,
prompt_tokens_details=SimpleNamespace(cached_tokens=10),
)
tr = Tracer(_FakeClient(usage), pricing=config.default_pricing())
tr.chat(step="turn-1", tool="query_order", model="m", messages=[])
s = tr.spans[0]
assert s.prompt_tokens == 100
assert s.completion_tokens == 20
assert s.cached_tokens == 10
assert s.cost_usd > 0
@@ -0,0 +1,96 @@
"""
Regression tests for offline trace parsing (实验 7-9 成本分析).
Covers two crash classes found in --offline mode:
- Tracer.from_records: trace JSON with explicit null token fields -> int(None) TypeError
- demo.collect_offline: scenario dict missing the optional "spans" key -> KeyError
"""
import json
import pytest
import config
import demo
from tracer import Tracer
def _span(**overrides):
span = {
"step": "turn-1",
"tool": "query_order",
"kind": "llm",
"prompt_tokens": 100,
"cached_tokens": 10,
"completion_tokens": 12,
"tool_ctx_tokens": 50,
"latency_s": 1.2,
}
span.update(overrides)
return span
def test_from_records_tolerates_null_fields():
"""Explicit JSON nulls in numeric fields are coerced, not int(None) TypeError."""
records = [_span(prompt_tokens=None, cached_tokens=None,
completion_tokens=None, tool_ctx_tokens=None, latency_s=None)]
tr = Tracer.from_records(records, pricing=config.default_pricing())
s = tr.spans[0]
assert s.prompt_tokens == 0
assert s.cached_tokens == 0
assert s.completion_tokens == 0
assert s.tool_ctx_tokens == -1 # null tool_ctx 视为「未知」
assert s.latency_s == 0.0
def test_from_records_tolerates_missing_fields():
"""Minimal span dicts (only step/tool) still parse."""
tr = Tracer.from_records([{"step": "turn-1", "tool": "query_order"}],
pricing=config.default_pricing())
assert tr.spans[0].prompt_tokens == 0
assert tr.spans[0].tool_ctx_tokens == -1
def test_from_records_keeps_real_values():
"""Normal values pass through unchanged (no coercion side effects)."""
tr = Tracer.from_records([_span()], pricing=config.default_pricing())
s = tr.spans[0]
assert (s.prompt_tokens, s.cached_tokens, s.completion_tokens) == (100, 10, 12)
assert s.tool_ctx_tokens == 50
assert s.latency_s == 1.2
def test_from_records_keeps_zero_tool_ctx_tokens():
"""Explicit 0 means known-zero tool context, not unknown (-1)."""
tr = Tracer.from_records([_span(tool_ctx_tokens=0)],
pricing=config.default_pricing())
assert tr.spans[0].tool_ctx_tokens == 0
assert tr.total_tool_ctx_tokens() == 0
def _write_trace(tmp_path, scenarios):
path = tmp_path / "trace.json"
path.write_text(json.dumps({"model": "gpt-5.6-luna", "scenarios": scenarios}),
encoding="utf-8")
return str(path)
def test_collect_offline_skips_scenario_without_spans(tmp_path, capsys):
"""A scenario missing 'spans' is skipped with a warning, not a KeyError crash."""
trace = _write_trace(tmp_path, [
{"key": "naive", "name": "A naive"}, # no spans -> skip
{"key": "both", "name": "B both", "spans": [_span()]}, # valid
])
tracers = demo.collect_offline(["naive", "both"], config.default_pricing(), trace)
assert [k for k, _ in tracers] == ["both"]
assert "缺少 spans" in capsys.readouterr().err
def test_collect_offline_exits_when_no_usable_scenario(tmp_path):
"""When every selected scenario lacks spans, exit cleanly like the empty-trace path."""
trace = _write_trace(tmp_path, [{"key": "naive", "name": "A naive"}])
with pytest.raises(SystemExit):
demo.collect_offline(["naive"], config.default_pricing(), trace)
if __name__ == "__main__":
pytest.main([__file__, "-v"])
+284
View File
@@ -0,0 +1,284 @@
"""
自建的轻量级 tracing / 可观测系统。
设计沿用分布式追踪的 span 树模型(见书 6.x「Agent 的可观测性」):
- 一次 agent 任务 = 一条 Trace
- 每次 LLM 调用 / 工具调用 = 一个 Span
- Span 记录:所属步骤、类型、token 用量(prompt/completion/cached)、时延、成本
用法:
tracer = Tracer(client)
resp = tracer.chat(step="turn-1", tool="query_order",
model=..., messages=..., temperature=0)
tracer.print_breakdown() # 打印按步骤/工具聚合的成本拆解
离线复用(不打模型、只算成本):
tracer = Tracer.from_records(records, pricing=..., name=...)
# records 里是此前真实运行录下的每一步 token 用量(canned token counts
"""
import math
import time
from dataclasses import dataclass, asdict, field
from typing import List, Optional
from config import Pricing, default_pricing
def _percentile(values: List[float], q: float) -> float:
"""最近秩(nearest-rank)百分位,避免引入 numpy 依赖。q 取 0~100。"""
if not values:
return 0.0
xs = sorted(values)
if len(xs) == 1:
return xs[0]
# 最近秩 = ceil(q/100 * N)。不能用 int(round(x + 0.5)):当 q/100*N 恰为
# 整数 k 时,round(k + 0.5) 的银行家舍入会得到 k+1(如 n=100、q=99 时
# 秩变成 100,把 p99 报成最大值)。
rank = max(1, min(len(xs), math.ceil(q / 100.0 * len(xs))))
return xs[rank - 1]
@dataclass
class Span:
"""一次被追踪的调用(这里主要是 LLM 调用)。"""
step: str # 逻辑步骤名,如 "turn-2"
tool: str # 该步骤关联的工具/动作名,用于归因“哪一步最贵”
kind: str = "llm" # span 类型:llm / tool
prompt_tokens: int = 0
cached_tokens: int = 0
completion_tokens: int = 0
reasoning_tokens: int = 0
# 该轮输入里「工具返回结果」占用的累计 token(同一份工具返回会在后续每轮被反复计费)。
# 由上层用 tokenizer 估算并填入;离线复用时从 records 读回。-1 表示未知。
tool_ctx_tokens: int = -1
latency_s: float = 0.0
cost_usd: float = 0.0
response_id: str = ""
response_model: str = ""
response_created: int = 0
@property
def total_tokens(self) -> int:
return self.prompt_tokens + self.completion_tokens
@property
def uncached_prompt_tokens(self) -> int:
return max(self.prompt_tokens - self.cached_tokens, 0)
class Tracer:
"""包裹 OpenAI client,自动记录每次 LLM 调用的 usage / 时延 / 成本。"""
def __init__(self, client=None, name: str = "trace",
pricing: Optional[Pricing] = None):
self.client = client
self.name = name
self.pricing = pricing or default_pricing()
self.spans: List[Span] = []
# ---------- 采集 ----------
def chat(self, step: str, tool: str, tool_ctx_tokens: int = -1, **kwargs):
"""发起一次被追踪的 chat.completions 调用。
kwargs 原样透传给 openai clientmodel / messages / temperature 等)。
tool_ctx_tokens:本轮输入里工具返回结果占用的累计 token(可选,用于成本归因)。
返回原始的 OpenAI response 对象,方便上层取 content。
"""
t0 = time.time()
resp = self.client.chat.completions.create(**kwargs)
latency = time.time() - t0
usage = resp.usage
# Some OpenAI-compatible providers omit usage (null); match from_records coercion.
if usage is None:
span = Span(
step=step,
tool=tool,
kind="llm",
tool_ctx_tokens=tool_ctx_tokens,
latency_s=latency,
cost_usd=0.0,
)
self.spans.append(span)
return resp
# cached_tokens 藏在 prompt_tokens_details 里,注意做防御式读取
cached = 0
details = getattr(usage, "prompt_tokens_details", None)
if details is not None:
cached = getattr(details, "cached_tokens", 0) or 0
prompt_tokens = int(getattr(usage, "prompt_tokens", 0) or 0)
completion_tokens = int(getattr(usage, "completion_tokens", 0) or 0)
completion_details = getattr(usage, "completion_tokens_details", None)
reasoning_tokens = int(getattr(completion_details, "reasoning_tokens", 0) or 0)
span = Span(
step=step,
tool=tool,
kind="llm",
prompt_tokens=prompt_tokens,
cached_tokens=cached,
completion_tokens=completion_tokens,
reasoning_tokens=reasoning_tokens,
tool_ctx_tokens=tool_ctx_tokens,
latency_s=latency,
cost_usd=self.pricing.cost_usd(
prompt_tokens, cached, completion_tokens),
response_id=str(getattr(resp, "id", "") or ""),
response_model=str(getattr(resp, "model", "") or ""),
response_created=int(getattr(resp, "created", 0) or 0),
)
self.spans.append(span)
return resp
# ---------- 离线复用(canned token counts → 重新计成本)----------
@classmethod
def from_records(cls, records: List[dict], name: str = "trace",
pricing: Optional[Pricing] = None) -> "Tracer":
"""用此前录下的 token 用量重建一条 trace,并按给定单价重算成本(不打模型)。"""
tr = cls(client=None, name=name, pricing=pricing)
for r in records:
span = Span(
step=r.get("step", ""),
tool=r.get("tool", ""),
kind=r.get("kind", "llm"),
# Missing/null tool_ctx → -1 (unknown); keep explicit 0 (known-zero).
prompt_tokens=int(r.get("prompt_tokens") or 0),
cached_tokens=int(r.get("cached_tokens") or 0),
completion_tokens=int(r.get("completion_tokens") or 0),
reasoning_tokens=int(r.get("reasoning_tokens") or 0),
tool_ctx_tokens=(-1 if r.get("tool_ctx_tokens") is None
else int(r.get("tool_ctx_tokens"))),
latency_s=float(r.get("latency_s") or 0.0),
response_id=str(r.get("response_id") or ""),
response_model=str(r.get("response_model") or ""),
response_created=int(r.get("response_created") or 0),
)
span.cost_usd = tr.pricing.cost_usd(
span.prompt_tokens, span.cached_tokens, span.completion_tokens)
tr.spans.append(span)
return tr
def to_records(self) -> List[dict]:
"""导出每一步的原始 token 用量(用于落盘成 canned trace,供离线复用)。"""
out = []
for s in self.spans:
d = asdict(s)
d.pop("cost_usd", None) # 成本由单价重算,不落盘固定值
out.append(d)
return out
# ---------- 聚合 ----------
def total_cost(self) -> float:
return sum(s.cost_usd for s in self.spans)
def total_prompt_tokens(self) -> int:
return sum(s.prompt_tokens for s in self.spans)
def total_cached_tokens(self) -> int:
return sum(s.cached_tokens for s in self.spans)
def total_completion_tokens(self) -> int:
return sum(s.completion_tokens for s in self.spans)
def total_uncached_prompt_tokens(self) -> int:
return sum(s.uncached_prompt_tokens for s in self.spans)
def total_tool_ctx_tokens(self) -> int:
return sum(s.tool_ctx_tokens for s in self.spans if s.tool_ctx_tokens >= 0)
def total_latency(self) -> float:
return sum(s.latency_s for s in self.spans)
def cache_rate(self) -> float:
pin = self.total_prompt_tokens()
return self.total_cached_tokens() / pin if pin else 0.0
def component_costs(self) -> dict:
"""把总成本拆成三个成本构成要素(对应书「成本的构成要素」):
- 未缓存输入 / 缓存输入 / 输出
以及输入侧里「工具返回注入」token 占比(若已知)。"""
p = self.pricing
uncached_in = self.total_uncached_prompt_tokens()
cached_in = self.total_cached_tokens()
out = self.total_completion_tokens()
return {
"uncached_input_cost": uncached_in / 1_000_000 * p.input_per_m,
"cached_input_cost": cached_in / 1_000_000 * p.cached_per_m,
"output_cost": out / 1_000_000 * p.output_per_m,
"uncached_input_tokens": uncached_in,
"cached_input_tokens": cached_in,
"output_tokens": out,
"tool_ctx_tokens": self.total_tool_ctx_tokens(),
}
def cost_distribution(self) -> dict:
"""按步骤的单步成本分布(p50/p95/p99)。对应书「成本分布 p50/p95/p99」。"""
costs = [s.cost_usd for s in self.spans]
n = len(costs)
return {
"n": n,
"mean": (sum(costs) / n) if n else 0.0,
"p50": _percentile(costs, 50),
"p95": _percentile(costs, 95),
"p99": _percentile(costs, 99),
"max": max(costs) if costs else 0.0,
}
# ---------- 打印 ----------
def print_breakdown(self, title: Optional[str] = None):
"""打印一次 agent 任务的按步骤成本拆解表,并指出最贵的一步、成本构成与分布。"""
print()
print(f"===== 成本拆解: {title or self.name} =====")
header = (
f"{'步骤':<8} {'工具/动作':<20} {'输入tok':>8} {'缓存tok':>8} "
f"{'工具tok':>8} {'输出tok':>8} {'时延(s)':>8} {'成本($)':>12}"
)
print(header)
print("-" * len(header))
for s in self.spans:
tctx = s.tool_ctx_tokens if s.tool_ctx_tokens >= 0 else "-"
print(
f"{s.step:<8} {s.tool:<20} {s.prompt_tokens:>8} {s.cached_tokens:>8} "
f"{str(tctx):>8} {s.completion_tokens:>8} {s.latency_s:>8.2f} "
f"{s.cost_usd:>12.6f}"
)
print("-" * len(header))
tctx_total = self.total_tool_ctx_tokens() if any(
s.tool_ctx_tokens >= 0 for s in self.spans) else "-"
print(
f"{'合计':<8} {'':<20} {self.total_prompt_tokens():>8} "
f"{self.total_cached_tokens():>8} {str(tctx_total):>8} "
f"{self.total_completion_tokens():>8} "
f"{self.total_latency():>8.2f} {self.total_cost():>12.6f}"
)
# 归因:哪一步最贵
if self.spans:
worst = max(self.spans, key=lambda s: s.cost_usd)
total = self.total_cost()
share = worst.cost_usd / total * 100 if total else 0
print(
f"\n最贵的一步 → {worst.step} / {worst.tool}: "
f"${worst.cost_usd:.6f}(占总成本 {share:.1f}%"
)
# 成本构成拆解(未缓存输入 / 缓存输入 / 输出)
comp = self.component_costs()
total = self.total_cost() or 1e-12
print("成本构成:")
print(f" 未缓存输入 {comp['uncached_input_tokens']:>8} tok "
f"${comp['uncached_input_cost']:.6f} ({comp['uncached_input_cost']/total*100:.1f}%)")
print(f" 缓存输入 {comp['cached_input_tokens']:>8} tok "
f"${comp['cached_input_cost']:.6f} ({comp['cached_input_cost']/total*100:.1f}%)")
print(f" 输出 {comp['output_tokens']:>8} tok "
f"${comp['output_cost']:.6f} ({comp['output_cost']/total*100:.1f}%)")
if comp["tool_ctx_tokens"] > 0:
print(f" 其中「工具返回注入」累计输入 {comp['tool_ctx_tokens']} tok "
f"(同一份工具返回在后续每轮被反复计费)")
# 单步成本分布
dist = self.cost_distribution()
print(f"单步成本分布(n={dist['n']}): 均值 ${dist['mean']:.6f} "
f"p50 ${dist['p50']:.6f} p95 ${dist['p95']:.6f} p99 ${dist['p99']:.6f}")