news 2026/10/3 5:14:56

LLM调用缓存三档架构:从Token计费到语义缓存降本实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LLM调用缓存三档架构:从Token计费到语义缓存降本实践

做LLM中台时间久了会发现一个尴尬的事实:大模型本身的能力天花板没那么快触到,先被击穿的是账单。尤其是RAG类应用和面向C端的Agent服务上线之后,问题几乎都会收敛到同一句疑问——为什么每个请求都在烧钱?之前的项目经历里,我们团队在LangChain体系内把LLM调用缓存做成了三档:无缓存、普通缓存、语义缓存。今天把三档架构的底层原理、工程实现、线上劣化问题和最终收益全部摊开来说,里面有不少是用生产事故换来的经验,建议收藏后细看。

1. 计费机制决定缓存方向:先搞懂Token为什么值得缓存

大部分人第一次接触"缓存"这个概念,习惯性地把它等同于Redis里的KV存储,或者HTTP层的响应缓存。但在LLM场景里,缓存的本质完全不同——它缓存的是"生成的token",而生成token的成本由模型解码阶段决定。

1.1 从Decoder的自回归特性看成本结构

自回归模型在每次生成token时,都需要把已生成的所有token作为上下文重新过一遍网络。这意味着,同一条对话在第十轮被重新请求时,前面九轮的所有token都要再计算一遍注意力。这也是为什么同样的提示词反复调用,成本依然按完整上下文计算——模型没有记忆,每次都是从头来。

从计费角度看,OpenAI、Anthropic、DeepSeek等主流API服务商的定价通常分为输入和输出,而很多国内开源模型的商用计费也采用相同逻辑。当你的应用存在高频重复问题、固定Query、相似语义检索时,如果每次都原样调用模型,等于在替用户重复支付计算成本。

1.2 三种缓存层级的分工逻辑

视频高赞的做法是把缓存分成三个层面。

  • 无缓存:每请求必回源,延迟高、成本全量计算,适合不确定性任务,比如Agent的开放式推理。
  • 普通缓存:Key完全匹配才命中,适合固定标题、固定代码段、规则问答,实现最简单。
  • 语义缓存:通过Embedding向量与距离检索找到"接近"的历史结果,直接复用答案,适合FAQ、客服问答、规范查询等大部分重复度高的生产场景。

这里要强调一个很多人混淆的点:语义缓存不只是缓存向量,它缓存的是"问题与答案的映射"。向量只是用来做相似度检索的索引,真正压成本的是复用生成好的答案文本。

1.3 开源模型场景的额外收益

如果你用的是自部署开源模型,缓存的意义更大——不只是省API费用,还能省显存算力。自建模型服务的QPS和并发余量本来就不高,接入语义缓存后,同样的显存预算能支撑更高吞吐,哪怕是一次请求的命中也能让整体RT(响应时间)从六秒降到几十毫秒级别。

提示:做成本分析之前,先用一个月的真实访问日志跑一遍文本相似度聚类。通常会发现30%-40%的请求在语义层面高度重复,这部分流量才是缓存的核心目标。不做这一步直接上缓存,属于盲人摸象。

2. 无缓存与普通缓存:LangChain调用链上的直连与Key碰撞

先看两条最普遍的路:直接透传与普通缓存。前者只是作为对照组存在——所有请求不加任何Cache中间件,每次原文到达LangChain的.invoke()入口后完整走一遍LLM调用。这套逻辑非常适合开发调试和写Demo,问题是生产环境没人敢这么跑。

2.1 普通缓存的LangChain实现路径

LangChain核心库其实内置了简单的缓存接口,最常用的是langchain.cache.InMemoryCache和RedisCache。前者只存在于进程内存,多实例场景命中率极低,不推荐生产使用;后者可落盘且支持分布式共享。

from langchain.cache import RedisCache from langchain.globals import set_llm_cache import redis redis_client = redis.Redis( host="your-redis-host", port=6379, decode_responses=True ) cache = RedisCache(redis_=redis_client) set_llm_cache(cache)

设置了全局缓存后,LangChain在前置阶段会直接检查请求对应的Prompt字符串哈希,如果哈希命中就直接返回缓存的生成结果,完全不进入模型推理。

