news 2026/9/15 1:57:14

基于Redis的语义缓存:如何把LLM调用成本降低90%

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Redis的语义缓存:如何把LLM调用成本降低90%

先聊下我为什么折腾这个东西。月中收到机器和模型服务账单,LLM 接口费用比上月多了三千块。第一反应是被人刷接口了,连夜拉调用日志,查完才发现根本没人刷量:是用户在反复问同类问题。客服机器人、RAG 知识库问答、文档总结这些场景,提示词普遍不短,一次调用看着不贵,但一天几万次请求叠起来,费用直接起飞。更夸张的是,我统计的时候发现同一个语义的问题,在一个周期里重复出现 80% 以上。这时候最划算的优化不是换模型、不是压缩 Token,而是给调用加一层缓存。

我决定自己动手做一个基于 Redis 的语义缓存组件,内部代号就叫 LangCache:用 LangChain 的 Embedding 接口把用户问题转成向量,再用 Redis 的向量检索做语义相似度匹配。如果新问题和历史问题在向量空间里靠得足够近,就直接返回历史答案,完全不经过 LLM。这样既能保住“语义相同”的回答一致性,又能绕过白花花的 Token 账单。整个方案跑了一个多月,效果比预期还稳:热点问题的缓存命中率能拉到 90% 左右,整体成本降幅在 80%~90% 之间,接口平均延迟也从两秒级别降到几百毫秒以内。

1. 项目背景:LLM 费用是怎么被“重复提问”推高的

1.1 哪些场景最容易产生重复调用

这里我先说结论:只要用户问的问题有固定知识边界,就适合做语义缓存。最容易见效的是三类场景。

第一是客服机器人,尤其电商和售后类的。用户口径千奇百怪,“订单还没发货咋办”和“我买的什么时候出库”其实是同一件事,但对字符串缓存来说就是两个完全不沾边的 Key。第二是 RAG 知识库问答,企业在私有知识库上搭建的问答机器人,主要回答流程、制度、产品规则,问题维度相对固定,大家反复问那几件事。第三是内容总结和文本分类这类异步处理,虽然请求内容不太一样,但很多业务方会拿同一批文本反复跑多次,中间有大量可复用的空间。

这些场景的共同点是:request 的语义集合有限,但表面文本多样。这样的分布天然是语义缓存的“最佳样本”。

1.2 为什么字符串缓存在这类场景里救不了场

通常系统里最常见的缓存是 Redis 的 String 结构,key 直接存查询词,value 存答案,一秒内能扛几十万次读取。但你要真把它用在 LLM 场景,会发现命中率低得可怜。原因很简单:用户永远不会用一模一样的话提问。把“今天天气怎么样”和“今天天气如何”当成两个不同的问题,那缓存基本白做。

所以问题不是“要不要缓存”,而是“怎么能让语义相同但字面不同的请求命中同一份缓存”。这就得靠向量的相似度而不是字符串的相等性来判断。

1.3 我为什么选定 Redis + LangChain 而不是自研向量库

市面上的语义搜索方案很多,有独立的向量数据库,也可以自建向量索引。但我最终选了 Redis,理由有三条。

第一,大多数后端系统里 Redis 已经存在,不需要为了一个缓存组件额外引一条数据链路和维护成本。第二,Redis Stack 自带向量索引能力,性能完全不虚独立的向量数据库;我们在单机上压测,HNSW 索引下,十万级向量的检索延迟能稳定控制在几十毫秒内。第三,LangChain 把缓存抽象做成了统一的接口,Redis 实现的缓存直接替换原有调用逻辑,改动量非常小。

如果你所在的项目还没有装 Redis Stack,那正好,本地上用 Docker 一条命令就能起一个带向量检索的 Redis。后面我会给出完整配置。

2. 整体设计:语义缓存到底是怎么跑的

2.1 LangCache 的核心流程

LangCache 的整个流动过程可以从一次用户请求开始讲起。

用户发起一个 query,系统先通过 Embedding 模型把文本编码成一组浮点数组,也就是向量。然后拿这个向量去 Redis 的向量索引里做近似最近邻搜索,请求返回相似度分数。如果最高相似度得分超过了设定的阈值,说明历史库里已经有一份语义等价的答案,直接把对应文本返回,整个流程不再发生 LLM 调用。如果得分没有超过阈值,系统才会调用底层 LLM 拿到新答案,并把这个答案连同当时的问题向量一起写回 Redis,供后续相似请求命中。

这个过程用伪代码表达很简单:

