Agent 工程的成本控制:把 API 费用降下来的系统性方法

AGENT STABILITY SERIES · 08

Agent 项目死亡的方式有很多种,费用失控是其中最安静的一种。

它不像 bug 那样报错,不像宕机那样有告警。它只是账单一个月比一个月高,直到某个季度财务问你:这个 AI 项目到底花了多少钱,ROI 在哪里?

这篇系统性拆解 Agent 的成本结构,从架构设计到运营优化,给出可落地的降本方法。
先搞清楚钱花在哪里

降本的前提是知道钱花在哪。Agent 的费用由三个部分构成:

总费用 = Σ (input_tokens × input_price + output_tokens × output_price)
× 调用次数

拆开来看:

费用驱动因素:
├─ input_tokens:每次调用发送给模型的 token 数
│ ├─ System Prompt(固定成本)
│ ├─ 历史对话(随轮次累积)
│ ├─ 工具 Schema(每次调用都带)
│ └─ 工具返回值(每次工具调用后追加)

├─ output_tokens:模型生成的 token 数
│ ├─ 推理过程(CoT)
│ └─ 最终输出

└─ 调用次数:
├─ 任务数量(业务量)
├─ 每任务的工具调用轮次
└─ 重试次数

大多数团队的费用问题出在 input_tokens,而不是 output_tokens。 input_tokens 随轮次累积,是二次方增长;output_tokens 相对稳定。
METHOD 01|压缩 input_tokens
每轮调用发送的内容,每一个 token 都要值得

优化一:System Prompt 瘦身

System Prompt 是每次调用都会发送的固定成本。很多团队的 System Prompt 随时间不断追加内容,从最初的 200 token 膨胀到 2000 token,却从没做过清理。

审计 System Prompt 的方法

def audit_system_prompt(prompt: str) -> dict:
tokens = count_tokens(prompt)
sections = parse_sections(prompt)

return {
    "total_tokens": tokens,
    "monthly_cost_usd": tokens * CALLS_PER_MONTH * INPUT_PRICE,
    "sections": [
        {
            "name": s.name,
            "tokens": count_tokens(s.content),
            "last_modified": s.last_modified,
            "usage_in_traces": count_section_usage(s),  # 这段内容被用到过吗?
        }
        for s in sections
    ]
}

常见的 System Prompt 冗余内容

redundant_patterns = [
“过时的业务规则(已经不适用的约束)”,
“重复表达同一个意思的段落”,
“详细的示例(可以移到 few-shot 按需注入)”,
“防御性说明(‘如果用户问X,你应该Y’,但X从未出现过)”,
]

优化二:工具 Schema 精简

每个工具的 Schema 都会进入 context,注册 20 个工具的 Agent,每次调用都要带着 20 个工具的描述。

反例:冗长的工具描述

tools_verbose = [
{
“name”: “query_database”,
“description”: “”"
这个工具用于查询数据库中的数据。它支持多种查询类型,
包括但不限于:按用户ID查询、按时间范围查询、按状态查询等。
使用此工具时,请确保提供正确的SQL语句。注意:只支持SELECT操作,
不支持INSERT、UPDATE、DELETE等写操作。查询结果将以JSON格式返回。
如果查询失败,将返回错误信息。请在使用前确认数据库连接正常。
“”", # 约 120 token

}
]

优化:精简描述,保留关键信息

tools_concise = [
{
“name”: “query_database”,
“description”: “执行 SELECT 查询,返回 JSON 结果”, # 约 15 token
“input_schema”: {
“type”: “object”,
“properties”: {
“sql”: {“type”: “string”, “description”: “SELECT 语句”}
},
“required”: [“sql”]
}
}
]

节省:(120 - 15) × 20个工具 × 调用次数 = 大量 token

优化三:工具返回值截断

工具返回的原始数据往往远超 Agent 实际需要的量。

在工具层做截断,而不是让完整数据进入 context

def query_database_tool(sql: str, max_rows: int = 20) -> dict:
results = db.execute(sql)

if len(results) > max_rows:
    return {
        "data": results[:max_rows],
        "truncated": True,
        "total_rows": len(results),
        "message": f"结果共 {len(results)} 行,已截断至前 {max_rows} 行"
    }

return {"data": results, "truncated": False}

对文本内容做摘要压缩

def fetch_document_tool(url: str, max_chars: int = 2000) -> dict:
content = fetch_url(url)

if len(content) > max_chars:
    # 只返回前后各 1000 字,中间省略
    summary = content[:1000] + "\n...[内容已截断]...\n" + content[-1000:]
    return {"content": summary, "truncated": True, "original_length": len(content)}

return {"content": content, "truncated": False}

优化四:历史压缩

这是 02 篇讲过的 context 管理,这里给出具体实现:

def compress_history(messages: list, keep_recent: int = 3) -> list:
“”"
保留最近 N 轮对话,其余压缩为摘要
“”"
if len(messages) <= keep_recent * 2:
return messages # 消息不多,不需要压缩

# 需要压缩的历史
to_compress = messages[:-keep_recent * 2]
recent = messages[-keep_recent * 2:]

