LLM 输入缓存命中率低怎么办?从 Prompt Caching 原理到工程排查清单

📅 2026/6/23 6:17:38
LLM 输入缓存命中率低怎么办?从 Prompt Caching 原理到工程排查清单
前言线上 LLM 服务账单里如果出现这种怪象——QPS 没涨多少输入 token 费用却翻倍或者 Dashboard 里cached_tokens长期接近 0——多半不是模型「变笨了」而是Prompt Caching输入前缀缓存根本没吃上。先别和推理侧的KV Cache搞混历史稿「KV Cache 与自回归推理加速」讲的是 decode 阶段省算力本文讲的是API/网关层对相同输入前缀的缓存复用——目标是少算 prefill、降 latency、省输入 token 钱。下面按工程师视角先分清概念 → 看指标 → 找根因 → 改架构文末有可直接贴进团队的排查清单。一、先给结论输入缓存到底在缓存什么Prompt Caching / Prefix Caching的核心规则只有一句同一段输入前缀从第一个 token 开始、逐字节一致在缓存有效期内再次被发送后续请求可以跳过这部分的 prefill 计算并按厂商规则享受cache read计费通常比全价 input 便宜。它缓存的是「已经算过的前缀」不是「语义相似」的段落——改一个空格前缀链就断了。概念层级你控制的杠杆KV Cache推理引擎 / 单次会话内序列长度、批处理、有状态服务Prompt Caching云 API / 网关消息拼装顺序、前缀稳定性、模型与 endpoint 一致二、怎么判断「命中率低」——先看这三类指标1厂商返回的 usage 字段最可靠OpenAI 兼容 API以官方文档为准字段名随 SDK 版本略有差异{usage:{prompt_tokens:4200,completion_tokens:180,prompt_tokens_details:{cached_tokens:0}}}cached_tokens 0且 prompt 很长 → 基本可判定本轮没命中连续多轮对话应看到cached_tokens随历史增长前缀变长、稳定部分被复用Anthropic常见字段cache_creation_input_tokens本轮新写入缓存的 tokencache_read_input_tokens本轮从缓存读取的 token理想情况稳定 system 文档放前缀后第二轮起cache_read应显著 0。2业务侧自建比率输入缓存命中率 ≈ cache_read_tokens / (cache_read_tokens cache_miss_tokens)在网关层对每次请求打日志# 伪代码统一封装 LLM 调用并打点deflog_cache_usage(usage:dict,route:str):cachedusage.get(prompt_tokens_details,{}).get(cached_tokens,0)promptusage.get(prompt_tokens,0)hit_ratecached/promptifpromptelse0metrics.histogram(llm.cache_hit_rate,hit_rate,tags{route:route})别只靠「感觉变快了」——账单和 metrics 不会骗人。3延迟侧面验证命中前缀缓存时长 system prompt 场景的 TTFT首 token 时间often 明显下降。但若你 prompt 只有 200 token本来也进不了缓存门槛TTFT 变化不明显——所以要结合 token 数一起看。三、命中率低的 7 个高频根因根因 1可变内容放在了前缀最致命现象每条请求 system 里带当前时间2026-06-20 09:03:17user 消息里塞request_iduuid()再拼 RAG 文档多租户把tenant_id、user_id写在 system 最前面为什么 miss缓存按前缀字节级匹配。第一行变了后面 8000 token 的文档即使完全相同也算新前缀。修复静态在前动态在后——固定模板[固定] system角色、规则、工具 schema版本化 [固定] 检索文档 / 知识库片段按 session 稳定排序 [可变] user本轮问题、request_id、时间戳messages[{role:system,content:STATIC_SYSTEM_V3},# 不变{role:user,content:f{rag_context}\n\n---\n{user_query}},]# 时间戳、trace_id 只出现在最后一段 user 里根因 2未达最小可缓存 token 门槛现象system prompt 只有 300 token抱怨「从没命中过」文档很短每轮都 re-prefill为什么 miss主流云厂商对 Prompt Caching 有最小前缀长度例如 OpenAI 侧常见门槛约1024 tokens量级Anthropic 常见2048以各厂商当前文档为准。低于门槛不会建立缓存条目。修复把可复用的长内容凑到门槛以上system 规则 工具定义 领域术语表 RAG 片段若业务 prompt 天然很短别强行为了缓存堆废话——算 ROI短 prompt 本来 prefill 成本就低合并多个小请求为「共享前缀 不同 suffix」架构见第五节根因 3多轮对话每轮「重开无状态请求」现象客户端每轮只发{user: 最新一句}历史由后端拼但不稳定或每轮重新 fetch RAG文档顺序、空格、JSON 字段顺序随机为什么 miss多轮要命中需要前缀包含上一轮及更早的稳定历史或至少system 文档块完全一致。修复会话级固定 RAG 块同一会话内检索结果缓存 530 分钟顺序 deterministic历史消息 append-only不要在中途改写过旧 assistant 内容网关维护session_id → 已缓存前缀 hash变更时打 debug 日志根因 4JSON / 模板序列化不稳定现象工具tools参数用 Pythondict直接json.dumpskey 顺序随机多语言团队有时中文有时英文 systemA/B 混发Prompt 从数据库读出尾随空格、\r\n不一致为什么 miss人眼看「一样」模型输入 token 序列不同。修复importjson tools_jsonjson.dumps(tools,ensure_asciiFalse,sort_keysTrue,separators(,,:))system prompt版本号写进常量STATIC_SYSTEM_V3发版才 bumpCI 里对prompts/*.md做snapshot test防止无意 diff统一 UTF-8、统一换行符.gitattributes管prompts/** text eollf根因 5模型、endpoint、参数不一致现象负载均衡在gpt-4o和gpt-4o-mini间轮询预发走官方生产走中转缓存不共享同一前缀但temperature从 0 改成 0.7——部分厂商缓存 key 含采样参数修复缓存友好的路由长前缀场景固定 model endpoint至少在一个 session 内读文档确认哪些参数进入 cache keymodel、tools、response_format 等中转站场景问清楚是否透传厂商 Prompt Caching很多聚合层会打断缓存根因 6RAG 把「每次检索结果不同」放在前缀现象TopK 向量检索分轻微波动就换文档把「检索 query 10 篇 chunk」全塞 systemquery 每轮都变修复分层拼装Layer A稳定可缓存system 规则 工具 静态 FAQ Layer B半稳定会话内固定的知识包首次检索后锁定 Layer C可变本轮 user query 少量动态补充检索放后缀对高价值文档按 doc_id 排序后再拼接避免分数相同顺序乱评测集里加指标同问复问 cache_read 是否 0根因 7缓存 TTL 过期 流量太散现象用户间隔 2 小时再来缓存已过期常见 TTL 约 560 分钟厂商而定10 万个独立 system prompt每客户定制每个前缀只出现 1 次修复抽高频 system 模板做标准化个性化字段下沉到 suffix对「慢会话」产品在 TTL 内做keep-warm定时 ping 同前缀需评估成本监控「每前缀出现次数」分布长尾场景别指望高命中率四、推荐架构「稳定前缀 可变后缀」模板# prompts/rag_v1.py — 纳入 Git 版本管理STATIC_SYSTEMopen(prompts/system_v3.md).read()TOOLSjson.loads(open(prompts/tools_v1.json).read())defbuild_messages(session:Session,user_query:str)-list:# Layer B会话内锁定避免每轮重排rag_blocksession.get_or_fetch_rag_block()# 内部 cache 5~30minreturn[{role:system,content:STATIC_SYSTEM},{role:user,content:(fknowledge\n{rag_block}\n/knowledge\nfquestion\n{user_query}\n/question),},]Agent / 多步工具调用同样适用工具 schema 和 system 放最前每步 observation 追加在后缀别插入前缀中间。五、10 分钟排查顺序照着做打日志prompt_tokens、cached_tokens或 Anthropic 的 read/create抽一条 miss 请求逐段 diff 与前一次 hit 请求的输入二进制级检查可变字段是否在前缀时间戳、UUID、tenant_id量前缀 token 数是否低于厂商最小门槛确认 model / base_url / tools JSON 是否 session 内一致RAG同 session 文档块是否 stable sort 锁定仍低看 TTL 与流量是否过于长尾六、和开源 / 自建链路怎么衔接场景建议OpenAI 兼容网关LiteLLM、One API 等查是否开启并透传 cache 相关 header/字段自建网关要打 usage 转发本地 Ollama无云厂商 Prompt Caching 概念靠KV 复用 有状态会话优化见 KV Cache 科普稿Prompt 版本管理prompts/入 GitPR review配合 GitHub Actions 跑 snapshot test可观测Prometheus Grafana 面板cache_hit_rateby route / model# .github/workflows/prompt-snapshot.yml 最小示意name:prompt-snapshoton:[pull_request]jobs:check:runs-on:ubuntu-lateststeps:-uses:actions/checkoutv4-run:python scripts/hash_prompts.py--check七、常见误区误区事实「语义一样就能命中」必须前缀 token 级一致「缓存 不用付钱」cache read 通常仍计费只是更便宜create 可能额外收费「命中率 100% 才正常」长尾 query、TTL、多租户会导致合理偏低「调 temperature 不影响缓存」以厂商文档为准部分参数在 cache key 内「Prompt Caching 等于 KV Cache」两层优化排查方向完全不同结语输入缓存命中率低90% 是工程拼装问题不是模型问题把静态长前缀放最前、动态信息沉后缀、序列化 deterministic、会话内锁定 RAG 块——再配合 usage 指标持续看cached_tokens通常一两轮迭代就能从「几乎全 miss」拉到「可接受水平」。