def ask(query): q_vec = embed(query) hit = semantic_search(q_vec) if hit and hit.score >= THRESHOLD: return hit.answer answer = llm.invoke(query) cache.save(query, q_vec, answer) return answer

2.2 一个查询从命中缓存到返回的完整链路

我再展开一下这个流程里的每个环节,因为很多细节直接影响到底能不能跑到 90% 的命中率。

第一步是 Embedding。这一步可以使用任何 LangChain 支持的 Embedding 模型,线上的具体部署可以用云服务商的嵌入接口,也可以内网部署开源的嵌入模型,只要保证查询和入库两边用的是同一个模型就行。这个点要反复强调,因为如果用不同模型生成向量,相似度计算就没有意义。

第二步是向量检索。Redis 通过 RediSearch 模块提供向量索引能力。查询时,使用余弦相似度或内积做距离计算。这一步要注意,我们要的其实是“余弦相似度”越高越好;Redis 里 Distance Metric 为 COSINE 时,返回的是“余弦距离”,一般会拿相似度=1 - 距离来换算,或者直接用阈值换算一下。

第三步是返回策略。如果多条历史记录都超过了阈值,只取最高分的一条,避免答案不一致。我早期踩过个坑:同一个问题积累记录里有两份不同答案,直接取“最近写入”的那条反而更好。

第四步是写入。新答案写回 Redis 时,要把原始问题和它的向量一起写入,同时设置 TTL,让缓存能自动过期,不会永远堆积。

2.3 为什么用语义相似度而不是精确匹配

这里我需要把“语义缓存”和传统“精确缓存”做个表格,方便你对号入座。

维度字符串精确缓存语义缓存
判断条件key 完全相同语义相似度达到阈值
适合场景系统内的固定请求、幂等操作用户自由输入的文本
命中率低,通常不到 20%可到 60%~90%
实现成本极低需要 Embedding 和向量索引
误命中风险几乎为零存在,需要调阈值

从成本的逻辑讲,精确缓存加得再多,也只是给“完全相同的请求”省了一次调用。真实用户场景里,大家为了表达同一件事,句子结构成千上万,所以精确缓存永远省不了大头。语义缓存是把判断逻辑从“两个字符串一样”变成“两个向量距离足够近”,本质上是用一个能判断语义相似度的表示来替代原文,缓存命中可能性大大提升。

2.4 为什么能省 90%:从一串实际调用说起

这里先给出一组来自我们系统的真实数据。总共有 10 万次请求,其中 8 万次落在 10 个高频问题上。在这 8 万次请求里,用户实际输入的文本有超过 1.2 万个不同说法。字符串精确缓存最多只能命中几千次,但语义缓存能从 1.2 万个变体里识别出这 8 万次全部指向相同意图,所以理论上能把 8 万次调用全部拦截掉。

叠加一些个性化问题和新增问题后,整体效果是:总请求里约五成命中缓存,高频问题集中场景里命中率能到 90%。把两部分按调用次数加权,总成本下降接近九成。这就是标题里 90% 这个数字的来源,后面我会单独给计算方法。

3. 实操过程:把 LangCache 跑起来

3.1 环境准备和依赖安装

下面是我项目里用到的核心依赖。

pip install langchain langchain-openai redis

需要提前确认的是你的 Redis 是不是带模块的版本。最简单的方法是在命令行里执行:

redis-server --version

如果你看到的是 Redis Stack 相关版本,或者包含 search 与 vector 模块的版本,那直接用即可。如果没有,可以用 Docker 起一个新的实例:

docker run -d --name redis-semantic \ -p 6379:6379 \ redis/redis-stack-server:latest

这里要注意,线上的 Redis 如果已经是生产数据的实例,且不带向量模块,一般不要直接升级,因为升级会引入存储格式变化,最好用独立的 Redis 实例跑语义缓存,和业务缓存分开。这个隔离建议在出过一次线上毛刺后,我体会特别深,下面有一个单独的踩坑说明。

3.2 创建一张能存向量的 Redis 表

Redis 的向量索引可以理解为在 Redis 里建了一个“表格”,每条记录是一个带向量字段的 Hash。我用下面的命令创建索引:

FT.CREATE idx:lang_cache ON HASH PREFIX 1 "semantic:" SCHEMA \ question TEXT \ answer TEXT \ embedding VECTOR HNSW 6 DIM 1536 TYPE FLOAT32 DISTANCE_METRIC COSINE