这条路的坑在于:普通缓存的Key是Prompt的原始文本,语序变一下、标点符号多一个空格,哈希就变得完全不匹配了。实测中,一段FAQ内容只要稍微改写提问方式,命中率直接掉到5%以下。

2.2 普通缓存的"数据污染"问题

更隐蔽的问题是合租效应。LangChain默认的通用缓存会把系统提示词、历史对话记录全拼进同一个Key里面,一旦你的系统提示词做过一次小版本升级,全量缓存几乎瞬间失效。而且这种全局弱缓存并没有区分不同用户、不同上下文,一个用户把回答改了会污染另一个用户的缓存结果。

想在生产环境用好普通缓存,至少要做三件事:

  • 缓存Key加上版本号、模型名、温度参数。
  • 只对用户输入的正文做正则归一化,剥离多余空白和不可见字符。
  • 对Prompt做hash前,先过滤掉动态时间戳这类不可控变量。

即使做完了这三步,普通缓存的命中率依然受限于"完全相同"这个硬条件。这也是为什么项目上线三周后,我们最终把主力切到了语义缓存。

2.3 适合普通缓存的几种高确定性场景

普通缓存不算鸡肋,它最匹配的是结构化程度极高、几乎不允许泛化的任务。比如:把自然语言转成固定SQL的模板、前端代码规范检测、版本化接口文档的问答。这些场景本身就不需要语义理解,精确匹配带来的零误判是最大优点。而且普通缓存的延迟极低,不需要额外调向量数据库,整体架构最轻,特别适合边缘服务快速介入。

3. 语义缓存核心设计:Embedding选型与Redis向量检索的碰撞

语义缓存的关键在于:提问的文字不同,但语义相近。我们需要的是一把"语义尺子",把两句完全不同的中文问法映射到同一个向量空间中的邻近区域,然后用相似度阈值决定是否复用历史答案。

3.1 Embedding模型怎么选

中文语义相似度场景下,我踩过几类模型的坑,最终留下来的方案是BGE系列和text-embedding系列。

模型维度中文效果显存占用备注
BGE-large-zh-v1.51024优秀约1.3GB中文场景首选,带指令前缀更准
BGE-small-zh-v1.5512良好约300MB性价比高,适合轻量部署
text-embedding-3-small1536优秀(在线)不占本地API调用,数据出网需注意
SentenceTransformer全系列384~1024因模型而异小多语言通用,做二次微调方便

需要说明的是,Embedding模型的维度与检索效果不是简单正比关系。BGE-large在长尾语义上的区分度明显优于small,但对CPU和内存的占用也同步提升。如果你的业务QPS不高,我建议直接用BGE-large,换来的是更低的误召回率;如果是高并发边缘节点,优先考虑small级模型加量化加速,毕竟一个请求多花30毫秒做embedding,整体收益会被稀释。

3.2 Redis向量检索到底怎么配置

语义缓存的存储层我选了Redis,因为大多数公司已经把Redis作为基础设施,不需要额外引入专用向量库。Redis从8.0版本开始原生支持向量数据类型,通过FT.CREATE命令建立HNSW索引,实现近邻检索。

下面是生产可用的缓存中间件示例。

import redis import numpy as np from sentence_transformers import SentenceTransformer class SemanticCache: def __init__(self, redis_url, model_name="BAAI/bge-large-zh-v1.5"): self.r = redis.from_url(redis_url, decode_responses=False) self.model = SentenceTransformer(model_name) self.sim_threshold = 0.86 def _embed(self, text): return self.model.encode(text, normalize_embeddings=True).astype(np.float32).tobytes() def get(self, query): query_vec = self._embed(query) # 模拟向量检索,生产建议使用 RediSearch 的 KNN 语法 res = self.r.execute_command( "FT.SEARCH", "llm_cache_idx", f"*=>[KNN 3 @embedding $vec AS score]", "PARAMS", "2", "vec", query_vec, "RETURN", "2", "answer", "score", "SORTBY", "score", "ASC" ) if res and res[0] > 0: # score 越小越相似,这里将距离转相似度 dist = float(res[1]["score"]) sim = 1 - dist if sim >= self.sim_threshold: return res[1]["answer"] return None def set(self, query, answer): query_vec = self._embed(query) self.r.execute_command( "FT.CREATE", "llm_cache_idx", "ON", "HASH", "PREFIX", "1", "llm_cache:", "SCHEMA", "answer", "TEXT", "embedding", "VECTOR", "HNSW", "6", "TYPE", "FLOAT32", "DIM", "1024", "DISTANCE_METRIC", "COSINE" ) # 实际写入逻辑,为避免重复建索引需做幂等处理 self.r.hset( f"llm_cache:{hash(query)}", mapping={"answer": answer, "embedding": query_vec} )

