【Claude】成本控制与用量监控实战 — 已解决

适用版本:Claude Code v1.0.x 及以上
受影响场景:API 费用管理、Token 消耗优化、团队用量追踪、预算控制
阅读时长:约 25 分钟


目录

  1. 问题现象
  2. 原理深挖:Token 计费模型
  3. 根因分析:成本失控的六大根因
  4. 多方案解决:从监控到优化
  5. 验证回归:成本控制验证
  6. 避坑最佳实践
  7. 附录:成本速查表

1. 问题现象

1.1 典型问题表现

问题一:API 费用突然飙升

# Anthropic Console 月账单
# 1月: $50  → 正常使用
# 2月: $500 → 10 倍增长!
# 不知道哪里的消耗暴增

问题二:单个会话消耗大量 Token

# 一个简单的代码审查任务
> 审查 src/auth.py
# Claude Code 读取了整个代码库 (50个文件)
# 消耗 200K input tokens
# 单次操作成本 $0.60

问题三:团队成员用量不透明

# 5人团队共享一个 API Key
# 月账单 $300
# 不知道谁用了多少
# 无法按人/项目分摊成本

问题四:缓存未命中导致重复消耗

# 每次对话都重新发送完整上下文
# 没有使用 prompt caching
# 相同的系统提示和上下文被重复计费

问题五:长会话 Token 累积

# 长时间会话 (50+ 轮)
# 早期对话不断被重新发送
# 每轮的 input tokens 递增
# 后期单轮消耗 100K+ tokens

2. 原理深挖:Token 计费模型

2.1 定价结构

┌──────────────────────────────────────────────────────────┐
│              Anthropic API 定价 (每百万 Token)             │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  模型               Input    Output   Cache Read  Cache Write │
│  ─────────────────────────────────────────────────────── │
│  Claude Opus 4      $15      $75      $1.50       $18.75   │
│  Claude Sonnet 4    $3       $15      $0.30       $3.75    │
│  Claude Haiku 4     $0.25    $1.25    $0.025      $0.3125  │
│                                                          │
│  计费规则:                                                │
│  - Input: 用户消息 + 系统提示 + 历史对话 + 工具结果        │
│  - Output: Claude 生成的文本和工具调用                    │
│  - Cache Read: 从缓存读取的 input (90% 折扣)              │
│  - Cache Write: 写入缓存的 input (1.25x 标准价)           │
│  - 缓存 TTL: 5 分钟                                       │
│                                                          │
│  隐藏成本:                                                │
│  - 工具调用结果计入 input                                 │
│  - 大文件读取 = 大量 input tokens                        │
│  - 长对话历史 = 递增 input                               │
│  - 错误重试 = 重复消耗                                    │
│                                                          │
└──────────────────────────────────────────────────────────┘

2.2 Token 计算规则

Token 估算:
  英文: 1 token ≈ 4 字符 ≈ 0.75 单词
  中文: 1 token ≈ 1-2 字符 (中文 token 密度高)
  代码: 1 token ≈ 3-4 字符
  
  示例:
  "Hello World" → ~3 tokens
  "你好世界" → ~4-6 tokens
  "def hello(): print('hi')" → ~12 tokens

Claude Code 每轮消耗:
  Input = 系统提示(~500 tokens) 
        + CLAUDE.md(~200-1000 tokens)
        + 历史对话(递增)
        + 工具调用结果
  Output = Claude 的回复 + 工具调用 JSON

2.3 长会话成本模型

会话成本递增模型 (无缓存):

  第 1 轮: Input = 1K, Output = 0.5K → $0.00375
  第 2 轮: Input = 2K, Output = 0.5K → $0.00750
  第 3 轮: Input = 3K, Output = 0.5K → $0.01125
  ...
  第 N 轮: Input = N*1K, Output = 0.5K → $0.00375*N
  
  总成本 = Σ(0.00375 * N) for N=1..50
         = 0.00375 * (1+2+...+50)
         = 0.00375 * 1275
         = $4.78  (50 轮对话)

带缓存优化后:
  第 1 轮: Input = 1K (cache write), Output = 0.5K
  第 2 轮: Input = 0.3K (cache read) + 1K (new), Output = 0.5K
  第 3 轮: Input = 0.6K (cache read) + 1K (new), Output = 0.5K
  ...
  总成本 ≈ $1.50 (约 70% 节省)