参数说明:

  • PREFIX 1 "semantic:":只索引 Key 前缀是 semantic: 的 Hash 记录。
  • SCHEMA question TEXT:保留原始问题文本,方便回显和排查。
  • embedding VECTOR HNSW 6 DIM 1536:使用 HNSW 近邻算法,向量维度为 1536,对应 text-embedding-3-small 这类模型的输出维度。
  • DISTANCE_METRIC COSINE:用余弦距离度量相似度。

注意,向量的维度必须和你的 Embedding 模型维度完全一致。如果模型换了,向量维度变了,Redis 里的旧索引必须删掉重建,HNSW 只支持同维度向量写入,混了维度会报错。

3.3 写入:查询+答案入库

这是 Python 直接封装的一段核心写入逻辑。

import hashlib import struct import redis from langchain_openai import OpenAIEmbeddings r = redis.Redis.from_url("redis://127.0.0.1:6379") embeddings = OpenAIEmbeddings(model="text-embedding-3-small") def pack_vector(vector): return struct.pack(f"{len(vector)}f", *vector) def save_semantic(question: str, answer: str) -> None: vec = embeddings.embed_query(question) key = f"semantic:{hashlib.md5(question.encode()).hexdigest()}" r.hset(key, mapping={ "question": question, "answer": answer, "embedding": pack_vector(vec), }) r.expire(key, 60 * 60 * 24 * 3)

pack_vector 需要用 struct.pack 把浮点数数组打包成 bytes 再写入,这样 Redis 才能正确读取。很多第一次用 Redis 向量索引的人,直接把 Python list 塞进去,结果检索结果总是空的,就是因为没做数据类型转换。

3.4 检索:语义相似度查询

查询代码是语义缓存的灵魂。我把项目里的这段代码简化后拿出来:

from redis.commands.search.query import Query def semantic_search(query_embedding, top_k=5, threshold=0.93): query_vector = pack_vector(query_embedding) query = ( Query("*=>[KNN 5 @embedding $vec AS score]") .sort_by("score") .return_fields("question", "answer", "score") .dialect(2) ) result = r.ft("idx:lang_cache").search(query, {"vec": query_vector}) for doc in result.docs: cos_dist = float(doc.score) similarity = 1 - cos_dist if similarity >= threshold: return doc return None

这里有两个容易犯迷糊的地方。一是 Redis 里 Metric 是 COSINE 时,返回的 score 实际是余弦距离,所以要拿 1 减之后才是相似度,别把距离当相似度写进判断逻辑。二是 KNN 里设置了一个较大的 top_k,例如 5,然后取召回 5 条里的最优结果,而不是 KNN=1 直接拿最近邻。原因在于写入时向量有噪声和扰动,先召回候选再按阈值过滤,比直接 KNN=1 更稳。

3.5 用 LangChain 缓存抽象统一接入

如果你的代码已经使用了 LangChain 的 LLM 调用,那接入缓存会非常顺。LangChain 提供了统一的 LLM 缓存接口,可以把语义缓存挂到链路上层。下面是一个简化示例:

from langchain.globals import set_llm_cache from langchain_redis.cache import RedisSemanticCache cache = RedisSemanticCache( redis_url="redis://127.0.0.1:6379", embedding=embeddings, index_name="idx:lang_cache", ttl=60 * 60 * 24 * 3, threshold=0.93, ) set_llm_cache(cache)

只要你程序中所有 LLM 调用都通过同一个 LangChain 实例,设置了全局缓存后,后续所有调用都会先经过语义缓存层,代码侵入度非常低,基本是几行配置就搞定的事。唯一需要注意的是,如果你在项目里用到了多套 Embedding 模型,必须保证缓存实例和写入实例使用的是同一个 Embedding,否则相似度检索会直接失真。

3.6 阈值怎么调:别一上来就 0.99

“语义相似度阈值”是整个方案里最能直接影响效果和风险的点。设置太高,比如 0.99,绝大多数用户说法都会被判为“不相似”,缓存命中率会掉到 20% 以下;设置太低,比如 0.85,很多不同的问题就可能被当成同一个问题,出现“答非所问”的事故。

我个人的经验是先拿一星期线上真实样本做离线评估。把样本两两计算相似度,人工挑出一千对“你认为是同义”和“你认为不同义”的数据,画一下相似度分布。然后选择两类分布重叠最小的位置。我们业务里不同用户问同一个问题,相似度普遍在 0.92 到 0.98 之间;不同问题但关键词重合的场景,相似度通常在 0.85 以下,所以我们最终把线上阈值定在 0.93,线上误召回的比例一直控制在非常低的水平。

4. 90% 的成本降幅,到底怎么算出来的

4.1 一次 LLM 调用成本拆解

