Agent 用了三个月,老板问:"到底花了多少钱?"——答不上来就麻烦了。本文设计一套成本事件采集、多维度拆账、异常检测和周报生成的完整方案,让 Agent 的每一分花费都可追溯、可归因、可优化。

Agent 成本周报:如何按项目、成员、任务类型拆账

Agent 用了三个月,老板问:"到底花了多少钱?"——答不上来就麻烦了。本文设计一套成本事件采集、多维度拆账、异常检测和周报生成的完整方案,让 Agent 的每一分花费都可追溯、可归因、可优化。

一、为什么成本拆账很重要

没有成本拆账的团队通常经历三个阶段:

  1. 蜜月期:Agent 效果惊艳,团队大量使用,没人关注成本。
  2. 账单冲击:月底收到 $5000 的 API 账单,没人知道钱花在哪了。
  3. 一刀切限制:管理层直接禁用 Agent,所有人受影响。

有了成本拆账后:

维度 没有拆账 有拆账
预算制定 拍脑袋 基于历史数据的精确预测
异常检测 月底才发现 实时告警
成本优化 不知道从哪优化 精确到"项目 A 的复杂 Bugfix 任务可以降级模型"
向管理层汇报 "大概 $X" 按项目/团队/任务类型的详细报告

二、成本事件数据模型

2.1 成本事件表

sql
-- 成本事件表:记录每次 API 调用的成本
CREATE TABLE cost_events (
    id              BIGSERIAL PRIMARY KEY,
    event_time      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    
    -- 任务维度
    task_id         VARCHAR(64) NOT NULL,
    task_type       VARCHAR(32) NOT NULL,       -- bugfix / review / refactor / test / docs
    task_priority   VARCHAR(8),                  -- P0 / P1 / P2 / P3
    
    -- 项目维度
    project_id      VARCHAR(64) NOT NULL,
    project_name    VARCHAR(128),
    repo_name       VARCHAR(256),
    
    -- 人员维度
    user_id         VARCHAR(64) NOT NULL,
    user_name       VARCHAR(128),
    team_id         VARCHAR(64),
    team_name       VARCHAR(128),
    
    -- 模型维度
    model_id        VARCHAR(64) NOT NULL,        -- claude-sonnet-4-6 / claude-opus-4-8 / ...
    model_tier      VARCHAR(16),                 -- haiku / sonnet / opus
    provider        VARCHAR(32),                 -- anthropic / openai
    
    -- Token 维度
    input_tokens    INTEGER NOT NULL,
    output_tokens   INTEGER NOT NULL,
    cache_read_tokens  INTEGER DEFAULT 0,        -- Prompt Cache 命中的 Token
    cache_write_tokens INTEGER DEFAULT 0,        -- Prompt Cache 写入的 Token
    
    -- 成本
    input_cost      DECIMAL(10,6) NOT NULL,
    output_cost     DECIMAL(10,6) NOT NULL,
    cache_discount  DECIMAL(10,6) DEFAULT 0,     -- Cache 节省的成本
    total_cost      DECIMAL(10,6) NOT NULL,
    
    -- 路由信息
    routing_rule    VARCHAR(64),                 -- 匹配的路由规则
    fallback_used   BOOLEAN DEFAULT FALSE,       -- 是否触发了 fallback 升级
    
    -- 质量信息
    success         BOOLEAN DEFAULT TRUE,
    quality_score   DECIMAL(3,2),                -- 0.00 - 1.00
    retry_count     INTEGER DEFAULT 0,
    
    -- 元信息
    metadata        JSONB DEFAULT '{}'
);

-- 索引:按时间范围查询
CREATE INDEX idx_cost_events_time ON cost_events (event_time);
CREATE INDEX idx_cost_events_project ON cost_events (project_id, event_time);
CREATE INDEX idx_cost_events_user ON cost_events (user_id, event_time);
CREATE INDEX idx_cost_events_model ON cost_events (model_tier, event_time);
CREATE INDEX idx_cost_events_task_type ON cost_events (task_type, event_time);