这段代码里最需要关注的是sim_threshold这个阈值,它直接决定缓存命中率和正确率的平衡。阈值过高,语义缓存退化成普通缓存,命中率上不去;阈值过低,大量不相关内容被误判为同一问题,返回的结果牛头不对马嘴,用户反馈会炸。

提示:不同业务场景相似度阈值差异极大。客服问答的FAQ,0.85阈值下命中率表现良好;而法律条款咨询、医疗建议必须把阈值拉到0.93以上,宁可少命中也不允许返回错误信息。

3.3 何时该引入独立向量数据库专用组件

Redis在中等规模负载下表现稳定,但如果你满足以下条件之一,需要重新考虑专用向量库:

  • 缓存条数超过千万级,向量内存开始吃紧。
  • 需要按业务线隔离索引,并对不同业务线动态调整阈值。
  • 对查询并发有极高性能要求,需靠GPU加速向量检索。
  • 历史缓存需要周期性离线重排、清洗、去重,而Redis的聚合能力太弱。

我们的线上架构里,Redis只是实时查询链路的入口,热数据全量在Redis,冷数据存在对象存储中,需要时再异步回填——这种混合架构比单库存全部数据更可控。

4. 生产级语义缓存的落地细节:从LangChain中间件到一致性保障

把语义缓存从Demo变成线上中间件,远不是往LangChain里挂一个缓存对象那么简单。需要处理的是与业务形态匹配的完整链路。

4.1 缓存中间件该加在哪个环节

很多人把缓存对象挂在ChatOpenAI层面,这样虽然能拦截重复请求,但粒度较粗。我们最终采用的方案是在chain级别插入一个自定义中间层。

from langchain_core.runnables import RunnableLambda def cached_chain(chain, semantic_cache): def invoke_with_cache(inputs): question = inputs["question"] cached_answer = semantic_cache.get(question) if cached_answer: return {"answer": cached_answer, "cache_hit": True} result = chain.invoke(inputs) semantic_cache.set(question, result["answer"]) return {"answer": result["answer"], "cache_hit": False} return RunnableLambda(invoke_with_cache)

这样写的原因是:Chain层的输入输出结构更贴近业务语义,比如对话系统通常传的是question和session_id,直接在LLM层缓存会混入很多上下文无关变量。而且中间层可以自由选择哪些字段参与Cache Key,哪些字段不参与——例如用户ID、会话ID都不应该进缓存Key,但问题正文必须保留。

4.2 多级缓存策略的工程实现

线上生产不能只靠一层语义缓存,我们实际搭建的是四级缓存体系。

  • 本地内存缓存:用LRU存最近几百条高频问答,QPS峰值时由它扛第一波流量,毫秒级响应。
  • 语义缓存主层:Redis承载,主要覆盖中低频请求,解决单机内存放不下全部缓存的问题。
  • 普通缓存兜底:对完全相同的关键Query再一次精确匹配,减少语义层embedding计算开销。
  • 回源保护:以上三层全部未命中才允许进入LLM调用链,并做了并发合并——同一个问题同时被100个用户问,只允许一个请求回源,剩下99个等结果返回后直接读缓存。

多级缓存里最容易被忽略的是缓存淘汰策略。我们设定语义缓存的有效期为一周,但热问题会让有效期自动延长,核心思路是"越多人问,活得越久"。同时每个缓存条目要有来源标记、响应时间、token消耗,用于成本核算和效果分析。

4.3 缓存一致性:避免"脏数据"和"陈旧答案"

LLM应用的缓存与数据库缓存最大的区别在于:LLM的答案不保证完全确定性。同一问题在模型升级前后,返回质量可能完全不同。因此缓存一致性问题不是"数据过期",而是"答案是否还是当前业务想要的答案"。

