14.2 成本优化:缓存、路由、小模型降级 + 评测兜底
14.1 量的是时间,本节量的是钱。Agent 的成本结构有一个和延迟一样反直觉的性质:它几乎全在模型调用上。检索、工具、序列化加起来的成本可以忽略不计,而每一次模型调用都在按 token 计费。更麻烦的是,成本随使用量线性放大——延迟可以靠加机器缓解,账单不行。所以成本优化不是「省钱的小技巧」,而是决定产品能不能规模化的硬约束。
本节把 ResearchCopilot 推进到「按需花钱」:给出缓存、路由、上下文瘦身三招成本优化手段,为路由写可测试的判定规则,真跑对比「全用强模型」与「按路由分流」的总成本与得分,并立下一条纪律——任何降本改动都必须过第 12 章的评测兜底。
14.2.1 成本三招:一张总览
降本手段很多,但真正有效且低风险的只有三招,按「收益 / 风险」排序:
| 招 | 做法 | 降本原理 | 风险 | 前置依赖 |
|---|---|---|---|---|
| 缓存 | 命中即返回,不调模型 | 命中即零模型成本 | 缓存过期/不一致 | 第 6 章的结果与语义缓存 |
| 路由 | 简单问题走小模型 | 降低单次单价 | 误判 → 质量掉 | 可判定的难度信号 |
| 瘦身 | 裁上下文、减工具 schema | 降低每轮 input token | 丢信息 | 第 6.2 章裁剪与摘要 |
三招的性质完全不同:缓存是「省掉整次调用」,收益最直接、风险最低(答案本来就一样);路由是「换更便宜的模型做同一件事」,收益大但要小心误判;瘦身是「让每次调用更便宜」,收益随上下文长度放大,但可能丢信息。所以优先级就是表里的顺序:先上缓存,再上路由,最后做瘦身。
14.2.2 缓存:命中即零模型成本
第 6 章已经建好了两层缓存:6.1 的结果缓存(把「问题 → 最终报告」整体缓存,命中即零模型成本)和 6.2 的语义缓存(把「语义相近但不完全相同」的问题也归到同一个答案)。从成本视角看,它们是最划算的一招——一次命中就省掉整条 Agent 循环的所有模型调用,而不是省一点单价。
命中率决定了收益上限。假设一个工作负载里 40% 是重复或近重复的问题,缓存能直接把这 40% 的成本抹成零。关键在于键的设计:结果缓存用「规范化后的问题 + 租户」做键,语义缓存用「嵌入向量的近邻阈值」做键。阈值定得松,命中率高但可能返回不匹配的答案;定得紧,安全但省得少——这条阈值就是缓存这一招唯一的调参点,必须靠第 12 章的评测来定,不能拍脑袋。
命中率对成本的影响是线性的,跑一组敏感性就能看清:
for hr in (0.0, 0.2, 0.4, 0.6):
n_hit = round(hr * 100)
cost = sum((STRONG_COST if route(r) == "strong" else SMALL_COST)
for r in reqs[n_hit:]) # 前 n_hit 条视为命中,成本为 0
print(f"{hr*100:>9.0f}%{cost:>14.3f}{cost/1.200:>9.2f}x")
真跑输出:
缓存命中率 总成本(USD) 相对成本
0% 0.738 0.61x
20% 0.666 0.56x
40% 0.615 0.51x
60% 0.459 0.38x
命中率每涨 20 个点,成本就往下掉一截,且与路由的收益是叠加的(0% 那行的 0.61x 已经是路由后的结果)。这条曲线的现实意义是:缓存收益几乎只取决于你的流量重复率。聊天场景重复率可能只有 10%,检索问答场景能到 50% 以上——所以同一个缓存策略在两个产品里的回报能差好几倍,必须按自己的真实流量测,不能照搬别人的命中率。
缓存有一个容易被忽略的成本前提:它不改变单次请求的质量,只改变「调不调模型」。所以只要缓存键选得对,命中的答案和重新生成的一模一样——这是它风险最低的原因。反过来说,缓存不该用来存「模型可能给出更好答案」的开放问题,只适合「同一问题答案稳定」的场景。
14.2.3 路由:判定规则与误判代价
路由的思路很直白:简单问题用小模型,难问题用强模型。难点全在「怎么判定难度」。一个可测试的路由器是一组规则的组合:
def route_signal(query: str, history_len: int) -> float:
"""把问题映射成一个「难度信号」,值越大越可能复杂。"""
s = 0.0
if len(query) > 120: # 长问句往往带多个约束
s += 0.3
if any(w in query for w in ("对比", "取舍", "为什么", "推导")):
s += 0.3 # 比较/论证类
if history_len > 6: # 已经多轮,上下文复杂
s += 0.2
if query.count("和") + query.count("以及") >= 2: # 多主体
s += 0.2
return min(s, 1.0)
规则的设计原则是:只用能从问题文本直接算出来的特征(长度、关键词、历史轮数),不用「需要先跑一遍才知道」的信息——否则路由本身就成了另一次模型调用,省下的钱又花回去了。
它是纯函数,所以可以直接喂几个样例看它的判断:
samples = [
("现在几点?", 0),
("帮我总结一下这篇文章的要点", 0),
("对比方案 A 和方案 B 的取舍,并推导出为什么 B 在延迟上更优", 8),
("什么是 Redis?", 0),
("解释一下缓存穿透和缓存雪崩以及它们的区别", 2),
]
真跑输出:
信号 路由 问题
0.0 小 现在几点?
0.0 小 帮我总结一下这篇文章的要点
0.5 强 对比方案 A 和方案 B 的取舍,并推导出为什么 B 在延迟上更优
0.0 小 什么是 Redis?
0.2 小 解释一下缓存穿透和缓存雪崩以及它们的区别
前四条符合直觉:寒暄、单点总结、概念问答都归了小模型;那条长问句因为同时命中「长度」「对比/推导关键词」「多轮历史」,信号累到 0.5,被送去强模型。最后一条暴露了规则的局限——「解释缓存穿透和雪崩以及它们的区别」其实是个需要组织多条信息的复杂问题,但它长度不够、也没命中关键词,信号只有 0.2,被误判为简单。
这条误判正说明关键词路由的天花板:它便宜、可解释、零额外成本,但覆盖不了「短而难」的问题。真实系统里的进阶做法是用一个小分类器(哪怕只是逻辑回归)替代关键词规则,代价是每次请求多几毫秒的本地推理——用一点点确定性成本,换更准的路由。但无论用哪种路由器,只要它会误判,就必须配 14.2.5 的评测门。
route_signal 返回 0~1 的信号,阈值之上走强模型、之下走小模型。阈值的选取就是收益与风险的旋钮,而误判的代价是不对称的:
| 误判方向 | 代价 | 量级 |
|---|---|---|
| 简单题被误判为复杂 | 多花钱(用强模型做了小事) | 每次多 $0.0105 |
| 复杂题被误判为简单 | 质量下降(小模型答不好) | 单题得分掉 0.38 |
两种误判的代价差一个数量级。简单题误判只是浪费一点钱;复杂题误判是用户拿到一个错答案——这在研究助理场景里是不可接受的。所以阈值要偏保守:宁可把一些简单题也送去强模型,也不要把复杂题漏给小模型。这条「误判不对称」的原则,比任何具体阈值都重要。
14.2.4 上下文瘦身:最隐蔽的一招
第三招是让每次调用本身更便宜。它有三个抓手,都来自前面几章:
- 裁上下文:用 6.2 的
compact把早期轮次压成摘要,减少每轮重传的input_tokens。 - 减工具 schema:工具越多,每次请求带的 schema 越长。一轮里用不到的工具 schema 应该按需挂载,而不是全量常驻。
- 减轮数:更好的提示词让 Agent 少绕弯,直接减少模型调用次数。
瘦身和延迟是同一个动作的两面——14.1 里上下文越长首 token 越慢,这里上下文越长越贵。所以 14.1 的占比表同时是成本优化的靶点表:模型那一栏占比高,往往意味着上下文有得瘦。
但瘦身是风险最高的一招,因为它真的会改变送进模型的信息。裁剪可能丢掉关键结论,减 schema 可能让模型选不到该用的工具,减轮数可能让 Agent 提前收敛到错答案。所以瘦身必须配合评测——这正是下一小节的纪律。
瘦身和缓存的收益曲线也不同:缓存是一次性砍掉整次调用(阶跃式),瘦身是让每次调用都便宜一点(线性、随上下文放大)。所以上下文越长的场景,瘦身越值;而短问答场景里,瘦身可能只省下几个 token,反而增加实现复杂度。判断要不要做瘦身,先看 14.1 的占比表里 input_tokens 的绝对量级——量级小就别折腾。
14.2.5 纪律:任何降本改动都必须过评测兜底
三招里有两招会改变质量(路由、瘦身),缓存那招也可能因为阈值松而返回不匹配的答案。于是本节的核心纪律只有一句:任何降本改动,都必须先过第 12 章的评测集,确认得分没有下跌,才能上线。
def gate(before_score: float, after_score: float, tol: float = 0.02) -> bool:
"""质量门:降本后的得分跌幅超过 tol 就拦下。"""
return after_score >= before_score - tol
gate 就是 12.3 质量门的降本版本:改动前跑一遍评测拿 before_score,改动后再跑一遍拿 after_score,跌幅超过容差 tol 就拦截上线。没有这道门,降本就是在偷偷降质——你省下的每一分钱,都可能是用一个用户没发现的错误答案换来的。
这条纪律把「成本优化」从一个纯工程动作,变成了一个受约束的优化问题:在「质量不跌」的约束下,把成本压到最低。这也解释了为什么本节要真跑一组「成本 + 得分」的对照——只报成本不报得分,等于只做了一半的验证。
14.2.6 真跑:全强模型 vs 路由分流
造 100 条模拟请求(50 简单 + 50 复杂),强模型 $0.012/次、小模型 $0.0015/次。简单题两者都能答好(0.96 vs 0.95),复杂题小模型明显掉分(0.93 vs 0.55)。路由用「难度信号 + 阈值 0.5」,信号带噪声以模拟真实误判:
STRONG_COST, SMALL_COST = 0.012, 0.0015 # 示例单价,非真实报价
def score(model: str, difficulty: str) -> float:
if model == "strong":
return 0.96 if difficulty == "simple" else 0.93
return 0.95 if difficulty == "simple" else 0.55 # 小模型在难题上掉分
THRESHOLD = 0.5
def route(r) -> str:
return "small" if r.signal < THRESHOLD else "strong"
真跑输出(Python 3.14.6):
100 条请求(50 简单 + 50 复杂),强模型 $0.012/次,小模型 $0.0015/次
策略 总成本(USD) 平均得分 相对成本
全用强模型 1.200 0.945 1.00x
按路由分流 0.738 0.933 0.61x
路由 + 缓存命中 0.629 0.933 0.52x
缓存命中 38 条 / 100;路由误判 10 条
复杂题走小模型:单题 0.55(强模型 0.93),差 0.38
简单题走强模型:多花 $0.0105/次
读这张表:
- 单上路由:成本从 1.200 降到 0.738,降到 0.61 倍,平均得分只从 0.945 掉到 0.933——掉了 0.012,在容差 0.02 之内,可以上线。这正是「受约束优化」的落地:用 1.3% 的得分换 39% 的成本。
- 路由 + 缓存:成本进一步降到 0.629(0.52 倍),得分不变(缓存不改答案)。缓存是叠加在路由之上的,两者收益相乘。
- 误判 10 条:这就是路由的代价。10 条里只要有一条是「复杂题被分给小模型」,得分就会被拉低 0.38——所以真实系统里,这 10 条误判的方向分布比数量更重要。
把误判按方向拆开看:
danger = sum(1 for r in reqs if r.difficulty == "complex" and route(r) == "small")
waste = sum(1 for r in reqs if r.difficulty == "simple" and route(r) == "strong")
真跑输出:
误判 10 条:复杂→小模型(危险)2 条,简单→强模型(浪费)8 条
危险误判致平均得分损失约 0.0076;浪费误判多花 $0.084
这个拆分才是路由的真实画像:10 条误判里,8 条是无害的浪费、只有 2 条是危险的漏判。危险的 2 条只把平均得分拉低 0.0076——因为它们是 50 道复杂题里的 2 道,被平均摊薄了。这正是「误判不对称」的量化:浪费 8 条只多花 8 分钱,危险 2 条只掉不到 0.01 分。如果阈值再松一点、危险误判涨到 10 条,得分就会掉 0.038,直接突破容差被质量门拦下——阈值该定多紧,就是靠这条曲线算出来的。
注意这张表里「全用强模型」的得分 0.945 并不是满分——因为强模型在简单题上也是 0.96,本身就有一点点错。这说明评测基准要选对:如果基准定成「理论最优 1.0」,路由永远过不了门;基准应该是「当前线上策略」,比的是相对变化。这一点 12.3 讲质量门时已经强调过。
14.2.7 成本优化的优先级清单
拿到三招和一组真实数字,落地顺序是确定的:
| 顺序 | 动作 | 预期收益 | 风险 | 验证方式 |
|---|---|---|---|---|
| 1 | 上结果缓存 + 语义缓存 | 命中率 × 全部成本 | 低(答案不变) | 命中率 + 抽样比对 |
| 2 | 上路由(阈值保守) | ~40% 成本 | 中(误判掉分) | 评测集得分不跌 |
| 3 | 上下文瘦身 | 随上下文线性 | 高(丢信息) | 评测集得分不跌 |
| 4 | 换更小的模型档位 | 取决于档差 | 高(全面掉分) | 评测集得分不跌 |
顺序的依据是**「收益 / 风险」比**:缓存收益最高、风险最低,永远先做;路由次之;瘦身和换档位风险最高,放最后,且每做一步都要重新过评测。真实项目里,光前两步就能砍掉一半成本,而质量几乎不动——这正是「先把低垂的果实摘掉」的工程含义。
这张清单还有一个隐含前提:每一项都要能单独回滚。缓存可以一键清空、路由阈值可以调回全强模型、瘦身可以退回完整上下文——如果某个降本改动和别的改动耦合在一起,出了质量问题就没法快速定位是哪一个的锅。所以落地时一次只上一个,跑一段观察评测与线上指标,稳了再上下一个。降本要像加索引一样灰度,而不是一次性把三招全压上去。
还有一个常被忽略的动作:给单次 run 设成本上限。像 14.1 给延迟设超时一样,给成本设硬上限——一次 run 累计花超阈值就中断并返回部分结果。它能防住「某个用户用一段超长上下文把 Agent 拖进 20 轮循环」这类单点超支,是成本治理的兜底,不是优化。
14.2.8 验收与真实环境
本节把成本优化从「凭感觉砍」变成了「受约束的优化」:
- 三招:缓存(零模型成本)、路由(降单价)、瘦身(降 token),按收益/风险排序。
- 误判不对称:简单题误判多花 $0.0105,复杂题误判掉分 0.38,阈值必须偏保守。
- 真跑:路由把成本降到 0.61 倍,得分只掉 0.012;叠加缓存到 0.52 倍。
- 纪律:任何降本改动都要过第 12 章的评测门,跌幅超容差即拦截。
验收清单:
- 缓存键设计合理,命中答案与重新生成的一致(有抽样比对)
- 路由规则只用「文本可直接算出」的特征,不引入额外模型调用
- 路由阈值偏保守,复杂题优先送强模型
- 每一项降本改动都有「改动前 / 改动后」的评测得分对照
真实环境 / 真实模型下会怎样:本节的请求、单价、得分都是模拟值,真实世界有几个差异会改写结论。第一,真实单价差比示例更极端——强模型与最小档位之间往往差几十倍,所以路由的收益上限比 0.61 倍更诱人。第二,真实的难度信号弱得多——本节假设信号与真实难度相关(gauss 分布),但真实问题里「长短」和「难度」经常不相关,一个短问题也可能极难,所以真实路由的误判率更高,阈值要更保守,甚至要用一个小分类器替代关键词规则。第三,缓存命中率取决于工作负载——聊天场景重复率低、检索场景重复率高,同一个缓存策略在两个产品里的收益能差好几倍,所以缓存收益必须按你自己的真实流量测,不能照搬别人的命中率。最后一点:成本优化的收益会随时间衰减——模型供应商会降价,你自己的流量结构会变,所以这套三招 + 评测门的框架要定期重跑,而不是上线一次就一劳永逸。
阅读导航:上一节:14.1 延迟剖析:模型/检索/工具各占多少 · 下一节:14.3 吞吐与并发压测:找容量上限与拐点 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。