首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >一次把提示词缓存命中率从 7% 拉到 84% 的排查记录

一次把提示词缓存命中率从 7% 拉到 84% 的排查记录

原创
作者头像
Archive
发布于 2026-10-15 14:11:35
发布于 2026-10-15 14:11:35
870
举报
文章被收录于专栏:随笔随笔

我们有个智能体业务,接入线上之后输入 Token 的账单一直不正常。查了一圈才发现:缓存这个开关我们是开了的,但基本没命中,等于白开。这篇把整个排查过程、接入代码和踩过的坑记下来。

一、先定位:账单高在哪

大模型不记事,每次请求都要把整段上下文从头算一遍(预填充),这部分算在输入上。我们的智能体办一件事大概要走二十多步、调几十次工具,每一步都带着同一份系统提示词——两万多 Token 的那种。也就是说同一段话,被重复计费了二十多次。

所以第一反应是用前缀缓存:把前面那段稳定的内容算完后存下来,后面请求直接复用。但真正接上以后,账单几乎没动。

二、接入代码:Anthropic 的断点写法

我们用的是 Anthropic 的接口,缓存的开关不是"全局打开",而是要显式打缓存断点(cache_control):

代码语言:python
复制
import anthropic

client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-...",              # 换成你实际使用的模型
    max_tokens=1024,
    system=[
        {
            "type": "text",
            "text": STATIC_SYSTEM_PROMPT,          # 稳定段:角色、规则、工具定义
            "cache_control": {"type": "ephemeral"}, # 断点打在稳定段的末尾
        }
    ],
    messages=build_messages(),
)

u = resp.usage
# 关键:这三个字段要一起落日志,否则根本查不出问题
print(u.input_tokens,                # 未命中、按基础价计的输入
      u.cache_creation_input_tokens, # 写入缓存的输入
      u.cache_read_input_tokens)     # 命中读取的输入

这段代码本身没问题,问题出在 STATIC_SYSTEM_PROMPT 和 messages 的边界上。

三、根因:会变的内容坐在了前缀中间

我们用 print(u.cache_read_input_tokens) 一看,几乎一直是 0,说明压根没读命中。

排查下来,原因是系统提示词里塞了一块"工作记忆"——智能体当前的状态、上一步查到的结果,都拼在里面。而这块东西几乎每走一步就会被改写一次,偏偏它夹在缓存断点之前。缓存存的是算完的前缀状态,要求逐字一致;前面隔几步就变一个字,后面的缓存全部作废。

改成只有一个动作:把那块会变的内容从 system 里挪出来,放到 messages 末尾的用户消息里。断点仍然只打在前面的稳定段。

代码语言:python
复制
messages=[
    {"role": "user",   "content": STATIC_EXAMPLES},   # 稳定示例,靠前
    {"role": "assistant", "content": STATIC_REPLY},
    # ……历史轮次保持追加、不重写……
    {"role": "user",   "content": WORKING_MEMORY},     # 会变的工作记忆,挪到最后
]

一次上线,命中率从 7% 升到 74%,几周后稳定到 84%,整体调用成本降了 59%。

四、算笔账:为什么值得把顺序排对

缓存不是免费的,第一次写入比不缓存贵一点,之后命中读取才便宜。以 Anthropic 公开口径为例(各模型有差异):

  • 基础输入价:100%
  • 缓存写入(5 分钟档):125%
  • 缓存写入(1 小时档):200%
  • 命中读取:10%(部分模型 5% 或 2.5%)

写入多付 0.25,一次命中省 0.9,所以第一次取出来就把多付的赚回来了。假设八成命中、两成没命中,输入这一栏的总开销能落到原来的三成三左右。

两个别算错的地方:

  • 折扣只打在输入上,输出不减。短输入长输出的活,缓存救不了。
  • 1 小时档写入收两倍,一小时里只取一次会亏,得取两次以上才划算。

五、四种静默失手,我踩过三个

缓存失手不报错、不打警告,返回内容一模一样,只有账单悄悄变贵。这四种要记牢:

  • 太短:最小可缓存长度按模型分档(Anthropic 512/1024/2048/4096,OpenAI 现行模型 1024)。不够长会被静默跳过。
  • 标记打在会变的块上:这是我们的主坑。写入只发生在标记位置,标记落在每次都变的内容上,就永远读不到。
  • 寿命从请求发出算:Anthropic 默认 5 分钟,OpenAI 默认 30 分钟。流式输出耗掉的时间也算在寿命里——跑了 4 分钟,下次请求只剩 1 分钟。好消息是命中读取会重置计时。
  • 标记名额不够:Anthropic 最多 4 个标记,回看窗口 20 个块,还占掉一个给自动缓存。

六、自建推理:vLLM 是另一套验收

如果模型是自己部署的,省的是显卡时间,验收方式也不一样。

  • vLLM 按块匹配,一块默认 16 Token。17 Token 的前缀,前 16 命中、剩 1 个重算,到不了满格。
  • SGLang 用基数树,粒度细到单个 Token,没有块边界的损耗。
  • vLLM 的 V1 引擎已经默认开启前缀缓存,命中率为 0 也几乎不掉速。

自建的验收不看开关看计数:拿同一段前缀冷热各压一轮,对比命中计数。参数写上去了,不等于真命中。

七、几点经验

  • 缓存的前缀要按"工具定义 → 系统提示词 → 消息"分层,上层一动,下层全废。改工具描述这种事,会让整段缓存清零。
  • 控制台的命中率是按一段时间聚合的,单次响应的计数只描述那一个请求,对不上先看是不是口径不同。
  • 有个长期矛盾值得提前想清楚:追加新消息、不重写前面轮次,才利于缓存;但长任务跑久了又得靠摘要把上下文压下来。这两个方向是打架的。

静态的放前面,会变的那几行挪到末尾;开着缓存不等于命中,先去看那两个计数。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先定位:账单高在哪
  • 二、接入代码:Anthropic 的断点写法
  • 三、根因:会变的内容坐在了前缀中间
  • 四、算笔账:为什么值得把顺序排对
  • 五、四种静默失手,我踩过三个
  • 六、自建推理:vLLM 是另一套验收
  • 七、几点经验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档