# 用 LLM 压缩历史
summary_prompt = f"""

请将以下对话历史压缩为简洁的要点摘要(不超过 200 字):

{format_messages(to_compress)}
“”"
summary = call_llm(summary_prompt)

# 重建消息列表
compressed = [
    {"role": "system", "content": f"[历史摘要] {summary}"},
    *recent
]

return compressed

📌 案例:System Prompt 审计节省 40% 费用

某团队的客服 Agent 运行了 8 个月,System Prompt 从最初的 380 token 增长到了 1840 token。没有人知道什么时候加了什么,也没有人删过内容。

做了一次 System Prompt 审计,发现:

• 约 400 token 是三个月前一个已下线功能的操作说明
• 约 300 token 是重复表达”礼貌回复”的三段不同措辞
• 约 200 token 是从未被触发过的边界 case 说明

清理后 System Prompt 压缩到 940 token,每月 token 消耗下降 38%,费用随之下降。 回复质量没有可感知的变化。

(场景基于行业通用模式整理,数据为示意量级)
METHOD 02|减少调用次数
不是每次任务都需要调用昂贵的大模型

优化一:任务路由——按复杂度选模型

并非所有任务都需要最强的模型。建立一个任务路由层,简单任务用便宜的小模型,复杂任务才用大模型。

class ModelRouter:
“”"
按任务复杂度路由到不同模型
价格参考(示意):
- claude-haiku: $0.25 / 1M tokens
- claude-sonnet: $3.00 / 1M tokens
- claude-opus: $15.00 / 1M tokens
“”"

def route(self, task: str, context: dict) -> str:
    complexity = self._assess_complexity(task, context)

    if complexity == "simple":
        return "claude-haiku-4-5"       # 价格约 sonnet 的 1/12
    elif complexity == "medium":
        return "claude-sonnet-4-6"      # 默认选择
    else:
        return "claude-opus-4-6"        # 复杂推理才用

def _assess_complexity(self, task: str, context: dict) -> str:
    # 简单任务特征:
    # - 纯信息查询(不需要推理)
    # - 格式转换(JSON/CSV 互转)
    # - 简单分类(是/否判断)
    simple_patterns = [
        r"查询.*状态",
        r"转换.*格式",
        r"是否.*",
    ]
    for pattern in simple_patterns:
        if re.search(pattern, task):
            return "simple"

    # 复杂任务特征:
    # - 多步骤推理
    # - 代码生成和审查
    # - 跨领域综合分析
    if any(kw in task for kw in ["分析", "设计", "优化", "架构"]):
        if context.get("requires_deep_reasoning"):
            return "complex"

    return "medium"

优化二:缓存——相同输入不重复调用

对于确定性高的任务,相同的输入应该返回缓存的结果,而不是每次都调用 LLM。

import hashlib
import json
from functools import lru_cache

class CachedAgent:
def init(self, agent, cache_ttl: int = 3600):
self.agent = agent
self.cache = {}
self.cache_ttl = cache_ttl

def run(self, task: str, use_cache: bool = True) -> dict:
    if not use_cache:
        return self.agent.run(task)

    cache_key = self._make_key(task)

    if cache_key in self.cache:
        entry = self.cache[cache_key]
        if time.time() - entry["timestamp"] < self.cache_ttl:
            return {**entry["result"], "from_cache": True}

    result = self.agent.run(task)
    self.cache[cache_key] = {
        "result": result,
        "timestamp": time.time()
    }
    return result

def _make_key(self, task: str) -> str:
    return hashlib.md5(task.encode()).hexdigest()

适合缓存的任务类型

cacheable_tasks = [
“FAQ 回答(问题相似度高,答案相对稳定)”,
“产品信息查询(数据不频繁变化)”,
“模板生成(相同参数生成相同结果)”,
“分类判断(同一文本反复分类)”,
]

不适合缓存的任务类型

non_cacheable_tasks = [
“实时数据查询(结果随时间变化)”,
“个性化推荐(依赖用户上下文)”,
“创意生成(需要多样性)”,
]

优化三:批处理——合并小任务

单次任务的固定成本(System Prompt、工具 Schema)是不变的。把多个小任务合并成一次调用,可以摊薄固定成本。

def batch_classify(items: list[str], batch_size: int = 20) -> list[dict]:
“”"
将多个分类任务合并为一次 LLM 调用
“”"
results = []

for i in range(0, len(items), batch_size):
    batch = items[i:i + batch_size]
    numbered = "\n".join(f"{j+1}. {item}" for j, item in enumerate(batch))

    prompt = f"""

请对以下 {len(batch)} 条内容逐一分类,返回 JSON 数组:

{numbered}

返回格式:[{{“id”: 1, “category”: “…”, “confidence”: 0.9}}, …]
“”"
response = call_llm(prompt)
batch_results = parse_json(response)
results.extend(batch_results)

return results

效果:20 个分类任务合并为 1 次调用

节省:19 次调用的固定成本(System Prompt + 工具 Schema)

📌 案例:模型路由把费用降了 65%

