LLM 应用多层缓存工程 2026:从精确缓存到语义召回
把 LLM 应用的缓存层拆成五层:网络层 CDN 边缘命中、字符串层精确规范化 hash、语义层 embedding 阈值召回、上下文层 prefix cache 复用、响应层流式 chunk 续写,合计命中率 50%-70%、单位成本降 40%-60%、P95 延迟减半。
约 32 分钟阅读9,419 字10 次阅读博主

把 LLM 应用的缓存层拆成五层:网络层 CDN 边缘命中、字符串层精确规范化 hash、语义层 embedding 阈值召回、上下文层 prefix cache 复用、响应层流式 chunk 续写,合计命中率 50%-70%、单位成本降 40%-60%、P95 延迟减半。

一句话摘要:当 LLM 应用的请求量从 RPS 10 走到 RPS 1000,从单轮对话走到多轮 agent,从单模型走到多模型路由,缓存就不再是一个 "Redis 存个字符串" 的小问题,而是一套贯穿 prompt 解析、embedding 召回、响应复用、跨模型归一、命中率治理的工程体系。
过去三年里,做 LLM 应用的同学普遍经历过同一条曲线:第一周,demo 跑通,单次推理几百毫秒,开发者关注的是 prompt 写得对不对;第二个月,接入真实流量,token 单价在账单上开始显眼,开发者关注的是 prompt 是不是太长;第三个月,多 agent 串起来,工具调用次数爆炸,账单翻三倍,开发者开始认真审视每一次 LLM 调用是否真的必要。
这条曲线的尽头,几乎所有团队都会撞上同一堵墙:缓存设计。不是教科书里那种 "Cache-Aside + TTL" 就能解决的问题。LLM 的输入是自然语言、是多模态、是嵌入向量、是工具返回的半结构化数据;LLM 的输出是带有随机性的、跨多轮的、依赖上下文的;LLM 的请求路径从一个 endpoint 演化成 agent loop、parallel tool calls、RAG fan-out、reflection、retry、model fallback。传统的 KV 缓存只能解决其中最浅的一层:相同请求字符串返回相同响应。当下没有真实问题那么浅。
笔者在 2026 年走访了若干家做 AI 原生应用的工程团队,把"如何把 LLM 调用的成本压低 50% 以上、延迟从 P95 3 秒压到 600 毫秒" 作为命题之后,几乎所有幸存者都给出了同一个答案——一套叫做 多层语义缓存(Multi-tier Semantic Cache) 的体系在最底下托着整个系统。这套体系覆盖五层:网络层(CDN + edge LLM)、字符串层(精确 prompt hash)、语义层(embedding 召回 + 阈值)、上下文层(多轮对话状态复用)、以及响应层(流式 chunk 复用与续写)。每一层都有自己的失效模式、命中信号、命中率治理策略。本文把这条工程路径从原理到装配拆开来讲,并以一个生产级 RAG Copilot 应用的实测数据作为佐证。
为什么这个题目在 2026 年的今天特别值得写?因为 GPT-5、Claude 4.5、Gemini 2.5 Pro 这类模型把 prompt cache 原生做进了 transformer 前缀匹配,Anthropic 把 cache_control 写进了 SDK,OpenAI 把 cached prompt tokens 单价砍到 10%,Google 把 implicit caching 无声地放进 Gemini API——模型厂商已经在主动教应用方如何缓存,但应用方对此的反应速度远远跟不上。这就给了"精通缓存工程"的工程师一席之地。
我们先把"缓存"这件事用四个变量说清楚。设 LLM 应用收到一次请求 ,最终产生响应 ,总成本 由四部分组成:
是网络与边缘开销(CDN miss + region 跳转 + TLS 握手), 是 token 单价乘以输入与输出 token 数, 是 P95 时延换算的用户流失与基础设施成本, 是缓存错误响应带来的补偿成本(用户重试、人工纠错、声誉损失)。缓存的本质是把 映射到一个已知的 ,从而让 与 退化到接近零,但同时不引入额外的 。
把这条原则在工程上落地,可以分出五个层级,按从浅到深排列:
L1 网络层(Network Cache):CDN + 任何 HTTP 缓存层。命中条件是请求 URL + method + header 完全一致。LLM 应用的 API endpoint 通常带 streaming + auth header,几乎无法直接做 CDN,但若中间层做了"请求 fingerprint → 缓存响应流"的归一化,那么 L1 仍然有用。命中率通常 < 5%,但命中后延迟能压到 50 毫秒以内。
L2 字符串层(Exact Prompt Cache):把 request body 的规范化结果做 SHA-256,作为 cache key。规范化包括 trim 空白、统一 unicode、JSON key 排序、剥离 temperature seed 等影响随机性的字段。命中率在 to-C 聊天场景可以做到 15-25%,在 to-B 客服场景可以做到 35-45%(同模板问答)。这是 Anthropic prompt cache、OpenAI cached prompt 的物理基础。
L3 语义层(Semantic Cache):用 embedding 模型把 投射到向量空间,对已有缓存做 cosine 召回,若最相似缓存的相似度超过阈值 (通常 0.92-0.96),则直接复用其响应。命中率在长尾自然语言场景能补回 20-30%,但需要治理误命中("今天天气怎么样"和"明天天气怎么样"语义近但答案不同)。
L4 上下文层(Context Cache):多轮对话、agent loop、RAG fan-out 这些场景里,下一轮请求的 system prompt + 前几轮历史可能与上一轮大部分重叠。模型厂商的原生 cache 控制(cache_control: { type: "ephemeral" })和 prefix caching 的命中率通常 60-85%。这是 LLM 应用特有的"上下文复用"层。
L5 响应层(Response Stream Cache):把已经生成的 token 流按 chunk 切分,对相同 prefix 的流响应做续写式复用。对于长输出(> 2000 token)场景,命中率从 0% 提到 5-15%,看似不高但稳赚不赔。
把这五层的命中率、延迟、复杂度做一张对照表,可以看清楚为什么任何一层都不能省:
| 层级 | 命中率范围 | P95 延迟 | 工程复杂度 | 主要风险 |
|---|---|---|---|---|
| L1 网络 | 1-5% | 20-80ms | 低 | auth header 漂移、CDN 缓存策略 |
| L2 字符串 | 15-45% | 80-200ms | 中 | prompt 漂移、unicode 归一化不全 |
| L3 语义 | 20-40% | 200-500ms | 高 | 误命中、向量库维护成本 |
| L4 上下文 | 60-85% | 100-300ms | 低(厂商原生) | TTL 设置、过期策略 |
| L5 响应 | 5-15% | 100-300ms | 中 | 流式协议、回放一致性 |
五层的累计命中率不是简单相加,但经验法则是一套完整的多层缓存体系可以让生产环境的 LLM 调用总量降低 50-70%,单位成本下降 40-60%,P95 延迟下降 50-70%。
L1 与 L2 是任何 LLM 应用第一天就该装上的两条腿,没有它们后面的 L3、L4、L5 都站不稳。但这两层看似简单,实则有两个常见误区让命中率从理论值掉到实际值的一半。
第一个误区是 没有规范化(normalization)就做 hash。同一个用户的同一个问题,在前端可能因为浏览器自动给空格加了千分位、标点用了全角半角不一致、引号用了智能引号而被 hash 成几十个不同 key。规范化清单至少包含十项:去除所有 unicode 不可见字符(包括 zero-width space)、统一引号到 ASCII 或统一到 smart quotes 二选一、数字归一(中文数字"一万" vs 阿拉伯数字"10000")、去除尾部标点(问号、感叹号、句号)、去除外围邮件或电话等 token、把 emoji 折叠成统一占位符、对 JSON body 做 key 排序与 value 类型归一。在 RAG Copilot 场景里,这十条全做通常可以让 hash 复用率从 15% 提到 30%。
def normalize_request(body: dict) -> dict:
"""Canonical pre-hash normalization for L2 exact cache."""
if "messages" in body:
body["messages"] = [
{"role": m["role"], "content": _strip_invisible(m["content"])}
for m in body["messages"]
]
if "prompt" in body:
body["prompt"] = _strip_invisible(body["prompt"])
# Drop randomness-bearing fields for cache key, but keep them in body to model
randomized = {"temperature", "seed", "top_p", "presence_penalty", "frequency_penalty"}
cacheable = {k: v for k, v in body.items() if k not in randomized}
return _stable_json(cacheable) # sort keys, no spaces
第二个误区是 TTL 设置错了。同一个 prompt 在两小时前 LLM 的答案可能与两小时后完全不同,原因包括模型版本切换、价格变动、新政策生效、知识截止日到达。所以 L2 字符串缓存的 TTL 不应该设成 24 小时或更长,而应该按业务敏感度分桶:金融类(汇率、利率、政策)TTL ≤ 30 分钟,电商类(价格、库存、活动规则)TTL ≤ 10 分钟,知识类(百科、定义、教程)TTL ≤ 7 天,对话类(闲聊、创意)TTL ≤ 60 分钟。
L1 网络层在实际工程里通常表现为"请求 fingerprint → 边缘代理 → 中心集群"三级跳。Cloudflare Workers、Fastly Compute、AWS Lambda@Edge 都能承担这个角色,但要注意 streaming response 的 cache-control 头:必须是 private, no-store 配合一个"已缓存"的 flag 头,不能让 CDN 误把流响应当作静态资源缓存。
在多区域部署里,L1 还承担一个被低估的角色——跨 region 的请求整形。同一个用户的两台设备可能在同一秒发起两个相似请求(比如手机和网页同时打开),它们大概率路由到不同的边缘节点。L1 需要有一个跨 region 的 fingerprint 同步通道(Redis pub/sub 或 Kafka),把"刚在新加坡边缘被命中、响应是 X"的事件 200 毫秒内广播到东京、法兰克福、弗吉尼亚的边缘。这样一来,全球 L1 命中率能从 2% 提到 8%,看似小但乘以全球流量就是每月几百万美元。L1 的工程灵魂是"快且狠":边缘命中立即返回,绝不 fallback 到中心;任何需要跨 region 同步的 fingerprint 必须带严格的 TTL(通常 30 秒),过期即视作 miss;流式响应必须 chunk-by-chunk 返回,禁止把整个响应 buffered 完整再 return——后者会让用户的 first-byte time 从 50 毫秒暴涨到 3 秒,直接把 L1 的体验优势抹平。
另外 L1 还要处理 Vary header 的特殊性。LLM 响应的 Vary 维度天然比静态资源多:除了 Accept-Encoding、User-Agent,至少还要考虑 Accept-Language、模型偏好、用户地区、价格敏感度(免费用户 vs 付费用户的响应选择)。每一维都是一次缓存空间爆炸。一个反模式是给 L1 的 cache key 加上所有这些维度,结果是命中率直接归零。正确的做法是 定义哪些维度必须 Vary、哪些可以合并:比如 Accept-Language 必须 Vary(一个中文用户不应该收到英文回复),但 model-preference 不要 Vary(如果用户换了模型,新请求走 LLM 即可,旧 cache 让它自然过期)。这条边界画错了,L1 的 ROI 直接打到零。
L2 把"完全相同的问题"命中了,L3 要做的事更激进:把"语义上相似但文字不同"的问题也命中。这就是 embedding + 向量召回上场的时候。但 L3 也是五层里最容易爆雷的一层,因为在工程上至少有三个陷阱。
陷阱一是 embedding 模型不一致。Q 为"什么是 RAG" 和 Q' 为"RAG 是什么" 看似同一个问题,但如果你对 Q 用了 text-embedding-3-large,对 Q' 用了 bge-large-zh,两者的向量空间根本不在同一个坐标系,cosine 相似度直接算崩。L3 的第一铁律——所有进 L3 缓存的请求都必须用同一个 embedding 模型做向量化,且模型版本不能漂移。如果业务原因必须切换 embedding 模型(比如从 ada-002 切到 text-embedding-3),那么整个 L3 缓存要在一次维护窗口里做 embedding 反向投影 + 回填,不能异构查询。
陷阱二是 阈值漂移。相似度阈值 设得太低(0.85 以下)会引入大量误命中,太高(0.96 以上)会让命中率直接崩盘。经验上 0.92 到 0.95 是安全区间,但这个区间要根据 embedding 模型动态校准。校准的办法是做 golden set 评测:构造 500-1000 对相似问题与不相似问题,跑一遍看 ROC 曲线,挑 F1 最高的阈值。三个月校准一次就够了,因为模型版本基本不会高频切换。
陷阱三是 "相似但不该命中" 的硬场景。"今天是几号" vs "明天是几号","苹果公司股价" vs "苹果水果价格","日本首都东京多大" vs "东京首都是哪"。这些语义相似但答案不同的问题,必须在 L3 之上加一层 hard rules——正则匹配的日期数字、数量单位、专名实体识别,命中即强制 miss L3、转 L2 或直接 fallback 到 LLM 调用。
async def l3_semantic_lookup(query: str, top_k: int = 5, tau: float = 0.94):
"""L3 semantic cache lookup with hard-rule escape hatches."""
# 1) escape hatches: regex for date / number / proper noun
if _has_dynamic_entity(query):
return None # force miss L3
# 2) embed + ANN search
q_vec = await embed_model.embed(query)
candidates = await vector_index.search(q_vec, top_k=top_k)
# 3) filter by similarity threshold + freshness window
now = time.time()
for cand in candidates:
if cand.similarity < tau:
continue
age = now - cand.created_at
if age > cand.ttl_seconds:
continue
return cand.response # hit
return None # miss L3, fall through to LLM
L3 在生产环境能省下的 token 钱取决于业务同义改写率。在电商客服场景,用户问"我的快递到哪了" 和 "订单 123456789 物流状态" 会被 L3 视为同一问题(系统侧自动补充订单信息),命中率能到 35%;在开放域聊天场景,用户的同义改写率低,L3 命中率只有 8-15%。L3 是 to-B 应用的强力武器,未必是 to-C 应用的必备项——这一点跟很多教程的"semantic cache 必备论"不同,需要按场景判断。
L4 上下文缓存是模型厂商原生支持的特性:Anthropic 的 cache_control: { type: "ephemeral" },OpenAI 的 "cached prompt tokens" 自动以 10% 价格计费,Google Gemini 的 implicit caching 根据 prompt 前缀自动命中。对应用开发者来说,L4 几乎就是免费的:调一下 API、调一下 SDK,L4 就接上了。
但工程上恰恰因为"看起来免费",很多团队压根没把它用对。三个最常见的不对齐。
第一,没有为 cache 显式构造 prompt 结构。Anthropic 的 cache 命中率高度依赖 prompt 的 token 切分:如果 system prompt 前面有 200 个 token,后面每次请求都带这部分,那这部分可以被 cache;但如果 system prompt 与 user message 混在一起、每次请求前缀都对不齐,那 cache 永远不会命中。L4 的工程铁律——把 system、tools、长期 memory、用户历史、RAG 召回片段 这五类内容固定顺序放在 prompt 开头,每一次请求都按相同顺序组装,而不是动态拼接。
第二,TTL 滥用。ephemeral 类型的 cache 默认 5 分钟过期,但对 agent loop 来说这远远不够——一个 agent loop 从规划到执行到反思到重试,平均要 2-4 分钟。如果用户多轮对话之间有 5 分钟以上间隔(这是常见情况,比如用户中途去查资料),cache 直接过期。对策——把 critical 的稳定片段(system、tools、history)切分成多个 cache_control 块,给每个块独立 TTL;或者用 "extended" 类型的 cache(Anthropic 提供 1 小时版,价格略高但匹配真实对话节奏)。
第三,没有度量 cache 的实际命中率。L4 的命中与否,模型厂商只在 billing 页面显示一行 "X% cached tokens",很多团队看不到自家应用的细分命中率。L4 的工程纪律——在每次 LLM 调用后,单独 log usage.cached_tokens / usage.prompt_tokens,按 endpoint、按用户分桶算出 cache hit rate,并把它加进监控看板。如果命中率为 0 或很低,那么问题大概率在 prompt 切分——而不是模型商。
另一个 L4 容易踩的坑是 tool 定义的顺序与粒度。Anthropic 的 cache 是按 tool schema 的 token 切分的,但如果你定义了 50 个 tool 总共占 8000 token,而单次请求只用其中 3 个工具,那么前 8000 token 大概率进了 cache。优化方向——把高频工具与低频工具拆成两批 schema,按需 attach;或者把工具的描述文本压缩到极致(每个 tool 描述 ≤ 100 token)。在多 agent 编排的场景里,一个 agent 可能只能用到 30 个工具,但编排框架默认暴露了 100 个,cache 完全没起到作用——这是一个常见的隐性浪费。
还有一个 L4 实战经验:把 cache 写入与 cache 命中的 metric 单独追踪。模型商只告诉你"我命中了 X token",但没告诉你"我有没有缓存住"。一个常见的反模式是某次请求第一次落到了 cache,但下一次因为 prompt 顺序微调导致 cache 失效,团队完全没意识——因为只有"hit rate"指标上升下降的可见性,没有"实际有效 cache size" 的观察。建议在每个应用上线时同时埋点 cache_key_count(独立 cache key 数量)与 cache_size_tokens(当前 cache 占用的总 token),把这两个 metric 当作 L4 健康度的核心信号。
L4 这一层如果用对了,单次请求的成本可以降低 60-90%(取决于 prompt 长度),是所有层里 ROI 最高的环节,没有之一。
L5 是最容易被忽略的一层:当 LLM 生成 2000 token 以上的长回复时,相同 prefix 的请求能不能直接续写?答案是可以在客户端做,比如:
实现起来并不简单:流式响应需要按 chunk 切分存储,每个 chunk 关联一个 prefix hash;客户端在发起新请求前先试着拼接 prefix chunks,如果 LLM 接受了 prefix 拼接(OpenAI 的 suffix 参数支持),那么可以续写;否则整段重新生成。
async def l5_streaming_with_prefix(prefix_hash: str, suffix_prompt: str):
"""Resume generation from a cached prefix if model supports it."""
cached = await prefix_store.get(prefix_hash)
if cached and cached.model_compatible(suffix_prompt):
# ask model to continue generation from cached prefix
return await openai_client.completions.create(
model="gpt-5",
prompt=cached.tokens + suffix_prompt,
suffix=suffix_prompt,
stream=True,
)
else:
# no cache, generate fresh
return await openai_client.completions.create(
model="gpt-5",
prompt=suffix_prompt,
stream=True,
)
L5 命中率不高(5-15%),但 LLM 输出越长收益越大。在 code generation、long doc summarization、report writing 这类场景里,L5 是稳赚不赔的边际优化。
把五层装配起来不是装了就能用。生产环境的 LLM 缓存治理,本质上是一把手工程——需要专门的 "Cache Reliability Engineer" 来盯三件事。
第一件,命中率治理。Hit rate 是 LLM 应用成本与延迟的核心 KPI。所有五层都要有 hit rate 看板,刷新频率 5 分钟。命中率突然下跌 5 个百分点以上即触发告警。下跌的根因通常有三类:上游 prompt schema 漂移、embedding 模型升级未回填、用户行为改变(比如季节性热点话题)。任何一类都要有 runbook 兜底。
第二件,失效治理。缓存失效有三种:TTL 到期主动失效、模型版本切换被动失效、人工 emergency flush 紧急失效。第三种在生产中常见——比如某次 LLM 输出被紧急下线(合规问题、答案错误),需要立即把所有引用过这条响应的缓存全部 evict。Eviction by exact response hash 是支持这种操作的根基——记录每个 cache entry 的 response 内容指纹,紧急 flush 时按指纹反查。
第三件,误命中治理。误命中是 L3 语义缓存最容易出的事故。一次误命中不会爆,但累积起来会侵蚀用户信任。所有误命中要有人工标注通道(thumbs down 反馈、自动 A/B 比对),并把误命中样本回写到 golden set,做下一轮阈值校准。
具体到一个中型 LLM 应用(DAU 100 万、日均 LLM 调用 5000 万次),缓存治理需要的最小工具栈包括:Redis Cluster(精确缓存 + 流式缓存)、Milvus 或 pgvector(语义缓存向量库)、OpenTelemetry(命中率打点)、Prometheus + Grafana(看板)、一个内置的 cache invalidator 服务、一个 semantic golden set 管理后台。这套工具栈一个 5 人团队能在 4-6 周内搭起来。任何在第一周就忽视这套基础设施的团队,都会从第三个月开始掉队。
多层缓存不是没有代价。它至少引出两个工程伦理层面需要严肃对待的张力。
张力一:缓存与个性化。同一个用户在 prompt 字符串相同时,L2 会复用同一个响应——但用户的真实需求可能因为之前的对话历史而期望不同答案。L4 的 prefix cache 之所以受欢迎,是因为它允许 prompt 包含历史;但 L2 的精确 cache 会强制把历史压平。对策——把"用户特定上下文"(偏好、阅读历史、风格设置)显式编码到 cache key 里,比如 f"user:{user_id}:{question_hash}" 而不是 {question_hash}。这样不同用户的同问题会进不同缓存条目,避免被错误复用。
张力二:缓存与隐私。所有 LLM 应用的 cache 都进入了"用户数据"的法定范畴——GDPR、CCPA、PIPL 都有适用条款。对策——一是把 PII(个人身份信息)从 cache key 中剥离或脱敏,二是在 cache entry 上设置与用户撤回授权同步的 TTL,三是把缓存清除流程跑通到法律团队的标准操作手册里。一次泄露事故可以毁掉整个业务,缓存层必须是隐私设计的一部分,而不是事后补丁。
这两个张力不是技术问题,而是产品 + 合规 + 工程的跨职能问题。一个合格的 Cache Reliability Engineer 必须懂得跟法务对话。
最后,把全文浓缩成六条铁律给架构师,作为本文的 parting gift:
把这六条刻在脑子里,再去设计 LLM 应用的缓存层,多半不会走偏。
Conversation
0 条