先说一个真实案例。

        Peter Steinberger,PSPDFKit 的创始人,把 Claude API 接进自己的日常开发工作流,用于代码审查、文档生成、调试辅助。一个月后收到账单:$20,000

        没有做什么异常的事,只是让 Agent 持续跑,每次调用都用最好的模型,上下文不加控制,工具输出不做优化。结果就是这样。

        这不是极端案例。是 Agent 成本不加控制的自然结果。


一、190 倍的价格差距

        在说优化策略之前,先理解成本结构。

        当前主流模型的价格(per million tokens)大约是:    

                                           

模型 输入价格 输出价格
Claude Opus 4.7 $15 $75
Claude Sonnet 4.6 $3 $15
Claude Haiku 4.5 $0.08 $0.4

           

        Opus 和 Haiku 的输入价格差:187 倍。输出价格差:187 倍

        这不是说 Haiku 比 Opus 差 187 倍——对于简单任务,Haiku 的质量完全够用。差的是模型能力,不是「简单任务的完成质量」。

        一个典型的 Agent 工作流,如果盲目用 Opus 做所有步骤,可能花 $5。用模型路由后,只有关键步骤用 Opus,其余用 Haiku,可能花 $0.3。节省 94%,质量无损。


        二、策略一:模型路由(最高杠杆)

        模型路由是成本优化里杠杆最高的一个——一行配置可能带来 80% 的成本降低。

        核心思路:按任务复杂度分配模型。简单的用小模型,复杂的才用大模型。

        任务复杂度分类框架:

from enum import Enum
from anthropic import Anthropic

class TaskComplexity(Enum):
    SIMPLE = "simple"      # Haiku
    MEDIUM = "medium"      # Sonnet
    COMPLEX = "complex"    # Opus

def classify_task(task: str) -> TaskComplexity:
    """用 Haiku 本身来分类任务复杂度——分类成本极低"""
    client = Anthropic()
    
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",  # 用最便宜的模型做分类
        max_tokens=10,
        messages=[{
            "role": "user",
            "content": f"""判断以下任务的复杂度,只回答 simple/medium/complex:

simple: 格式转换、简单问答、代码补全、信息提取
medium: 代码审查、文档撰写、逻辑分析、多步推理
complex: 架构设计、复杂调试、跨模块重构、需要深度推理

任务:{task}

只回答一个词:"""
        }]
    )
    
    complexity = response.content[0].text.strip().lower()
    return TaskComplexity(complexity) if complexity in ["simple", "medium", "complex"] else TaskComplexity.MEDIUM

def route_model(complexity: TaskComplexity) -> str:
    return {
        TaskComplexity.SIMPLE: "claude-haiku-4-5-20251001",
        TaskComplexity.MEDIUM: "claude-sonnet-4-6",
        TaskComplexity.COMPLEX: "claude-opus-4-7",
    }[complexity]

def run_task(task: str) -> str:
    complexity = classify_task(task)
    model = route_model(complexity)
    
    # 实际执行任务
    client = Anthropic()
    response = client.messages.create(
        model=model,
        max_tokens=4096,
        messages=[{"role": "user", "content": task}]
    )
    return response.content[0].text

        注意:分类本身也有成本,用 Haiku 做分类,每次分类大约花 0.001 美元——可以忽略不计。

        预设路由规则:如果你的 Agent 工作流步骤固定,可以在 Harness 层直接预设每个步骤的模型,不需要动态分类:

# 三 Agent 代码审查流水线(第11篇案例)的模型配置
PIPELINE_MODEL_CONFIG = {
    "code_writer": "claude-sonnet-4-6",      # 代码生成:Sonnet(质量和成本平衡)
    "test_engineer": "claude-haiku-4-5-20251001",  # 测试生成:Haiku(模式化任务)
    "security_reviewer": "claude-opus-4-7",   # 安全审查:Opus(关键,不省)
}

        何时不路由:安全审查、架构决策、需要深度推理的关键步骤,固定用最强模型,不要为了省钱在关键节点降级。


        三、策略二:Prompt 缓存

        Anthropic的 Prompt Caching 可以缓存系统提示和重复出现的上下文,命中缓存后价格降低 90%(Claude 3 系列),延迟降低约 75%。

        基本原理:第一次调用时,Anthropic 缓存你标记的内容块。后续调用如果命中缓存,这部分内容不重新计算,只收取缓存读取费(约为正常价格的 10%)。

        接入方式