某团队的 Agent 处理三类任务:简单状态查询(占 60%)、标准分析报告(占 30%)、复杂推理决策(占 10%)。上线初期全部用 claude-sonnet,月费用约 $8000。

实施模型路由后:

• 简单查询 → claude-haiku(费用约 sonnet 的 1/12)
• 标准报告 → claude-sonnet(不变)
• 复杂决策 → claude-opus(费用约 sonnet 的 5 倍,但占比低)

整体月费用降至约 $2800,降幅 65%。 三类任务的质量评分均未出现显著下降。

(场景基于行业通用模式整理,数据为示意量级)
METHOD 03|架构层面的成本设计
在设计阶段就考虑成本,而不是上线后再优化

原则一:每个架构决策都有成本标签

架构决策 成本影响
────────────────────────────────────────────────
并行 SubAgent × N 费用 × N(最显著)
无压缩历史积累 费用随轮次二次方增长
大模型做所有任务 可能是最优,也可能是最浪费
工具返回完整数据 每轮浪费大量 input tokens
每次从头开始规划 重复的固定成本,可用缓存优化

原则二:设计时估算成本上限

def estimate_monthly_cost(
tasks_per_day: int,
avg_rounds: int,
avg_tokens_per_round: int,
input_price: float, # 每 1K token 的价格
output_price: float,
) -> dict:
“”"
上线前估算月费用,发现设计问题
“”"
tokens_per_task = avg_rounds * avg_tokens_per_round
daily_tokens = tasks_per_day * tokens_per_task
monthly_tokens = daily_tokens * 30

# 假设 input:output = 4:1
input_tokens = monthly_tokens * 0.8
output_tokens = monthly_tokens * 0.2

monthly_cost = (
    input_tokens / 1000 * input_price +
    output_tokens / 1000 * output_price
)

return {
    "tokens_per_task": tokens_per_task,
    "monthly_tokens": monthly_tokens,
    "monthly_cost_usd": monthly_cost,
    "cost_per_task_usd": monthly_cost / (tasks_per_day * 30),
    "warning": "HIGH" if monthly_cost > 5000 else "OK"
}

示例:设计阶段发现问题

estimate = estimate_monthly_cost(
tasks_per_day=1000,
avg_rounds=10, # 10 轮工具调用
avg_tokens_per_round=3000, # 每轮 3000 token(无压缩)
input_price=0.003, # $3 / 1M tokens
output_price=0.015,
)

结果:月费用 $27,000 → 触发"为什么要 10 轮?为什么每轮 3000 token?"的讨论

原则三:成本预算和质量指标一起管理

成本不是孤立的指标,要和质量指标放在一起看。

成本 ↓ + 质量 ↓ = 做了错误的优化(砍过头了)
成本 ↓ + 质量 → = 好的优化(降本不降质)
成本 ↓ + 质量 ↑ = 优秀的优化(降本还提质)
成本 ↑ + 质量 ↑ = 可接受的投资(要有 ROI 支撑)

📌 案例:过度优化导致质量崩塌

某团队为了降低成本,把所有任务切换到最小的模型,同时把工具返回值截断到 500 字。月费用从 降到1200。

但两周后,用户投诉率上升了 3 倍,LLM-as-Judge 评分从 0.82 降到了 0.54。复查发现:最小模型无法处理多步骤推理任务,而 500 字的截断导致关键数据丢失。

降本 80%,但质量崩了,最终不得不回滚。 重新设计后:只对简单任务用小模型,截断阈值提高到 2000 字,月费用 $2800,质量评分 0.79——这才是可持续的优化。

(场景基于行业通用模式整理,数据为示意量级)
成本优化的优先级排序

不是所有优化都值得做。按投入产出比排序:

优先级 优化项 实施难度 预期降幅
──────────────────────────────────────────────────────────
P0 工具返回值截断 低 15-30%
P0 System Prompt 审计清理 低 10-40%(取决于膨胀程度)
P1 历史压缩策略 中 20-50%
P1 模型路由(按复杂度分流) 中 30-70%
P2 结果缓存 中 取决于重复率
P2 批处理合并 中 10-30%
P3 工具 Schema 精简 低 5-15%
P3 架构重设计 高 50%+(但风险最大)

先做 P0,一周内见效;P1 需要两周;P2/P3 按需评估。
Tech Lead 的成本控制检查清单

设计阶段:

•  是否在上线前做了月费用估算?
•  是否为每种任务类型选择了合适的模型?
•  工具返回值是否设置了截断上限?

上线后:

•  是否有费用告警(每小时/每日上限)?
•  是否按任务类型拆分了费用,知道钱花在哪里?
•  System Prompt 是否定期审计?上次审计是什么时候?

优化时:

•  每次优化是否同时跟踪成本和质量指标?
•  是否有回滚方案,防止过度优化导致质量崩塌?
•  缓存命中率是否达到预期?哪类任务缓存效果最好?

成本控制不是”能省就省”,是”每一分钱都花在刀刃上”。知道钱花在哪里,才能做出正确的取舍。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