大模型 Token 缓存与语义去重后端成本优化的工程实践一、Token 消耗的温水煮青蛙大模型后端的隐性成本大模型应用后端面临一个严峻的成本问题相同或相似的请求被重复发送到 LLM每次都消耗完整的 Token。用户反复询问今天天气如何每次都消耗 500 Token 的 Prompt不同用户询问Python 如何读取 CSV语义相同但措辞不同无法命中缓存每次都重新调用 LLM。按 GPT-4 的定价计算一个日均 10 万次调用的应用如果 30% 的请求可以通过缓存命中每月可节省数千美元。Token 缓存与语义去重是大模型后端从能用走向经济可用的关键优化。二、Token 缓存的分层架构Token 缓存分为两层精确匹配缓存基于请求哈希和语义匹配缓存基于向量相似度。前者命中率高但覆盖窄后者覆盖宽但需要向量检索。flowchart TD A[用户请求] -- B{精确缓存命中} B --|命中| C[直接返回缓存结果] B --|未命中| D{语义缓存命中} D --|相似度 阈值| E[返回缓存结果 微调] D --|未命中| F[调用 LLM] F -- G[结果写入精确缓存] F -- H[请求向量写入语义缓存] G -- I[返回结果] E -- I C -- I精确缓存使用请求的完整 Prompt 哈希作为 Key适合完全相同的重复请求。语义缓存将 Prompt 转换为向量通过余弦相似度查找语义相近的历史请求适合措辞不同但意图相同的请求。三、工程化实现3.1 精确匹配缓存// exact_cache.go package cache import ( crypto/sha256 encoding/hex time ) type ExactCache struct { store map[string]*CacheEntry maxItems int ttl time.Duration } type CacheEntry struct { Response string CreatedAt time.Time HitCount int } func NewExactCache(maxItems int, ttl time.Duration) *ExactCache { return ExactCache{ store: make(map[string]*CacheEntry), maxItems: maxItems, ttl: ttl, } } // 生成请求的缓存 Key func (c *ExactCache) generateKey(prompt string, model string) string { h : sha256.New() h.Write([]byte(prompt | model)) return hex.EncodeToString(h.Sum(nil)) } // 查询缓存 func (c *ExactCache) Get(prompt string, model string) (string, bool) { key : c.generateKey(prompt, model) entry, exists : c.store[key] if !exists { return , false } // 检查 TTL if time.Since(entry.CreatedAt) c.ttl { delete(c.store, key) return , false } entry.HitCount return entry.Response, true } // 写入缓存 func (c *ExactCache) Set(prompt string, model string, response string) { // LRU 淘汰超过容量时删除最久未访问的条目 if len(c.store) c.maxItems { c.evictOldest() } key : c.generateKey(prompt, model) c.store[key] CacheEntry{ Response: response, CreatedAt: time.Now(), HitCount: 0, } } func (c *ExactCache) evictOldest() { var oldestKey string var oldestTime time.Time first : true for k, v : range c.store { if first || v.CreatedAt.Before(oldestTime) { oldestKey k oldestTime v.CreatedAt first false } } delete(c.store, oldestKey) }3.2 语义匹配缓存// semantic_cache.go package cache import ( context math sort ) type SemanticCache struct { embeddings []*CacheVector embedder Embedder threshold float64 maxItems int } type CacheVector struct { Prompt string Response string Embedding []float64 CreatedAt int64 } type Embedder interface { Embed(ctx context.Context, text string) ([]float64, error) } type SearchResult struct { Prompt string Response string Similarity float64 } func NewSemanticCache(embedder Embedder, threshold float64, maxItems int) *SemanticCache { return SemanticCache{ embeddings: make([]*CacheVector, 0), embedder: embedder, threshold: threshold, maxItems: maxItems, } } // 语义搜索找到与请求最相似的历史缓存 func (c *SemanticCache) Search(ctx context.Context, prompt string) (*SearchResult, error) { queryVec, err : c.embedder.Embed(ctx, prompt) if err ! nil { return nil, err } var results []SearchResult for _, entry : range c.embeddings { sim : cosineSimilarity(queryVec, entry.Embedding) if sim c.threshold { results append(results, SearchResult{ Prompt: entry.Prompt, Response: entry.Response, Similarity: sim, }) } } if len(results) 0 { return nil, nil } // 返回相似度最高的结果 sort.Slice(results, func(i, j int) bool { return results[i].Similarity results[j].Similarity }) return results[0], nil } // 写入语义缓存 func (c *SemanticCache) Set(ctx context.Context, prompt string, response string) error { vec, err : c.embedder.Embed(ctx, prompt) if err ! nil { return err } if len(c.embeddings) c.maxItems { c.embeddings c.embeddings[1:] } c.embeddings append(c.embeddings, CacheVector{ Prompt: prompt, Response: response, Embedding: vec, CreatedAt: 0, }) return nil } func cosineSimilarity(a, b []float64) float64 { var dot, normA, normB float64 for i : range a { dot a[i] * b[i] normA a[i] * a[i] normB b[i] * b[i] } if normA 0 || normB 0 { return 0 } return dot / (math.Sqrt(normA) * math.Sqrt(normB)) }3.3 两级缓存协调器// cache_coordinator.go package cache import ( context fmt ) type CacheCoordinator struct { exact *ExactCache semantic *SemanticCache } func NewCacheCoordinator( exact *ExactCache, semantic *SemanticCache, ) *CacheCoordinator { return CacheCoordinator{exact: exact, semantic: semantic} } // 查询缓存先精确后语义 func (cc *CacheCoordinator) Get( ctx context.Context, prompt string, model string, ) (string, bool, error) { // 第一层精确匹配 if resp, hit : cc.exact.Get(prompt, model); hit { return resp, true, nil } // 第二层语义匹配 result, err : cc.semantic.Search(ctx, prompt) if err ! nil { return , false, fmt.Errorf(语义搜索失败: %w, err) } if result ! nil { return result.Response, true, nil } return , false, nil } // 写入缓存同时写入两层 func (cc *CacheCoordinator) Set( ctx context.Context, prompt string, model string, response string, ) error { cc.exact.Set(prompt, model, response) if err : cc.semantic.Set(ctx, prompt, response); err ! nil { // 语义缓存写入失败不影响精确缓存 return fmt.Errorf(语义缓存写入失败: %w, err) } return nil }四、Token 缓存的 Trade-offs语义缓存的准确性风险余弦相似度 0.92 的两个请求语义可能接近但答案不同。Python 如何读取 CSV和Python 如何写入 CSV的向量相似度可能超过 0.9但答案完全不同。阈值设置过高会降低命中率过低会返回错误答案。建议对事实性问答使用 0.95 以上的阈值对开放式对话使用 0.90。Embedding 调用的额外成本语义缓存每次查询都需要一次 Embedding 调用虽然比 LLM 便宜约为 1/100但在高 QPS 场景下仍是一笔不小的开销。优化策略是对短 Prompt 50 字优先使用精确缓存只对长 Prompt 启用语义缓存。缓存一致性问题LLM 的回答具有随机性相同 Prompt 可能得到不同答案。缓存命中时返回的是历史答案可能不是最优答案。对于需要准确性的场景如代码生成建议在缓存结果中标注来自缓存并允许用户选择重新生成。缓存淘汰策略的影响LRU 淘汰可能删除高频但时间较早的缓存条目。对于大模型应用建议使用 LFU最不经常使用策略保留高频命中的条目。五、总结Token 缓存与语义去重是大模型后端成本优化的核心手段。精确缓存处理完全相同的请求语义缓存覆盖措辞不同但意图相同的请求。落地路线上建议先实现精确缓存实现简单、零额外成本积累数据后评估语义缓存的命中率再决定是否引入。关键原则缓存命中率比缓存覆盖率更重要宁可少命中也不要返回错误答案。
大模型 Token 缓存与语义去重:后端成本优化的工程实践
发布时间:2026/6/12 9:48:12
大模型 Token 缓存与语义去重后端成本优化的工程实践一、Token 消耗的温水煮青蛙大模型后端的隐性成本大模型应用后端面临一个严峻的成本问题相同或相似的请求被重复发送到 LLM每次都消耗完整的 Token。用户反复询问今天天气如何每次都消耗 500 Token 的 Prompt不同用户询问Python 如何读取 CSV语义相同但措辞不同无法命中缓存每次都重新调用 LLM。按 GPT-4 的定价计算一个日均 10 万次调用的应用如果 30% 的请求可以通过缓存命中每月可节省数千美元。Token 缓存与语义去重是大模型后端从能用走向经济可用的关键优化。二、Token 缓存的分层架构Token 缓存分为两层精确匹配缓存基于请求哈希和语义匹配缓存基于向量相似度。前者命中率高但覆盖窄后者覆盖宽但需要向量检索。flowchart TD A[用户请求] -- B{精确缓存命中} B --|命中| C[直接返回缓存结果] B --|未命中| D{语义缓存命中} D --|相似度 阈值| E[返回缓存结果 微调] D --|未命中| F[调用 LLM] F -- G[结果写入精确缓存] F -- H[请求向量写入语义缓存] G -- I[返回结果] E -- I C -- I精确缓存使用请求的完整 Prompt 哈希作为 Key适合完全相同的重复请求。语义缓存将 Prompt 转换为向量通过余弦相似度查找语义相近的历史请求适合措辞不同但意图相同的请求。三、工程化实现3.1 精确匹配缓存// exact_cache.go package cache import ( crypto/sha256 encoding/hex time ) type ExactCache struct { store map[string]*CacheEntry maxItems int ttl time.Duration } type CacheEntry struct { Response string CreatedAt time.Time HitCount int } func NewExactCache(maxItems int, ttl time.Duration) *ExactCache { return ExactCache{ store: make(map[string]*CacheEntry), maxItems: maxItems, ttl: ttl, } } // 生成请求的缓存 Key func (c *ExactCache) generateKey(prompt string, model string) string { h : sha256.New() h.Write([]byte(prompt | model)) return hex.EncodeToString(h.Sum(nil)) } // 查询缓存 func (c *ExactCache) Get(prompt string, model string) (string, bool) { key : c.generateKey(prompt, model) entry, exists : c.store[key] if !exists { return , false } // 检查 TTL if time.Since(entry.CreatedAt) c.ttl { delete(c.store, key) return , false } entry.HitCount return entry.Response, true } // 写入缓存 func (c *ExactCache) Set(prompt string, model string, response string) { // LRU 淘汰超过容量时删除最久未访问的条目 if len(c.store) c.maxItems { c.evictOldest() } key : c.generateKey(prompt, model) c.store[key] CacheEntry{ Response: response, CreatedAt: time.Now(), HitCount: 0, } } func (c *ExactCache) evictOldest() { var oldestKey string var oldestTime time.Time first : true for k, v : range c.store { if first || v.CreatedAt.Before(oldestTime) { oldestKey k oldestTime v.CreatedAt first false } } delete(c.store, oldestKey) }3.2 语义匹配缓存// semantic_cache.go package cache import ( context math sort ) type SemanticCache struct { embeddings []*CacheVector embedder Embedder threshold float64 maxItems int } type CacheVector struct { Prompt string Response string Embedding []float64 CreatedAt int64 } type Embedder interface { Embed(ctx context.Context, text string) ([]float64, error) } type SearchResult struct { Prompt string Response string Similarity float64 } func NewSemanticCache(embedder Embedder, threshold float64, maxItems int) *SemanticCache { return SemanticCache{ embeddings: make([]*CacheVector, 0), embedder: embedder, threshold: threshold, maxItems: maxItems, } } // 语义搜索找到与请求最相似的历史缓存 func (c *SemanticCache) Search(ctx context.Context, prompt string) (*SearchResult, error) { queryVec, err : c.embedder.Embed(ctx, prompt) if err ! nil { return nil, err } var results []SearchResult for _, entry : range c.embeddings { sim : cosineSimilarity(queryVec, entry.Embedding) if sim c.threshold { results append(results, SearchResult{ Prompt: entry.Prompt, Response: entry.Response, Similarity: sim, }) } } if len(results) 0 { return nil, nil } // 返回相似度最高的结果 sort.Slice(results, func(i, j int) bool { return results[i].Similarity results[j].Similarity }) return results[0], nil } // 写入语义缓存 func (c *SemanticCache) Set(ctx context.Context, prompt string, response string) error { vec, err : c.embedder.Embed(ctx, prompt) if err ! nil { return err } if len(c.embeddings) c.maxItems { c.embeddings c.embeddings[1:] } c.embeddings append(c.embeddings, CacheVector{ Prompt: prompt, Response: response, Embedding: vec, CreatedAt: 0, }) return nil } func cosineSimilarity(a, b []float64) float64 { var dot, normA, normB float64 for i : range a { dot a[i] * b[i] normA a[i] * a[i] normB b[i] * b[i] } if normA 0 || normB 0 { return 0 } return dot / (math.Sqrt(normA) * math.Sqrt(normB)) }3.3 两级缓存协调器// cache_coordinator.go package cache import ( context fmt ) type CacheCoordinator struct { exact *ExactCache semantic *SemanticCache } func NewCacheCoordinator( exact *ExactCache, semantic *SemanticCache, ) *CacheCoordinator { return CacheCoordinator{exact: exact, semantic: semantic} } // 查询缓存先精确后语义 func (cc *CacheCoordinator) Get( ctx context.Context, prompt string, model string, ) (string, bool, error) { // 第一层精确匹配 if resp, hit : cc.exact.Get(prompt, model); hit { return resp, true, nil } // 第二层语义匹配 result, err : cc.semantic.Search(ctx, prompt) if err ! nil { return , false, fmt.Errorf(语义搜索失败: %w, err) } if result ! nil { return result.Response, true, nil } return , false, nil } // 写入缓存同时写入两层 func (cc *CacheCoordinator) Set( ctx context.Context, prompt string, model string, response string, ) error { cc.exact.Set(prompt, model, response) if err : cc.semantic.Set(ctx, prompt, response); err ! nil { // 语义缓存写入失败不影响精确缓存 return fmt.Errorf(语义缓存写入失败: %w, err) } return nil }四、Token 缓存的 Trade-offs语义缓存的准确性风险余弦相似度 0.92 的两个请求语义可能接近但答案不同。Python 如何读取 CSV和Python 如何写入 CSV的向量相似度可能超过 0.9但答案完全不同。阈值设置过高会降低命中率过低会返回错误答案。建议对事实性问答使用 0.95 以上的阈值对开放式对话使用 0.90。Embedding 调用的额外成本语义缓存每次查询都需要一次 Embedding 调用虽然比 LLM 便宜约为 1/100但在高 QPS 场景下仍是一笔不小的开销。优化策略是对短 Prompt 50 字优先使用精确缓存只对长 Prompt 启用语义缓存。缓存一致性问题LLM 的回答具有随机性相同 Prompt 可能得到不同答案。缓存命中时返回的是历史答案可能不是最优答案。对于需要准确性的场景如代码生成建议在缓存结果中标注来自缓存并允许用户选择重新生成。缓存淘汰策略的影响LRU 淘汰可能删除高频但时间较早的缓存条目。对于大模型应用建议使用 LFU最不经常使用策略保留高频命中的条目。五、总结Token 缓存与语义去重是大模型后端成本优化的核心手段。精确缓存处理完全相同的请求语义缓存覆盖措辞不同但意图相同的请求。落地路线上建议先实现精确缓存实现简单、零额外成本积累数据后评估语义缓存的命中率再决定是否引入。关键原则缓存命中率比缓存覆盖率更重要宁可少命中也不要返回错误答案。