2.4 Anthropic Console 用量统计

console.anthropic.com → Usage 页面:

  按时间: 日/周/月用量趋势
  按模型: 各模型的 Token 消耗
  按项目: 不同 API Key 的用量
  按类型: Input/Output/Cache 分布

  限制: 
  - 无法按用户区分 (共享 API Key)
  - 无法按会话区分
  - 无实时告警
  - 只有日级粒度

3. 根因分析:成本失控的六大根因

3.1 根因一:未使用 Prompt Caching

每次请求都重新发送完整上下文,没有利用缓存,input 成本高出数倍。

3.2 根因二:长会话不截断

会话越长,每轮的 input tokens 越多(历史对话累积),成本呈二次增长。

3.3 根因三:大文件全量读取

Claude Code 读取大文件时消耗大量 input tokens,特别是 node_modules、lock 文件等。

3.4 根因四:模型选择不当

简单任务用 Opus($15/M),应该用 Haiku($0.25/M)或 Sonnet($3/M)。

3.5 根因五:无用量监控

没有实时监控 Token 消耗,成本在不知不觉中累积。

3.6 根因六:团队共享 Key

多人共用一个 API Key,无法按人/项目追踪和限制用量。


4. 多方案解决:从监控到优化

4.1 方案一:实时成本监控 Hook

#!/usr/bin/env python3
# .claude/hooks/cost-monitor.py — 实时成本监控

import json
import sys
import os
from datetime import datetime
from pathlib import Path

COST_DIR = Path(".claude/costs")
COST_DIR.mkdir(parents=True, exist_ok=True)

PRICING = {
    "claude-opus-4-20250514":   {"input": 15.0,  "output": 75.0},
    "claude-sonnet-4-20250514": {"input": 3.0,   "output": 15.0},
    "claude-haiku-4-20250422":  {"input": 0.25,  "output": 1.25},
}

# 预算阈值 (美元)
DAILY_BUDGET = float(os.environ.get("CLAUDE_DAILY_BUDGET", "10.0"))
MONTHLY_BUDGET = float(os.environ.get("CLAUDE_MONTHLY_BUDGET", "200.0"))

def calculate_cost(model, usage):
    """计算成本"""
    pricing = PRICING.get(model, PRICING["claude-sonnet-4-20250514"])
    
    input_cost = usage.get("input_tokens", 0) * pricing["input"] / 1_000_000
    output_cost = usage.get("output_tokens", 0) * pricing["output"] / 1_000_000
    cache_read = usage.get("cache_read_input_tokens", 0) * pricing["input"] * 0.1 / 1_000_000
    cache_write = usage.get("cache_creation_input_tokens", 0) * pricing["input"] * 1.25 / 1_000_000
    
    return round(input_cost + output_cost + cache_read + cache_write, 6)

def get_daily_total():
    """获取今日总成本"""
    today = datetime.utcnow().strftime("%Y-%m-%d")
    log_file = COST_DIR / f"cost-{today}.jsonl"
    
    total = 0.0
    if log_file.exists():
        with open(log_file) as f:
            for line in f:
                try:
                    data = json.loads(line)
                    total += data.get("cost_usd", 0)
                except:
                    pass
    
    return total

def get_monthly_total():
    """获取本月总成本"""
    month = datetime.utcnow().strftime("%Y-%m")
    total = 0.0
    
    for log_file in COST_DIR.glob(f"cost-{month}-*.jsonl"):
        with open(log_file) as f:
            for line in f:
                try:
                    data = json.loads(line)
                    total += data.get("cost_usd", 0)
                except:
                    pass
    
    return total

