Agent 工程的成本控制:把 API 费用降下来的系统性方法
只返回前后各 1000 字,中间省略summary = content[:1000] + "\n...[内容已截断]...\n" + content[-1000:]优化四:历史压缩这是 02 篇讲过的 context 管理,这里给出具体实现:“”"保留最近 N 轮对话,其余压缩为摘要“”"return messages# 消息不多,不需要压缩# 需要压缩的历史# 用 LLM 压缩历史请将以下对话历
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 是否定期审计?上次审计是什么时候?
优化时:
• 每次优化是否同时跟踪成本和质量指标?
• 是否有回滚方案,防止过度优化导致质量崩塌?
• 缓存命中率是否达到预期?哪类任务缓存效果最好?
成本控制不是”能省就省”,是”每一分钱都花在刀刃上”。知道钱花在哪里,才能做出正确的取舍。
更多推荐



所有评论(0)