返回博客列表

Agent 账单失控?10 个方法让 Token 消耗降下来

2026-06-15T15:00:00+08:00
AI AgentToken 优化LLM成本控制Prompt Caching企业 AI

Agent 账单失控?10 个方法让 Token 消耗降下来

上次看到 API 账单的时候,有没有怀疑过自己的 Agent 是不是在偷偷读小说?

一个客服 Agent 上线第一个月花了 200 美元,第二个月 800 美元,第三个月 3500 美元——用户量只涨了 2 倍,账单涨了 17 倍。这不是个例,几乎每个把 Agent 推到生产环境的公司都会经历这个阶段。

问题出在哪?Agent 不像传统的 API 调用,它是一个不断"思考"的循环。每一次工具调用、每一段上下文、每一个多余的 system prompt,都在烧 Token。更关键的是,大多数团队在开发阶段根本不关注 Token 消耗——功能跑通了就行,等账单炸了才开始慌。

这篇文章系统梳理了 10 个 Token 优化方法,从最简单的 Prompt 层面到架构层面,覆盖从"立竿见影"到"长期治理"的完整光谱。每个方法都附带具体的量化效果和落地建议。

本文提纲

  1. Prompt Caching:效果最大的一个开关
  2. 上下文窗口管理:别把聊天记录全塞进去
  3. Prompt 精简:砍掉你 System Prompt 里的废话
  4. 模型分级路由:简单任务别用大模型
  5. 结构化输出约束:让模型别"自由发挥"
  6. RAG 替代长上下文:检索比全量塞入便宜 100 倍
  7. 工具调用优化:减少 Agent 循环次数
  8. Batch API:异步任务直接打五折
  9. Token 监控与预算控制:看不见就管不了
  10. 缓存层设计:语义级别的去重

1. Prompt Caching:效果最大的一个开关

如果只做一个优化,做这个。

Anthropic 的 Prompt Caching(Claude API)和 OpenAI 的 Cached Input Tokens 都允许你对重复使用的 Prompt 前缀做缓存。缓存命中后,那部分 Token 的费用直接降到原来的 10%(Anthropic)或 50%(OpenAI)。

Agent 场景下,System Prompt、工具定义、few-shot 示例这些内容在每次调用中都完全一样。一个典型的 Agent,System Prompt + 工具定义可能占 3000-8000 Token,而用户实际输入可能只有 200 Token。不缓存的话,每次调用你都在为那 8000 Token 全价买单。

Anthropic Claude Prompt Caching:

# Anthropic Prompt Caching
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": "<长篇 system prompt 和工具说明...>",
            "cache_control": {"type": "ephemeral"}  # 标记缓存
        }
    ],
    messages=[{"role": "user", "content": "用户问题"}]
)

OpenAI 自动缓存:

OpenAI 的缓存是自动的——不需要改代码。只要你的 Prompt 前缀(至少 1024 Token)和之前请求一致,就会命中缓存。缓存保持 5-10 分钟的空闲窗口。

实际效果:一个日均 10 万次调用的客服 Agent,开启 Prompt Caching 后月费从 12000 美元降到 4500 美元。其中 System Prompt(约 5000 Token)从全价变成 1 折,占了节省的大头。

注意:缓存有 5 分钟 TTL(Anthropic)或 5-10 分钟(OpenAI)。如果你的 Agent 调用频率低于这个窗口,缓存命中率会很低。高并发场景效果最好。

2. 上下文窗口管理:别把聊天记录全塞进去

Agent 跑久了,上下文会像滚雪球一样膨胀。第 1 轮对话 500 Token,第 10 轮就变成了 5000 Token,第 50 轮可能突破 20000 Token。如果你的 Agent 还会把工具返回结果全部留在上下文里,膨胀速度更快。

大部分 Agent 框架(LangChain、LlamaIndex、Hermes Agent)的默认行为是把完整的对话历史发给模型。这在开发阶段没问题,但到生产环境就是 Token 黑洞。

解决方案有三个层次

第一层:滑动窗口截断。只保留最近 N 轮对话,超出部分直接丢弃。最简单但最粗暴:

# 只保留最近 6 轮对话
recent_messages = conversation_history[-12:]  # 6 轮 = 12 条消息

第二层:摘要压缩。把旧对话用一次便宜的模型调用压缩成摘要,替代原始内容。这比直接丢弃好,因为关键信息被保留了:

# 当对话超过 10 轮时,压缩前 8 轮
if len(conversation_history) > 20:
    old_messages = conversation_history[:16]
    summary = cheap_model.summarize(old_messages)
    conversation_history = [
        {"role": "system", "content": f"之前的对话摘要:{summary}"},
        *conversation_history[16:]
    ]

第三层:工具结果清理。工具返回的数据往往非常大(一个数据库查询可能返回几千 Token 的 JSON),但在后续对话中几乎不会被引用。把已经处理过的工具结果替换成简短摘要:

