Prompt 缓存策略:语义相同的请求不应该重复计算

发布时间:2026/7/11 16:58:14
Prompt 缓存策略:语义相同的请求不应该重复计算 Prompt 缓存策略语义相同的请求不应该重复计算一、重复计算是推理服务里最隐蔽的资源浪费大模型推理服务上线后成本账单里有一笔很难被发现的支出语义相同或高度相似的请求被重复计算。用户问Python怎么读取JSON另一个用户问Python读取JSON的方法模型内部执行的计算几乎一致但系统照例跑了一遍完整的前向传播。这不是模型的问题是工程架构的疏忽。Prompt 缓存要做的事情很简单——识别语义重叠跳过重复计算见证奇迹的时刻在于缓存命中率从5%升到60%时推理成本的陡降。很多人以为缓存就是KV存储按字符串哈希匹配就行。但大模型服务的缓存难点不在存储在匹配。相同语义的Prompt可能字面差异很大字面相同的Prompt可能语义不同。纯字符串哈希会漏掉大量可复用的计算纯语义向量检索又会引入误匹配。工程上需要一条折中路径。二、缓存命中链路从请求到复用的三层过滤flowchart TD A[新请求到达] -- B[字符串精确匹配] B -- 命中 -- C[直接返回缓存结果] B -- 未命中 -- D[语义向量相似度检索] D -- 相似度≥阈值 -- E[复用KV Cache加速推理] D -- 相似度阈值 -- F[完整前向传播] F -- G[结果写入缓存] E -- GPrompt缓存的核心不是存最终输出文本而是存Transformer中间层的KV Cache。这样做的好处是即使语义不完全相同只要前缀或大部分内容重叠模型可以从缓存点继续推理跳过已计算的部分。这也是Anthropic和OpenAI推出Prompt Caching功能的技术基础。三层过滤的设计原因第一层字符串精确匹配处理完全相同的请求成本最低、速度最快第二层语义向量检索处理字面不同但语义相近的请求需要向量化和检索但避免完整推理第三层才是真正的新计算。过滤越靠前节省越多。三、缓存管理器从匹配到复用的工程实现下面是一个Prompt缓存管理器的核心结构。代码注释解释了关键设计决策。from dataclasses import dataclass, field from typing import Optional import hashlib import time dataclass class CacheEntry: 缓存条目存储KV Cache引用而非完整文本节省显存 prompt_hash: str # 字面精确哈希用于第一层匹配 prompt_vector: list[float] # 语义向量用于第二层匹配 kv_cache_ref: str # KV Cache在显存中的引用标识 result_text: str # 最终输出文本用于精确命中直接返回 created_at: float # 创建时间用于过期淘汰 hit_count: int 0 # 命中计数用于淘汰优先级 # 设计原因记录hit_count而非只用时间淘汰 # 因为高频Prompt即使较早创建也应保留 class PromptCacheManager: def __init__(self, max_entries: int 5000, ttl_seconds: float 3600, semantic_threshold: float 0.92): self.max_entries max_entries self.ttl_seconds ttl_seconds # 默认1小时过期 self.semantic_threshold semantic_threshold # 语义匹配阈值 self._exact_map: dict[str, CacheEntry] {} # 精确匹配字典 self._entries: list[CacheEntry] [] # 全量条目列表 def _compute_hash(self, prompt: str) - str: 标准化后计算哈希去除多余空格和标点差异 避免格式微调导致精确匹配失效 normalized prompt.strip().lower() return hashlib.sha256(normalized.encode()).hexdigest() def lookup(self, prompt: str, prompt_vector: list[float]) - Optional[CacheEntry]: 两层查找先精确后语义见证奇迹的时刻在命中率飙升 # 第一层精确匹配 exact_hash self._compute_hash(prompt) if exact_hash in self._exact_map: entry self._exact_map[exact_hash] entry.hit_count 1 return entry # 第二层语义相似度检索 for entry in self._entries: sim self._cosine_similarity(prompt_vector, entry.prompt_vector) if sim self.semantic_threshold: entry.hit_count 1 # 设计原因语义命中不直接返回旧结果 # 而是返回KV Cache引用让模型从缓存点继续推理 return entry return None def _cosine_similarity(self, a: list[float], b: list[float]) - float: dot sum(x * y for x, y in zip(a, b)) norm_a sum(x ** 2 for x in a) ** 0.5 norm_b sum(x ** 2 for x in b) ** 0.5 return dot / (norm_a * norm_b) if norm_a and norm_b else 0.0 def evict(self) - None: 淘汰策略LFU TTL双条件先淘汰过期低频条目 now time.time() # 设计原因同时考虑时间和频率避免冷门长驻和热门被误删 self._entries [ e for e in self._entries if (now - e.created_at) self.ttl_seconds or e.hit_count 10 ] # 超容量时按hit_count升序淘汰 if len(self._entries) self.max_entries: self._entries.sort(keylambda e: e.hit_count) self._entries self._entries[self.max_entries // 5:] self._rebuild_exact_map() def _rebuild_exact_map(self) - None: self._exact_map {e.prompt_hash: e for e in self._entries}四、缓存取舍命中率、延迟和语义误匹配的三方博弈Prompt缓存的工程权衡集中在三个维度。第一是命中率与语义精度阈值设高0.95以上则命中率低但误匹配少阈值设低0.85以下则命中率飙升但可能把语义不同的请求判为相同返回错误结果。第二是缓存粒度与显存占用缓存KV Cache比缓存输出文本更省推理时间但KV Cache占用显存远大于一段文本5000条缓存在大模型上可能吃掉数GB显存。第三是过期策略与实时性业务规则和知识库更新后旧缓存可能包含过时信息TTL要配合知识更新频率调整。还有一个容易被忽略的问题缓存命中时返回的是旧结果但用户可能期望最新信息。例如问今天北京天气昨天缓存的回答就不可用。工程上需要区分两类Prompt——事实性查询不应缓存结构性生成如翻译、格式化适合缓存。这个分类本身也需要设计。并发安全同样重要。多请求同时写缓存、多请求同时读同一个KV Cache引用需要锁或原子操作。推理服务通常用异步架构缓存读写必须线程安全否则命中率数据会漂移甚至出现缓存损坏。五、总结Prompt缓存通过三层过滤精确匹配、语义检索、完整推理减少大模型服务的重复计算。核心存储对象是KV Cache而非输出文本命中率与语义阈值需要按场景调优事实性查询不适合缓存过期策略应兼顾LFU和TTL并发安全是生产部署的基本要求。