缓存与成本优化:让 Agent 的每一次调用都花在刀刃上
作者:yumking | 2026 年 9 月 23 日 | 技术标签:语义缓存 / 前缀缓存 / 模型路由 / 成本治理 / 差异化定价
摘要
第 23 篇的可观测性把成本归因做到了 100%——每个 span 记 cost_usd,按用户/工具/模型聚合,钱花在哪清清楚楚。但"知道花在哪"不等于"花得值":同一个问题反复问 70B、公共 system prompt 每次重算 4K token、简单问题也请最贵的模型——归因照亮了浪费,本文消灭浪费。推理加速主线第五篇,从"加速"走到"省钱":语义缓存(相似问题命中已有答案,客服场景命中率 32%)、Prompt 前缀缓存(复用公共 KV,prefill 延迟 -60%)、模型路由(简单问题走小模型,成本 -61%)、差异化定价与预算控制(Top 5% 用户占 47% 成本,预算降级控住长尾)。四层叠加实测总成本降 73%,且与第 18-21 篇的算法/系统/算子/精度四层正交可叠加——五层合起来,同一个 70B 模型的单次服务成本能压到原来的 1/8。
一、第 23 篇归因之后:知道花在哪,怎么少花
1.1 成本归因 vs 成本优化
第 23 篇解决的是"看见":
第 23 篇成本归因:
span 记 cost_usd → 按用户/工具/模型聚合 → 仪表盘
→ 发现:Top 5% 用户占 47% 成本
→ 发现:70B 模型承担了 82% 的调用,但其中 68% 是简单问题
→ 发现:同一个问题被问了 340 次,每次都从头算
归因把浪费暴露了,但浪费还在。本文解决的是"消灭":
| 第 23 篇(归因) | 本文(优化) |
|---|---|
| 每次调用记 cost | 让该省的调用不发生 |
| 知道哪贵 | 让贵的调用变便宜 |
| 事后看账单 | 事前/事中控预算 |
1.2 三大成本杠杆
杠杆一:缓存 —— 让重复的调用不发生
语义缓存:相似问题命中已有答案
前缀缓存:公共前缀的 KV 不重算
杠杆二:路由 —— 让便宜的模型干能干的活
难度路由:简单问题走 8B,复杂的才走 70B
杠杆三:定价 —— 让无节制的调用有预算约束
差异化定价:按用户/任务设 token 预算
超预算降级:大模型→小模型→拒绝
三个杠杆从不同角度压成本:缓存减少调用次数、路由降低单次成本、定价约束总用量。
1.3 与第 19/21/23 篇的关系
第 23 篇(可观测性)
→ 提供成本数据,本文用数据驱动优化决策
第 19 篇(KV Cache/连续批处理)
→ 引擎层缓存(PagedAttention),本文补应用层缓存(语义)
第 21 篇(量化)
→ 省显存成本(模型变小),本文省 token 成本(调用变少/变便宜)
→ 两者正交:量化让单次调用便宜,缓存让调用不发生
第 17 篇(选型方法论)
→ "从便宜到贵",本文的模型路由是这条原则的运行时落地
推理加速前四篇(18-21)是"让单次推理更快更省显存",本文是"让推理次数更少、每次更便宜"——从单点优化到系统级成本治理。
二、语义缓存:相似问题命中已有答案
2.1 为什么 LLM 调用可以缓存
传统观点认为 LLM 输出不可缓存——模型有温度、输出非确定。但实际场景里大量请求是语义重复的:
客服场景统计(某电商平台,日 5 万次查询):
"怎么退款" × 1200 次
"如何申请退货" × 860 次 ← 和上面语义相同
"退款流程是什么" × 540 次 ← 还是同一个意思
"我要退钱" × 420 次 ← 依然
→ 一个意图问了 3020 次,每次都调 70B 从头生成
→ 如果命中缓存,3020 次调用变 1 次
语义缓存的核心:不缓存精确字符串,缓存语义意图——用嵌入向量判断"这个问题之前答过没"。
2.2 语义相似度匹配
# semantic_cache.py —— 语义缓存
import numpy as np
from dataclasses import dataclass
@dataclass
class CacheEntry:
query_embed: np.ndarray # 问题嵌入
answer: str # 已生成的答案
prompt_version: str # prompt 版本(失效依据)
model_version: str # 模型版本
created_at: float
hit_count: int = 0
class SemanticCache:
"""嵌入相似度匹配 + 版本感知失效"""
def __init__(self, embed_fn, threshold: float = 0.92,
max_entries: int = 10000):
self._embed = embed_fn # 嵌入函数
self._threshold = threshold # 相似度命中阈值
self._entries: list[CacheEntry] = []
self._max = max_entries
def get(self, query: str, prompt_version: str,
model_version: str) -> str | None:
q_embed = self._embed(query)
best, best_sim = None, 0
for entry in self._entries:
# 版本不匹配直接跳过——prompt/模型变了,旧答案失效
if (entry.prompt_version != prompt_version or
entry.model_version != model_version):
continue
sim = self._cosine(q_embed, entry.query_embed)
if sim > best_sim:
best, best_sim = entry, sim
if best_sim >= self._threshold:
best.hit_count += 1
return best.answer
return None
def put(self, query: str, answer: str,
prompt_version: str, model_version: str) -> None:
self._entries.append(CacheEntry(
query_embed=self._embed(query),
answer=answer,
prompt_version=prompt_version,
model_version=model_version,
created_at=__import__("time").time(),
))
if len(self._entries) > self._max:
self._evict() # 淘汰低频旧条目
def _evict(self) -> None:
# 淘汰:hit_count 最低 + 最老的先走
self._entries.sort(key=lambda e: (e.hit_count, -e.created_at))
self._entries = self._entries[self._max // 10:] # 砍 10%
@staticmethod
def _cosine(a: np.ndarray, b: np.ndarray) -> float:
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
三个设计要点:
① 阈值 0.92:太高命中率低,太低误命中("退款"命中"退货"是对的,
但"退款"命中"充值"就错了)。按业务调,客服 0.92、代码问答 0.95
② 版本感知失效:prompt 改了/模型换了,旧缓存全部不命中——防过期答案
③ 淘汰策略:hit_count 低的先淘汰,高频问题常驻缓存
2.3 缓存失效的三种触发
| 失效原因 | 检测方式 | 对接 |
|---|---|---|
| Prompt 版本变更 | prompt_version 不匹配 | 第 22 篇 prompt 版本管理 |
| 模型版本变更 | model_version 不匹配 | 第 10 篇灰度发布 |
| 知识库更新 | 依赖文档的缓存条目整体失效 | 第 4 篇 RAG 索引版本 |
关键:缓存必须带版本标签,否则知识库更新后还返回旧答案——这是语义缓存最危险的坑。
三、Prompt 前缀缓存:复用公共 KV
3.1 第 19 篇 PagedAttention 的延伸
第 19 篇的 PagedAttention 把 KV Cache 分页管理,消除了显存碎片。但每次新请求还是要从头算 prefill——如果多个请求共享公共前缀,这部分 KV 重复计算了。
请求 A:[system prompt 4K] + [用户问题 A 200 token]
请求 B:[system prompt 4K] + [用户问题 B 180 token]
请求 C:[system prompt 4K] + [用户问题 C 350 token]
无前缀缓存:每个请求都算 4K system prompt 的 prefill → 3 × 4K = 12K 重复计算
有前缀缓存:system prompt 的 KV 算一次,三个请求复用 → 4K + 200 + 180 + 350
3.2 哪些场景有公共前缀
| 场景 | 公共前缀 | 占比 |
|---|---|---|
| 同一 Agent 的所有请求 | system prompt(角色/规则/工具定义) | 60-80% |
| Few-shot 学习 | system + 示例们 | 70-90% |
| 多轮对话 | 历史对话前 N 轮 | 随轮数增长 |
| RAG | system + 检索模板 | 50-70% |
Agent 场景天然有长公共前缀——system prompt 动辄 2-4K token,工具定义又占一大块。前缀缓存对这些场景收益极大。
3.3 vLLM/SGLang 的 prefix caching 实践
# prefix_cache_config.py —— 前缀缓存配置(vLLM)
# vLLM 启用 prefix caching 只需一个参数
VLLM_LAUNCH = """
python -m vllm.entrypoints.openai.api_server \\
--model meta-llama/Llama-3-70B-Instruct \\
--enable-prefix-caching \\
--max-num-seqs 64 \\
--gpu-memory-utilization 0.9
"""
# 应用层:把公共前缀显式标记,提高命中率
class PrefixCacheOptimizer:
"""让请求结构最大化前缀复用"""
def build_request(self, system_prompt: str,
history: list, query: str) -> str:
# ① system prompt 放最前——所有请求共享
# ② 历史按时间序——同一会话的请求前缀递增复用
# ③ 用户问题放最后——变化的部分不破坏前缀匹配
parts = [system_prompt]
parts.extend(msg["content"] for msg in history)
parts.append(query)
return "\n".join(parts)
# vLLM 自动按 token 序列做前缀哈希匹配,
# 公共前缀的 KV 直接复用,不重算 prefill
两个工程要点:
① 请求结构要"前缀稳定":不变的部分放前面,变化的放后面
→ 反例:把用户 query 放最前,每次前缀都不同,缓存全失效
② --enable-prefix-caching 零代码改动,vLLM/SGLang 都支持
→ 但要监控 prefix hit rate,确认请求结构确实有公共前缀
实测:4K system prompt 场景,prefix caching 开启后 prefill 延迟从 180ms 降到 72ms(命中后只算增量部分),prefix hit rate 87%。
四、模型路由:按难度选模型,便宜优先
4.1 不是所有问题都需要 70B
第 23 篇的成本归因暴露了一个普遍浪费:
某平台调用分布(归因数据):
70B 模型承担 82% 调用
但难度评估显示:
68% 是简单问题(闲聊、FAQ、格式转换)→ 8B 就够
22% 是中等问题(摘要、单跳检索问答) → 32B 够
10% 是复杂问题(多跳推理、代码生成) → 才需要 70B
→ 68% 的调用用 70B 答简单问题,成本浪费 5-8 倍
模型路由的思路:先判难度,再选模型——能 8B 解决的不上 32B,能 32B 解决的不上 70B。
4.2 难度路由
# model_router.py —— 模型路由:难度评估 + 路由
from dataclasses import dataclass
@dataclass
class ModelChoice:
model: str
est_cost: float # 每千 token 成本(美元)
est_quality: float # 该难度下的质量估计
MODEL_TIER = {
"small": ModelChoice("llama-3-8b", 0.0002, 0.82),
"medium": ModelChoice("llama-3-32b", 0.0008, 0.91),
"large": ModelChoice("llama-3-70b", 0.0020, 0.96),
}
class ModelRouter:
"""按问题难度路由到最便宜的够用模型"""
DIFFICULTY_PROMPT = """评估这个问题的难度,只回 1/2/3:
1=简单(闲聊/FAQ/格式转换/单步查询)
2=中等(摘要/单跳问答/简单代码)
3=复杂(多跳推理/代码生成/规划)
问题:{query}"""
def __init__(self, llm_judge):
self._judge = llm_judge # 用 8B 当裁判,便宜
def route(self, query: str, history_len: int) -> ModelChoice:
difficulty = self._assess_difficulty(query, history_len)
if difficulty == 1:
return MODEL_TIER["small"]
elif difficulty == 2:
return MODEL_TIER["medium"]
else:
return MODEL_TIER["large"]
def _assess_difficulty(self, query: str,
history_len: int) -> int:
# 用最便宜的 8B 判难度
resp = self._judge.complete(
self.DIFFICULTY_PROMPT.format(query=query))
difficulty = int(resp.strip()[0] if resp.strip() else "2")
# 长对话自动升级:历史 >20 轮至少走 medium
if history_len > 20 and difficulty < 2:
difficulty = 2
return difficulty
路由的代价是要多一次"难度评估"调用——但用 8B 当裁判,成本只有 70B 的 1/100,且可以缓存(同一 query 的难度不变)。
4.3 与第 17 篇选型的对接
第 17 篇说"从便宜到贵"选手段,模型路由是这条原则的运行时落地:
第 17 篇(静态选型): 开发时决定"这个任务用 RAG 还是微调"
本文模型路由(动态): 运行时决定"这个问题用 8B 还是 70B"
两者互补:第 17 篇选"手段",本文选"模型档位"——手段定了之后,同一手段内还能按难度选不同大小的模型。
五、差异化定价与预算控制
5.1 按用户/任务设 token 预算
第 23 篇归因发现"Top 5% 用户占 47% 成本"——长尾用户用最贵的模型跑最多调用。无节制的调用需要有预算约束:
# budget_guard.py —— 预算控制与降级
import time
from dataclasses import dataclass
@dataclass
class UserBudget:
user_id: str
daily_token_limit: int
used_today: int = 0
tier: str = "standard" # free/standard/premium
TIER_LIMITS = {
"free": 50_000, # 日 5 万 token
"standard": 500_000, # 日 50 万
"premium": 2_000_000, # 日 200 万
}
class BudgetGuard:
"""按用户预算控制调用,超预算降级或拒绝"""
def __init__(self, cost_store, router):
self._cost = cost_store # 接第 23 篇成本归因数据
self._router = router
def check_and_route(self, user_id: str, query: str,
history_len: int) -> dict:
budget = self._get_budget(user_id)
# ① 超预算 → 降级到最便宜模型
if budget.used_today > budget.daily_token_limit * 0.9:
if budget.tier == "free":
return {"action": "reject",
"reason": "daily_limit_exceeded"}
# 付费用户超 90% 预算 → 降级到 small 模型
return {"action": "degrade",
"model": "llama-3-8b",
"reason": "approaching_limit"}
# ② 预算内 → 正常路由
model = self._router.route(query, history_len)
return {"action": "allow", "model": model.model}
def record_usage(self, user_id: str, tokens: int,
cost_usd: float) -> None:
# 记入预算消耗 + 第 23 篇成本归因
self._cost.add(user_id, tokens, cost_usd)
budget = self._get_budget(user_id)
budget.used_today += tokens
def _get_budget(self, user_id: str) -> UserBudget:
# 从第 23 篇归因数据拉今日用量,配上限
used = self._cost.daily_total(user_id)
tier = self._cost.get_tier(user_id)
return UserBudget(
user_id=user_id,
daily_token_limit=TIER_LIMITS[tier],
used_today=used, tier=tier)
三级响应:
预算 < 90%:正常路由(该 70B 就 70B)
预算 90-100%:降级到小模型(保服务但降质量)
预算 > 100%:free 用户拒绝 / 付费用户继续降级
5.2 与第 23 篇成本归因的闭环
第 23 篇归因: 记录每次调用的 cost_usd → 聚合到用户/任务
本文预算控制: 读归因数据 → 判断是否超预算 → 决定放行/降级/拒绝
→ 调用结果再记回归因 → 闭环
归因是"仪表盘",预算控制是"油门"——两者共享同一套成本数据,归因驱动预算决策,预算执行结果又回流到归因。
六、实测数据
在某 Agent 平台(日 5 万次调用,70B 主模型)灰度 30 天,四层成本优化逐层开启:
| 优化层 | 命中/路由率 | 单层成本降幅 | 累计成本 |
|---|---|---|---|
| 基线(无优化) | — | — | $2,400/日 |
| +语义缓存 | 32% 命中 | -28% | $1,728 |
| +前缀缓存 | 87% prefix hit | -19%(prefill 减少) | $1,400 |
| +模型路由 | 68% 走 8B / 22% 走 32B / 10% 走 70B | -52%(路由后单次成本) | $672 |
| +预算控制 | Top 5% 用户降级 | -12%(长尾约束) | $592 |
最终累计:$2,400 → $592,降 75%。
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 日均调用成本 | $2,400 | $592(-75%) |
| 语义缓存命中率 | 0 | 32%(客服场景重复问题多) |
| 前缀缓存 prefix hit rate | 0 | 87% |
| 70B 模型调用占比 | 82% | 10%(路由后只复杂问题上 70B) |
| 平均 P50 延迟 | 1.2s | 0.6s(缓存命中 + 小模型更快) |
| 缓存误命中率 | — | 0.3%(阈值 0.92 下可接受) |
| 预算超限用户降级率 | 0(无控制) | 8%(超 90% 预算的降级) |
两个值得说的点:
① 成本降 75% 不是牺牲质量换的——路由时 68% 的简单问题用 8B
答得和 70B 一样好(难度评估保证),只有真正复杂的才上 70B。
质量损失 < 1pp,成本省 75%,ROI 极高。
② 延迟反而降了:缓存命中直接返回(0ms),小模型更快(8B 比 70B 快 5 倍)。
成本优化和延迟优化在这里同向——省钱的路径恰好也更快。
6.1 与推理加速前四篇的叠加关系
第 18 篇 投机解码:单次 decode ×2.1 ─┐
第 19 篇 连续批处理:吞吐 ×20 ─┤
第 20 篇 算子融合:单次计算 ×1.3 ─┤ 五层正交可叠加
第 21 篇 量化:显存 -75%、decode ×1.8 ─┤
本文 缓存+路由+定价:调用成本 -75% ─┘
叠加效果:单次推理又快又便宜 + 调用次数又少 + 每次路由到最便宜模型
→ 同一个 70B 服务的单位成本压到原来的 ~1/8
七、实施难度评估与落地建议
7.1 难度评估
| 模块 | 难度 | 说明 |
|---|---|---|
| 语义缓存 | ★★★☆☆ | 嵌入匹配简单,难点在阈值调优 + 版本失效 |
| 前缀缓存 | ★☆☆☆☆ | vLLM 一个参数,零代码,但要监控 hit rate |
| 模型路由 | ★★★☆☆ | 难度评估裁判要准,误判会把复杂问题路由到 8B |
| 预算控制 | ★★☆☆☆ | 接第 23 篇归因数据,逻辑清晰 |
| 整体调优 | ★★★★☆ | 四层叠加后各层阈值互相影响,需联合调 |
7.2 四步落地路线
第一步(1 天,零代码最大收益):
开启 vLLM --enable-prefix-caching
→ 前缀缓存零成本,prefill 延迟立降 60%,先吃这个果子。
第二步(1-2 周):
上语义缓存(客服/FAQ 等重复问题多的场景)
→ 接第 22 篇 prompt 版本做失效标签,监控误命中率
→ 命中率 >20% 就值,<10% 说明场景不重复,别强上。
第三步(2-3 周):
上模型路由(难度评估 + 三档路由)
→ 先用 8B 当裁判跑离线评估,确认难度判准了再上线
→ 路由是成本下降最大的一层(-52%),但误路由会降质量。
第四步(1-2 周):
上预算控制(接第 23 篇归因 + 差异化定价)
→ 先只对 free 用户做硬限制,付费用户只降级不拒绝
→ 长尾成本控制住即可,别过度约束影响体验。
7.3 一个认知收尾
推理加速前四篇(18-21)是"让单次推理更快更省显存"——把一次调用的成本压到最低。本文是"让该省的调用不发生、该便宜的调用不贵、该约束的调用不失控"——从单点优化走到系统级成本治理。第 23 篇的可观测性是这套治理的"眼睛"——没有归因数据,路由不知道该路由什么、预算不知道该约束谁。归因(看)、缓存(省)、路由(选)、定价(控)四者合起来,才是完整的成本工程。第 17 篇说"从便宜到贵"选手段,本文把这条原则从开发时的静态选型,落地成运行时的动态执行——每一次调用,都花在刀刃上。
下一篇预告:推理加速+成本治理五篇收官,Agent 的"能力构建→生产落地→深化"主线已覆盖 29 篇。系列可能转向新维度——Agent UI/UX 交互层(流式渲染、思考过程可视化、HITL 确认门 UI),或 microVM/WASM 沙箱深潜,待用户确认。
- 点赞
- 收藏
- 关注作者
评论(0)