AI Agent 单会话成本监控实现:从零构建 Token 追踪系统
AI Agent 的成本管理远比单次 API 调用复杂。一个自主 Agent 可能在一次用户请求中发起 3 次、10 次甚至 50 次模型调用(工具调用、重试、子任务拆分等),如果没有会话级别的成本追踪,账单超支往往在月底才被发现。
本文从零构建一套单会话 token 成本监控系统,覆盖实时追踪、预算告警、多模型成本聚合,以及如何将成本数据接入可观测性平台。
一、问题定义:Agent 成本难在哪里
单次 API 调用的成本追踪相对简单——从响应的 usage 字段读取 token 数乘以单价即可。但 Agent 场景带来几个新挑战:
1. 单会话包含多次调用
用户发起一个”帮我分析这份合同”的请求,Agent 内部可能依次调用:
- 第 1 次:判断任务类型(50 token)
- 第 2 次:调用 OCR 工具解析 PDF(无成本,外部 API)
- 第 3 次:逐段分析合同条款(800 token × 5 段)
- 第 4 次:汇总生成报告(1200 token)
- 合计:约 5250 token,但来自 6 次调用
2. 多模型混用
现代 Agent 常见策略:路由层用 Haiku(便宜快速),实际任务用 Sonnet,复杂推理用 Claude Sonnet 扩展思考。不同模型单价差异可达 20 倍,必须按模型分别计价。
3. 工具调用的隐性成本
工具定义(Tool Schema)会占用大量输入 token,且每轮对话都会重复携带,这部分成本容易被忽略。
4. 预算超限需要中止
Agent 运行中途如果发现本次会话已超预算,需要能够优雅地停止并返回已有结果,而不是继续产生费用。
二、数据结构设计
首先定义成本追踪的核心数据结构:
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import uuid
# 各模型的每千 token 价格(美元,近似估算,以实际账单为准)
MODEL_PRICING = {
"claude-haiku-4-5-20251001": {"input": 0.001, "output": 0.005},
"claude-sonnet-4-5": {"input": 0.003, "output": 0.015},
"claude-opus-4-8": {"input": 0.015, "output": 0.075},
"gpt-4o": {"input": 0.0025, "output": 0.01},
"gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
"hunyuan-turbo": {"input": 0.002, "output": 0.007},
}
@dataclass
class CallRecord:
"""单次 LLM 调用记录"""
call_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
model: str = ""
input_tokens: int = 0
output_tokens: int = 0
cache_read_tokens: int = 0 # Prompt Cache 命中的 token(通常折扣价)
cost_usd: float = 0.0
timestamp: datetime = field(default_factory=datetime.utcnow)
purpose: str = "" # 此次调用的用途标注,如 "route", "analyze", "summarize"
duration_ms: int = 0
@dataclass
class SessionCostTracker:
"""单会话成本追踪器"""
session_id: str = field(default_factory=lambda: str(uuid.uuid4()))
user_id: Optional[str] = None
budget_usd: float = 0.10 # 默认 $0.10 预算上限
calls: list[CallRecord] = field(default_factory=list)
started_at: datetime = field(default_factory=datetime.utcnow)
@property
def total_cost(self) -> float:
return sum(c.cost_usd for c in self.calls)
@property
def total_input_tokens(self) -> int:
return sum(c.input_tokens for c in self.calls)
@property
def total_output_tokens(self) -> int:
return sum(c.output_tokens for c in self.calls)
@property
def remaining_budget(self) -> float:
return self.budget_usd - self.total_cost
@property
def is_over_budget(self) -> bool:
return self.total_cost >= self.budget_usd
def cost_by_model(self) -> dict[str, float]:
result = {}
for call in self.calls:
result[call.model] = result.get(call.model, 0.0) + call.cost_usd
return result
def summary(self) -> dict:
return {
"session_id": self.session_id,
"total_cost_usd": round(self.total_cost, 6),
"total_input_tokens": self.total_input_tokens,
"total_output_tokens": self.total_output_tokens,
"call_count": len(self.calls),
"budget_usd": self.budget_usd,
"remaining_budget_usd": round(self.remaining_budget, 6),
"cost_by_model": {k: round(v, 6) for k, v in self.cost_by_model().items()},
"duration_seconds": (datetime.utcnow() - self.started_at).total_seconds(),
}
三、核心追踪逻辑
将 LLM 调用封装在一个带成本记录的 wrapper 中:
import time
from openai import OpenAI
from contextlib import contextmanager
def calculate_cost(model: str, input_tokens: int, output_tokens: int,
cache_read_tokens: int = 0) -> float:
pricing = MODEL_PRICING.get(model, {"input": 0.003, "output": 0.015})
# Cache 命中的 token 按 10% 折扣计算(以实际提供商策略为准)
cache_discount = 0.1
input_cost = (input_tokens - cache_read_tokens) * pricing["input"] / 1000
cache_cost = cache_read_tokens * pricing["input"] * cache_discount / 1000
output_cost = output_tokens * pricing["output"] / 1000
return input_cost + cache_cost + output_cost
class TrackedLLMClient:
"""带成本追踪的 LLM 客户端"""
def __init__(self, client: OpenAI, tracker: SessionCostTracker):
self.client = client
self.tracker = tracker
def chat(self, model: str, messages: list, purpose: str = "",
max_tokens: int = 1000, **kwargs):
# 预算检查
if self.tracker.is_over_budget:
raise BudgetExceededError(
f"会话已超预算 ${self.tracker.budget_usd:.4f},"
f"已消费 ${self.tracker.total_cost:.4f}"
)
start = time.time()
response = self.client.chat.completions.create(
model=model,
messages=messages,
max_tokens=max_tokens,
**kwargs
)
duration_ms = int((time.time() - start) * 1000)
usage = response.usage
cache_read = getattr(usage, "prompt_tokens_details", None)
cache_read_tokens = getattr(cache_read, "cached_tokens", 0) if cache_read else 0
cost = calculate_cost(
model=model,
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens,
cache_read_tokens=cache_read_tokens
)
record = CallRecord(
model=model,
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens,
cache_read_tokens=cache_read_tokens,
cost_usd=cost,
purpose=purpose,
duration_ms=duration_ms
)
self.tracker.calls.append(record)
# 超预算告警(不中止,但记录警告)
if self.tracker.total_cost > self.tracker.budget_usd * 0.8:
print(f"⚠️ [Session {self.tracker.session_id[:8]}] "
f"已用 {self.tracker.total_cost/self.tracker.budget_usd:.0%} 预算")
return response
class BudgetExceededError(Exception):
pass
四、在 Agent 循环中集成
下面是一个简化的 Tool-use Agent,展示如何在实际 Agent 循环中使用成本追踪:
import json
TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "搜索互联网获取最新信息",
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}
}
]
def run_agent_with_cost_tracking(user_query: str, budget_usd: float = 0.05) -> dict:
tracker = SessionCostTracker(budget_usd=budget_usd)
client_raw = OpenAI(api_key="YOUR_KEY", base_url="https://api.yotradeapi.com/v1")
llm = TrackedLLMClient(client_raw, tracker)
messages = [
{"role": "system", "content": "你是一个有用的助手,使用工具完成用户任务。"},
{"role": "user", "content": user_query}
]
max_iterations = 10
for i in range(max_iterations):
try:
response = llm.chat(
model="claude-sonnet-4-5",
messages=messages,
purpose=f"agent_loop_iter_{i}",
max_tokens=800,
tools=TOOLS,
tool_choice="auto"
)
except BudgetExceededError as e:
return {
"status": "budget_exceeded",
"error": str(e),
"partial_result": messages[-1].get("content", ""),
"cost_summary": tracker.summary()
}
msg = response.choices[0].message
# 没有工具调用,任务完成
if not msg.tool_calls:
return {
"status": "success",
"result": msg.content,
"cost_summary": tracker.summary()
}
# 处理工具调用(此处模拟工具执行)
messages.append(msg.model_dump())
for tool_call in msg.tool_calls:
tool_result = execute_tool(tool_call.function.name,
json.loads(tool_call.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(tool_result, ensure_ascii=False)
})
return {
"status": "max_iterations_reached",
"cost_summary": tracker.summary()
}
def execute_tool(name: str, args: dict) -> dict:
# 实际项目中替换为真实工具调用
return {"result": f"模拟执行 {name}({args})"}
五、成本数据上报与可观测性
生产环境中,成本数据需要接入监控平台:
import logging
import json
from datetime import datetime
logger = logging.getLogger("agent.cost")
def report_session_cost(tracker: SessionCostTracker, outcome: str = "success"):
"""结构化日志上报,可接入 Datadog/Prometheus/自建 ELK"""
summary = tracker.summary()
summary["outcome"] = outcome
summary["timestamp"] = datetime.utcnow().isoformat()
# 结构化日志,方便 ELK / Loki 解析
logger.info(json.dumps(summary, ensure_ascii=False))
# 也可以推送到 Prometheus pushgateway、InfluxDB 等
# push_to_metrics(summary)
告警规则示例(Prometheus AlertManager):
# 单会话成本超过 $0.50 触发告警
- alert: HighAgentSessionCost
expr: agent_session_cost_usd > 0.50
for: 0m
labels:
severity: warning
annotations:
summary: "Agent 会话成本过高:{{ $value }}$"
# 每小时 API 总成本超过 $50 触发告警
- alert: HourlyAPIBudgetExceeded
expr: sum(rate(agent_session_cost_usd[1h])) > 50
for: 5m
labels:
severity: critical
六、工具调用 Token 的成本细节
工具定义(Schema)会在每次调用时计入输入 token,这部分成本容易被忽略:
import tiktoken
def estimate_tool_schema_tokens(tools: list) -> int:
"""估算工具 Schema 占用的 token 数"""
enc = tiktoken.get_encoding("cl100k_base")
schema_text = json.dumps(tools, ensure_ascii=False)
return len(enc.encode(schema_text))
# 如果你有 10 个工具,每个工具 Schema 约 100 token
# 每次调用都会多 1000 input token
# 按 claude-sonnet-4-5 的价格:1000/1000 * $0.003 = $0.003/次
# 100 次调用/天 × $0.003 = $0.3/天,不可忽视
tools_token_cost = estimate_tool_schema_tokens(TOOLS)
print(f"工具 Schema 占用约 {tools_token_cost} tokens/次调用")
优化建议:
- 只在需要时注入相关工具,而不是每次都携带全部工具
- 对于只用文本输出的调用(如路由、摘要),完全移除
tools参数
七、相关阅读
想统一管理多个 Agent 的 API 账单并自动获得用量折扣,YoTradeApi 提供完善的用量统计面板,支持 Claude、GPT、混元等主流模型,一个平台搞定所有账单。