news 2026/6/12 9:47:53

大模型 Token 缓存与语义去重:后端成本优化的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型 Token 缓存与语义去重:后端成本优化的工程实践

大模型 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 缓存与语义去重是大模型后端成本优化的核心手段。精确缓存处理完全相同的请求,语义缓存覆盖措辞不同但意图相同的请求。落地路线上,建议先实现精确缓存(实现简单、零额外成本),积累数据后评估语义缓存的命中率,再决定是否引入。关键原则:缓存命中率比缓存覆盖率更重要,宁可少命中也不要返回错误答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 9:46:01

旋转数组里找数,AI 用二分写了 3 版才写对,差距在哪

读完本文你将了解&#xff1a; 旋转排序数组的二分查找核心思路 | AI 从暴力到最优的 3 版演进 | 面试中的关键边界处理&#x1f4cb; 题目 原题&#xff1a; 给定一个经过旋转的升序数组 nums&#xff08;如 [4,5,6,7,0,1,2]&#xff09;和一个目标值 target&#xff0c;在 O(…

作者头像 李华
网站建设 2026/6/12 9:43:56

X-AnyLabeling一键可用的YOLOX-s轻量ONNX自动标注方案

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;直接放进X-AnyLabeling就能用的YOLOX-s自动标注组合&#xff1a;包含已导出的yolox_s.onnx模型文件和配套yolox_s.yaml配置文件&#xff0c;无需转换、不需编译&#xff0c;复制到软件models目录后&#xff0c;…

作者头像 李华
网站建设 2026/6/12 9:39:52

推荐系统范式迁移:从预测喜欢到支持决策

1. 项目概述&#xff1a;这不是在调参&#xff0c;是在重建推荐系统的底层认知“Reimagining the Recommendation Engine”——这个标题里没有一个技术名词&#xff0c;却比任何“TransformerGraphSAGEMulti-Task Learning”的堆砌更让人脊背一紧。我在电商中台干了七年推荐系统…

作者头像 李华
网站建设 2026/6/12 9:38:16

三万随便用 +AI Copilot,报表工具从商业到技术的双重颠覆

引子&#xff1a;两个场景&#xff0c;一场关于成本的噩梦与渴望 场景一&#xff1a;好消息变坏消息的报表成本失控案例 好消息是&#xff1a;今年新签了 8 个项目&#xff0c;要部署 20 个节点 坏消息是&#xff1a;每个节点都需要买报表工具&#xff0c;一套大几万&#xff…

作者头像 李华
网站建设 2026/6/12 9:36:54

3步掌握碧蓝航线自动化脚本:让游戏回归乐趣

3步掌握碧蓝航线自动化脚本&#xff1a;让游戏回归乐趣 【免费下载链接】AzurLaneAutoScript Azur Lane bot (CN/EN/JP/TW) 碧蓝航线脚本 | 无缝委托科研&#xff0c;全自动大世界 项目地址: https://gitcode.com/gh_mirrors/az/AzurLaneAutoScript 还在为碧蓝航线中重复…

作者头像 李华
网站建设 2026/6/12 9:36:51

3步配置实现PotPlayer字幕实时翻译:百度翻译插件完全指南

3步配置实现PotPlayer字幕实时翻译&#xff1a;百度翻译插件完全指南 【免费下载链接】PotPlayer_Subtitle_Translate_Baidu PotPlayer 字幕在线翻译插件 - 百度平台 项目地址: https://gitcode.com/gh_mirrors/po/PotPlayer_Subtitle_Translate_Baidu 还在为观看外语视…

作者头像 李华