2.2 成本聚合查询

sql
-- 周报查询:按项目、团队、任务类型多维聚合
WITH weekly_summary AS (
    SELECT
        -- 时间范围
        date_trunc('week', event_time) AS week_start,
        
        -- 项目维度
        project_name,
        team_name,
        
        -- 任务维度
        task_type,
        
        -- 模型维度
        model_tier,
        
        -- 成本汇总
        COUNT(*) AS call_count,
        SUM(input_tokens) AS total_input_tokens,
        SUM(output_tokens) AS total_output_tokens,
        SUM(total_cost) AS total_cost,
        SUM(cache_discount) AS total_cache_savings,
        
        -- 质量指标
        AVG(quality_score) AS avg_quality,
        SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) AS failure_count,
        SUM(retry_count) AS total_retries,
        
        -- 重试成本(重试浪费的成本)
        SUM(CASE WHEN retry_count > 0 THEN total_cost * retry_count / (retry_count + 1) ELSE 0 END) AS wasted_cost
        
    FROM cost_events
    WHERE event_time >= NOW() - INTERVAL '7 days'
    GROUP BY 1, 2, 3, 4, 5
)
SELECT * FROM weekly_summary
ORDER BY total_cost DESC;

三、周报生成

3.1 周报模板

python
# app/reports/weekly_cost_report.py
"""
Agent 成本周报生成器。
按项目、团队、任务类型、模型四个维度生成拆账报告。
"""
import json
from datetime import date, timedelta
from dataclasses import dataclass

@dataclass
class WeeklyCostReport:
    week_start: date
    week_end: date
    
    # 总览
    total_cost: float
    total_calls: int
    total_tokens: int
    cost_change_pct: float          # vs 上周
    top_cost_project: str
    top_cost_project_amount: float
    
    # 按项目
    by_project: list[dict]
    
    # 按团队
    by_team: list[dict]
    
    # 按任务类型
    by_task_type: list[dict]
    
    # 按模型
    by_model: list[dict]
    
    # 异常检测
    anomalies: list[dict]
    
    # 优化建议
    recommendations: list[str]

def generate_weekly_report(week_start: date) -> WeeklyCostReport:
    """生成周报"""
    week_end = week_start + timedelta(days=6)
    
    # 1. 总览数据
    overview = db.query("""
        SELECT 
            SUM(total_cost) as total_cost,
            COUNT(*) as total_calls,
            SUM(input_tokens + output_tokens) as total_tokens
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s
    """, (week_start, week_end))
    
    # 2. 按项目拆账
    by_project = db.query("""
        SELECT 
            project_name,
            COUNT(*) as calls,
            SUM(total_cost) as cost,
            AVG(quality_score) as avg_quality,
            team_name
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s
        GROUP BY project_name, team_name
        ORDER BY cost DESC
    """, (week_start, week_end))
    
    # 3. 按任务类型拆账
    by_task_type = db.query("""
        SELECT 
            task_type,
            COUNT(*) as calls,
            SUM(total_cost) as cost,
            model_tier as primary_model,
            AVG(quality_score) as avg_quality
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s
        GROUP BY task_type, model_tier
        ORDER BY cost DESC
    """, (week_start, week_end))
    
    # 4. 按模型拆账
    by_model = db.query("""
        SELECT 
            model_tier,
            COUNT(*) as calls,
            SUM(total_cost) as cost,
            SUM(cache_discount) as cache_savings,
            AVG(total_cost) as avg_cost_per_call
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s
        GROUP BY model_tier
        ORDER BY cost DESC
    """, (week_start, week_end))
    
    # 5. 异常检测
    anomalies = detect_anomalies(week_start, week_end)
    
    # 6. 生成优化建议
    recommendations = generate_recommendations(by_project, by_task_type, by_model)
    
    return WeeklyCostReport(
        week_start=week_start,
        week_end=week_end,
        total_cost=overview["total_cost"],
        total_calls=overview["total_calls"],
        total_tokens=overview["total_tokens"],
        cost_change_pct=calc_change_pct(overview["total_cost"], week_start),
        top_cost_project=by_project[0]["project_name"] if by_project else "",
        top_cost_project_amount=by_project[0]["cost"] if by_project else 0,
        by_project=by_project,
        by_team=aggregate_by_team(by_project),
        by_task_type=by_task_type,
        by_model=by_model,
        anomalies=anomalies,
        recommendations=recommendations,
    )

