跳到主要内容

使所有 Prompt 缓存前缀失效的分词器升级

· 阅读需 10 分钟
Tian Pan
Software Engineer

发布说明只有两行。“改进了多语言分词(Tokenization)。模型输出无破坏性变更。”一共不到二十个字。你的评估(Evals)确认了这一点:相同的提示词,相同的生成内容,相同的评分。你的平台团队在周五下午批准了升级。到了周二早上,你的缓存命中率从 80% 下降到 4%,每日推理费用翻了两番,而凌晨 6 点把你叫醒的轮值工程师在你的代码里找不到任何一行改动。

你的代码确实没有任何改动。但服务商发布了一个新的分词器,它对某个 Unicode 字符的一个字节划分与旧版本不同。你系统中每个缓存的前缀现在都是基于一个已不再存在的 Token 序列生成的指纹。模型的表现完全一致 —— 这确实是事实。但发布说明中未曾提及的缓存层,却为此付出了全额代价。

这种失效模式是提示词缓存(Prompt Caching)在架构上带来的风险:你所依赖的成本契约,并不是服务商测试的那个契约。服务商测试的是模型对相同的输入产生相同的输出。而你依赖的是模型对相同的输入产生相同的 Token,以相同的顺序、相同的字节边界,并哈希成相同的前缀指纹。这是两种截然不同的契约,而其中只有一种拥有回归测试套件。

缓存究竟是以什么为键的

目前各大服务商部署的提示词缓存本质上都是前缀缓存(Prefix Cache)。系统从第一个 Token 开始,按分块(Chunks)对 Token 序列进行哈希处理,并存储以这些哈希值为键的中间 KV 状态。当新请求到来时,缓存从头开始遍历,对 Token 进行哈希,直到发现一个与存储哈希不匹配的分块边界。在那之前的所有内容都算命中,之后的所有内容都要按原价重新计算。

关键的细节在于:缓存是对 Token 进行哈希,而不是字符。服务商在计算哈希之前会先对你的原始输入进行分词。如果分词器发生了变化 —— 即使相同的字符串现在产生了不同的 Token 序列 —— 那么所有已缓存的前缀都会变得不可访问。磁盘上的字符串没变,但由此衍生的指纹变了。

对于大多数提示词内容来说,这并不是问题,因为分词器对于绝大多数 ASCII 文本和成熟的多语言内容都是稳定的。危险潜伏在边缘地带:

  • 可以通过不同规范化形式(NFC 与 NFD)表示的 Unicode 字符,例如 é 既可以是一个码点,也可以是两个
  • 组合字符、带有零宽连字符(ZWJ)的表情符号序列以及区域指示符号
  • 较冷门语种的字符,其分词器的 BPE 合并过程是针对不同语料库重新训练的
  • 跨越新分词器划分的预分词边界的字节序列

分词器的“升级” —— 通常被描述为更好的多语言覆盖或更大的词表 —— 几乎总是会改变这些边缘情况。模型行为在设计上得到了保留:通过使用新分词器和足够的数据继续训练,使输出分布保持匹配。而缓存指纹的保留则纯属偶然,仅适用于那些在升级前后恰好落在相同 Token 边界上的内容。对于其他任何内容,前缀都会失效。

为什么在账单寄来之前它是隐形的

这种模式之所以会静默失效,是因为它保持了正确性。每个请求依然有效,每个生成结果依然正确。评估套件依然通过。延迟预算可能会稍微增加,因为缓存命中的返回速度比缓存未命中快,但缓存命中本身也存在波动,所以长尾端 20–40% 的延迟增加看起来就像正常的噪声,除非有人在盯着分布图。唯一崩坏的是成本曲线,而成本报告通常比流量增长预留了余量的预算延迟 24 小时。

标准的遥测堆栈抓不住这个问题。大多数团队在 LLM 调用上只监测三件事:延迟、错误率和总 Token 数。在分词器引发的缓存崩溃期间,这三者都不会有明显变化。原本只需支付 10% 费用的缓存前缀现在按 100% 计费,而暴露该细分项目的请求路径 —— cached_input_tokensinput_tokens —— 通常在仪表盘上被聚合成了单一的“输入 Token”指标。即使缓存完全失效,你可能也无法在轮值人员监控的图表中发现它。

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates