缓存与成本优化:让 Agent 的每一次调用都花在刀刃上

举报
yd_288476769 发表于 2026/09/23 14:21:51 2026/09/23
【摘要】 作者:yumking | 2026 年 9 月 23 日 | 技术标签:语义缓存 / 前缀缓存 / 模型路由 / 成本治理 / 差异化定价 摘要第 23 篇的可观测性把成本归因做到了 100%——每个 span 记 cost_usd,按用户/工具/模型聚合,钱花在哪清清楚楚。但"知道花在哪"不等于"花得值":同一个问题反复问 70B、公共 system prompt 每次重算 4K toke...

作者: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 沙箱深潜,待用户确认。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。