处理方案是给每条缓存附上三个版本字段:模型版本、缓存时间、人工复审状态。当模型升级后,系统自动按时间段逐步过期旧缓存,而不是一次性全部清除——一次性清空会瞬间把所有流量压到底层LLM,容易触发限流甚至雪崩。

另一个关键点是:用户长期多轮对话里的缓存应该有上下文范围约束。我们的做法是在语义缓存记录里额外保存上下文摘要向量,只有在当前对话摘要与缓存摘要的相似度也达到阈值时,才允许复用答案。这样能避免跨场景的"串味"回答。

5. 线上性能实测:无缓存、普通缓存、语义缓存的真实成本分歧

理论说得再多,不如直接看线上数据。以下数据来自我们生产环境一个日活约5万的RAG类问答服务,运行了七天。

5.1 响应时间与Token消耗的对照情况

指标无缓存普通缓存(命中率约50%)语义缓存(阈值0.86)
P50响应时间6.2s3.4s0.3s
P95响应时间12.8s7.1s2.1s
平均输出Token/请求420260130
日消耗Token总量约380万约210万约110万
命中率0%47%68%

同一个业务,语义缓存能把日Token消耗砍到无缓存的30%左右,同时响应时间下降一个数量级。需要承认的是,语义缓存并不能完全替代普通缓存——因为语义层的相似度匹配可能会导致部分Query的有效信息变化却不被察觉,比如"价格多少"和"价格包含什么"在向量空间里距离过近。

5.2 置信度分布才是利润源泉

很多人只看平均命中率,其实更值得看的是置信度分布。我们统计发现,语义缓存命中的答案中,相似度在0.93以上的区间,人工复审的采纳率能超过95%;而相似度在0.86-0.90区间的采纳率只有72%。因此线上配置时,我们把相似度低于0.90的命中结果都强制标记为"需人工二次校验",不对用户直接展示。

另外,将缓存命中的响应标记为cache_hit=True后,可以单独做一轮"缓存答案质量消歧"。方法是定期抽取缓存命中的问题,令模型以"是否完整、是否准确、是否匹配用户意图"三个维度重新评分,不达标的缓存直接删除,同时降低对应区域相似度阈值。

5.3 成本收益怎么算才科学

计算语义缓存的投入产出,不能只算API费用。我给出一个通用收益模型:

假设每次未命中请求的平均成本是C_miss,命中请求省下的成本是C_save,语义缓存的综合成本包含Embedding计算费用、向量存储费用、缓存维护与人工校验成本。当满足公式C_save * HIT_RATE > C_miss * (1 - HIT_RATE) + C_maintain时,投入为正收益。

实际我们量化下来,单日成本从无缓存的约75美元降到语义缓存后的约30美元,再叠加多级缓存和并发合并,最终日成本约23美元,节省比例近70%。

6. 语义缓存的适用边界与误用红线

把语义缓存吹得天花乱坠的同时,我必须把它的边界说透。它不是一个万能缓存,使用不当反而会引入严重问题。

6.1 不适用语义缓存的场景清单

  • 强时效性内容:股票行情、天气预警、新闻热点、抢购信息。用户问十秒前的"当前价格"和十秒后的"当前价格",语义完全相同,但答案完全不能复用,这类必须强制绕过缓存。
  • 个性化强规则内容:用户问"我家楼下有什么好吃的推荐",不同人的位置、口味偏好完全不同,直接用语义缓存会导致"张三的回答给李四"。
  • 多轮任务型对话:Agent的每一步action依赖前序状态,即使自然语言表达相同,上下文状态不同时执行路径也不同,需要把状态签名纳入缓存Key判断。
  • 合规要求严格的内容:金融、医疗、法律等领域一旦返回陈旧或错误的缓存答案,风险远大于省下的几千块成本。

6.2 我们内部的红线规则

在生产系统里,我们为缓存中间件设置了如下强制约束逻辑。

首先,缓存只允许作用于幂等性自然的场景,即"同样的问题,在任何时间都期望得到同样可靠答案"的知识问答。其次,任何缓存层都不允许绕过安全审核——入库前,答案文本必须经过内容合规检查;出库前,同样需要经历一遍脱敏和合规扫描,防止历史敏感信息卷土重来。最后,全部缓存行为必须有审计日志,一旦用户投诉,可以按缓存时间、命中路径和原始模型调用记录完成全链路回溯。

