当前位置:首页 > 编程开发 > 正文

prompt caching 省钱实战:缓存价差 10 倍,但九成人的 prompt 结构吃不到

约 5495 字 · 预计阅读 14 分钟
一句话总结:prompt 缓存的价格差是 10 倍(Mistral 缓存输入 $0.07 对输入 $0.70),但它的生效条件很硬——从第一个 Token 开始完全一致才能复用;大多数人的 prompt 把动态内容放在前面、静态内容放在后面,正好把缓存全打掉。改结构比换模型省钱。

Agent 类应用的 token 消耗,七成以上是重复的:系统提示、工具定义、知识库片段、历史对话,每次调用都要重发一遍。

厂商为此做了前缀缓存(prefix caching):如果这次请求的前缀和上次完全一致,已经算过的 KV 直接复用,不用重新 prefill。价格上体现得很直白——Mistral Large 4 的缓存输入是 $0.07,普通输入 $0.70,差 10 倍。

听起来是白捡的钱。但实际跑起来,很多人发现账单没降。原因通常不在厂商,在 prompt 的拼接顺序。

缓存的唯一硬条件:前缀逐 token 一致

这张图值得记住:

请求 A:[系统提示 2000 token][文档 8000 token][问题 "退款政策"]
请求 B:[系统提示 2000 token][文档 8000 token][问题 "发票怎么开"]
                       ↑ 到这里为止完全一致,这 10000 token 命中缓存
请求 A:[系统提示 2000 token][文档 A][问题 X]
请求 B:[系统提示 2000 token][文档 B][问题 X]
                            ↑ 文档不同,从这之后全部失效

规则是:从第一个 token 开始,连续相同的最长部分才能命中。 中间只要有一个字符不同,它后面的一切都不算缓存。

这解释了为什么「加了缓存但没省钱」:很多实现把时间戳、用户名、随机 ID 放在系统提示开头,第一句就分叉了,后面一万 token 全部重算。

图 1:左边是静态在前、命中整段前缀;右边是动态在前,从第一个变量起全部失效

正确结构:静态在前,动态在后

一条规则能解决大部分问题:越稳定的内容越靠前,越易变的内容越靠后。

def build_messages(user_question, doc_text, session_Meta):
    # ✅ 正确:完全静态的部分放最前面
    system = (
        "你是一个企业知识库助手。回答要求:\n"
        "1. 只依据给定文档回答,不要编造\n"
        "2. 引用原文时标注段落编号\n"
        "3. 无法回答时明确说「文档中未提及」\n"
        "...(2000 token 的固定规则,一字不变)"
    )

    # 文档是半静态的:同一份文档会被连续问很多次,所以紧跟系统提示
    doc_block = f"【参考文档】\n{doc_text}"

    # ❌ 错误示范:把会话变量插到最前面
    # system = f"当前时间 {now},用户 {user_name},会话 {session_id}\n" + system

    return [
        {"role": "system", "content": system + "\n\n" + doc_block},
        {"role": "user", "content": user_question},   # 只有这部分每次都变
    ]

对比一下把时间戳放开头会怎样:

# 每次请求的第一个 token 就不同 → 缓存命中率 0
system = f"[{datetime.now().isoformat()}] 你是企业知识库助手..."

如果确实需要把时间和用户信息传给模型,两个办法:放到 user 消息里(反正那部分本来就不缓存),或者把动态字段挪到静态内容的后面。

显式缓存标记:Anthropic 那套更可控

各家实现不同,用法差别不小:

厂商 方式 计费
OpenAI自动,前缀超过阈值即缓存缓存命中部分按折扣价
DeepSeek自动,按 64 token 块对齐命中/未命中分别计价
Anthropic需要显式标注 cache_control写入 1.25 倍,读取 0.1 倍
Mistral自动缓存输入 $0.07 vs 输入 $0.70

Anthropic 的显式标记要多说一句,因为它有「写入费」:

messages = [{
    "role": "system",
    "content": [{
        "type": "text",
        "text": LONG_STATIC_PROMPT,
        "cache_control": {"type": "ephemeral"}   # 打在这个块的末尾
    }]
}]

第一次请求要为写入付 1.25 倍,之后每次读取只要 0.1 倍。这意味着一份缓存要被读 3 次以上才划算——如果某个 prompt 只调用一两次,标 cache_control 反而更贵。

这个细节很多人不知道,以为打了标记就一定省。

命中率怎么监控

不监控就等于没做。三家返回的字段名不一样,但要看的都是「缓存命中的 token 数」:

