← 文章

前缀缓存的命中率去哪了

约 3 分钟#LLM#成本#可观测性

命中率掉了,光看总数找不到原因。按 128 token 一块去看,问题出在一段被放错位置的资料,以及两家供应商完全不同的缓存规则。

目录

Autarkia 每一轮对话发给模型的请求都很长:前面是几千字的角色设定,中间是对话历史,最后是这一轮才有的动态内容。供应商对命中前缀缓存的输入 token 收费低得多,所以前缀复用得越多,账单越低。

之前我在这上面栽过一次:缓存锚点锚在一个被上游截断成常数的量上,量化从头到尾是空操作(经过写在《修了三次还在》里)。那次之后我一直看的是总命中率,但光看总数,看不出是哪一段在变。

01缓存是按块算的

先得知道缓存的粒度。我是从 2840 条带命中 token 数的日志反推的:99.3% 的命中数是 128 的整数倍,只有 49.6% 是 256 的整数倍,最小命中就是 128。也就是说缓存以 128 token 为一块,从第一个字节开始比,哪一块变了,从那块往后全部重算。

于是加了一层只观测、不改请求的埋点:每次组装完前缀,按 128 token 切块,记下哪些块和上一轮相同、哪些块变了、断点落在哪一段。会话只记哈希,不落原始用户 ID;容易变的那段资料只记一个指纹。配套一个脚本,按会话比对相邻两轮,统计断点出现的频率和每次白算了多少块。

02放错位置的一段

埋点一开就看到了问题。组装前缀的那个函数,文件头的注释写着顺序是「系统设定 → 历史 → 动态尾部」,实际代码把检索召回的资料段插在了系统设定和历史之间。这段资料几乎每轮都不一样,它一变,后面整段历史都跟着重算。历史越长,每次白算得越多。

修法看起来只是挪一段的位置,但我这次只加了观测,没有调顺序。资料放在历史前面还是后面,会改变模型读到它的位置,进而影响它用不用、怎么用这段资料。这是行为变更,要先 A/B 看回复质量,再决定挪不挪。

估 token 数时还有个小坑。上下文预算那边为了不超限,用的是很保守的换算,会把中文 token 数高估一倍以上。拿它来切块,块数也跟着翻倍,断点位置全是错的。埋点改用单独的口径(中文约 1.5 字一个 token),并且把这个换算系数一起写进日志,方便事后校准。

03两家供应商,两种规则

第二个发现跟代码无关。在 DeepSeek 上,我观察到缓存是按 API key 隔离的:几个角色共用一个默认 key 时,各自的前缀会互相挤占。给每个角色单独配 key 之后,同一个角色的前缀能逐字节固定下来,夜间记忆提取的新段命中率在四个角色上是 95.4% 到 97.9%。

同样的前缀布局放到 MiniMax 的订阅套餐上,只有整个请求完全相同时才命中:一批 31 段跑下来是 10.9%,新段大约 2%。套餐也不分角色 key,这一层没有可以调的地方。

所以现在比较供应商的时候,我会拿自己的 prompt 单独测一遍缓存命中率,只比单价会算错。

夜间提取能命中这么高,还因为它刻意复用了主对话的角色前缀:离线构建时走和主聊天同一套模板、同一个拼装顺序,逐字节一致,提取指令只接在后面。小样本上一次两段的提取,6997 个输入 token 里有 6528 个命中缓存。

作者Shihao Qi