def detect_anomalies(week_start: date, week_end: date) -> list:
    """异常检测"""
    anomalies = []
    
    # 1. 单任务成本异常(超过均值 5 倍)
    avg_cost = db.query("SELECT AVG(total_cost) FROM cost_events WHERE event_time BETWEEN %s AND %s",
                         (week_start, week_end))
    high_cost_tasks = db.query("""
        SELECT task_id, task_type, total_cost, user_name, project_name
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s AND total_cost > %s * 5
        ORDER BY total_cost DESC
        LIMIT 10
    """, (week_start, week_end, avg_cost))
    
    for task in high_cost_tasks:
        anomalies.append({
            "type": "high_cost_task",
            "severity": "HIGH" if task["total_cost"] > avg_cost * 10 else "MEDIUM",
            "message": f"任务 {task['task_id']} 成本 ${task['total_cost']:.2f}(均值 ${avg_cost:.2f}{task['total_cost']/avg_cost:.1f} 倍)",
            "task_id": task["task_id"],
            "user": task["user_name"],
            "project": task["project_name"],
        })
    
    # 2. 重试率异常(>30% 的任务触发了重试)
    retry_rate = db.query("""
        SELECT task_type, 
               COUNT(*) FILTER (WHERE retry_count > 0)::float / COUNT(*) as retry_rate
        FROM cost_events
        WHERE event_time BETWEEN %s AND %s
        GROUP BY task_type
        HAVING COUNT(*) FILTER (WHERE retry_count > 0)::float / COUNT(*) > 0.3
    """, (week_start, week_end))
    
    for r in retry_rate:
        anomalies.append({
            "type": "high_retry_rate",
            "severity": "MEDIUM",
            "message": f"{r['task_type']} 类型任务重试率 {r['retry_rate']:.0%}",
            "task_type": r["task_type"],
        })
    
    # 3. 成本环比异常(周环比增长 > 50%)
    prev_week_cost = get_weekly_total(week_start - timedelta(days=7))
    if prev_week_cost > 0:
        change_pct = (overview_total - prev_week_cost) / prev_week_cost
        if change_pct > 0.5:
            anomalies.append({
                "type": "cost_spike",
                "severity": "HIGH",
                "message": f"本周成本环比增长 {change_pct:.0%}",
            })
    
    return anomalies

3.2 周报输出示例

json
{
  "report_period": "2024-06-10 ~ 2024-06-16",
  "summary": {
    "total_cost": "$1,234.56",
    "total_calls": 12450,
    "total_tokens": "89.2M",
    "cost_change": "+12% vs 上周",
    "top_project": "电商订单系统 ($456.78, 37%)"
  },
  "by_project": [
    {"project": "电商订单系统", "team": "交易组", "cost": "$456.78", "calls": 3200, "avg_quality": 0.82},
    {"project": "SaaS CRM", "team": "平台组", "cost": "$345.67", "calls": 4100, "avg_quality": 0.79},
    {"project": "内部工具", "team": "效能组", "cost": "$234.56", "calls": 2800, "avg_quality": 0.85},
    {"project": "文档站", "team": "前端组", "cost": "$197.55", "calls": 2350, "avg_quality": 0.76}
  ],
  "by_task_type": [
    {"type": "bugfix", "cost": "$456.78", "calls": 2100, "model": "Sonnet 60% / Opus 40%", "quality": 0.81},
    {"type": "code_review", "cost": "$345.67", "calls": 4200, "model": "Sonnet 95%", "quality": 0.78},
    {"type": "test_generation", "cost": "$234.56", "calls": 3100, "model": "Sonnet 80% / Haiku 20%", "quality": 0.75},
    {"type": "refactor", "cost": "$197.55", "calls": 1050, "model": "Opus 70%", "quality": 0.83}
  ],
  "anomalies": [
    {"severity": "HIGH", "message": "任务 TASK-5678 成本 $12.34(均值的 15 倍)— 原因:上下文包过大(80K tokens)"},
    {"severity": "MEDIUM", "message": "refactor 类型任务重试率 42% — 建议:大型重构默认使用 Opus"}
  ],
  "recommendations": [
    "电商订单系统的 bugfix 任务中 30% 可以降级到 Haiku(预估节省 $45/周)",
    "SaaS CRM 的 code_review 任务可以启用 Prompt Cache(预估节省 $30/周)",
    "内部工具项目建议设置每日预算上限 $50(当前无限制)"
  ]
}