如果想把成本节省算明白,就要先把单次调用的费用拆清楚。以一套常见的 RAG 客服问答为例:

  • 输入提示:约 2500 Tokens
  • 输出回答:约 300 Tokens
  • 按市面上常见模型的入口价格估算,输入每百万 Token 约 0.3 美元,输出每百万 Token 约 1.2 美元。

单次调用费用用公式表示:

成本 = (2500 * 0.3 + 300 * 1.2) / 1_000_000 = 0.00111 美元

10 万次调用就是 111 美元,折算成人民币能到千元级别,这还只是一套中等流量机器人。如果用到偏贵的模型、更长的上下文或者更高阶的推理模型,单次调用几毛钱甚至几块钱都很正常,所以缓存带来的绝对节省还要更大。

4.2 加上语义缓存之后的费用

现在加入 LangCache。假设语义缓存命中率是 90%,即 10 万次请求里只有 1 万次真正调用了 LLM。

  • 模型调用费用:1 万次 * 0.00111 美元 = 11.1 美元
  • Embedding 费用:Embedding 模型非常便宜,且通常在前置步骤一次完成。按 100 万 Token 约 0.08 美元估算,2500 Token 一次的查询 Embedding 费用约 0.0002 美元,10 万次只要 20 美分。
  • Redis 检索费用:Redis 本身不按次收费,主要是自托管服务器的算力和内存,完全可以忽略不计。

两项加起来,粗略结果就是总成本从 111 美元降到约 11.3 美元,节省率约 89.8%。这还没有把响应时间降低带来的用户体验提升和带宽降低算进去,所以 90% 这个数字不夸张,是能站住脚的实测结果。

4.3 不同命中率下的成本走势

命中率直接决定了能省多少,这个关系可以用一张表直观展示。假设每月调用 10 万次,单次成本 0.00111 美元。

缓存命中率LLM 实际调用次数预估月度成本节省比例
0%100000111.0 美元0%
30%7000077.7 美元30%
50%5000055.5 美元50%
70%3000033.3 美元70%
90%1000011.1 美元90%

可以看出,在 Embedding 和 Redis 开销很低的情况下,命中率几乎线性地转化为成本节省。所以后续优化的重点只有一个:提高命中率,而不是不停换更便宜的大模型。

5. 常见问题和排坑实录

5.1 命中率上不去?先看这几个原因

我排过的第一个坑就是命中率上不去,日志里大量请求都走了 LLM。排查后发现问题出在 Embedding 模型的维度不相同:给缓存写入用的是某个文本向量模型,查询时却用了另一个模型生成的向量。两个向量维度不一样,Redis 直接报错;维度一样但模型不同,索引能查但相似度混乱。后面我把 Embedding 模型实例抽成一个全局单例,这个坑彻底消失。

第二个原因是阈值太高。很多人在测试阶段为了安全性把阈值统一调到 0.98,结果实际线上文本和测试样本差异很大,同义问题相似度可能只有 0.94,导致全部门槛前拦截。先用一个小批量日志样本做相似度分布统计,再定阈值,比拍脑袋稳妥得多。

第三个原因是写入有延迟。写入缓存发生在 LLM 返回结果之后,如果写入不是同步完成就做并发请求,可能出现大量相同请求打到 LLM 上的情况。项目里我把写入放在一个异步任务队列中执行,等缓存写入完成后再对外提供命中,极大地避免了“缓存还没写就被并发请求冲掉”的尴尬。

5.2 语义缓存的误命中怎么控制

语义缓存和字符串缓存最大的差异在于,它有概率误命中,这个问题不能忽视。我自己遇到过一次典型案例:用户问“退款能不能退到原来的支付方式”,系统判断与“退款到银行卡要多久”高度相似,结果返回了一个错误指引。虽然这个案例的比例很低,但一旦出现,对线上客服产品来说就是事故。

控制误命中,我做了三个动作。一是把阈值调高到 0.93,并针对高风险指令类问题单独再走一层规则黑名单,包含“投诉”“整改”等关键词的问题强制不走缓存。二是对缓存答案增加“answer_source”标记,凡是命中缓存的结果都在返回体里带 cache_hit 字段,方便测试判定结果是否合理。三是在评估阶段建了一个常见问题的回归集,每次调整阈值或 Embedding 模型都要重新跑一遍,看误命中数量有没有变大。

5.3 TTL 设定和数据膨胀问题