from anthropic import Anthropic

client = Anthropic()

# 系统提示缓存——这部分内容在多次调用间不变
SYSTEM_PROMPT = """你是一个代码审查专家...(通常 1000-3000 tokens 的详细规则)"""

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=4096,
    system=[
        {
            "type": "text",
            "text": SYSTEM_PROMPT,
            "cache_control": {"type": "ephemeral"}  # 标记为可缓存
        }
    ],
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": code_to_review,
                    # 如果代码库背景也是固定的,也可以缓存
                    "cache_control": {"type": "ephemeral"}
                }
            ]
        }
    ]
)

# 查看缓存效果
usage = response.usage
print(f"缓存命中: {usage.cache_read_input_tokens} tokens")
print(f"新计算: {usage.cache_creation_input_tokens} tokens")

        最大化缓存命中率的关键:缓存的内容必须完全一致(字符级别)。实践中常见的破坏缓存的原因:

  •  每次调用在系统提示里插入时间戳(别这样做)
  •  系统提示里有动态生成的内容
  •  消息顺序不一致

        缓存适用场景

  •  固定的详细系统提示(最典型)
  •  每次调用都包含的大型参考文档
  •  固定的代码库背景信息

        不适合缓存:用户输入、每次都变化的任务上下文。


        四、策略三:Prompt 压缩

        当上下文里有大量冗余内容时,压缩可以显著降低 Token 消耗。

        LLMLingua 是目前最成熟的 Prompt 压缩工具,可以在保留语义的前提下压缩 Prompt,最高实现 20x 压缩(但激进压缩会损失质量,实际生产中 3-5x 更稳妥):

from llmlingua import PromptCompressor

compressor = PromptCompressor(
    model_name="microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank",
    use_llmlingua2=True
)

# 原始上下文:可能包含大量工具输出、历史对话
original_context = """
[大量工具输出、文档内容、历史对话...]
"""

# 压缩
compressed = compressor.compress_prompt(
    original_context,
    rate=0.5,          # 压缩到原始大小的 50%
    force_tokens=["\n", "。", ":"]  # 保留这些 token
)

print(f"原始: {len(original_context)} chars")
print(f"压缩后: {len(compressed['compressed_prompt'])} chars")
print(f"压缩率: {compressed['ratio']:.1f}x")

        压缩适用场景

  •  工具输出特别长(日志文件、搜索结果、数据库查询结果)
  •  历史对话积累了大量冗余信息
  •  参考文档里大部分内容和当前任务无关

        不适合压缩的内容

  •  代码(压缩会破坏语法)
  •  结构化数据(JSON、YAML)
  •  需要精确引用的内容

        更实用的替代方案——工具输出卸载(Tool Output Offloading,第4篇讲过):工具输出不放进上下文,只保留摘要,全量内容写到文件,Agent 需要时再读取。这比压缩更简单,也更可靠:

def run_tool_with_offload(tool_name: str, args: dict) -> str:
    """运行工具,长输出卸载到文件"""
    result = run_tool(tool_name, args)
    
    if len(result) > 2000:  # 超过阈值就卸载
        # 写到文件
        output_path = f"/tmp/tool_output_{tool_name}_{timestamp()}.txt"
        with open(output_path, "w") as f:
            f.write(result)
        
        # 返回摘要 + 文件路径
        return f"""[工具输出已卸载到文件]
路径: {output_path}
摘要: {result[:500]}...
如需完整内容,读取上述文件。"""
    
    return result

        五、策略四:子 Agent 隔离

        这是策略一的延伸,但关注点不同:不是路由到不同模型,而是通过独立上下文控制每个子任务的 Token 规模。

        问题:单 Agent 长时间运行,上下文不断积累。一个任务从 2000 tokens 的初始上下文跑到 80,000 tokens,每次 LLM 调用都要付 80,000 tokens 的钱,哪怕大部分内容和当前步骤无关。

        解决方案:把大任务拆成独立的子 Agent,每个子 Agent 只获取完成子任务所需的最小上下文:

def run_subtask(subtask: str, relevant_context: str) -> str:
    """子 Agent 在干净的上下文里运行,只包含当前子任务的相关信息"""
    client = Anthropic()
    
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",  # 子任务通常用小模型
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"""上下文:
{relevant_context}

任务:
{subtask}"""
        }]
    )
    return response.content[0].text