四、真实经验与踩坑

4.1 Cache 折扣不能漏算

场景:周报显示本周成本 $1500,管理层准备砍预算。 问题:实际 API 账单只有 $800。因为周报计算成本时没考虑 Prompt Cache 折扣(Cache 命中的 Token 价格是原价的 10%)。 解决方案:成本计算必须区分 input_tokens(全价)和 cache_read_tokens(折扣价)。实际成本 = input_tokens × 全价 + cache_read_tokens × 折扣价。周报中单独列出"Cache 节省"一栏,让管理层看到优化的效果。

4.2 重试成本要单独统计

场景:周报显示 bugfix 任务成本偏高,团队质疑 Agent 不划算。 问题:实际很多成本是重试浪费的——Agent 第一次没修好,重试 2-3 次才修好。这些重试成本不应该算在"正常成本"里。 解决方案:在成本事件中记录 retry_count,计算"有效成本"(首次成功的成本)和"总成本"(包含重试)。周报中分别展示,让团队看到"如果提高首次成功率,能省多少钱"。

4.3 用户维度的成本要谨慎展示

场景:周报按团队成员列出成本排名:"张三 $200,李四 $150,王五 $50"。 问题:成本高的成员不一定是浪费——可能是负责的项目更复杂。直接排名会导致团队内部矛盾。 解决方案:成本排名只在"同项目同任务类型"维度下展示。不按个人排名,而是按"项目 × 团队"展示。如果需要个人维度,用"成本效率"(成本 / 产出质量)而不是绝对成本。

五、参数说明表

参数 类型 默认值 说明
report_period string "weekly" 报告周期:daily / weekly / monthly
dimensions list 4 个 拆账维度:project / team / task_type / model
anomaly_threshold float 5.0 异常检测阈值(均值的倍数)
cost_change_alert float 0.5 成本环比变化告警阈值
currency string "USD" 货币单位
include_cache_savings bool true 是否在报告中展示 Cache 节省
retry_cost_separate bool true 重试成本是否单独统计
top_n_projects int 10 报告中展示的前 N 个项目
recommendations_enabled bool true 是否生成优化建议
delivery_channels list ["email"] 报告发送渠道

六、落地检查清单

  • 每次 API 调用都记录了成本事件(包含 Token 数和成本)
  • 成本事件包含项目、团队、任务类型、模型四个维度
  • Prompt Cache 折扣正确计算
  • 重试成本单独统计
  • 异常检测覆盖了高成本任务、高重试率、成本突增
  • 周报按项目/团队/任务类型/模型四个维度拆账
  • 优化建议基于数据而非拍脑袋
  • 成本数据可追溯到单个任务
  • 报告发送给了项目负责人和财务
  • 历史报告可查询(至少保留 12 个月)

七、系列导航

上一篇:多模型路由系统:任务难度、风险等级与成本预算如何决策 下一篇:Lab 007:Claude / GPT / DeepSeek / 本地模型编码任务横评