Agent 和普通大模型调用在成本结构上有一个本质区别:它不是调一次,而是循环。规划、推理、调工具、看结果、再循环,每一轮都把上下文重新发一遍。在按 token 计费的模式下,循环深度和上下文长度是相乘关系——一个 10 步的 Agent,带着 4000 token 系统提示与 8000 token 历史,可能把 12000 token 重复发送十次。
这篇教程不讲「换便宜的模型」这种一句话结论,而是给出一套可落地的治理顺序:先量化、再瘦身、后路由、加缓存、上批处理、建看板。每一步都能独立见效,也都能被验证。
Step 1:先量化,否则优化就是猜
优化的前提是能归因。建议在调用封装层统一记录四类字段:模型、提示词模板、业务归属、调用阶段。没有这层标签,你无法回答「钱花在哪个环节」这个问题。
import time, json, logging
def call_llm(model, messages, *, stage, template, tenant, **kw):
t0 = time.time()
resp = client.chat.completions.create(model=model, messages=messages, **kw)
u = resp.usage
logging.info(json.dumps({
"event": "llm_call",
"model": model,
"stage": stage, # 例如 plan / tool / summarize / judge
"template": template, # 提示词模板标识
"tenant": tenant, # 业务归属
"in_tokens": u.prompt_tokens,
"out_tokens": u.completion_tokens,
"latency_ms": int((time.time() - t0) * 1000),
}, ensure_ascii=False))
return resp
stage 聚合后,通常会看到一个反直觉的结果:真正吃掉预算的往往不是规划步骤,而是反复把工具返回的大块 JSON 塞回上下文。Step 2:上下文瘦身——最省钱的一层,且不需要换模型
这一层的收益通常在 10%–15% 起,且不牺牲质量,属于「免费层」。三个动作按优先级排列:
# ① 工具输出:只留下一步真正需要的字段,不要把整块 JSON 回灌
def slim_tool_result(raw: dict, keep=("id", "status", "owner")):
return {k: raw.get(k) for k in keep if k in raw}
# ② 历史:把中段对话压成一条摘要,只保留系统提示 + 最近若干轮
def trim_context(messages, max_recent=6, summary_model="gpt-4o-mini"):
if len(messages) <= max_recent:
return messages
system = [m for m in messages if m["role"] == "system"]
recent = messages[-max_recent:]
middle = messages[len(system):-max_recent]
summary = call_llm(summary_model, [{
"role": "user",
"content": "把以下对话压成给下一步用的要点,保留结论与约束,删掉过程:\n" + str(middle),
}], stage="summarize", template="ctx_trim", tenant="internal")
return system + [{"role": "user",
"content": "前情摘要:" + summary.choices[0].message.content}] + recent
③ 给输出设硬上限。会「自言自语」的 Agent 可能产出比最终答案多 3–10 倍的输出 token,而输出通常比输入贵 4–5 倍。为每个工作流单独设置输出上限,比全局设一个值更合理。
压缩不能压掉约束。摘要时如果丢掉了「必须引用来源」「金额不得四舍五入」这类硬约束,Agent 后面的行为会漂移。建议把约束单独维护成一个不可压缩的段落,始终放在提示词里,而不是混在历史里被摘要掉。
Step 3:模型分级路由——最大的单一杠杆
模型价格带大致分三档,最便宜与最贵之间可能相差百倍。把 60%–80% 的子任务路由到小模型,是收益最集中的一步。
| 档位 | 典型任务 | 判断信号 |
|---|---|---|
| 小 / 快 | 抽取、分类、改写、单步查询 | 有明确正确答案、歧义低 |
| 中 | 摘要、起草、常规问答 | 需要一点组织能力但不需要长链推理 |
| 强 | 多步规划、工具选择、代码生成 | 答错会引发级联失败 |
def route(task_type: str) -> str:
# 规则优先:能用规则判断的,不要交给模型判断
if task_type in ("extract", "classify", "reformat"):
return "gpt-4o-mini"
if task_type in ("summarize", "draft"):
return "gpt-4o"
return "o-series-deep" # 规划类,保持强模型
# 关键判断:这次失败会不会触发「带着大上下文重跑」?
# 会 → 一开始就用强模型,贵得值
# 不会 → 先让小模型试,失败再升级
「便宜模型更贵」是真实存在的。如果一个小模型在复杂步骤上失败,重试意味着把整段长上下文再发一遍,成本可能远超一开始就用强模型。所以路由的判断依据应该是失败的代价,而不是单次调用的标价。
Step 4:提示缓存——结构比参数更重要
提示缓存复用重复前缀背后的键值张量,稳定部分常能享受 50%–90% 的输入折扣。但缓存收益是有条件的,账要算清楚:写入通常按 1.25 倍计价、读取按 0.1 倍计价,因此需要至少两次读取才能回本。
| 做法 | 后果 |
|---|---|
| 把时间戳、请求 ID、会话 token 放在系统提示顶部 | 每次请求哈希都不同,缓存直接失效 |
| 稳定前缀(系统提示 + 工具 schema)放前面,动态内容放最后 | 命中率大幅提升 |
| 中途截断对话历史 | 前缀被打断,已缓存条目被逐出 |
| 上下文只追加、不改写 | 缓存持续命中,多轮会话越往后越省 |
Step 5:结果缓存与语义缓存
Agent 会重复调用完全相同的工具:读同一个数据库 schema、抓同一份文档、查同一份配置。把这些结果缓存下来,命中时零边际成本。
import hashlib, time
_cache = {}
def cached_call(fn, *args, ttl=300, **kw):
key = hashlib.sha256((fn.__name__ + repr(args) + repr(kw)).encode()).hexdigest()
now = time.time()
hit = _cache.get(key)
if hit and now - hit["ts"] < ttl:
return hit["value"]
value = fn(*args, **kw)
_cache[key] = {"value": value, "ts": now}
return value
再往上一层是语义缓存:把高度相似的用户提问直接返回历史结果,不转发给模型。对于客服、知识问答这类重复度高的场景,通常能吸收 15%–30% 的流量。代价是缓存失效逻辑更复杂,需要有明确的过期与版本策略。
语义缓存的误命中是质量事故。「如何退款」和「如何取消退款」向量距离可能很近,但答案完全相反。建议对高风险意图(金额、权限、法律、医疗)直接禁用语义缓存,只对低风险问答开启。
Step 6:批处理换成本
批处理接口对可容忍延迟的请求通常提供约 50% 的折扣。适合批处理的场景很明确:夜间报表生成、批量文档分类、历史数据富化、批量内容生成、非紧急文档分析。
| 适合批处理 | 不适合批处理 |
|---|---|
| 后台数据富化与归类 | 面向用户的实时对话 |
| 定时报表与摘要 | 实时决策(风控、路由) |
| 批量内容生成 | 交互式编码辅助 |
Step 7:把成本变成一线指标
成本应该和延迟、错误率放在同一块看板上,而不是月底才在财务报表里看到。核心指标只有一个:cost per successful task(每个成功任务的花费),并按模型与任务类型拆分。
这个指标能同时暴露三件事:某个模型在悄悄重试、某个工具返回了臃肿输出、某个子任务本该路由到更便宜的档位。当单任务成本突然抬升,那通常是一个 bug,而不是一张账单。
# 建议固定跟踪的三个信号
# 1) 缓存命中率:稳定提示词场景低于 60% 说明结构有问题
# 2) 每类任务的重试次数:每次重试都是整段上下文的重新发送
# 3) 每成功任务成本:按模型 × 任务类型拆分,观察趋势而非绝对值
常见问题 FAQ
Q1:优化应该按什么顺序做?
按本教程的顺序:先打成本标签(否则无法归因)→ 上下文瘦身(不需要换模型、不牺牲质量)→ 模型路由(收益最大)→ 提示缓存(结构改造)→ 语义缓存与批处理(按场景)→ 建看板持续跟踪。把成本优化当成持续实践而不是一次性项目,效果差别很大。
Q2:模型路由会不会让输出质量变差?
会有边界情况的风险。建议对路由过的链路保留「失败可升级」机制:小模型结果过不了校验就自动升到强模型,并把升级次数记进看板。升级率高说明路由规则需要调整,而不是说明路由方向错了。
Q3:缓存会不会让回答变旧?
会,所以必须有 TTL 与版本策略。提示缓存针对的是「不变的提示词前缀」,不会让答案变旧;语义缓存针对的是「相似提问」,必须按业务设定过期时间,并对高风险意图禁用。
Q4:自托管开源模型能省多少?
高并发、可预测负载下,自托管的边际成本优势明显,但要把 GPU 折旧、运维与弹性损失算进去。低流量场景通常不如按量付费。目前官方及行业暂未披露更多细节,后续将持续跟进迭代动态。