# 主 Agent 拆分任务,分发给子 Agent
def run_with_isolation(big_task: str, codebase_info: dict) -> str:
    subtasks = decompose_task(big_task)  # 分解任务
    results = []
    
    for subtask in subtasks:
        # 只传递和这个子任务相关的上下文
        relevant_context = extract_relevant_context(subtask, codebase_info)
        result = run_subtask(subtask, relevant_context)
        results.append(result)
    
    # 汇总结果(也可以用小模型做)
    return summarize_results(results)

        成本对比

  •  单 Agent 处理 5 个子任务,上下文累积到 60,000 tokens:5 次调用 × 60,000 tokens = 300,000 tokens
  •  子 Agent 隔离,每个子任务 5,000 tokens 上下文:5 次调用 × 5,000 tokens = 25,000 tokens
  •  节省 92%

        六、策略五:上下文管理

        上下文管理是贯穿整个 Harness 的基础策略,前面几篇都提过,这里从成本视角重新整理。

        Progressive Disclosure(渐进式披露):系统提示里只包含 Agent 完成当前任务所需的信息,不把所有可能用到的内容一次性塞进去:

❌ 错误做法:把 100 个工具的完整文档都放进系统提示
✓ 正确做法:系统提示只包含工具目录(每个工具一行描述),Agent 按需读取完整文档

        Compaction 时机优化:不要等上下文快溢出才压缩(那时候已经花了很多 token)。设置一个更早的阈值触发压缩:

def should_compact(usage: dict) -> bool:
    """在使用 60% 时就触发压缩,而不是等到 90%"""
    context_limit = 200000  # Claude 的上下文限制
    return usage["input_tokens"] > context_limit * 0.6

        工具描述精简:工具描述越长,每次调用的 Token 成本越高。精简工具描述,去掉不必要的例子和解释:

# 冗长的工具描述(每次调用增加 300 tokens)
TOOL_READ_VERBOSE = {
    "name": "read_file",
    "description": "读取指定路径的文件内容。支持文本文件和代码文件。可以指定起始行和结束行来读取部分内容。如果文件不存在会返回错误。建议在读取大文件时指定行范围...",
}

# 精简后(每次调用只增加 50 tokens)
TOOL_READ_CONCISE = {
    "name": "read_file",
    "description": "读取文件内容。参数:path(必填),start_line/end_line(可选,指定行范围)。",
}

        七、成本监控与预算控制

        优化策略落地之后,还需要监控系统来确认效果,并防止成本失控。

        7.1 实时成本追踪:每次 LLM 调用后记录成本,按任务类型和模型归因:

import json
from datetime import datetime

def track_cost(model: str, usage, task_type: str):
    # 当前价格(per million tokens)
    PRICING = {
        "claude-opus-4-7":         {"input": 15.0,  "output": 75.0},
        "claude-sonnet-4-6":       {"input": 3.0,   "output": 15.0},
        "claude-haiku-4-5-20251001": {"input": 0.08, "output": 0.4},
    }
    
    prices = PRICING.get(model, {"input": 3.0, "output": 15.0})
    
    # 计算成本(缓存命中按 10% 计算)
    cache_read = getattr(usage, "cache_read_input_tokens", 0)
    normal_input = usage.input_tokens - cache_read
    
    cost = (
        (normal_input / 1_000_000) * prices["input"] +
        (cache_read / 1_000_000) * prices["input"] * 0.1 +  # 缓存读取 10%
        (usage.output_tokens / 1_000_000) * prices["output"]
    )
    
    # 写日志
    log_entry = {
        "timestamp": datetime.now().isoformat(),
        "model": model,
        "task_type": task_type,
        "input_tokens": usage.input_tokens,
        "output_tokens": usage.output_tokens,
        "cache_read_tokens": cache_read,
        "cost_usd": round(cost, 6),
    }
    
    with open("cost_log.jsonl", "a") as f:
        f.write(json.dumps(log_entry) + "\n")
    
    return cost

        7.2 预算硬顶告警:设置每日 / 每周预算上限,超限停止调用:

def check_budget(daily_limit_usd: float = 10.0) -> bool:
    """检查今日成本是否超限"""
    today = datetime.now().strftime("%Y-%m-%d")
    today_cost = 0.0
    
    try:
        with open("cost_log.jsonl") as f:
            for line in f:
                entry = json.loads(line)
                if entry["timestamp"].startswith(today):
                    today_cost += entry["cost_usd"]
    except FileNotFoundError:
        return True  # 没有日志,允许继续
    
    if today_cost >= daily_limit_usd:
        print(f"⚠️ 今日成本 ${today_cost:.2f} 已达上限 ${daily_limit_usd},停止执行")
        return False
    
    return True

        7.3 每周成本报告:汇总按模型和任务类型的成本分布,找出最贵的调用类型:

