新闻详情

新闻详情

首页 / 资讯中心 / 详情

大模型应用缓存实战:从普通缓存到语义缓存

发布时间:2026/10/1 5:37:58来源:尧图网络
大模型应用缓存实战:从普通缓存到语义缓存
大模型应用上线之后最先让你失眠的不是模型能力而是账单和延迟。我在 LangChain 项目里做内部知识库问答助手时每天大概 5000 次请求每次平均 2000 token 输入、1000 token 输出上线两周查账单数字直接把我从椅子上吓了起来。然后我花了大概两周时间把缓存方案从无到有做了三档无缓存、普通缓存、语义缓存逐个压测、逐步上线。这篇就把完整的对比分析和生产落地记录复盘出来适合正在做 LLM 应用降本提速或者准备在 LangChain 服务里加缓存层的同学参考。这档方案的核心差异其实只有一句话什么条件下允许复用过去的大模型回答。无缓存是一律不复用普通缓存是只有一模一样的问题才复用语义缓存是意思相近就直接复用。从这句话出发成本、延迟、误命中、key 设计、失效策略全部都能串起来。1. 先算明白成本账为什么缓存是必选项1.1 一次 LLM 调用到底花多少钱先别急着写代码把账算清楚你才知道缓存值得投入多少精力。LLM 的计费几乎都是按 token 算而且输入和输出分开计价。假设你接的模型定价是输入 0.003 美元/1K token、输出 0.006 美元/1K token一次典型的问答请求按 2000 输入 token 1000 输出 token 来算单次成本 2 x 0.003 1 x 0.006 0.012 美元这个数字看起来很小但乘以业务量就不是回事了。每天 5000 次请求一天就是 60 美元一个月按 22 个工作日算就是 1320 美元一年接近 1.6 万美元。如果你们的模型更贵或者每次请求携带的上下文更长这个数字还会成倍放大。更容易被忽略的是 Agent 场景。一个带工具调用的 Agent用户问一个问题内部可能触发 3 到 5 次 LLM 调用规划一次、调工具时生成参数一次、拿到工具结果后总结一次。也就是说真实账单不是按用户问题的数量算而是按内部完整交互链路算成本系数直接翻好几倍。1.2 延迟不仅是体感问题成本之外是延迟。LLM 的响应时间由两部分决定处理输入 token 的时间和逐字生成输出 token 的时间。输入越长第一个 token 出来的时间越长输出越长整体等待越久。在默认情况下p95 延迟跑到 3 到 5 秒非常常见。这个延迟在内部工具里还能忍放到客服、销售、面对客户的场景里就直接影响转化率。更麻烦的是很多服务端有超时限制LLM 一旦慢下来客户端先等不及断开服务端还得处理超时重试又白烧一次 token。这就是为什么缓存不只是省钱而是在同时解决可用性和成本两个问题。1.3 三档方案的一页纸对比维度无缓存普通缓存语义缓存复用什么条件不复用输入完全相同语义相似度达到阈值命中率参考0%5%-15%模板化场景可到 40%-60%30%-70%FAQ 场景更高命中响应延迟无命中概念1-5ms50-150ms成本节省幅度0低高主要风险成本不可控、延迟不可控几乎没有副作用误命中导致错误回答额外依赖无Redis 或 SQLite向量库 Embedding 模型典型场景强实时、强个性化固定模板、结构化任务知识库问答、客服、政策查询这个表格是我给团队评审时用的底稿。从运营角度普通缓存是“零风险拿一点收益”语义缓存是“控制风险拿大收益”无缓存则是“复杂度最低但什么都不拿”。选哪档取决于你对误回答的容忍度有多高。2. 三档方案原理拆解从裸奔到语义命中2.1 无缓存逻辑简单代价也简单无缓存就是最传统的调用方式请求进来拼好 prompt直接发给 LLM拿到结果返回。逻辑上没有任何中间层代码最简单也最容易排查问题。但并不是所有场景都适合加缓存。我遇到过两类业务必须保持无缓存一类是强实时数据比如查股价、查库存、查订单状态答案每次都在变缓存只会返回过期信息另一类是强个性化内容比如根据用户实时行为生成的推荐理由同一个问题对不同人要有不同答案。无缓存不等于完全裸奔。就算不做缓存仍然可以做三件事来控制成本给每条请求设置 max_tokens 上限、对超时和失败做熔断重试、在 prompt 里压缩历史上下文。这些手段和缓存不冲突准确说它们是缓存方案的基础设施后面两档也要靠它们兜底。2.2 普通缓存精确匹配等价的请求只算一次钱普通缓存的原理非常简单把请求的完整 prompt 作为 key把 LLM 的返回结果作为 value 存起来。下一次请求进来key 完全一样就直接返回不一样就继续调用模型。这个方案落地最快。LangChain 内置了 InMemoryCache、SQLiteCache、RedisCache 等实现开发环境用 SQLite 落盘生产环境换 Redis 共享两个实例之间还能互相命中。它的价值在于几乎零风险只有完全相同才会命中不存在答非所问的情况。但普通缓存的命中率通常很低。自然语言表达太灵活了“帮我写个会议纪要”和“请帮我写一份会议纪要”在语义上没有区别在字符串层面却是完全不同的 key缓存完全失效。想让普通缓存发挥价值核心思路不是靠用户输入碰运气而是把 key 收敛成结构化内容。比如把用户问题解析成固定参数再拼成标准模板generate_sqltableordersfilterstatus:paid这种 key 的重复率会大幅提升。或者更直接一点固定问题的答案直接用业务 ID 当 key压根不走完整 prompt。2.3 语义缓存让“相似”的问题共享答案语义缓存是在普通缓存之上加了一层向量检索。请求进来后先把用户问题通过 Embedding 模型转成向量然后去向量库里做相似度检索找到最相似的历史问题当相似度超过阈值时直接返回历史问题的答案。这里的关键点在于语义缓存缓存的是“历史上真实调用模型生成过的答案”而不是像 RAG 那样去外部知识库检索资料再拼 prompt。RAG 解决的是“模型不知道私有知识”的问题语义缓存解决的是“相同问题反复花钱生成同一份答案”的问题。两者可以组合使用RAG 场景下第一次问需要检索加生成第二次问同样的问题语义缓存直接把第一次的最终答案吐出来。这套方案里有两个核心参数。第一个是相似度阈值通常设置在 0.90 到 0.95 之间阈值越低命中率越高但误命中风险也越大。第二个是向量检索的返回数量生产环境一般只取 top1因为缓存场景只需要一个最相似的答案取多了反而增加误判概率。3. LangChain 生产落地实操代码级复现3.1 先做统一的缓存网关层无论选哪一档都不要把缓存逻辑散落在业务代码里。因为缓存逻辑一旦分散你就无法统一控制 TTL、无法观测命中率、无法在故障时快速降级。我建议在业务层和 LLM 调用之间加一个薄薄的调用层所有请求统一走这个入口。这个层对外暴露一个自定义的call_llm(question)方法内部先查缓存命中就返回未命中就调用 LangChain 的 LLM成功后回写缓存。后面要切换缓存档位、调整阈值只需要改这一层。3.2 普通缓存落地先从 SQLite 起步再切 Redis本地开发和单机部署用 LangChain 内置的 SQLiteCache 就够from langchain_community.cache import SQLiteCache from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4o-mini, temperature0) llm.cache SQLiteCache(database_path.llm_cache.db) first llm.invoke(用一句话介绍什么是缓存) second llm.invoke(用一句话介绍什么是缓存) # 直接命中缓存不再消耗 tokenLangChain 的机制是只要给 llm 对象挂上 cache 属性它会在调用前自动查缓存、调用后自动写缓存。key 由内部根据模型名、温度参数、prompt 内容组合生成不需要你手动管理。多实例部署后SQLite 这种本地存储就不行了必须切到 Redis让所有实例共享同一份缓存from langchain_community.cache import RedisCache from redis import Redis redis_client Redis.from_url(redis://localhost:6379/0) llm.cache RedisCache(redisredis_client)不同版本 LangChain 的 RedisCache 构造参数可能有差异有些是 redis有些是 redis_遇到 ImportError 或参数报错时查一下当前 langchain-community 的 API 文档即可不影响整体思路。普通缓存的核心收益是消除完全重复请求。如果业务里重复提问占比不高命中率会让你失望这时候就该考虑语义缓存了。3.3 语义缓存落地自己写一个缓存类LangChain 生态里有语义缓存的第三方实现但我更建议自己封装一层核心原因有两个一是你要完全控制相似度阈值和 TTL 逻辑二是你需要在写缓存前对答案做业务校验第三方实现通常无法覆盖这些定制逻辑。下面是一个基于 RedisSearch 的语义缓存实现核心逻辑分三块向量化、相似度检索、结果回写。import hashlib import json import numpy as np from redis import Redis from redis.commands.search.field import TextField, VectorField from redis.commands.search.query import Query from redis.commands.search.indexDefinition import IndexDefinition, IndexType class SemanticCache: def __init__(self, embedding, redis, threshold0.92, ttl3600): self.embedding embedding self.redis redis self.threshold threshold self.ttl ttl self._ensure_index() def _ensure_index(self): try: self.redis.ft(semantic_idx).info() except Exception: schema ( TextField(question), VectorField(vec, HNSW, { TYPE: FLOAT32, DIM: 1536, DISTANCE_METRIC: COSINE, }), ) self.redis.ft(semantic_idx).create_index( schema, definitionIndexDefinition( prefix[semantic:], index_typeIndexType.HASH, ), ) def _embed(self, text): return np.array(self.embedding.embed_query(text), dtypefloat32) def get(self, question): vec self._embed(question) result self.redis.ft(semantic_idx).search( Query(*[KNN 1 vec $vec AS score]) .sort_by(score) .return_fields(question, answer, score) .dialect(2), query_params{vec: vec.tobytes()}, ) if not result.docs: return None doc result.docs[0] similarity 1 - float(doc.score) # 余弦距离转相似度 if similarity self.threshold: return json.loads(doc.answer) return None def set(self, question, answer): vec self._embed(question) key fsemantic:{hashlib.md5(question.encode()).hexdigest()} self.redis.hset(key, mapping{ question: question, vec: vec.tobytes(), answer: json.dumps(answer), }) self.redis.expire(key, self.ttl)这段代码有几个细节值得展开第一向量的维度必须和 Embedding 模型输出维度一致我写的是 1536对应 OpenAI 的部分 Embedding 模型如果你换了模型要同步改 DIM。第二HNSW 是近似最近邻索引检索速度快但索引构建需要一定内存数据量小时也可以换 FLAT。第三RedisSearch 的 KNN 查询返回的 score 是距离cosine 距离越小越相似所以我要转成相似度再和 threshold 比较这里是最容易写错的地方。使用方式很简单cache SemanticCache(embeddingembeddings, redisredis_client, threshold0.92, ttl3600) answer cache.get(question) if answer is None: real_answer llm.invoke(question) cache.set(question, real_answer.content) answer real_answer.content实际生产代码里我会再给这个类包一层统一的call_llm入口同时把缓存未命中的 LLM 调用也统一打到网关层这样语义缓存和普通缓存可以按业务路由切换。3.4 压测与收益测算实录我们上线前用 500 条真实用户 query 做了对比压测样本全部来自历史日志保证分布贴近线上。三档结果如下方案命中率未命中 p95 延迟命中延迟无缓存0%3.2s无普通缓存9%3.2s4ms语义缓存46%3.2s120ms按照第一节的账本模型每天 5000 次请求无缓存一天 60 美元。普通缓存命中 9%一天节省约 5.4 美元语义缓存命中 46%一天节省约 27.6 美元。减去 Embedding 调用成本每天 5000 次语义查询的向量化费用相对主模型几乎可以忽略通常不到 1 美元。语义缓存额外带来的收益是延迟下降。命中请求从 3 秒降到 120 毫秒用户的感知从天差地别变成毫无感知服务端超时重试率也跟着降了。4. 生产环境的坑与排查实录4.1 语义缓存误命中最贵的错误语义缓存最大的坑是误命中。我和团队踩过一个典型例子知识库里同时有“怎么开通发票”和“怎么取消发票”两句话的向量非常接近相似度能到 0.91但答案是相反的操作流程。如果阈值设成 0.90第二个问题就会命中第一个问题的答案等于直接把错误操作步骤发给了用户。处理误命中没有银弹我的组合拳是阈值提到 0.93 以上、对包含否定词的 query 单独降级不命中、把缓存命中日志定期抽样人工审查。对于答案稳定性要求极高的金融、医疗场景可以再加一道校验只对有标准答案的业务域开放语义缓存。4.2 缓存 key 设计别拿整段 prompt 当 key普通缓存的 key 如果直接用完整 promptprompt 里随便一个空格差异、标点差异都会导致命中失败。更严重的是大模型调用时的温度参数也会影响结果稳定性temperature0 和 temperature0.7 生成的答案风格完全不同不能共用缓存。我的习惯是缓存 key 至少包含五个维度模型名、温度参数、prompt 模板版本、业务方标识、prompt 内容哈希。多租户系统还要把租户 ID 加进去避免一个租户的问题命中另一个租户的答案。完整 prompt 本体存 valuekey 只放哈希值避免 Redis 里塞进大字符串。4.3 数据一致性知识库更新后缓存怎么办语义缓存最怕知识库更新后旧答案继续被命中。比如公司报销政策变了用户问“报销标准是多少”缓存里还是旧政策的答案TTL 没到之前就一直输出错误内容。TTL 只是兜底手段更可靠的做法是主动失效。我们在缓存条目里额外存了来源文档 ID 列表当文档更新或删除时通过事件驱动把相关缓存条目清理掉。同时Embedding 模型一旦升级新老向量不在同一空间必须按模型版本分 namespace 存储否则相似度计算会全面失真。4.4 缓存系统故障兜底缓存是优化组件不是核心链路。Redis 宕机、向量检索超时都不应该阻塞用户请求。我们线上做了两层兜底第一层是缓存访问超时设置为 50 毫秒超过直接放行到 LLM第二层是对缓存节点做熔断连续失败一定次数后自动降级为无缓存模式。同时监控三个指标缓存命中率、缓存访问延迟、缓存服务健康度。命中率突然下跌往往意味着 key 设计失效或者 namespace 被清空需要立刻告警。5. 三档怎么选我的选型对照表5.1 业务场景速查表业务场景推荐档位理由实时行情、订单状态无缓存数据一秒一变缓存无意义固定格式生成SQL、代码片段、JSON 转换普通缓存输入可参数化命中率可控且零风险企业内部知识库 FAQ语义缓存问法多样但答案固收益最大客服机器人语义缓存 普通缓存双层先用普通缓存挡重复再用语义缓存挡相似强个性化推荐文案无缓存答案必须按人变化缓存会伤体验判断逻辑其实很简单先问自己答案会不会经常变会变就无缓存再问用户表达是不是高度一致一致就普通缓存如果答案稳定但问法飘忽语义缓存就是你的菜。5.2 上线节奏与扩展空间不要一上来就上语义缓存。我的建议是按照“无缓存采集数据 - 普通缓存热身 - 语义缓存增量”的节奏推进。第一阶段先把请求日志打全用历史数据估算命中率空间至少攒两到四周第二阶段上普通缓存改动最小先吃到重复请求的收益第三阶段再上语义缓存用真实样本调阈值灰度观察一周没有误命中案例再全量放开。这个内容后续还可以接着扩展比如按请求类型做多级缓存路由、把缓存命中结果再压缩存储、在网关层做模型自动路由。但所有扩展都要回到最开始那本账先算清楚成本和延迟再决定值不值得做。缓存不是越高级越好而是越匹配业务越划算。最后说一点我个人的体会。语义缓存上线那天我看到自动挡掉的请求数量时非常兴奋但第二天就出了误命中案例当场把阈值往上调了两档。踩过几次坑之后我现在对缓存的态度是先想清楚验证问题再决定要不要上高级方案。普通的精确匹配缓存虽然收益小但永远不会给你惹事语义缓存是把双刃剑用好了省一半钱用不好就是事故现场。希望这篇复盘能帮你少走我走过的弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

