缓存命中率只有 12%:三个让前缀缓存白做的隐形改动

举报
yd_238528621 发表于 2026/11/02 12:39:58 2026/11/02
【摘要】 一个工单分类服务,system 提示加六条 few-shot 共 5000 token 固定前缀,缓存命中率却只有 12%。排查找到三个漏点:system 里塞了时间戳、few-shot 与用户输入拼反顺序、负载均衡把缓存打散到六个副本。改完命中率 78%,首字延迟 1.9 秒降到 0.72 秒,输入成本降 41%。

引言

去年十一月我接手一个工单分类服务,每天四万次调用。prompt 结构很简单:system 提示 3200 token,六条 few-shot 示例合起来 1800 token,加起来是 5000 token 的固定前缀,每次请求一字不差。按道理这是最该被前缀缓存吃掉的部分——它稳定、量大、每天都在重复。

上线一个月我拉账单,缓存命中率只有 12%。

5000 token × 4 万次,等于每天两亿 token 的输入量,命中率 12% 意味着其中一亿七千六百万 token 全价重算。花钱还在其次,首字延迟更难看:全量重算 1.9 秒,命中缓存 0.72 秒,差的这 1.2 秒用户能感觉到。后来花了三天把三个漏点逐个堵上,命中率做到 78%,没动模型,没加机器。

一、先算清楚这 5000 token 值多少钱

  • 命中缓存的部分不是免费,是按折扣计价。核了几笔账单,命中部分约按原价九分之一结算,各家比例不同,量级相近。
  • 按这个口径,命中率从 12% 提到 80%,输入侧的成本能降四成左右。这个数后来验证是对的:改造完当月输入 token 支出降了 41%。
  • 延迟侧更直观。我们把同一个请求跑了两百次做对比,全量重算的 P50 首字延迟 1.9 秒,命中缓存的 0.72 秒;P99 从 4.6 秒掉到 2.9 秒。缓存省的不只是钱,是把长 prompt 的启动开销从秒级拉回到亚秒级。
  • 命中率个位数,等于为长 prompt 付两次钱:一次买 token,一次买延迟。

二、漏点一:system 里那行"当前时间"

我们最早的 system 提示第一句是这样写的:

你是客服工单分类助手,当前时间为 2026-11-02 14:23:07,请按下述规则分类。

同事的想法很朴素,让模型知道今天几号。结果是整套前缀在第一个字节就分叉了,后面 4999 个 token 全部作废。

这里有个反直觉的点:前缀缓存是按 token 序列逐位比对的,不是"看起来像"就算命中。改一个字符、加一个空格、换行从 \n 变成 \r\n,都会从差异点之后整段失效。更隐蔽的版本是模板引擎自动给每段插入了 request_id 注释,肉眼看不见,序列已经完全不同。

修法很干脆,把动态内容全部搬到 user 消息尾部,system 只保留永久不变的部分:

  • 时间、请求 ID、租户名、A/B 实验分组,一律后置
  • 模板渲染后先做一次哈希比对,同一天内所有请求的 system 段必须哈希一致
  • 上线前加一条断言:任何包含数字时间戳的 system 模板直接拒绝发布

只改这一处,命中率从 12% 到 34%。

三、漏点二:few-shot 和用户输入拼反了顺序

第二个漏点藏得更深。我们的 prompt 模板长这样:

prompt 模板:

  • user: {工单正文}
  • ---
  • 以下是示例:
  • 示例一……
  • 示例二……

工单正文放在了最前面,示例跟在后面。写的人出于阅读习惯,觉得先看问题再看示例更自然——但对缓存来说这等于把最不稳定的内容放在了最前面。

缓存只认前缀。前缀一旦包含用户输入,后面再稳定的东西都等于不存在。改成标准顺序之后就好了:

  • system 段(3200 token,恒定)
  • few-shot 段(1800 token,作为独立的 user/assistant 轮次,恒定)
  • 真正的用户输入(放在最后,唯一变化的部分)

改完命中率 34% 到 71%。这条规则可以浓缩成一句话:把最不像的内容放到最前面,把唯一会变的内容留到最后一个 token。任何把动态内容插在静态内容之前的模板,都是在主动放弃缓存。

四、漏点三:负载均衡把缓存打散

命中率 71% 之后卡住了,怎么调 prompt 都上不去。后来才想明白:缓存是留在推理实例本地的。我们前面挂了六个副本,轮询分发,同一段 5000 token 的前缀要在六个副本上各算一遍才能全命中,理论命中率上限就是 1/6。实测 71%,已经接近这个天花板。

改成按前缀哈希做一致性路由,同一套 system 固定落到同一个副本,命中率到 78%。代价要说清楚:

  • 副本重启后它那部分缓存清零,会有一次延迟毛刺。我们的做法是启动后先发一条极短的预热请求,把前缀烤进去再接流量,毛刺基本消失。
  • 高并发时会"缓存击穿":同一前缀的三十个请求同时打进来,第一批全部 miss,一起重算。加了一个进程内的 singleflight,同前缀只让一个请求真算,其余等待复用,首字延迟的 P99 因此又掉了 0.4 秒。
  • 路由变了,故障隔离的边界也跟着变。某个副本挂掉时影响的是特定前缀的那批请求,不是平均分布,监控需要按前缀维度看错误率,不能只看总体。

五、多轮对话:留不住的那部分怎么办

单轮场景改完就结束了,多轮对话才是真正的难点:每轮 history 都在增长,前缀每轮都变,理论上永远命中不了。我们最后用的是"固定前缀加滚动摘要"的三段式:

  • 前 5000 token 永远是 system 加 few-shot,一个字节都不动
  • 中间 5001 到 8000 token 放对话摘要,每十轮重写一次,重写才失效
  • 最后三段放最近三轮的原文,这部分不指望命中

效果是固定部分始终命中,摘要段落平均十轮才失效一次,多轮会话整体命中率守在 60% 上下。不如单轮,但比每轮全量重算好太多。

小结

改造花了两天,改动集中在模板拼接和一个路由函数,模型和推理参数一行没动。最终数字:命中率 12% 到 78%,P50 首字延迟 1.9 秒到 0.72 秒,P99 从 4.6 秒到 2.9 秒,输入 token 支出降 41%。

  • 缓存是按字节前缀比对的,任何前置的动态内容都会让后面全部作废。这一条能解释绝大部分低命中率。
  • 模板顺序不是排版问题,是成本问题。静态内容必须前置,动态内容必须后置。
  • 缓存有物理位置。多副本部署不加前缀亲和路由,命中率上限就是副本数分之一。
  • 排查顺序建议从"前缀里有什么会变"开始,而不是从"缓存参数怎么调"开始。前两周一直在调参数,方向完全错了。

最难的不是这些技术点,是改掉"把所有东西都塞进 system 提示"的习惯。那种写法在单次调用里看起来无害,放大到每天四万次,就是一张多付四成的账单。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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