利用 Redis 实现大语言模型智能体的语义缓存:降低成本与延迟
语义缓存如何降低大语言模型的成本与延迟:嵌入向量、阈值设置、Redis与RedisVL、精度调优,以及在智能体工作流中缓存子问题。
来源: Semantic Caching for AI Agents: How to Make Your LLM Apps Faster and Cheaper
调用强大的大型语言模型需要支付实际费用,且响应速度很慢。任何在真实用户面前使用智能代理后看到API账单不断上升的人都能体会到这种矛盾:回答效果最好的模型往往也是最昂贵、响应最慢的。GPT-4级别的模型每千次请求的成本可能高达数美元,而智能代理工作流还会使情况更糟,因为一个用户查询可能会触发8到20次模型调用。
语义缓存直接针对这一成本问题,但大多数团队都未充分加以利用。使用Redis构建语义缓存非常简单。
为何普通缓存无法解决问题
普通缓存仅当两个请求的字节内容完全一致时才会将其视为相同请求。这种方法适用于数据库查询和HTTP请求,但对于自然语言处理则行不通。想象一下,一名客服人员收到了来自三位不同用户的如下请求:
- 一位顾客询问如何申请退款
- 还有一个人要求退款
- 第三个人想知道退款政策的具体内容
字节级缓存会识别出三个互不相关的输入,并为三次大语言模型调用付费。而人会认为这三个人其实想要相同的答案。语义缓存则是基于含义而非字符来工作的。
四步实现机制
- 使用嵌入模型将输入的查询转换为高维向量。
- 利用余弦距离或类似指标将该向量与所有存储的条目进行比较。
- 如果最近的条目在您设定的距离阈值范围内,则视为命中,立即返回存储的答案。
- 如果没有合适的条目,就将查询发送到RAG流程或大语言模型中处理,随后将问题和答案存储起来以备下次使用。
经过改写的问题最终会共享相同的答案。随着缓存逐渐填满,到达模型的流量比例就会减少。
从零开始的最简版本
在使用成熟的库之前,先了解其内部机制会很有帮助。首先从导入部分开始:
from sentence_transformers import SentenceTransformer
import numpy as np
然后嵌入一组简短的常见问题解答,并编写一个最近邻查找功能:
model = SentenceTransformer("all-mpnet-base-v2")# Embed your FAQ dataset
faq_questions = ["How do I get a refund?", "Where is my order?", ...]
faq_embeddings = model.encode(faq_questions)def cosine_distance(a, b):
return 1 - np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))def check_cache(query: str, threshold: float = 0.3):
query_embedding = model.encode(query)
distances = [cosine_distance(query_embedding, e) for e in faq_embeddings]
best_idx = np.argmin(distances)
best_distance = distances[best_idx]
if best_distance < threshold:
return faq_answers[best_idx] # Cache hit
return None # Cache miss
尝试查询一个关于退款需要多少天到账的测试问题,系统返回的却是存储中关于退款流程的常见问题,其相似度系数约为0.33。仅从共享词汇来看并未找到匹配项,只是这些向量在空间上较为接近而已。
在 Redis 上运行
这个简化版本没有数据持久化功能,也没有过期设置,无法区分不同用户,且向量搜索未经过优化。对于任何面向用户的场景,都需要使用真正的数据库。Redis结合开源的RedisVL SDK,能够直接提供这些功能。
from redisvl.extensions.cache.llm import SemanticCache
from redisvl.utils.vectorize import HFTextVectorizer
选择专为该任务设计的嵌入模型,然后创建缓存:
# Load a cache-optimized embedding model
vectorizer = HFTextVectorizer("redis/langcache-embed-v1")# Create the cache
cache = SemanticCache(
name="customer_support_cache",
vectorizer=vectorizer,
redis_client=redis_client,
distance_threshold=0.3
)# Set TTL (time to live) — keeps cache fresh
cache.set_ttl(86400) # 24 hours
langcache-embed-v1 模型是一种为语义缓存而微调的开放权重嵌入模型,因此在此类任务上往往能优于通用嵌入模型。
Redis 路由提供了三项生产级功能:
- TTL。 条目会自动过期,这样随着产品更新,过时的答案就不会持续存在。
- 命名空间。 不同团队、用户或租户都可以在同一个 Redis 实例中拥有独立的缓存空间。
- 优化的向量搜索。 HNSW 索引即使在大规模场景下也能实现低延迟的最近邻查询。
四个需要关注的关键指标
构建缓存只是容易的部分,还需要确保其正常运行。
命中率指的是在阈值范围内找到匹配结果的查询比例,它决定了您的节省量。如果一个系统每天进行10,000次大语言模型调用,且命中率为30%,那么就有3,000次调用无需执行。
精确度是指在所有命中结果中,正确答案所占的比例。一个总是返回错误答案的缓存比没有缓存还要糟糕。
召回率是指在存在对应条目的情况下,实际命中结果的查询比例。召回率低会导致无法获得应有的节省。
延迟降低效果则是比较两种路径的延迟情况。测试数据显示,缓存响应时间大约在2到5毫秒之间,而典型的GPT-4调用延迟则在300到500毫秒之间,前者速度大约是后者的160倍。实际感受到的速度差异取决于命中频率,如下公式所示:
With-Cache Latency = (avg_llm_latency × (1 - hit_rate)) + (avg_cache_latency × hit_rate)
当命中率为30%时,整体延迟预计会下降25%到30%左右。一旦缓存的使用率提升到60%到75%,其带来的收益将十分显著。
将缓存视为分类器
每次查询都会有四种结果之一:
| 缓存返回命中结果 | 缓存返回未命中结果 | |
|---|---|---|
| 本应命中 | 真阳性 | 假阴性 |
| 本应未命中 | 假阳性 | 真阴性 |
真正的危险在于误报:有些问题看似相似,但实际上含义不同,导致缓存返回错误答案。例如,关于改变主意后如何退款的问题与关于一般退款流程的问题,在嵌入空间中可能位置相近,但两者都需要独立的回答。这种情况下缓存必须格外谨慎,因为它返回的答案听起来完全合理。
提升准确性
调整阈值。距离阈值是最具影响力的设置。较低的阈值会提高精确度但降低召回率,较高的阈值则反之。在具有代表性的测试集上尝试不同的阈值值,选择能使精确度与召回率的调和平均值F1最大的那个值。通过这种方法,精确度从79%提升到了90%,同时保持25%的命中率。
使用交叉编码器进行重排序。传统的嵌入模型会单独对每个句子进行编码。而交叉编码器则同时处理查询句和候选句,从而捕捉到双编码器所忽略的细微差别。例如,一句是关于银行上调利率的,另一句是关于大雨过后银行附近的河流泛滥的:虽然两者都包含“bank”这个词,使得它们的嵌入向量较为接近,但交叉编码器能识别出它们其实并无关联。在实验中,通过重排序,精确度可提升至94%。由于交叉编码器无法生成可用于搜索的向量,因此它通常作为嵌入模型检索出的前K个候选项之后的第二道处理步骤使用。
让小型大语言模型来判定。为获得最高精度,可使用GPT-4o-mini这类低成本模型,通过简单提示来验证边界情况:这两个问题在语义上是否等价?它能在几毫秒内给出答案,且成本仅为完整调用费用的一小部分。测试显示其准确率为100%,但召回率较低,因为该模型有时会拒绝有效的匹配结果。这种表现是否可接受取决于具体应用场景。在客户支持领域,稍有错误的缓存答案可能比重新发起通话更糟糕。
先进行模糊匹配。在计算任何嵌入向量之前,先对查询内容与存储的条目进行编辑距离匹配。如果两者仅存在两三个字符的差异,那几乎可以肯定是同一个问题的拼写错误,应直接将其匹配起来。这样就可以完全跳过嵌入向量计算步骤,延迟几乎为零。在较高的相似度阈值下,这种方法在充满拼写错误的测试集上能达到100%的精确率和94%的召回率。
更大的优势:在智能体内部进行缓存
仅缓存对用户问题的最终回复是一种不完整的思维模型。智能体的工作方式有所不同。处理复杂支持问题的典型流程如下:
- 将查询拆分为3到5个子问题。
- 从知识库中检索相关文档。
- 让大语言模型综合生成答案。
- 评估答案的质量,必要时进行迭代优化。
每个子问题都是进行缓存的机会。如果智能体已经回答过“您的 API 速率限制是多少?”这一问题,那么当其他用户的复杂请求中出现相同子问题时,它就无需再次向模型提问。
基于 LangGraph 的深度研究智能体在三种场景下接受了测试:
| 场景 | 命中率 | LLM 调用次数 | 延迟时间 |
|---|---|---|---|
| 新用户,冷缓存 | 25% | 8 | 约 20 秒 |
| 回头客,热缓存 | 75% | 4 | 约 13 秒 |
| 购买前咨询 | 75% | 6 | 约 18 秒 |
在第三种场景下,每四个子问题中有三个来自缓存。模型调用次数从8次降至4到6次之间,端到端延迟减少了约三分之一。响应质量保持不变,无论哪种方式都能得到个性化且准确的答案。
这种效果会不断累积。每次成功运行都会为缓存增添内容,之后提出类似问题的用户就能受益于此前的所有数据,因此当系统处理成百上千次查询时,节省的效果会愈发显著。
相关阅读
- 为什么AI分析代理需要语义层而非原始SQL —— 直接在原始数据表上实现文本到SQL的转换会自行定义度量标准并忽略访问规则。了解如何通过Cube这样的语义层结合MCP,为代理提供受规则约束的业务含义。