FRI 与 KG-TOWER 二次开发教程(20):收官——FRI/KG-TOWER 二次开发水力学核算工具包(完整项目) 2026/10/1 11:06:33

FRI 与 KG-TOWER 二次开发教程(20):收官——FRI/KG-TOWER 二次开发水力学核算工具包(完整项目)

FRI 与 KG-TOWER 二次开发教程(20):收官——FRI/KG-TOWER 二次开发水力学核算工具包(完整项目)版本与事实声明 项目名 fri_kt,版本 0.1.0;环境:Python 3.8,numpy 2.2.6、…

阅读更多 →
AI+CAD落地实战:从DXF/DWG解析到FreeCAD批量改图的工程避坑指南 2026/10/1 11:06:33

AI+CAD落地实战:从DXF/DWG解析到FreeCAD批量改图的工程避坑指南

1. 为什么“AI CAD”看起来很美,落地却处处碰壁过去两年,我参与过三个跟“AI 辅助 CAD”相关的内部项目,也帮朋友的公司做过几次技术选型评估。一个非常明显的感受是:Demo 满天飞,工程走不通。你在网上能看到大量“上…

阅读更多 →
B3616队列模板题全解:从FIFO到单调队列与消息队列 2026/10/1 11:06:32

B3616队列模板题全解:从FIFO到单调队列与消息队列