# Hook 入口
try:
    hook_input = json.load(sys.stdin)
    
    if "usage" in hook_input:
        model = hook_input.get("model", "claude-sonnet-4-20250514")
        usage = hook_input["usage"]
        
        cost = calculate_cost(model, usage)
        
        # 记录
        entry = {
            "timestamp": datetime.utcnow().isoformat() + "Z",
            "model": model,
            "input_tokens": usage.get("input_tokens", 0),
            "output_tokens": usage.get("output_tokens", 0),
            "cost_usd": cost,
            "project": os.path.basename(os.getcwd()),
            "session": os.environ.get("CLAUDE_SESSION_ID", "unknown")
        }
        
        today = datetime.utcnow().strftime("%Y-%m-%d")
        with open(COST_DIR / f"cost-{today}.jsonl", "a") as f:
            f.write(json.dumps(entry) + "\n")
        
        # 预算检查
        daily_total = get_daily_total()
        monthly_total = get_monthly_total()
        
        if daily_total > DAILY_BUDGET:
            print(f"⚠ 日预算告警: ${daily_total:.2f} / ${DAILY_BUDGET:.2f}", file=sys.stderr)
        
        if monthly_total > MONTHLY_BUDGET:
            print(f"⚠ 月预算告警: ${monthly_total:.2f} / ${MONTHLY_BUDGET:.2f}", file=sys.stderr)
        
        # 实时显示
        print(f"[Cost] ${cost:.4f} | 日: ${daily_total:.2f}/{DAILY_BUDGET} | 月: ${monthly_total:.2f}/{MONTHLY_BUDGET}", file=sys.stderr)

except Exception as e:
    print(f"Cost monitor error: {e}", file=sys.stderr)

sys.exit(0)

4.2 方案二:Prompt Caching 配置

"""
Prompt Caching 优化
通过缓存系统提示和上下文,减少重复 input 成本
"""
import anthropic

client = anthropic.Anthropic(api_key="sk-ant-xxx")

# 缓存系统提示 (长期不变的部分)
SYSTEM_PROMPT = """你是一个代码助手。

项目规则:
- 使用 TypeScript
- 代码风格遵循 ESLint 配置
- 测试使用 Jest
- 文档使用 JSDoc

项目结构:
- src/ → 源代码
- tests/ → 测试
- docs/ → 文档"""

# 带 cache_control 的请求
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=4096,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"}  # 缓存 5 分钟
        }
    ],
    messages=[
        {"role": "user", "content": "修复 src/auth.ts 的 bug"}
    ]
)

# 第一次请求: cache_creation_input_tokens > 0 (写入缓存)
# 后续 5 分钟内的请求: cache_read_input_tokens > 0 (读取缓存, 90% 折扣)

print(f"Input: {response.usage.input_tokens}")
print(f"Output: {response.usage.output_tokens}")
print(f"Cache Read: {response.usage.cache_read_input_tokens}")
print(f"Cache Write: {response.usage.cache_creation_input_tokens}")
# 多级缓存策略
class CachedConversation:
    """带多级缓存的对话"""
    
    def __init__(self, client, system_prompt, model="claude-sonnet-4-20250514"):
        self.client = client
        self.model = model
        self.messages = []
        
        # 系统提示缓存
        self.system = [
            {
                "type": "text",
                "text": system_prompt,
                "cache_control": {"type": "ephemeral"}
            }
        ]
        
        # 对话历史缓存 (在历史达到一定长度时设置)
        self._cache_breakpoint = 4  # 每 4 轮设置一个缓存断点
    
    def chat(self, user_message):
        """对话"""
        self.messages.append({"role": "user", "content": user_message})
        
        # 在历史消息中设置缓存断点
        # 最后一个消息不缓存(因为可能会变)
        system_with_cache = list(self.system)
        
        # 如果历史较长,在历史中设置缓存