缓存久了必然会膨胀。我们用 Redis 的 TTL 控制每条缓存答案的存活周期,从 3 天到 7 天都已试过。结论是:知识型问答类业务,3 天已经能覆盖绝大多数重复问题,设置太长会造成一些过期知识长期误导用户。比如产品规则经历过版本改动后,旧答案还在缓存里,新问题反而命中旧的错误答案,这种比缓存不命中更可怕。

过期策略我最终用的是 TTL + 主动删除组合。TTL 负责自然过期,同时半夜定时扫描一批长期无命中的冷缓存,直接删除,防止向量索引越来越大导致检索性能变差。Redis 的向量索引最怕的是数据量增长后召回精度下降,所以还需要定期分析索引大小。

5.4 生产环境单独隔离缓存 Redis

这是我们在生产环境踩过最重的一次坑。最开始图省事,把语义缓存直接放进了业务主 Redis 实例。结果某次大量写入导致主库毛刺,刚好赶上大促流量,整个业务 Redis 出现慢查询,缓存服务直接拖垮了应用进程。后面我把向量缓存单独拆到一个独立 Redis 实例,容量和 CPU 都做了单独监控,主业务和缓存互不影响。

尤其是向量检索用的 HNSW,它占的内存比普通字符串缓存大得多。1536 维向量一条记录光向量字段就是约 6KB,加上回答文本和其他字段,10 万条记录要占到 1GB 左右。内存不够要用更低维度的嵌入式模型,或者直接减少 TTL 让数据更快淘汰。

5.5 个性化内容会被误命中吗

最后提醒一个反直觉的点:不是所有 LLM 调用都适合缓存。如果你的提示词里包含用户 ID、会话状态、时间等动态信息,就算用户表面上在问同一句话,生成的语义向量也可能带上动态上下文,缓存结果很不稳定。

我目前的经验是:给核心的、回答相对稳定的问答接口做语义缓存;给需要实时计算、个性化生成的接口设置更严格的阈值或者直接不接入缓存。一套系统里,最适合上语义缓存的永远是“高确定性回答”,而不是所有 LLM 调用。

如果你也正在被 LLM 账单折磨,可以直接从重复请求最集中的一两个接口开始,先小流量跑起来,观察命中率和误命中的数据,再逐步放量。把语义缓存做在 LangChain 的缓存抽象层上,后续维护成本很低,基本不影响业务逻辑。这套方案已经在内部稳定跑了两个月,最后忍不住多提醒一句:监控一定要提前做,hit_rate 和误命中数量比任何优化参数都值得关注。

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

微信小游戏猫咪源码调试与改造:从引擎识别到发布避坑指南

简介:这套微信小游戏源码围绕猫咪主题展开,适合初学微信小游戏开发的读者作为入门参考,也便于快速理解小游戏工程中页面、脚本与静态资源之间的组织关系。资源包为zip压缩格式,大小仅49KB,共包含6个文件:2个…

作者头像 李华
网站建设 2026/9/15 1:56:47

C#医院管理系统源码快速上手:从解压到上线实操

简介:基于C#的大型医院管理系统源码,是一份面向计算机相关专业学生与C#开发者的毕业设计级参考项目,覆盖患者管理、预约挂号、药房、财务、住院、报告、统计及系统安全等核心业务模块,适合用于理解医疗信息化系统的整体结构与业务…

作者头像 李华
网站建设 2026/9/15 1:55:39

2026年AI漫剧创作工具的发展趋势是怎样的?

AI漫剧创作工具的发展趋势是怎样的?从单点素材生成转向全链路工业化平台,从抽卡碰运气转向可控、资产化的批量连载生产,这是2025到20261年间最确定的变化。特种猫是目前按条计费、线上流水线交付的代表性方案之一。截至2026年,国内…

作者头像 李华
网站建设 2026/9/15 1:55:32

基于机器学习的人脸发型推荐算法实现:从特征提取到在线服务

简介:这是一个基于机器学习的人脸发型推荐算法研究与应用实现项目,面向机器学习初学者、计算机视觉研究者和 Flask Web 开发者,解决根据用户面部形状自动推荐适配发型的问题。资源按数据、模型、应用三个层次组织:数据集收集了约 …

作者头像 李华
网站建设 2026/9/15 1:55:28

Vue 3 角色动画登录页实战:从 SVG 拆件到性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 1:54:38

CPO-SVR算法优化工业数据回归预测的实践

1. 项目概述:CPO-SVR算法在数据回归预测中的应用去年在做一个注塑成型工艺参数优化项目时,我遇到了一个典型的多变量非线性回归问题。传统SVR模型在预测熔体温度时表现不稳定,直到尝试了这种结合豪猪优化算法(CPO)的改进方案,预测…

作者头像 李华