刷过洛谷“模板”系列的人,大概率都跟这道 B3616 打过照面。它挂着“【模板】队列”的名头,看起来就是一道入门的不能再入门的裸题,但很多新手恰恰就是在这里翻了车——不是不会队列,而是不会“正确地模拟队列”。这道题表面上在考…

阅读更多 →
JDK 8u131安装与生产环境适配实战指南 2026/10/1 11:06:26

JDK 8u131安装与生产环境适配实战指南

1. 为什么现在还要讲 JDK 8u131?这不是“古董”吗?JDK 8u131 这个版本,乍一看确实像考古现场——它发布于2017年4月,距今已超七年。但如果你正在维护一套运行在金融核心系统、电力调度平台、大型国企ERP或老版本Spring Boot 1.x微…

阅读更多 →
GAMMA 2020在Ubuntu 20.04安装全指南:从解压到License配置 2026/10/1 11:06:26

GAMMA 2020在Ubuntu 20.04安装全指南:从解压到License配置

用了大半天时间,总算在实验室那台Ubuntu 20.04工作站的坑坑洼洼里把GAMMA 2020版装利索了。这期间朋友问得最多的就是:GAMMA到底怎么装?和网上搜出来的gamma校正是一回事吗?这里先把最关键的结论放在开头:如果你做的是…

阅读更多 →
Redis三件套实战:redis-cli、hiredis与redis-benchmark完全指南 2026/10/1 11:06:12

Redis三件套实战:redis-cli、hiredis与redis-benchmark完全指南

1. 开篇:Redis三件套,到底指的是哪三样如果你跟Redis打交道超过一周,迟早会在各种文档、招聘要求、生产事故复盘里撞见这三个名字:redis-cli、hiredis 和 redis-benchmark。它们偶尔被混为一谈,但实际上分工完全不同。…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