当前位置: 首页 > news >正文

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

http://www.jsqmd.com/news/997956/

相关文章:

  • 从‘数1’程序看LC-3架构:机器码如何操控CPU与内存?
  • 告别消息撤回遗憾:PC版微信QQ防撤回补丁终极指南
  • 从‘买不到票’到‘看到幽灵票’:一个订票系统的崩溃现场,带你理解CAP定理中的A和C
  • 旋转数组里找数,AI 用二分写了 3 版才写对,差距在哪
  • 从 0 到 1 搭一个合同审查 Agent:流程、Prompt、规则库全拆解
  • 避开EMC坑:从原理图到PCB,详解伺服驱动器接口滤波的布局布线要点
  • ArcMap结合PPT绘制学术论文多图幅研究区域示意图全流程解析
  • 3步实现电话号码地理位置查询的完整解决方案
  • 肿瘤临床AI落地实践:GPT-4在Dana-Farber的三层隔离与工作流嵌入
  • 机器学习模型上线后的真实风险与生产级治理实践
  • 别再死记硬背CAP定理了!用Redis、Eureka和RocketMQ的实战例子,5分钟搞懂CP和AP怎么选
  • Mythos:面向可验证叙事的AI世界状态建模技术
  • MATLAB机器人关节S型轨迹生成工具:自动适配运动约束的七段式速度规划
  • i.MX6ULL平台libmodbus 3.1.6交叉编译实操资源包(含补丁说明与完整构建脚本)
  • 别再傻傻分不清了!HarmonyOS 5.0、NEXT、API Level到底啥关系?一张图给你讲明白
  • 西安汽车价格密采找谁?云岭调查专攻 4S 店破价暗访
  • 告别“黑边”困扰!动态调整滤波窗口的EIS防抖策略详解与效果对比
  • 2026年苏州工作服定做源头厂家测评:五大厂商技术服务深度解析 - 资讯快报
  • Spring Boot 3 虚拟线程与响应式编程:从线程池到协程的范式迁移
  • Mythos状态化推理引擎:解锁多步逻辑与跨文档一致性
  • # 2026年国内绿化公司实力排行榜:长三角等地口碑优质,基于绿化行业市场的5大权威推荐榜单 - 十大品牌榜
  • HoRain云--Rust 面向对象
  • 2026年安徽合肥理工学校寿春实验班怎么样?在哪报名?官网最新发布 - 小张zc
  • 2026华东地区吨袋投料站厂家测评:五大头部厂商技术与应用解析 - 资讯快报
  • 拆解一个充电宝,聊聊DW01-A这颗‘电池保姆’芯片是如何工作的
  • Spring Cloud Gateway 的 SpEL 表达式注入漏洞(CVE-2022-22947)
  • 对“麦克斯韦方程组与世毫九IGP/SRC理论关系论断”的深入研究报告(世毫九实验室原创研究)
  • 别再怕牛顿法发散!手把手教你用Python实现带下山因子的稳定求解(附完整代码)
  • 国际中文教师考点与培训选择指南:北京言汉汉语考点业务真实性 - 资讯快报
  • 2026证件照换底色保姆级教程:这4款免费软件最好用(附详细步骤) - 办公小帮手