# 工具调用完成后,将原始返回替换为摘要
if tool_result_token_count > 500:
    history[-1] = {
        "role": "tool",
        "content": f"[已处理] 查询返回 {len(result)} 条记录,"
                   f"关键字段:{extracted_fields}"
    }

实际效果:一个代码编辑 Agent,通过"摘要 + 工具结果清理",将平均上下文从 18000 Token 压缩到 6000 Token,单次调用成本降低约 65%。

3. Prompt 精简:砍掉你 System Prompt 里的废话

去审计一下你的 System Prompt,大概率能砍掉 30-50%。

常见的浪费模式:

重复说明:同一个规则用了三种不同的方式表达。"你必须用中文回答" / "回答语言为中文" / "不要使用英语"——模型不是人,说一次就够了。

过度举例:few-shot 示例是好东西,但 10 个示例和 3 个示例的效果差异通常不大。每个示例都在烧 Token。把 10 个砍到 3 个,省下的 Token 可能让你的 System Prompt 从 8000 降到 3000。

冗余的工具描述:如果你的 Agent 有 20 个工具,每个工具的 description 写了 200 字,光工具定义就 4000 Token 了。精简工具描述,把详细说明放到参数的 description 里(只在工具被选中时才需要详细上下文)。

# ❌ 冗长的工具描述
{
    "name": "search_database",
    "description": "这个工具用于在公司的客户数据库中搜索客户信息。"
    "你可以通过客户姓名、邮箱、电话号码来搜索。"
    "支持模糊匹配和精确匹配。返回结果包含客户的基本信息、"
    "订单历史和联系记录。如果搜索不到结果会返回空列表。"
    "请尽可能提供精确的搜索条件以获得最佳结果..."
}

# ✅ 精简的工具描述
{
    "name": "search_database",
    "description": "Search customer by name, email, or phone."
}

Markdown 格式税:System Prompt 里大量的 Markdown 标题、列表、加粗,这些格式字符也是 Token。一个 2000 字的 Markdown 格式 Prompt,纯格式开销可能有 300-400 Token。

实际效果:我们见过最极端的案例——一个团队把 System Prompt 从 12000 Token 精简到 4500 Token(去掉重复规则、减少示例、精简工具描述),Agent 行为几乎没有变化,但每次调用成本直接降了 60%。

4. 模型分级路由:简单任务别用大模型

不是所有任务都需要 Claude Sonnet 或 GPT-4o。

一个 Agent 的工作流里,大约 60-70% 的子任务是简单的:分类、提取关键词、判断是否需要调用工具、格式化输出。这些任务用 Haiku、GPT-4o-mini 甚至更小的开源模型就能做好,成本只有大模型的 1/10 到 1/30。

路由策略

def route_model(task_type: str, complexity: str) -> str:
    """根据任务类型和复杂度选择模型"""
    routing = {
        # 简单任务 → 小模型
        ("classification", "low"): "claude-haiku",
        ("extraction", "low"): "claude-haiku",
        ("formatting", "any"): "claude-haiku",
        ("summarization", "low"): "claude-haiku",

        # 中等任务 → 中模型
        ("rag_answer", "medium"): "claude-sonnet",
        ("code_review", "medium"): "claude-sonnet",

        # 复杂任务 → 大模型
        ("reasoning", "high"): "claude-opus",
        ("code_generation", "high"): "claude-opus",
    }
    return routing.get((task_type, complexity), "claude-sonnet")

更聪明的做法:先用一个极小的模型(Haiku)做意图分类,判断当前请求的复杂度,再路由到对应级别的模型。分类本身只花几十个 Token,但可能帮你省下几千 Token 的调用费用。

实际效果:一个有 3 级路由的客服 Agent,70% 的请求用 Haiku 处理($0.25/M input),25% 用 Sonnet($3/M input),5% 用 Opus($15/M input)。相比全部用 Sonnet,整体成本降低约 75%。

注意:路由不是免费的。如果你的 Agent 一次对话需要多轮调用,每次都走一遍分类路由会增加延迟。对于简单场景,可以用规则路由(关键词匹配)替代模型路由。

5. 结构化输出约束:让模型别"自由发挥"

模型每多输出一个 Token,你就要多付一个 Token 的钱。而很多 Agent 的输出中有大量"废话"——开场白、解释性文字、过渡句——这些内容对下游处理毫无价值。

用结构化输出(JSON Mode、Function Calling、Outlines/Guidance)强制模型只输出你需要的内容,可以从源头减少输出 Token。

对比示例

# ❌ 无约束输出:模型可能写一大段
response = model.generate("分析这段代码的安全问题,给出修复建议")
# 输出:"好的,我来帮你分析这段代码。首先,我注意到..."
#       + 200 Token 的分析 + 150 Token 的建议 + 50 Token 的客套话

