LLM 服务缓存策略:从重复计算到智能命中的性能优化

📅 2026/6/17 8:50:56
LLM 服务缓存策略:从重复计算到智能命中的性能优化
LLM 服务缓存策略从重复计算到智能命中的性能优化一、当 Token 账单飙升LLM 服务的缓存困境LLM 推理服务的成本主要由两部分构成计算成本GPU 时间和 Token 成本输入 输出 Token 数。在实际业务中大量请求存在重复或相似的 Prompt 模式——同一用户反复询问类似问题、不同用户查询相同的知识库内容、系统 Prompt 在每次请求中重复发送。这些重复计算不仅浪费了 GPU 资源还产生了大量不必要的 Token 费用。LLM 服务缓存的核心价值在于将重复的推理计算转化为一次计算 多次缓存命中。但与传统的 HTTP 缓存或数据库缓存不同LLM 缓存面临独特的挑战——语义等价但文本不同的 Prompt如什么是微服务和微服务是什么意思应该命中同一缓存条目这要求缓存系统具备语义匹配能力。二、LLM 缓存的分层架构从精确匹配到语义相似LLM 服务缓存需要根据业务场景选择不同粒度的缓存策略flowchart TD A[LLM 请求] -- B{精确缓存命中?} B --|是| C[返回缓存结果] B --|否| D{语义缓存命中?} D --|相似度 阈值| E[返回缓存结果 标注] D --|否| F{Prompt 前缀缓存?} F --|系统 Prompt 匹配| G[复用 KV Cache] F --|否| H[完整推理计算] G -- I[仅计算差异部分] I -- J[合并结果] J -- K[写入缓存] H -- K subgraph 缓存层级 B D F end style C fill:#51cf66,color:#fff style E fill:#4dabf7,color:#fff style G fill:#ffd43b,color:#333 style H fill:#ff6b6b,color:#fff三、生产级 LLM 缓存方案3.1 精确缓存基于 Prompt 哈希// 精确缓存实现适用于完全相同的请求 Service public class ExactCacheStrategy { private final RedisTemplateString, CachedResponse redisTemplate; private final HashFunction hashFunction Hashing.sha256(); // 构建缓存 Key对完整 Prompt 做哈希 private String buildCacheKey(CompletionRequest request) { // 将影响输出的所有参数纳入哈希 String cacheInput String.join(|, request.getModel(), request.getPrompt(), String.valueOf(request.getTemperature()), String.valueOf(request.getMaxTokens()), request.getStop() ! null ? String.join(,, request.getStop()) : ); return llm:cache:exact: hashFunction.hashString(cacheInput, UTF_8); } // 查询缓存 public OptionalCachedResponse get(CompletionRequest request) { String key buildCacheKey(request); CachedResponse cached redisTemplate.opsForValue().get(key); if (cached ! null) { // 检查缓存是否过期 if (cached.getExpireAt() System.currentTimeMillis()) { cached.setCacheHit(true); return Optional.of(cached); } redisTemplate.delete(key); } return Optional.empty(); } // 写入缓存 public void put(CompletionRequest request, CompletionResponse response) { String key buildCacheKey(request); CachedResponse cached new CachedResponse(); cached.setContent(response.getContent()); cached.setModel(response.getModel()); cached.setTokenUsage(response.getUsage()); cached.setExpireAt(System.currentTimeMillis() getTTLForModel(request.getModel())); // 根据模型设置不同的 TTL redisTemplate.opsForValue().set(key, cached, getTTLForModel(request.getModel())); } private Duration getTTLForModel(String model) { // 事实性内容缓存时间更长创意性内容缓存时间更短 if (model.contains(creative)) return Duration.ofMinutes(30); return Duration.ofHours(6); } }3.2 语义缓存基于向量相似度// 语义缓存实现适用于语义等价但文本不同的请求 Service public class SemanticCacheStrategy { private final VectorStore vectorStore; private final EmbeddingService embeddingService; // 语义缓存查询 public OptionalCachedResponse get(CompletionRequest request, double threshold) { // 将 Prompt 转为向量 float[] queryEmbedding embeddingService.embed(request.getPrompt()); // 在向量数据库中搜索相似 Prompt ListSimilarityResult results vectorStore.search( queryEmbedding, 5, // 返回 Top 5 threshold // 相似度阈值默认 0.95 ); if (results.isEmpty()) { return Optional.empty(); } // 验证最相似结果的上下文兼容性 SimilarityResult best results.get(0); if (isContextCompatible(request, best.getMetadata())) { CachedResponse cached best.getCachedResponse(); cached.setCacheHit(true); cached.setSimilarityScore(best.getScore()); return Optional.of(cached); } return Optional.empty(); } // 写入语义缓存 public void put(CompletionRequest request, CompletionResponse response) { float[] embedding embeddingService.embed(request.getPrompt()); SemanticCacheEntry entry new SemanticCacheEntry(); entry.setPrompt(request.getPrompt()); entry.setEmbedding(embedding); entry.setResponse(response); entry.setModel(request.getModel()); entry.setTemperature(request.getTemperature()); entry.setCreatedAt(System.currentTimeMillis()); vectorStore.upsert(entry); } // 上下文兼容性检查 private boolean isContextCompatible( CompletionRequest request, MapString, Object cachedMetadata ) { // 模型必须一致 if (!request.getModel().equals(cachedMetadata.get(model))) { return false; } // Temperature 差异不能太大影响输出确定性 double tempDiff Math.abs( request.getTemperature() - (Double) cachedMetadata.get(temperature) ); return tempDiff 0.1; } }3.3 Prompt 前缀缓存复用 KV Cache// Prompt 前缀缓存复用 LLM 推理引擎的 KV Cache Service public class PrefixCacheStrategy { private final LLMInferenceEngine inferenceEngine; // 带前缀缓存的推理请求 public CompletionResponse completeWithPrefixCache(CompletionRequest request) { // 分离系统 Prompt 和用户 Prompt String systemPrompt extractSystemPrompt(request); String userPrompt extractUserPrompt(request); // 查找是否有匹配的系统 Prompt KV Cache OptionalKVCacheHandle cachedPrefix inferenceEngine.findKVCache(systemPrompt, request.getModel()); if (cachedPrefix.isPresent()) { // 复用 KV Cache仅计算用户 Prompt 部分 return inferenceEngine.completeWithCachedPrefix( cachedPrefix.get(), userPrompt, request.getParameters() ); } // 无缓存完整计算并存储 KV Cache CompletionResponse response inferenceEngine.complete( request.getPrompt(), request.getParameters() ); // 存储系统 Prompt 的 KV Cache inferenceEngine.storeKVCache( systemPrompt, request.getModel(), response.getKVCacheHandle() ); return response; } }3.4 缓存失效策略// 智能缓存失效管理 Service public class CacheInvalidationManager { // 基于时间的自动失效 Scheduled(fixedRate 300000) // 每 5 分钟扫描 public void evictExpiredEntries() { // 精确缓存按 TTL 自动过期Redis 原生支持 // 语义缓存检查条目的时效性 ListSemanticCacheEntry staleEntries vectorStore.findEntriesOlderThan(Duration.ofHours(24)); vectorStore.delete(staleEntries); } // 基于内容变更的主动失效 public void invalidateBySource(String sourceId) { // 当知识库内容更新时清除相关缓存 ListString relatedKeys redisTemplate.keys( llm:cache:*: sourceId :* ); if (relatedKeys ! null !relatedKeys.isEmpty()) { redisTemplate.delete(relatedKeys); } // 清除语义缓存中与该知识库相关的条目 vectorStore.deleteByMetadata(sourceId, sourceId); } // 缓存命中率监控 Scheduled(fixedRate 60000) public void monitorCacheMetrics() { CacheMetrics metrics collectMetrics(); if (metrics.getHitRate() 0.3) { // 命中率过低可能需要调整缓存策略 log.warn(缓存命中率过低: {}%, 建议检查缓存配置, metrics.getHitRate() * 100); } if (metrics.getStaleHitRate() 0.1) { // 过期命中率过高需要缩短 TTL log.warn(过期命中比例过高: {}%, 建议缩短 TTL, metrics.getStaleHitRate() * 100); } } }四、LLM 缓存的代价与适用边界LLM 缓存方案的代价需要审慎评估语义缓存的误命中风险相似度阈值设置过高会导致命中率低设置过低会导致返回不相关的结果。更危险的是语义缓存可能返回看似相关但实际错误的答案——用户问Java 的内存模型缓存命中了JVM 内存区域的回答两者相关但答案不同。需要在返回语义缓存结果时标注相似度分数让用户判断是否可信。存储成本语义缓存需要向量数据库存储 Embedding每个条目约 1.5KB768 维 float32。百万级缓存条目需要约 1.5GB 存储空间加上索引开销总存储需求约 5GB。相比 GPU 计算成本存储成本通常可以接受。缓存一致性当 LLM 模型更新后旧缓存的结果可能不再准确。需要在模型版本变更时清除所有缓存但这会导致短暂的命中率归零。渐进式失效按模型版本标记缓存可以缓解但增加了缓存管理的复杂度。适用边界精确缓存适合重复查询多的场景如 FAQ、知识库问答语义缓存适合用户表述多样但意图集中的场景前缀缓存适合系统 Prompt 固定、用户 Prompt 变化的场景。禁用场景当业务要求每次推理结果必须是最新的如实时数据分析、代码执行结果不应使用缓存。五、总结LLM 服务缓存是降低推理成本的有效手段但需要根据业务场景选择合适的缓存层级。精确缓存实现简单、可靠性高但只能命中完全相同的请求语义缓存覆盖了语义等价的请求但存在误命中风险前缀缓存复用了推理引擎的 KV Cache对系统 Prompt 固定的场景效果显著。在实际落地中建议采用精确缓存优先 语义缓存补充 前缀缓存优化的三层策略并通过命中率监控持续调优缓存参数。核心原则是缓存的价值在于减少重复计算而非替代推理——当缓存结果的准确性无法保证时宁可重新计算。