7. 从实际运营中总结的经验清单

整轮改造下来,把值得记住的几点单独列出来,这些是用真金白银换来的。

关于命中率:不要迷信语义缓存的命中率能无限提高——当阈值调到0.92以上,最理想命中也就在50%左右。合理目标应该定在"比普通缓存高20个百分点且不引入明显误差",而不是追求90%以上命中。

关于冷启动:新业务上线第一天,缓存是空的,前一周命中率会非常难看。建议上线前用历史日志做一次离线预填充,用真实提问语料批量生成缓存条目,把冷启动周期压缩到几个小时。

关于可观测性:缓存不是一锤子买卖,线上监控必须覆盖命中率、回源率、缓存落后时间、语义相似度分布、缓存条目增长率五个核心指标。我在LangSmith之外另外配置了Prometheus和Grafana,统一做仪表盘,任何一项指标异常都能第一时间拉到告警。

关于阈值调参:语义相似度阈值需要按业务模块分桶配置,不能全局一刀切。同一个应用里,FAQ问答用0.85,复杂技术咨询用0.92,内容合规审查用0.95,把不同风险等级的答案用不同置信度门槛隔离,才是最优策略。

最后再分享一个小技巧:给用户的提问做轻量归一化再进入Embedding模型,能显著提升语义缓存的稳定性。比如中文全角半角统一转换为半角,问题里的连字符、下划线全部按空格替换,数字保留占位符(把"3000元"替换为"[NUM]元"),这部分预处理不会消歧义,但可以让向量检索的精度提高三五个百分点。

这套三档缓存体系上线后,我们LLM网关的整体响应成本下降了接近七成。如果你的业务正被高频重复请求压得喘不过气,先从日志里找出最热门的50个问题开始,手动构造它们的语义缓存条目,很快就能感受到从"每次都要问模型"到"问一次,用N次"的转变。

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

QMC5883L磁力计完全指南:从寄存器配置到校准与替换

如果你玩过电子罗盘、做过自动导航小车,或者只是去淘宝搜过“磁力计模块”,大概率见过这个长相和经典板子几乎一模一样的蓝色小板。焊着QMC5883L的模块,很多时候和HMC5883L模块引脚排布、封装尺寸都高度接近,价格却便宜一大截。可…

作者头像 李华
网站建设 2026/10/3 5:14:41

用1/6扇区模型算出整周涡轮叶盘模态:循环对称原理与MATLAB实现

把整个涡轮叶片直接拿去做有限元模态分析,是很多刚开始接触旋转机械的人第一反应。叶片表面曲率复杂,根部还要跟轮盘接触,网格一加密自由度轻松上百万,工作站跑一宿未必能出结果。我早期也走过这条路,直到真正把循环对…

作者头像 李华
网站建设 2026/10/3 5:12:55

Python二手车价格预测:从数据清洗到业务可解释建模

简介:本资源是一份面向Python初学者与课程设计学生的二手车价格数据挖掘与预测实战项目,完整覆盖数据清洗、特征工程、模型训练(含回归算法对比)及可视化分析全流程,适合作为期末大作业或课程设计参考。压缩包共27个文…

作者头像 李华
网站建设 2026/10/3 5:11:49

昇腾AI算力解析:从达芬奇架构到CANN与MindSpore实践指南

2020年站在AI产业的路口,手里攥着算力需求的人,多少都经历过一种“没得选”的憋屈。训练要排队,推理要算成本,边缘场景想塞进一个能跑的模型,功耗和体积又卡得死死的。“昇腾路标”这四个字,在那一年被反复…

作者头像 李华
网站建设 2026/10/3 5:11:46

Unity脚本入门:从组件生命周期到性能优化与报错排查

先说个我一直想吐槽的事情:很多刚接触 Unity 的新人,会把“Unity 脚本”理解成“用某门编程语言写代码”。这个理解不能说错,但容易带偏方向。Unity 里的脚本,本质上是挂在场景里某个游戏对象上的“行为组件”——它控制这个对象在…

作者头像 李华