# 简单的 jq 分析
cat cost_log.jsonl | jq -r '[.model, .task_type, .cost_usd] | @csv' | \
  awk -F',' '{sum[$1","$2] += $3; count[$1","$2]++} 
  END {for (k in sum) print k, sum[k], count[k]}' | \
  sort -t' ' -k3 -rn | head -20

图片


        八、延迟优化:成本和速度的双赢

        成本和延迟往往可以同时优化——用小模型不只省钱,也更快。

        Claude Haiku vs Opus 的延迟差距:Haiku 的首 token 时间约 200-400ms,Opus 约 1000-2000ms。在工具密集的 Agent 工作流里,如果有 20 次 LLM 调用,这个差距会被放大 20 倍。

        并行化:如果多个子任务没有依赖关系,并行运行而不是顺序运行。第11篇里 TestEngineer 和 SecurityReviewer 并行跑,不等待彼此——这不只省时间,也在单位时间内完成了更多工作:

import asyncio
from anthropic import AsyncAnthropic

async def run_parallel_agents(tasks: list[dict]) -> list[str]:
    client = AsyncAnthropic()
    
    async def run_one(task: dict) -> str:
        response = await client.messages.create(
            model=task["model"],
            max_tokens=task.get("max_tokens", 2048),
            messages=[{"role": "user", "content": task["prompt"]}]
        )
        return response.content[0].text
    
    # 并行执行所有任务
    results = await asyncio.gather(*[run_one(t) for t in tasks])
    return list(results)

# 使用
results = asyncio.run(run_parallel_agents([
    {"model": "claude-haiku-4-5-20251001", "prompt": "生成测试用例..."},
    {"model": "claude-opus-4-7", "prompt": "安全审查..."},
]))

        快速路由:用 Haiku 做任务分类(< 500ms),决定用哪个模型执行——路由本身的延迟可以接受,换来的是后续调用的大幅提速。


        九、真实节省空间:综合案例

        把五个策略组合起来,看一个典型 Agent 任务的成本变化:

        场景:代码审查流水线,每天处理 50 个 PR,每个 PR 平均 500 行代码

        优化前

  •  全程用 Claude Opus 4.7
  •  无 Prompt 缓存
  •  工具输出完整进入上下文
  •  每个 PR 约 30,000 input tokens + 5,000 output tokens
  •  每 PR 成本:$0.45 + $0.375 = $0.83
  •  每天 50 个 PR:$41.5/天,$1,245/月

        优化后

  •  代码生成/测试用 Haiku,安全审查用 Opus(模型路由)
  •  系统提示缓存(节省 40% input tokens)
  •  工具输出卸载(上下文压缩 60%)
  •  子 Agent 隔离(每个 Agent 只看相关代码段)
  •  每 PR 实际 input tokens 降到约 8,000,结构调整后 Opus 调用约 5,000 tokens
  •  每 PR 成本:Haiku 部分 $0.01 + Opus 部分(缓存后)$0.05 = 约 $0.06
  •  每天 50 个 PR:$3/天,$90/月

        节省 93%,质量没有明显下降(安全审查这个关键步骤仍然用 Opus)。


        十、什么时候不要省

        最后说一个反向原则:关键节点不要省

        成本优化的核心逻辑是「按价值付费」——高价值的步骤用高价值的模型,低价值的步骤用低价值的模型。不是所有步骤都用最便宜的。

        几类不应该为了省钱而降级的场景:

  •  安全审查:降级可能漏掉真实的安全漏洞,事后的代价远大于节省的钱
  •  最终决策:Agent 工作流的最后一步,影响实际输出质量的关键推理
  •  错误诊断:当 Agent 失败需要分析原因时,让最聪明的模型来看

        省钱是手段,不是目的。目的是在保证质量的前提下,让相同质量的结果花更少的钱。

图片


下一篇:框架层坍缩——随着模型能力增强,LangChain 们正在被重新定义。哪些功能被模型吸收了,哪些仍然在 Harness 层,以及这对你选择技术栈意味着什么。

参考文献:

第13篇:成本优化实战——用最少的钱驾驭最强的 Agent

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