![配图](https://i-blog.csdnimg.cn/img_convert/ee70ea807bd1e213ac721bfbfb4025d2.png)
        if len(self.messages) > self._cache_breakpoint * 2:
            # 在倒数第 4 轮设置缓存断点
            cache_idx = len(self.messages) - self._cache_breakpoint * 2
            # 需要将消息转为 content block 格式并设置 cache_control
            pass  # 实际实现需要更复杂的消息格式处理
        
        response = self.client.messages.create(
            model=self.model,
            max_tokens=4096,
            system=system_with_cache,
            messages=self.messages
        )
        
        self.messages.append({"role": "assistant", "content": response.content})
        
        # 报告缓存命中
        usage = response.usage
        cache_hit = usage.cache_read_input_tokens > 0
        print(f"  Cache: {'✓ 命中' if cache_hit else '✗ 未命中'} "
              f"(read: {usage.cache_read_input_tokens}, write: {usage.cache_creation_input_tokens})")
        
        return response.content[0].text

4.3 方案三:模型分级策略

"""
模型分级策略: 根据任务复杂度选择模型
"""

MODEL_TIERS = {
    "simple": {
        "model": "claude-haiku-4-20250422",
        "cost_per_million": {"input": 0.25, "output": 1.25},
        "tasks": ["简单问答", "格式转换", "简单搜索", "变量重命名"]
    },
    "medium": {
        "model": "claude-sonnet-4-20250514",
        "cost_per_million": {"input": 3.0, "output": 15.0},
        "tasks": ["代码编写", "Bug 修复", "代码审查", "测试生成"]
    },
    "complex": {
        "model": "claude-opus-4-20250514",
        "cost_per_million": {"input": 15.0, "output": 75.0},
        "tasks": ["架构设计", "复杂重构", "安全分析", "算法优化"]
    }
}

# 任务分类 → 模型选择
TASK_CLASSIFICATION = {
    # 简单任务 → Haiku (最低成本)
    "解释这行代码": "simple",
    "变量重命名": "simple",
    "格式化代码": "simple",
    "查找 import": "simple",
    "简单的语法问题": "simple",
    
    # 中等任务 → Sonnet (性价比)
    "写一个函数": "medium",
    "修复 bug": "medium",
    "添加测试": "medium",
    "代码审查": "medium",
    "重构函数": "medium",
    
    # 复杂任务 → Opus (最高质量)
    "系统架构设计": "complex",
    "大规模重构": "complex",
    "安全漏洞分析": "complex",
    "算法优化": "complex",
    "复杂并发问题": "complex",
}

def select_model(task_description):
    """根据任务描述选择模型"""
    task_lower = task_description.lower()
    
    for pattern, tier in TASK_CLASSIFICATION.items():
        if pattern in task_lower:
            return MODEL_TIERS[tier]["model"]
    
    # 默认中等
    return MODEL_TIERS["medium"]["model"]

# Claude Code 配置
# .claude/settings.json
"""
{
  "model": "claude-sonnet-4-20250514",     // 主模型 (中等任务)
  "smallModel": "claude-haiku-4-20250422", // 小模型 (简单任务)
}
"""

4.4 方案四:上下文压缩

"""
上下文压缩: 减少长会话的 input token 消耗
"""
import anthropic

class ContextCompressor:
    """对话上下文压缩器"""
    
    def __init__(self, client, model="claude-haiku-4-20250422"):
        self.client = client
        self.model = model  # 用便宜模型做压缩
    
    def compress_history(self, messages, keep_recent=4):
        """
        压缩历史对话
        
        保留最近 N 轮原始对话,之前的对话用摘要替代
        """
        if len(messages) <= keep_recent * 2:
            return messages  # 不需要压缩
        
        # 分割: 需要压缩的 + 保留的
        to_compress = messages[:-keep_recent * 2]
        to_keep = messages[-keep_recent * 2:]
        
        # 用 Haiku 生成摘要
        summary_prompt = "请总结以下对话的关键信息,保留: 讨论的问题、做出的决定、修改的文件、关键代码片段。\n\n"
        for msg in to_compress:
            role = msg["role"]
            content = msg["content"] if isinstance(msg["content"], str) else str(msg["content"])
            summary_prompt += f"[{role}]: {content[:500]}\n"
        
        response = self.client.messages.create(
            model=self.model,
            max_tokens=1000,
            messages=[{"role": "user", "content": summary_prompt}]
        )
        
        summary = response.content[0].text
        
        # 构建压缩后的消息列表
        compressed = [
            {"role": "user", "content": f"[之前对话的摘要]\n{summary}"},
            {"role": "assistant", "content": "了解,我记住了之前的对话内容。请继续。"}
        ] + to_keep
        
        # 成本报告
        original_tokens = sum(len(str(m.get("content", ""))) // 4 for m in to_compress)
        compressed_tokens = len(summary) // 4
        savings = max(0, original_tokens - compressed_tokens)
        
        print(f"  上下文压缩: {original_tokens} → {compressed_tokens} tokens (节省 {savings})")
        
        return compressed

# 使用
client = anthropic.Anthropic(api_key="sk-ant-xxx")
compressor = ContextCompressor(client)

# 长对话压缩
# messages = [很多轮对话...]
# messages = compressor.compress_history(messages, keep_recent=4)

4.5 方案五:团队用量追踪

#!/usr/bin/env python3
"""
团队用量追踪系统
按用户、项目、会话维度追踪 API 成本
"""
import json
import os
from datetime import datetime, timedelta
from pathlib import Path
from collections import defaultdict

class TeamCostTracker:
    """团队成本追踪器"""
    
    def __init__(self):
        self.cost_dir = Path(os.environ.get("CLAUDE_COST_DIR", ".claude/costs"))
    
    def generate_team_report(self, days=30):
        """生成团队报告"""
        stats = defaultdict(lambda: {
            "total_cost": 0.0,
            "total_input": 0,
            "total_output": 0,
            "api_calls": 0,
            "by_model": defaultdict(float),
            "by_project": defaultdict(float),
            "by_day": defaultdict(float)
        })
        
        # 加载日志
        for i in range(days):
            date = (datetime.utcnow() - timedelta(days=i)).strftime("%Y-%m-%d")
            log_file = self.cost_dir / f"cost-{date}.jsonl"
            
            if not log_file.exists():
                continue
            
            with open(log_file) as f:
                for line in f:
                    try:
                        entry = json.loads(line)
                        
                        # 按 session (用户代理) 统计
                        session = entry.get("session", "unknown")
                        stats[session]["total_cost"] += entry.get("cost_usd", 0)
                        stats[session]["total_input"] += entry.get("input_tokens", 0)
                        stats[session]["total_output"] += entry.get("output_tokens", 0)
                        stats[session]["api_calls"] += 1
                        stats[session]["by_model"][entry.get("model", "unknown")] += entry.get("cost_usd", 0)
                        stats[session]["by_project"][entry.get("project", "unknown")] += entry.get("cost_usd", 0)
                        stats[session]["by_day"][date] += entry.get("cost_usd", 0)
                        
                    except json.JSONDecodeError:
                        continue
        
        # 打印报告
        print(f"=== 团队成本报告 ({days} 天) ===\n")
        
        grand_total = sum(s["total_cost"] for s in stats.values())
        print(f"总成本: ${grand_total:.2f}")
        print(f"总 API 调用: {sum(s['api_calls'] for s in stats.values())}")
        print(f"总 Input: {sum(s['total_input'] for s in stats.values()):,} tokens")
        print(f"总 Output: {sum(s['total_output'] for s in stats.values()):,} tokens")
        
        print(f"\n--- 按会话 ---")
        for session, data in sorted(stats.items(), key=lambda x: -x[1]["total_cost"]):
            print(f"\n  会话: {session}")
            print(f"    成本: ${data['total_cost']:.4f}")
            print(f"    调用: {data['api_calls']}")
            print(f"    Input: {data['total_input']:,} / Output: {data['total_output']:,}")
            
            if data["by_model"]:
                print(f"    按模型:")
                for model, cost in sorted(data["by_model"].items(), key=lambda x: -x[1]):
                    print(f"      {model}: ${cost:.4f}")
            
            if data["by_project"]:
                print(f"    按项目:")
                for project, cost in sorted(data["by_project"].items(), key=lambda x: -x[1]):
                    print(f"      {project}: ${cost:.4f}")
        
        # 日均趋势
        print(f"\n--- 日均成本 ---")
        all_days = set()
        for data in stats.values():
            all_days.update(data["by_day"].keys())
        
        for date in sorted(all_days):
            day_total = sum(data["by_day"].get(date, 0) for data in stats.values())
            bar = "█" * int(day_total * 10)  # 每美元 10 个字符
            print(f"  {date}: ${day_total:.2f} {bar}")

# 使用
tracker = TeamCostTracker()
tracker.generate_team_report(days=30)

4.6 方案六:文件读取优化

"""
文件读取 Token 优化
避免不必要的大文件全量读取
"""
import os

class FileReader:
    """优化的文件读取器"""
    
    # 应该跳过的文件/目录
    SKIP_PATTERNS = [
        "node_modules", ".git", "dist", "build", "__pycache__",
        "*.lock", "*.min.js", "*.min.css", "*.map",
        "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
        "go.sum", "Cargo.lock", "Gemfile.lock",
    ]
    
    # 大文件阈值 (行数)
    MAX_LINES = 500
    
    # 大文件阈值 (字节)
    MAX_SIZE = 50_000  # 50KB
    
    @classmethod
    def should_read(cls, filepath):
        """判断是否应该读取文件"""
        filepath = str(filepath)
        
        # 检查跳过模式
        for pattern in cls.SKIP_PATTERNS:
            if pattern in filepath:
                return False, f"跳过: 匹配 {pattern}"
        
        # 检查文件大小
        if os.path.exists(filepath):
            size = os.path.getsize(filepath)
            if size > cls.MAX_SIZE:
                return False, f"文件过大: {size} bytes (> {cls.MAX_SIZE})"
        
        return True, "OK"
    
    @classmethod
    def read_optimized(cls, filepath, max_lines=None):
        """优化的文件读取"""
        should, reason = cls.should_read(filepath)
        if not should:
            return None, reason
        
        max_lines = max_lines or cls.MAX_LINES
        
        with open(filepath, "r", errors="replace") as f:
            lines = []
            for i, line in enumerate(f):
                if i >= max_lines:
                    lines.append(f"\n... (截断,共 {i+1}+ 行,仅显示前 {max_lines} 行)")
                    break
                lines.append(line)
            
            content = "".join(lines)
            estimated_tokens = len(content) // 4
            
            return content, f"读取 {len(lines)} 行, ~{estimated_tokens} tokens"

# CLAUDE.md 中配置读取策略
"""
# 文件读取策略

## 跳过的文件
- node_modules/
- *.lock 文件
- *.min.js / *.min.css
- dist/ / build/

## 大文件处理
- 超过 500 行的文件只读取相关部分
- 超过 50KB 的文件先摘要再选择性读取
- 使用 grep/search 定位再读取

## 优先级
- 先用 search_content 定位
- 再用 read_file offset/limit 精确读取
- 避免全量读取大文件
"""

4.7 方案七:多 API Key 管理

"""
多 API Key 管理: 按项目/团队分配不同的 Key
"""
import os
import json
from pathlib import Path

class APIKeyManager:
    """多 API Key 管理器"""
    
    def __init__(self):
        self.keys_file = Path.home() / ".claude" / "api-keys.json"
        self.keys = self._load_keys()
    
    def _load_keys(self):
        """加载 API Key 配置"""
        if not self.keys_file.exists():
            return {}
        
        with open(self.keys_file) as f:
            return json.load(f)
    
    def get_key_for_project(self, project_name):
        """获取项目对应的 API Key"""
        project_config = self.keys.get("projects", {}).get(project_name)
        
        if project_config:
            key_env = project_config.get("env_var")
            if key_env:
                return os.environ.get(key_env)
        
        # 返回默认 Key
        return os.environ.get("ANTHROPIC_API_KEY")
    
    def get_budget_for_project(self, project_name):
        """获取项目预算"""
        project_config = self.keys.get("projects", {}).get(project_name, {})
        return project_config.get("daily_budget", 10.0)

# api-keys.json 示例
"""
{
  "projects": {
    "my-app": {
      "env_var": "ANTHROPIC_API_KEY_APP",
      "daily_budget": 5.0,
      "monthly_budget": 100.0
    },
    "internal-tools": {
      "env_var": "ANTHROPIC_API_KEY_TOOLS",
      "daily_budget": 2.0,
      "monthly_budget": 50.0
    },
    "research": {
      "env_var": "ANTHROPIC_API_KEY_RESEARCH",
      "daily_budget": 20.0,
      "monthly_budget": 400.0
    }
  },
  "default": {
    "env_var": "ANTHROPIC_API_KEY",
    "daily_budget": 10.0,
    "monthly_budget": 200.0
  }
}
"""

# .zshrc 中配置多个 Key
"""
export ANTHROPIC_API_KEY="sk-ant-xxx-default"
export ANTHROPIC_API_KEY_APP="sk-ant-xxx-app"
export ANTHROPIC_API_KEY_TOOLS="sk-ant-xxx-tools"
export ANTHROPIC_API_KEY_RESEARCH="sk-ant-xxx-research"
"""

5. 验证回归:成本控制验证

5.1 成本验证脚本

#!/bin/bash
# verify-cost-control.sh — 成本控制验证

echo "=== 成本控制验证 ==="

# 1. 检查成本日志
if [ -d ".claude/costs" ]; then
    TODAY=$(date -u +%Y-%m-%d)
    LOG_FILE=".claude/costs/cost-${TODY}.jsonl"
    
    if [ -f "$LOG_FILE" ]; then
        TODAY_COST=$(python3 -c "
import json
total = 0
with open('$LOG_FILE') as f:
    for line in f:
        try:
            data = json.loads(line)
            total += data.get('cost_usd', 0)
        except:
            pass
print(f'{total:.4f}')
" 2>/dev/null)
        
        echo "今日成本: \$$TODAY_COST"
    fi
fi

# 2. 检查预算配置
DAILY_BUDGET=${CLAUDE_DAILY_BUDGET:-"未设置"}
echo "日预算: $DAILY_BUDGET"

# 3. 检查模型配置
if [ -f ".claude/settings.json" ]; then
    python3 -c "
import json
with open('.claude/settings.json') as f:
    data = json.load(f)
print(f\"主模型: {data.get('model', '未设置')}\")
print(f\"小模型: {data.get('smallModel', '未设置')}\")
" 2>/dev/null
fi

echo ""
echo "=== 验证完成 ==="

5.2 验证清单

# 验证项 预期 方法
1 成本 Hook 已配置 settings.json
2 成本日志 有记录 检查 .claude/costs/
3 预算告警 功能正常 超阈值告警
4 Prompt Caching 命中率高 cache_read > 0
5 模型分级 按任务选模型 配置检查
6 上下文压缩 长会话压缩 压缩工具
7 文件跳过 大文件不读 读取策略
8 多 Key 按项目分配 Key 管理器

6. 避坑最佳实践

6.1 成本控制原则

原则 1: 实时监控 — 用 Hook 记录每次 API 调用的成本
原则 2: Prompt Caching — 缓存系统提示和上下文
原则 3: 模型分级 — 简单任务用 Haiku,复杂用 Opus
原则 4: 上下文压缩 — 长会话定期压缩历史
原则 5: 文件优化 — 跳过大文件和无用文件
原则 6: 预算告警 — 设置日/月预算阈值
原则 7: 多 Key — 按项目/团队分配 API Key
原则 8: 定期审查 — 周度/月度成本分析

6.2 成本优化效果

优化措施 节省比例 实施难度
Prompt Caching 50-70%
模型分级 (Haiku) 80-90%
上下文压缩 30-50%
文件读取优化 20-40%
长会话截断 40-60%
多 Key 管理 N/A

6.3 常见陷阱

# 陷阱 后果 解决
1 无缓存 重复全价计费 开启 Prompt Caching
2 简单任务用 Opus 成本 60x 模型分级
3 长会话不截断 二次增长 定期压缩
4 全量读大文件 浪费 input 跳过/分页
5 无成本监控 不知花费 Cost Hook
6 共享 Key 无法追踪 多 Key 管理
7 无预算限制 失控 预算告警
8 不审查账单 持续浪费 定期审查

7. 附录:成本速查表

7.1 模型定价对比

模型 Input $/M Output $/M 适用场景
Opus 4 $15 $75 架构/安全/复杂
Sonnet 4 $3 $15 编码/审查/日常
Haiku 4 $0.25 $1.25 简单/格式/搜索

7.2 成本优化优先级

优先级 措施 预期节省
P0 Prompt Caching 50-70%
P0 模型分级 80%+ (简单任务)
P1 文件读取优化 20-40%
P1 长会话压缩 30-50%
P2 成本监控 可见性
P2 预算告警 防失控
P3 多 Key 按项目追踪

7.3 预算推荐

团队规模 日预算 月预算
个人 $5-10 $100-200
小团队 (5人) $20-50 $400-1000
中团队 (20人) $50-100 $1000-2000
大团队 (50+) $200+ $4000+

结语

成本控制是长期使用 Claude Code 不可忽视的方面。通过实时成本监控、Prompt Caching、模型分级、上下文压缩、文件读取优化、预算告警和多 Key 管理,可以将 API 成本降低 50-80%,同时保持开发效率。

核心要点回顾:

  1. Prompt Caching:缓存系统提示,减少 50-70% 的 input 成本
  2. 模型分级:简单任务用 Haiku($0.25/M),日常用 Sonnet($3/M),复杂用 Opus($15/M)
  3. 上下文压缩:长会话定期压缩历史,避免 Token 二次增长
  4. 文件优化:跳过 node_modules、lock 文件、大文件,用 search 定位再读取
  5. 实时监控:用 Hook 记录每次 API 调用的成本和 Token 消耗
  6. 预算告警:设置日/月预算阈值,超限自动告警
  7. 多 Key 管理:按项目/团队分配不同 API Key,实现成本追踪
  8. 定期审查:周度/月度生成成本报告,识别优化空间
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