# ✅ 结构化输出:只返回你需要的字段
response = model.generate(
    "分析这段代码的安全问题",
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "security_analysis",
            "schema": {
                "type": "object",
                "properties": {
                    "issues": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "severity": {"type": "string", "enum": ["high", "medium", "low"]},
                                "description": {"type": "string"},
                                "fix": {"type": "string"}
                            }
                        }
                    }
                }
            }
        }
    }
)
# 输出:{"issues": [{"severity": "high", "description": "...", "fix": "..."}]}

更进一步的方案:用 OutlinesGuidance 在 Token 级别约束输出。这不仅能减少输出长度,还能保证输出格式 100% 合规,省掉后续解析和重试的成本。

实际效果:一个数据分析 Agent,从自由文本输出改为 JSON 结构化输出后,平均输出 Token 从 850 降到 320,降幅 62%。同时,下游解析的失败率从 15% 降到 0。

6. RAG 替代长上下文:检索比全量塞入便宜 100 倍

很多团队在构建 Agent 时,会把所有相关文档全部塞进上下文。"我们有 200K 的上下文窗口,怕什么?"——怕的是你的账单。

假设你有 50 页的产品文档(约 25000 Token),每次 Agent 调用都全量塞入。一天 10000 次调用,就是 2.5 亿 Token 的输入——仅这一项就是每月数千美元。

用 RAG(检索增强生成)替代全量塞入:

# ❌ 全量塞入:每次调用都带上所有文档
context = load_all_documents()  # 25000 Token
response = model.generate(system=context, question=user_query)

# ✅ RAG:只检索相关的片段
relevant_chunks = vector_db.search(user_query, top_k=3)  # ~1500 Token
context = "\n".join(relevant_chunks)
response = model.generate(system=context, question=user_query)

25000 Token → 1500 Token,降幅 94%。即使加上 embedding 和向量检索的成本,整体费用也远低于全量塞入。

但注意一个趋势:随着长上下文模型的出现(Claude 200K、Gemini 1M、GPT-4o 128K),很多团队开始"偷懒"全量塞入。这在开发阶段可以,但生产环境必须做 RAG。Prompt Caching 能缓解部分成本,但检索仍然是更经济的选择——尤其当文档库持续增长时。

一个更精细的策略:混合模式。对于高频常见问题(FAQ),用 RAG;对于需要全局理解的复杂问题,用长上下文 + Prompt Caching。根据查询意图动态切换。

7. 工具调用优化:减少 Agent 循环次数

Agent 的成本不仅取决于每次调用的 Token 数,还取决于调用次数。一个 Agent 如果需要 8 轮工具调用才能完成任务,它的成本就是一个直接返回答案的模型的 8 倍(甚至更多,因为上下文会累积)。

减少循环次数的方法

并行工具调用。Claude 和 GPT-4o 都支持在一次响应中调用多个工具。如果你的 Agent 需要查询 3 个独立的数据源,让它在一次调用中同时发起 3 个查询,而不是串行 3 轮:

# ✅ 并行调用 3 个独立工具
tool_calls = [
    {"name": "get_weather", "args": {"city": "北京"}},
    {"name": "get_news", "args": {"category": "tech"}},
    {"name": "get_calendar", "args": {"date": "today"}}
]

提前终止。在 System Prompt 中明确告诉 Agent 什么时候可以停止:"如果用户的问题已经回答完毕,直接给出最终回复,不要调用多余的工具。"

计划-执行分离。让一个模型先做计划(需要调用哪些工具、按什么顺序),然后直接执行,而不是让 Agent 在每一步都重新"思考"下一步做什么。这能减少大量的中间推理 Token:

# 第一步:规划(用便宜模型)
plan = haiku.generate(f"用户想{user_goal},列出需要的工具调用步骤")
# plan = ["search_docs(query)", "summarize(results)"]

# 第二步:按计划执行(不需要每步都问模型)
for step in plan:
    result = execute_tool(step)

实际效果:一个客服 Agent 从"每步思考"模式改为"计划-执行"模式后,平均工具调用轮次从 5.3 次降到 2.8 次,单次会话成本降低约 45%。

8. Batch API:异步任务直接打五折

不是所有的 Agent 调用都需要实时响应。批量处理任务(文档分析、数据标注、日志分类、历史数据迁移)完全可以用 Batch API。

OpenAI 的 Batch API 和 Anthropic 的 Message Batches API 都提供 50% 的折扣,代价是结果在 24 小时内返回(实际通常几小时内就完成了)。

# OpenAI Batch API
import openai

client = openai.OpenAI()

# 创建批量任务
batch = client.batches.create(
    input_file_id=file_id,
    endpoint="/v1/chat/completions",
    completion_window="24h",
    metadata={"description": "批量分类客服工单"}
)
# 半价,24 小时内完成

