1. 成本从哪来
MCP 工具调用会花钱的地方,比大多数人想的多。理解成本构成是优化的前提——把每一分钱花在哪算清楚,才知道省哪里。
1.1 成本的四个来源
# LLM + MCP 应用的成本构成
# 1) 输入 token: 系统提示、工具定义、工具结果、对话历史
# 2) 输出 token: 模型回复、工具调用参数
# 3) 外部依赖: 工具背后的 API 调用、数据库查询
# 4) 基础设施: 服务器运行、向量库、渲染等
# 其中输入 token 里的"工具结果"往往被严重低估
1.2 工具调用的隐藏成本
# 一次工具调用产生的 token
# 1) 工具定义(tools/list 在系统提示里)
# 2) 模型生成的调用参数(输出)
# 3) 工具返回的结果(重新进上下文)
# 4) 结果被后续对话反复引用(历史累积)
# 一次"看起来免费"的查询,实际可能吃掉几千 token
1.3 优化目标
# 优化的三个目标
# 1) 省: 同样的任务,更少 token / 更低成本
# 2) 稳: 优化不能显著牺牲效果
# 3) 透明: 成本可度量、可归因、可预算
# 成本优化是"工程权衡",不是"一刀切的抠门"
2. Token 预算管理
没有预算的成本管理是空谈。给每次任务、每个阶段设 token 预算,让优化有「参照系」。
2.1 上下文预算分配
# 示例: 8k token 上下文的分配
# 1) 系统提示 + 工具定义: 1.5k
# 2) 用户任务: 0.5k
# 3) 工具结果(检索/抓取/查询): 3k
# 4) 对话历史: 2k
# 5) 预留余量: 1k
# 预算先分好,工具结果超预算就压缩
2.2 按任务分预算
# 不同任务给不同预算
# 1) 简单问答: 2k(不检索,少工具)
# 2) 知识检索: 6k(检索结果多)
# 3) 长文档分析: 12k(要读文档)
# 4) 代码任务: 8k(文件内容 + 错误信息)
# 预算与任务类型绑定,才能"该花的别省,不该花的别浪费"
2.3 Token 估算工具
# 1) 用 tokenizer 精确计数(输入前估算)
# 2) 工具结果返回时附带 token 估算
# 3) 服务器侧统计每工具累计 token 贡献
# 4) 超预算触发压缩/截断策略
# 估算 → 控制 → 复盘,是预算的三闭环
3. 工具结果的缓存
工具调用往往是重复的:同一个查询、同一份文档、同一个页面,不同会话/多轮对话里可能反复需要。结果缓存是投入产出比最高的优化。
3.1 缓存什么
# 可缓存的
# 1) 只读工具结果(查询/检索/抓取)
# 2) 相同参数 + 短 TTL
# 3) 确定性输出(函数计算、配置读取)
# 不可缓存的
# 4) 写操作(缓存写结果会重复副作用)
# 5) 实时强数据(价格/库存/行情)
# 规则: 只缓存幂等、可接受短暂过期的结果
3.2 缓存实现
# 工具结果缓存(TTL + 键 = 工具名 + 参数哈希)
CACHE: dict[str, tuple[float, object]] = {}
CACHE_TTL = {"search_knowledge": 300, "fetch_webpage": 600}
def cache_key(tool: str, args: dict) -> str:
raw = f"{tool}:{json.dumps(args, sort_keys=True)}"
return hashlib.sha256(raw.encode()).hexdigest()
async def call_with_cache(tool, args, handler):
key = cache_key(tool, args)
if key in CACHE:
ts, result = CACHE[key]
if time.time() - ts < CACHE_TTL.get(tool, 60):
return {"cached": True, "data": result}
result = await handler(**args)
CACHE[key] = (time.time(), result)
return {"cached": False, "data": result}
3.3 缓存的落地要点
# 1) 缓存结果标注"cached"(模型可感知)
# 2) TTL 按数据新鲜度定(价格短、文档长)
# 3) 缓存放服务器侧(跨会话共享),不依赖客户端
# 4) 缓存失效(写操作后)主动清理相关键
# 命中率高是健康信号: 说明工具在服务重复需求
4. 批处理与惰性加载
很多场景下「一次拿全」比「多次零取」省。但「一次拿全」也可能浪费。关键是按需。
4.1 批处理工具
# 批处理的价值
# 1) 多次单查 → 一次批量查(减少往返)
# 2) 参数数组: query_batch([q1, q2, q3])
# 3) 结果合并返回(一次进上下文)
# 4) 外部 API 调用合并(省第三方费用)
# 批处理省的是"往返 + 重复的框架 token"
4.2 批量工具设计
# 批量查询工具(替代多次单查)
async def query_batch(queries: list[str], limit: int = 20):
results = []
for q in queries:
rows = await run_query(q, limit=limit)
results.append({"query": q, "rows": rows})
return {"results": results, "count": len(results)}
# 模型一次给 3-5 个相关问题,服务器一次处理
# 比"一个一个查"省 2-3 倍的工具定义/调用开销
4.3 惰性加载与分页
# 按需加载的哲学
# 1) 摘要先行: 先给结果摘要/前 N 行
# 2) 详细按需: 模型需要再取详情
# 3) 分页续取: has_more + cursor
# 4) 元数据先行: 列表先给文件名/标题,内容再点开
# 惰性加载: 不把"可能用不上"的细节提前花掉
5. 上下文压缩与截断
工具结果进了上下文就很难出来。压缩是在「信息价值」与「token 成本」之间做取舍。
5.1 结果精简策略
# 按重要性保留
# 1) 截断: 长文本按字符/行截断
# 2) 抽样: 只保留前 N 行 + 摘要
# 3) 压缩: 去重、去掉冗余格式
# 4) 结构化: 用紧凑 JSON 而非自然语言
# 5) 聚合: 返回统计而非明细(sum 而非每行)
# 给模型"足够作答"的信息,而不是"全部信息"
5.2 压缩模板
# 抓取/文档结果的压缩
# 原文 5000 字 → 返回
# 1) 标题 + 摘要(200 字)
# 2) 小标题列表(章节导航)
# 3) 按需展开(模型说"展开第三章"再取)
# 摘要 + 导航比"全文截断"更省且更可用
5.3 对话历史压缩
# 1) 旧工具结果从历史中裁剪(只留结论)
# 2) 关键数字/事实沉淀成"事实表"
# 3) 历史摘要(模型生成,压缩比高)
# 4) 上下文窗口管理(滑动窗口 + 摘要)
# 工具结果的"一生":用的时候贵,用完就要想办法"瘦身"
6. 降级策略
预算紧张或依赖故障时,系统应能「降级」——用更便宜的路径完成任务。降级是有计划的备选,不是无奈。
6.1 模型层降级
# 按任务复杂度选模型
# 1) 简单工具调用/格式化: 便宜快模型
# 2) 复杂推理/长任务: 大模型
# 3) 可路由: 任务特征 → 模型选择
# 4) 失败降级: 大模型超预算/失败 → 换路径
# 模型路由是成本优化的"大开关"
6.2 工具层降级
# 同一需求的多条路径
# 需求"查某产品价格"
# 路径 1: 官方 API(免费/便宜)→ 最优
# 路径 2: 抓取页面(token 贵)→ 次优
# 路径 3: 搜索摘要(信息少但省)→ 兜底
# 模型先走便宜路径,失败再升级——但别反着来
6.3 降级的成本信号
# 触发降级的信号
# 1) 预算剩余不足
# 2) 外部依赖限流/故障
# 3) 任务实际复杂度低于预期(模型可选)
# 4) 用户明确"快速回答即可"
# 降级要可感知: 告诉模型/用户"用了简化路径"
7. 工具定义与调用的瘦身
工具定义(schema)在每次对话都进系统提示。工具越多、定义越长,固定成本越高。
7.1 工具定义精简
# 工具定义的 token 成本
# 1) 描述精简: 一句话讲清"做什么",别写论文
# 2) 参数最小化: 只留必需参数
# 3) 枚举/格式省 token: 用紧凑描述
# 4) 合并相似工具: 5 个抓取工具 → 1 个带参数
# 每个工具的 description 都是"每轮对话的固定税"
7.2 动态工具暴露
# 按需注册/隐藏工具
# 1) 场景化工具集: 不同任务只暴露相关工具
# 2) 热门工具常驻: 低频工具按需加载
# 3) 工具分组: 客户端按任务注入工具清单
# 4) 缩略工具集 + 完整工具集两级
# 暴露的工具越少,模型选择越省 token 且越准
7.3 参数编码
# 1) 用紧凑参数名(q 而非 query_string)
# 2) 默认值不写进调用(服务器补全)
# 3) 可选参数不常传
# 4) 结果用压缩格式(无缩进 JSON)
# 每一处小省,乘以上千次调用就是大钱
8. 成本监控与测量
成本优化没有度量就是「感觉在省」。要建立成本的可观测性。
8.1 成本归因
# 成本归因维度
# 1) 按会话: 一次对话花了多少
# 2) 按工具: 哪个工具贡献最多 token
# 3) 按任务类型: 检索类 vs 代码类
# 4) 按模型: 大小模型的成本分布
# 归因清晰才能"精准优化",而不是"全局抠门"
8.2 监控指标
# 关键指标
# 1) 每次任务的总 token(输入/输出分列)
# 2) 工具结果占输入 token 比例(找"大头")
# 3) 缓存命中率(越高越省)
# 4) 批处理效率(单查 vs 批量)
# 5) 单任务成本($ / 任务)
# 6) 降级触发次数
# 告警: 某工具 token 占比异常升高 → 检查是否泄露整表
8.3 预算控制
# 1) 会话级预算: 超预算停止工具调用/提示用户
# 2) 每日/月配额: 团队级成本封顶
# 3) 预算用尽: 降级到便宜模型或只读模式
# 4) 成本报告: 定期复盘 Top 消耗工具
# 预算是"红灯",监控是"仪表盘",两者都要有
9. 生产实践
9.1 优化优先级
# 按投入产出排优先级
# 1) 结果缓存(最易、收益最大)
# 2) 批处理 + 惰性加载(架构级节省)
# 3) 结果压缩/截断(见效快)
# 4) 工具定义精简 + 动态暴露(固定成本)
# 5) 模型路由/降级(大开关,最后动)
# 先做"不改变行为"的优化,再动模型策略
9.2 优化与效果的平衡
# 每项优化都要评估效果
# 1) 压缩后回答正确率是否下降
# 2) 缓存过期是否导致信息过时
# 3) 降级后用户是否满意
# 4) 用评测集回归(见检索/可靠性专题)
# 优化的底线: 省下来的钱不能以"答错"为代价
9.3 落地清单
# ☐ 估算每工具调用的 token 成本
# ☐ 设置上下文预算分配
# ☐ 只读结果缓存(TTL 分层)
# ☐ 批处理工具 + 摘要先行
# ☐ 结果压缩/截断模板
# ☐ 动态工具暴露
# ☐ 模型路由 + 降级路径
# ☐ 成本监控与告警
10. 常见陷阱
- 只优化大模型价格:忽视工具结果占的输入 token——往往大头在这。
- 缓存不过期:价格/库存类数据缓存太久,答错——按数据新鲜度设 TTL。
- 一次拉全不惰性:模型要什么给全部——摘要先行 + 按需展开。
- 工具定义冗长:每轮对话的固定税——精简描述 + 动态暴露。
- 盲目压缩丢信息:截断后答错——压缩要有摘要兜底。
- 降级无信号:用户不知道用了便宜路径——降级可感知。
- 无监控:不知道钱花哪了——成本归因 + 告警。
- 优化伤效果:省了钱答错率升——评测集回归。
11. 总结
MCP 成本与 Token 优化,是把「每一轮对话的工具开销」变成可控工程的过程:用上下文预算分配让花费有参照,用结果缓存消灭重复查询,用批处理与惰性加载减少往返和冗余,用压缩截断把「够用的信息」交给模型而非「全部信息」,用模型路由与工具降级在预算紧张时走便宜路径,最后用成本归因与监控让每一分钱看得见。优化的本质是给信息定价、按价值取舍、让系统可降级——省不是目的,把同样的预算花出更高的任务完成质量,才是 MCP 成本工程的最终目标。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。