def report_usage(usage, provider: str) -> dict:
    if provider == "openai":
        cached = getattr(usage.prompt_tokens_details, "cached_tokens", 0)
        total = usage.prompt_tokens
    elif provider == "deepseek":
        cached = usage.prompt_cache_hit_tokens
        total = usage.prompt_tokens
    elif provider == "anthropic":
        cached = usage.cache_read_input_tokens
        total = (usage.input_tokens
                 + usage.cache_read_input_tokens
                 + usage.cache_creation_input_tokens)
    else:
        return {}

    return {
        "prompt_tokens": total,
        "cached_tokens": cached,
        "hit_rate": round(cached / total, 3) if total else 0.0,
    }

判断标准很直接:

  • hit_rate 低于 30%:结构有问题,静态内容可能被动态内容隔开了
  • hit_rate 在 70% 到 90%:正常,剩下的部分是每次都变的问题和上下文
  • hit_rate 突然掉到 0:有人在系统提示里插了时间戳、随机 ID 或者用户昵称

第二种情况里那 10% 到 30% 是省不掉的,别追求 100%。

一笔具体的账

假设一个客服 Agent:系统提示 + 工具定义 + 知识片段共 12000 token,用户问题平均 200 token,每天 20000 次调用。

场景 每次输入 token 有效单价 每日输入成本(按 $0.70/M)
无缓存12200$0.70/M约 $170.8
全部命中前缀12200(其中 12000 命中)12000×$0.07 + 200×$0.70约 $17.1

每天的差距在 150 美元量级,一个月四千多。这不是优化,这是结构性差异。

(这个计算只算输入,不含输出。输出的 token 不走缓存。)

五个坑,按命中率排

坑 1:动态内容放开头。 时间戳、用户名、会话 ID、随机 seed 放在系统提示第一行。这是最常见也最致命的一种,命中率直接归零。

坑 2:工具定义每次重新序列化。 工具 schema 用字典生成,每次遍历顺序变、字段顺序变,导致前缀不同。工具定义要固定序列化(排序后输出)。

坑 3:多轮对话把历史全塞在前面。 聊到第五轮,历史已经变长,前缀跟着变。正确做法是让历史自然增长(只追加不修改),这样前四轮的部分仍然命中;一旦你对历史做摘要、删除或重排,缓存全废。

坑 4:给短 prompt 打 cache_control。 只调用一两次的 prompt 打标记,写入费 1.25 倍,净亏。缓存要算「读取次数」,不是「能打就打」。

坑 5:不同租户共享缓存导致串味。 如果你的系统提示里嵌了租户特定配置,不同租户的前缀不同,各自独立命中,这是对的;但如果为了省缓存把所有租户的系统提示做成完全一样、把租户信息塞进用户消息,要确认模型不会把 A 租户的上下文用到 B 租户的回答里。安全边界优先于省钱。

边界

这篇讲的是「减少重复计算」,不是「减少 token 用量」。真要压 token,得从 prompt 精简、上下文裁剪、RAG 召回质量上下手,那是另一套工程。监控命中率的思路和Agent 可观测性那篇里的 token 账是一回事,可以合起来做。

另外,缓存的有效期各家不同(从几分钟到几小时不等),过期就得重新写入。高频调用的服务影响不大,低频的批处理任务可能每次都在写缓存、吃不到读的便宜,这种场景要先算「调用频率 × 缓存有效期」够不够覆盖写入成本,用压测那篇的方法量一下真实调用频率再定。

参考资料

目录导航
  • 缓存的唯一硬条件:前缀逐 token 一致
  • 正确结构:静态在前,动态在后
  • 显式缓存标记:Anthropic 那套更可控
  • 命中率怎么监控
  • 一笔具体的账
  • 五个坑,按命中率排
  • 边界
    • 参考资料
  • 相关文章:

  • 给 Agent 搭一层业务上下文:三层注入的实操与冲突裁决2026-10-30 00:31:06
  • 给 Agent 加预算护栏:四道闸门、一个检查点和分级降级2026-10-30 00:31:06
  • 腾讯混元 Hy4 正式版预告:模型之外,Buddy One 才是这次真正的新东西2026-10-30 00:31:06
  • Agent 上线之后:工具调用日志、token 账和失败归因怎么做2026-10-30 00:31:06
  • 写一个最小的 Agent 循环:80 行代码,和三个必须处理的边界2026-10-30 00:31:06
  • Step5 Preview 深度解读:600B 开源旗舰,单任务成本只有 Opus 5 的八分之一2026-10-30 00:31:06
  • 美团 LongCat 2.5 深度解读:1.6 万亿参数,国产算力训出来的长任务模型2026-10-30 00:31:06
  • DeepSeek-V4-Pro-0813 全面解析:1.6T 旗舰正式版,Agent 跑分翻倍、峰谷定价后还划算吗2026-10-30 00:31:06
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。