适用场景

  • 每天需要对数千条客服工单做分类和情感分析
  • 批量处理积压的文档/合同/邮件
  • 对历史对话数据做质量评估
  • 训练数据生成和标注

实际效果:一个每天处理 50000 条工单分类的 Agent,从实时 API 切换到 Batch API 后,这部分成本直接减半——从每月 8000 美元降到 4000 美元,而且因为不占用实时 API 的并发额度,系统稳定性也提升了。

9. Token 监控与预算控制:看不见就管不了

大多数团队在账单爆掉之前,根本不知道自己的 Agent 到底消耗了多少 Token。你需要的是实时可见性和自动预算控制。

第一层:监控。在每次 API 调用后记录 Token 使用量,按 Agent、按用户、按任务类型维度聚合:

import time
from collections import defaultdict

token_usage = defaultdict(lambda: {"input": 0, "output": 0, "cost": 0})

def call_agent_with_tracking(agent_id, messages):
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        messages=messages
    )

    # 记录 Token 使用量
    usage = response.usage
    token_usage[agent_id]["input"] += usage.input_tokens
    token_usage[agent_id]["output"] += usage.output_tokens
    token_usage[agent_id]["cost"] += calculate_cost(usage)

    # 超预算告警
    if token_usage[agent_id]["cost"] > BUDGET_LIMIT:
        send_alert(f"Agent {agent_id} 超预算!")

    return response

第二层:预算控制。给每个 Agent、每个用户、每个会话设置 Token 预算。超限后自动降级(切换到更便宜的模型)或直接拒绝:

def call_with_budget(session, messages):
    remaining = session.token_budget - session.tokens_used
    if remaining < 1000:
        # 预算不足,降级到小模型
        return call_model("claude-haiku", messages)
    return call_model("claude-sonnet", messages)

第三层:异常检测。一个正常的 Agent 会话消耗 2000-8000 Token,如果某次会话突然消耗了 50000 Token,大概率是出了问题——上下文没清理、Agent 陷入了死循环、工具返回了异常大的结果。设置阈值告警,在问题扩大之前切断。

工具推荐:LangSmith、Helicone、Langfuse 都提供 LLM 调用的 Token 监控面板。如果是自建系统,至少把 usage.input_tokensusage.output_tokens 记到数据库里,配上 Grafana 看板。

10. 缓存层设计:语义级别的去重

Prompt Caching 解决的是"同一个 Prompt 前缀"的重复。但如果两个用户问了语义相同但文字不同的问题呢?"今天北京天气怎么样"和"北京今天天气如何",模型会给出几乎一样的答案,但你为两次调用都付了全价。

语义缓存:在 Agent 和 LLM 之间加一层缓存。先对用户输入做 embedding,在向量数据库中查找是否有语义相似的历史查询。如果命中(相似度 > 阈值),直接返回缓存的回答,不调用 LLM:

import numpy as np

def agent_with_semantic_cache(user_input):
    # 生成 embedding
    query_embedding = embed(user_input)

    # 语义搜索缓存
    cached = vector_cache.search(
        query_embedding,
        threshold=0.95,  # 相似度阈值
        max_age="7d"     # 缓存有效期
    )

    if cached:
        return cached.answer  # 命中,直接返回,0 Token 消耗

    # 未命中,调用 LLM
    answer = llm.generate(user_input)

    # 存入缓存
    vector_cache.store(query_embedding, answer, metadata={
        "input": user_input,
        "timestamp": time.now()
    })

    return answer

适用场景

  • 客服 Agent:大量用户问的问题高度重复("怎么退款"、"怎么改密码")
  • FAQ Agent:本质上就是在回答固定集合的问题
  • 数据查询 Agent:相同维度不同表述的查询

实际效果:一个电商客服 Agent,通过语义缓存获得了 35% 的命中率——35% 的请求没有调用 LLM,直接返回缓存结果。整体 Token 消耗降低 35%,同时 P99 延迟从 2.3 秒降到 0.1 秒(缓存命中时)。

注意:语义缓存需要仔细设置相似度阈值。太低会返回错误答案("怎么退款"和"退款多久到账"语义相似但答案不同),太高则命中率低。建议从 0.95 开始,根据 badcase 反馈调整。另外,涉及实时数据(天气、库存、订单状态)的查询不适合做缓存,或者需要配合 TTL 短期缓存。

参考文档与链接

你的 Agent 每天烧多少 Token?评论区聊聊你的账单和优化心得。觉得有用就点个赞,让更多被账单吓到的朋友看到。


作者: itech001
来源: 公众号:AI人工智能时代
网站: https://www.theaiera.cn/ 每日分享最前沿的AI新闻资讯和技术研究。

本文首发于 AI人工智能时代,转载请注明出处。

觉得文章不错?分享给更多人!