DLAI-智能体语义搜索笔记-全-
DLAI 智能体语义搜索笔记(全)
001:课程概述 🚀

在本课程中,我们将学习如何通过添加语义缓存,使你的AI代理运行更快、成本效益更高。
在许多项目中,推理成本和延迟会影响应用程序的扩展能力。传统的输入/输出缓存仅在输入文本完全相同时才有效,这在某些情况下有帮助。但如果一个人问“我如何获得退款”,而另一个人问“我想要回我的钱”,一个普通的精确匹配缓存会将它们视为完全不同的查询。
另一方面,语义缓存关注的是含义。它使用嵌入向量来衡量两个问题在语义空间中的相似程度。如果它们在语义上相似,就可以复用模型对旧问题的回答,而无需再次调用模型。
课程内容概览
首先,你将从头开始构建一个缓存,以逐步了解语义缓存的内部原理。
以下是构建基础语义缓存的步骤:
- 创建嵌入向量。
- 比较向量间的距离。
- 设定一个阈值,以决定两个查询何时在语义上足够相似。
接着,我们将使用Redis的开源SDK来实现一个生产级的缓存。这将使你的缓存更接近实际部署状态,因为它将包含诸如生存时间等功能以保持缓存的新鲜和精简,甚至可以为不同的用户、团队或租户提供独立的缓存。
你将使用我们专为缓存准确性微调的开源嵌入模型。在拥有一个可工作的缓存后,我们将评估其性能。
我们将关注命中率、精确率和召回率。这些指标将显示你的缓存有多大帮助以及其正确性如何。你将在混淆矩阵中可视化这些指标,并了解改变相似度阈值如何影响精确率与召回率之间的平衡。
我们还会查看延迟,并观察几次命中如何快速累积成显著的节省时间。
在评估了缓存的有效性之后,你将学习四种增强缓存的方法:
- 优化相似度阈值。
- 使用交叉编码器进行重新排序。
- 使用一个小型语言模型来确认两个问题是否含义相同。
- 添加模糊匹配以处理用户提问时常见的简单拼写错误。
最后,我们将把所有内容整合到一个AI代理中。该代理将一个大问题分解成更小的部分,并为每个部分检查缓存,只在必要时才调用大语言模型。
这意味着,随着时间的推移,缓存逐渐“预热”,每个新用户或略有不同的措辞都能从系统已有的知识中受益。模型调用次数减少,响应质量不变,但速度却快得多。
课程资源与致谢
许多人为创建本课程付出了努力。我们要感谢来自Redis的应用人工智能、人工智能研究、产品和教育团队,以及来自DeepLearning.AI的Isaac和Serena,他们也为本课程做出了贡献。

第一课将是语义缓存的概述。你还将学习一个真实用例,其中沃尔玛发布了改进其生产缓存系统的技术。
本节课中,我们一起学习了语义缓存的基本概念、课程的整体结构以及它将带来的好处。在接下来的视频中,我们将正式开始深入探索。
002:语义缓存概述 🧠
在本节课中,我们将学习什么是语义缓存,它为何重要,以及它如何帮助AI代理重用结果以降低成本和延迟。
引言:成本与性能的权衡
模型质量往往随着每个令牌的成本增加而提升。虽然这条曲线在不断改善,但价格和延迟之间的权衡仍然是现实世界部署的关键限制因素。
这是一个真实的矛盾,因为用于推理和解决复杂任务(如GPT-4或Claude)的流行模型通常更昂贵,也更慢。
以下是Artificial Analysis对多个基础模型进行的比较结果,展示了智能与价格的关系。对于许多团队而言,推理(而非数据处理)已成为AI系统中的主要单位成本。
RAG系统与AI代理的挑战
组织正在构建检索增强生成系统(简称RAG)。这些系统可以将特定领域的知识注入大型语言模型的提示中,通过在正确的时间插入正确的信息来减少幻觉,并通过在运行时提供模型从未见过的最新信息来保持内容的新鲜度。
然而,AI代理是“令牌消耗大户”。它们本质上需要提取、规划、行动、反思并进行多次迭代。因此,代理在执行过程中会使用多次LLM调用。这会消耗更多令牌,增加额外延迟,并且提示长度也会随时间增长。
根据某代理公司在2025年9月发布的一篇论文中的基准测试,在多个现实世界任务中,即使在某些情况下,一次完整的端到端执行成本也高达6.8美元。
现实用例:客户支持
让我们讨论一个代理的常见现实用例,例如客户支持。在客户支持场景中,代理旨在加速客户支持工单或查询的解决时间。缓慢的代理会对最终用户体验产生负面影响。客户支持代理会产生大量常见问题,这些冗余数据会随时间累积,而对这些数据进行的RAG操作将推高基础设施成本。
例如,多个用户询问如何获得退款、如何拿回他们的钱或退款政策是什么。代理必须从头开始解决所有这些场景。
缓存的核心原则
接下来,我们谈谈一个称为缓存的经典原则。其核心思想是不在冗余信息上重复自己。用户可能会向代理提出多个问题。在这些情况下,我们可以检查缓存中是否已有过去已回答的查询,并在不需要时避免调用大型语言模型。这在理论上是个好主意。
但问题是,对于自然语言,简单的精确匹配缓存会失效。
让我们再看这个例子:
I want my money backHow do I get a refundWhat is your refund policy
在传统缓存中,对字符串数据进行精确匹配。单词、令牌和字符必须完全相同才能获得缓存命中。这带来了完美的精确率,但召回率非常低,在大多数情况下,自然语言的缓存命中率很低。
在这个场景中,所有这三个问题都会是缓存未命中。
引入语义缓存
引入语义缓存,我们可以利用问题的含义。这带来了更高的召回率和更高的缓存命中率,从而影响性能。
然而,这也引入了误报的风险。换句话说,这增加了缓存命中错误内容的机会。
语义缓存的工作原理
现在,让我们从头开始讨论语义缓存的工作原理。
第一步,我们将嵌入用户问题。我们将把这个用户问题转换成一个可以用于语义搜索的向量。
第二步,我们将比较用户查询与语义缓存中每个条目的相似度。
接下来,我们将对这个结果进行分类。如果结果在语义距离上与原问题足够接近,我们可以直接将缓存中存储的答案返回给用户。
但如果它是缓存未命中,那么我们需要调用我们的RAG系统。该系统涉及生成某种搜索,然后调用LLM。
最后,我们仍然需要将答案返回给用户。一旦我们有了答案,我们就可以用那个问答对更新我们的缓存,以确保它在未来可重用。
语义缓存的支柱:向量搜索
语义缓存的支柱是向量搜索的概念。简单回顾一下,向量是数字列表。这些数字代表数据并编码意义和语义。
向量搜索在现实中有许多应用实例,包括内容发现搜索、推荐系统,甚至欺诈和异常检测。
生产中的挑战
生产中的语义缓存引入了新的挑战。它不仅仅是向量搜索。在这里,我们必须特别关心缓存的有效性:
- 准确性:当我们获得缓存命中时,我们是否从缓存中提供了正确的结果?
- 性能:我们是否确保有足够高的缓存命中率以获得价值?
- 可扩展性:我们能否在不影响往返延迟的情况下大规模提供缓存服务?
接下来,我们需要关心可操作性或可扩展性。随着数据随时间演变,我们能否刷新、使失效或预热语义缓存?
最后是可观测性。我们能否确保测量和观察正确的指标,如缓存命中率、延迟、成本节约和缓存质量?
本课程的衡量指标
在本课程中,我们将重点关注四个不同的指标来衡量缓存的有效性:
- 缓存命中率:在给定距离阈值下,我们命中缓存的频率。这主要影响我们通过添加语义缓存获得的成本节约。
- 排序指标:如精确率、召回率和F1分数。这些将用于帮助我们理解当实际获得缓存命中时,我们的缓存有多准确。
提升缓存性能的方法
本课程还将帮助我们介绍提高语义缓存准确性和性能的不同方法。
为了提高缓存的精确率和召回率,我们将重点关注几种不同的方法,例如:
- 调整距离阈值
- 添加额外的重排序步骤,如交叉编码器模型或大型语言模型。
此外,像模糊匹配这样的技术可以帮助我们在甚至调用与缓存相关的嵌入之前处理拼写错误和精确匹配的情况。这有助于我们节省计算资源。
最后,为不同类型的数据和上下文添加额外过滤器,例如:
- 时间数据:时间敏感的查询。
- 代码检测:如特定领域的代码、Python代码、Java代码等。
所有这些都可能从一开始就绕过任何类型的缓存操作。
在本课程中,我们将主要实现一个使用技术来提高精确率、召回率和效率的缓存。
现实世界案例:沃尔玛
让我们讨论一个现实世界的例子,以沃尔玛这样的大型零售商为例。沃尔玛最近发表了一篇论文,讨论了在内部用例和外部客户支持用例中对语义缓存的需求。他们称之为“沃尔玛缓存”。
在这个缓存中,他们引入了几种不同的技术,例如跨多个节点分发缓存、添加灵活的决策引擎,甚至预加载常见查询。这有助于将整体准确率提高到近90%。
让我们快速了解一下他们是如何达到这个特定基准的。
我们在沃尔玛缓存中看到的第一个贡献是添加了负载均衡器。这个负载均衡器使他们能够非常容易地水平扩展系统,意味着他们可以添加额外的计算节点,并且缓存可以扩展其能够服务的操作数量。这对于像沃尔玛这样在全球范围内大规模运营的组织来说至关重要。
其次,他们为缓存添加了双层存储层。所谓双层,是指既有L1层也有L2层存储。在L1层,这是一个向量数据库,用于基于语义搜索的简单检索,以在语义缓存中找到相似的条目。在L2层,这是一个内存缓存(如Redis或其他数据库),仅用于基于L1缓存产生的ID进行简单的数据查找。
最后是多租户。考虑到像沃尔玛这样组织的规模,他们可以让多个团队、多个租户和应用程序通过从同一缓存存储服务多个租户来使用相同的存储基础设施。
接下来,沃尔玛添加了一个决策引擎。决策引擎的存在是为了将缓存精确率提升到纯语义搜索之上。在他们的特定决策引擎中,他们添加了用于代码检测和时间上下文检测的模块。在任何情况下,涉及代码或时间敏感查询的用户查询都会完全避免缓存操作,直接走传统的LLM或基于RAG的工作流程。这些都是在语义搜索发生之前添加的。
本课程实践目标
在本课程中,我们将以使用LangGraph工作流从头构建一个AI代理作为结束。在这个LangGraph工作流中,我们将处理一个来自客户支持用例的大型、复杂的用户查询。代理将把该查询分解成更小的部分。然后,代理将为每个查询检查缓存,以查找我们过去是否已回答过。如果没有,代理将继续进行额外的研究和评估迭代,以确保我们获得高质量的答案。最后,LLM将以个性化的方式将结果综合起来返回给用户。
这将与我们提供的前端体验配对,允许你输入你最喜欢的网站的URL。我们可以抓取该网站的所有原始内容,使你能够与数据聊天。到最后,我们将从头构建一个语义缓存,其中填充了你测试中的常见问题。这个代理将能够利用这些语义缓存条目,随着时间的推移提高性能。
在下一课中,我们将首先从从头构建一个语义缓存开始。
让我们开始吧。




本节课总结:在本节课中,我们一起学习了语义缓存的基本概念、其重要性以及工作原理。我们探讨了传统精确匹配缓存的局限性,理解了语义缓存如何通过向量搜索利用查询的“含义”来提高命中率。我们还分析了生产环境中面临的挑战、关键的衡量指标(如命中率、精确率、召回率),并了解了像沃尔玛这样的公司如何在实际中应用和优化语义缓存。最后,我们明确了本课程的实践目标:构建一个能够利用语义缓存的AI代理。下一课,我们将动手开始构建。
003:构建你的第一个语义缓存 🧠
在本节课中,我们将从零开始构建一个可工作的语义缓存,以便理解其各个组成部分的工作原理。之后,我们将使用 Redis 的开源 SDK 和数据库重新实现它。
概述
语义缓存的核心流程是:首先通过语义搜索检查缓存,看是否存在与当前用户问题足够相似的、过去处理过的问题。如果足够相似,则命中缓存并直接返回结果给用户。如果缓存未命中,则需要走完我们智能体的 RAG 流程,最终将结果返回给用户并更新缓存。

选择 Redis 作为缓存后端
上一节我们介绍了语义缓存的基本概念,本节中我们来看看如何实现它。我们将使用 Redis 作为后端。Redis 代表远程字典服务器,是一个开源、快速的内存键值数据库。这意味着你可以在 Redis 服务器上存储不同的数据结构,并在多个节点间分发。你的应用程序可以大规模地读写和访问这些数据。
Redis 通常用于缓存,但也具备二级索引的能力,这意味着我们可以跨向量、文本、数字、标签甚至地理空间数据进行存储和索引。这些特性使我们能够在 Redis 中实现语义缓存。
以下是使用 Redis 的几个优势:
- 低延迟检索和索引:我们可以优化向量搜索,以便快速插入和检查与用户查询相似的缓存问题。
- 专用嵌入模型:我们将使用一个开源的、经过微调的嵌入模型来提高语义缓存的准确性。
- 易用的 SDK:我们将使用开源的 Redis VL SDK,它为我们提供了对缓存配置和各种 CRUD 操作的符合人体工程学的控制。
- 数据管理策略:我们可以利用 TTL(生存时间)和命名空间策略来配置数据在缓存中的流动方式,以及如何隔离不同租户的数据。
从零开始构建语义缓存

让我们通过代码来具体看看。首先,我们需要加载数据集。

加载 FAQ 数据集
我们将处理的数据集来自一个客户支持系统中的 CSV 文件,其中包含常见问题、问答对以及一些可用于测试的数据。
以下是加载 FAQ 并查看其内容的步骤:
import pandas as pd
# 加载 FAQ 数据集
faq_df = pd.read_csv('faqs.csv')
print(faq_df.head())
你会注意到,我们的 FAQ 数据集中的每个条目都包含一个问题(例如“如何获得退款?”)和一个答案(例如“要申请退款,请访问我们的订单页面并选择...”)。
生成文本嵌入
为了进行语义缓存,我们需要文本嵌入。我们将使用 sentence-transformers 库来实现。对于第一个例子,我们将使用流行的 all-MiniLM-L6-v2 模型。
我们将使用这个模型来编码我们的 FAQ 数据集,即问题列表,生成嵌入向量。
from sentence_transformers import SentenceTransformer
# 加载嵌入模型
model = SentenceTransformer('all-MiniLM-L6-v2')
# 首次运行需要下载模型,根据网络情况可能需要几秒钟
# 为所有问题生成嵌入
faq_embeddings = model.encode(faq_df['question'].tolist())
print(f"嵌入向量示例(前几个值): {faq_embeddings[0][:5]}")

实现语义搜索功能
接下来,我们将使用两个函数来实现从零开始的语义搜索。
第一个函数计算两组向量之间的余弦距离。第二个函数实现语义搜索:它接收一个查询,构建查询的嵌入向量,计算查询嵌入与 FAQ 嵌入矩阵之间的余弦距离,最后找到最佳匹配条目的索引和距离值。
import numpy as np
def cosine_distance(vec_a, vec_b):
"""计算两个向量间的余弦距离。"""
return 1 - np.dot(vec_a, vec_b) / (np.linalg.norm(vec_a) * np.linalg.norm(vec_b))
def semantic_search(query, model, faq_embeddings):
"""执行语义搜索,返回最相似FAQ的索引和距离。"""
query_embedding = model.encode([query])
distances = [cosine_distance(query_embedding[0], emb) for emb in faq_embeddings]
best_index = np.argmin(distances)
return best_index, distances[best_index]
让我们用一个示例查询来测试这个语义搜索函数。
query = "我的订单退款需要多长时间?"
index, distance = semantic_search(query, model, faq_embeddings)
print(f"查询: '{query}'")
print(f"最相似的FAQ: '{faq_df.iloc[index]['question']}'")
print(f"余弦距离: {distance:.3f}")
构建语义缓存检查函数
现在,我们将把语义搜索功能转化为一个语义缓存。我们将实现另一个名为 check_cache 的辅助函数,它接收一个查询字符串和一个距离阈值浮点值。
在缓存检查函数内部,我们首先使用查询进行语义搜索以获取索引和距离值。如果余弦距离值小于设定的阈值,我们将其视为缓存命中并返回缓存中的条目。否则,返回 None,表示缓存未命中。
def check_cache(query, threshold=0.3):
"""检查语义缓存。命中则返回缓存条目,否则返回None。"""
index, distance = semantic_search(query, model, faq_embeddings)
if distance < threshold:
return faq_df.iloc[index], distance
else:
return None, distance
让我们用几个查询来测试我们的缓存。
test_queries = [
"可以退款吗?",
"我的包裹丢了怎么办?",
"如何更改送货地址?"
]
for q in test_queries:
result, dist = check_cache(q)
if result is not None:
print(f"查询 '{q}' -> 缓存命中!距离: {dist:.3f}")
print(f" 答案: {result['answer'][:50]}...")
else:
print(f"查询 '{q}' -> 缓存未命中。距离: {dist:.3f}")
扩展缓存
我们还可以随着时间的推移,在新数据进入时扩展我们的缓存。让我们添加一个辅助函数来实现这一点。
这个辅助函数接收一个问答对,将其添加到我们的数据框中,生成新的嵌入向量,并将其添加到 FAQ 嵌入矩阵中。
def add_to_cache(new_question, new_answer):
"""将新的问答对添加到缓存中。"""
global faq_df, faq_embeddings
# 添加到 DataFrame
new_row = pd.DataFrame({'question': [new_question], 'answer': [new_answer]})
faq_df = pd.concat([faq_df, new_row], ignore_index=True)
# 生成新嵌入并添加到矩阵
new_embedding = model.encode([new_question])
faq_embeddings = np.vstack([faq_embeddings, new_embedding])
print(f"已添加新条目。缓存现在有 {len(faq_df)} 个条目。")
现在,我们将尝试用三个新条目更新原始缓存。
new_entries = [
("我的包裹丢了怎么办?", "如果包裹丢失,请立即联系客服并提供订单号。"),
("如何更改送货地址?", "在订单发货前,您可以在‘我的订单’页面修改送货地址。"),
("产品有质量问题怎么办?", "对于质量问题,请拍照并联系我们的售后团队申请退换货。")
]
for q, a in new_entries:
add_to_cache(q, a)
在添加了新条目之后,让我们再次运行测试,看看现在哪些查询能命中缓存。
print("\n--- 添加新条目后的缓存测试 ---")
for q in test_queries:
result, dist = check_cache(q)
if result is not None:
print(f"查询 '{q}' -> 缓存命中!距离: {dist:.3f}")
else:
print(f"查询 '{q}' -> 缓存未命中。距离: {dist:.3f}")
使用 Redis 构建生产级语义缓存
现在我们已经了解了缓存如何从零开始工作,让我们转向一个更接近生产环境的场景。我们将迁移到 Redis 数据库实例。
连接到 Redis
首先,我们需要连接到 Redis 服务器。你可以通过 Redis URL 参数来实现,在大多数情况下是 redis://localhost:6379。
import redis
from redisvl import RedisVL
# 连接到 Redis
redis_client = redis.Redis.from_url('redis://localhost:6379')
# 快速测试连接
try:
redis_client.ping()
print("成功连接到 Redis!")
except Exception as e:
print(f"连接 Redis 失败: {e}")
加载专用嵌入模型
下一个要素是经过缓存优化的嵌入模型 lcache-embed-v1。这是一个在 Hugging Face 上可用的开源模型,专门针对语义缓存操作进行了微调。
让我们使用 Hugging Face 的 AutoTokenizer 和 AutoModel 类来加载这个模型。这个类将从 Hugging Face 获取并下载模型权重到我们的服务器上。
from transformers import AutoTokenizer, AutoModel
import torch
model_name = "makercommunity/lcache-embed-v1"
tokenizer = AutoTokenizer.from_pretrained(model_name)
lcache_model = AutoModel.from_pretrained(model_name)
def get_embedding(text):
"""使用 lcache-embed-v1 模型生成文本嵌入。"""
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
with torch.no_grad():
outputs = lcache_model(**inputs)
# 使用平均池化获取句子嵌入
embeddings = outputs.last_hidden_state.mean(dim=1)
return embeddings.numpy()[0]
创建 RedisVL 语义缓存
现在我们已经下载并准备好了嵌入模型,我们可以使用 RedisVL 开源 SDK 创建我们的语义缓存。
在这里,我们为语义缓存传递一个名称,这将在数据库中创建一个唯一的命名空间。其次,我们传递刚刚创建的 lcache 嵌入模型。第三,我们传递 Redis 客户端连接对象。最后,我们配置一个用于缓存检查的基线距离阈值。
# 初始化 RedisVL 客户端
rvl = RedisVL(redis_client=redis_client)
# 创建语义缓存索引配置
index_schema = {
"index": {
"name": "faq_semantic_cache",
"prefix": "cache",
"storage_type": "hash"
},
"fields": [
{"name": "question", "type": "text"},
{"name": "answer", "type": "text"},
{
"name": "embedding",
"type": "vector",
"attrs": {
"dims": 768, # 根据模型维度调整
"distance_metric": "COSINE",
"algorithm": "HNSW"
}
}
]
}
# 创建索引
rvl.create_index(index_schema)
print("语义缓存索引创建成功。")
用数据填充缓存
让我们用 FAQ 数据填充缓存。这将用所有数据设置 Redis,并使其在我们的示例中准备就绪。
这里我们只是遍历数据框,逐条将数据存储到缓存中。
# 假设 faq_df 是之前加载的 DataFrame
for _, row in faq_df.iterrows():
embedding = get_embedding(row['question'])
# 存储到 Redis Hash 中,key 为 cache:{id}
key = f"cache:{_}"
redis_client.hset(key, mapping={
'question': row['question'],
'answer': row['answer'],
'embedding': embedding.tobytes() # 将向量转换为字节存储
})
print(f"已成功将 {len(faq_df)} 条 FAQ 加载到 Redis 缓存。")
测试 Redis 缓存
让我们快速检查一下缓存中有什么。我们可以提问:“我需要为我的购买退款。”
test_query = "我需要为我的购买退款。"
query_embedding = get_embedding(test_query)
# 在 Redis 中进行向量搜索(简化示例,实际使用 RedisVL 的搜索接口)
# 这里演示原理,生产环境应使用 rvl.search()
best_key = None
best_distance = float('inf')
for key in redis_client.scan_iter("cache:*"):
item = redis_client.hgetall(key)
# 从字节转换回向量
cached_embedding = np.frombuffer(item[b'embedding'], dtype=np.float32)
dist = cosine_distance(query_embedding, cached_embedding)
if dist < best_distance:
best_distance = dist
best_key = key
if best_key and best_distance < 0.3: # 使用阈值 0.3
hit_item = redis_client.hgetall(best_key)
print(f"缓存命中!距离: {best_distance:.3f}")
print(f" 匹配问题: {hit_item[b'question'].decode()}")
print(f" 答案: {hit_item[b'answer'].decode()[:50]}...")
else:
print(f"缓存未命中。最近距离: {best_distance:.3f}")
配置生存时间
最后,在我们用大语言模型进行端到端测试之前,我们将实现 TTL。TTL 代表生存时间,这是数据库知道何时驱逐在缓存中停留了特定时间段的数据的方式。当系统中的数据发生变化或演进时,通常使用它来保持缓存的新鲜度。
使用 Redis 开源 SDK 实现这一点非常简单。你只需调用 expire 命令。
# 为所有缓存键设置 TTL 为一天(86400 秒)
for key in redis_client.scan_iter("cache:*"):
redis_client.expire(key, 86400)
print("已为所有缓存条目设置 TTL 为 24 小时。")
与大语言模型结合的端到端示例
我们将使用 OpenAI 的 API 来演示一个端到端的大语言模型示例。我们将使用 openai SDK 和 ChatOpenAI 包装器来连接 GPT-4o-mini。
本课程的 OpenAI 密钥应该已经加载到你的环境中。为了与我们的语言模型对话,我们有一个辅助函数来发送提示词。在这个特定场景中,提示词是:“你是一个有用的客户支持助手。” 这个语言模型将简洁而专业地回答客户问题,根据你给出的任何问题,提供一到两句话的回复。
import openai
import time
# 假设 openai.api_key 已设置
client = openai.OpenAI()
def ask_llm(question):
"""向大语言模型提问。"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一个有用的客户支持助手。请简洁专业地回答客户问题,回复限制在一到两句话。"},
{"role": "user", "content": question}
],
max_tokens=100
)
return response.choices[0].message.content
性能评估
现在,对于我们的端到端示例,我们有一个测试问题列表。我们还有一个性能评估类。性能评估类是作为课程材料的一部分构建的,包含对缓存检查、LLM 调用、命中和未命中进行细粒度计时的能力。这将允许我们在运行整个测试后比较结果。
准备好我们的性能评估类来记录时间后,我们将遍历每个测试问题,检查缓存,如果不在缓存中,则将其记录为未命中并转向我们的大语言模型。
这个循环遍历我们之前看到的测试问题列表并打印出结果。
class PerformanceEvaluator:
def __init__(self):
self.cache_times = []
self.llm_times = []
self.hits = 0
self.misses = 0
def time_cache_check(self, query):
start = time.time()
# 这里应调用集成了 Redis 的 check_cache 函数
result, dist, cache_time = check_cache_redis(query, threshold=0.3)
elapsed = time.time() - start
self.cache_times.append(elapsed)
if result:
self.hits += 1
return result, dist, True, elapsed
else:
self.misses += 1
return None, dist, False, elapsed
def time_llm_call(self, question):
start = time.time()
answer = ask_llm(question)
elapsed = time.time() - start
self.llm_times.append(elapsed)
return answer, elapsed
def get_stats(self):
stats = {
"平均缓存检查时间 (ms)": np.mean(self.cache_times) * 1000 if self.cache_times else 0,
"平均LLM响应时间 (ms)": np.mean(self.llm_times) * 1000 if self.llm_times else 0,
"缓存命中数": self.hits,
"缓存未命中数": self.misses,
"命中率": self.hits / (self.hits + self.misses) if (self.hits + self.misses) > 0 else 0
}
return stats
# 模拟一个集成了 Redis 的缓存检查函数
def check_cache_redis(query, threshold=0.3):
start = time.time()
query_embedding = get_embedding(query)
best_distance = float('inf')
best_item = None
# 简化搜索逻辑,实际应使用 Redis 向量搜索
for key in redis_client.scan_iter("cache:*"):
item = redis_client.hgetall(key)
cached_embedding = np.frombuffer(item[b'embedding'], dtype=np.float32)
dist = cosine_distance(query_embedding, cached_embedding)
if dist < best_distance:
best_distance = dist
if dist < threshold:
best_item = {
'question': item[b'question'].decode(),
'answer': item[b'answer'].decode()
}
cache_time = time.time() - start
return best_item, best_distance, cache_time
# 测试问题列表
test_questions = [
"可以退款吗?",
"我的包裹丢了怎么办?",
"如何更改送货地址?",
"你们的营业时间是什么?",
"这个产品有保修吗?"
]
evaluator = PerformanceEvaluator()

print("--- 开始端到端测试 ---")
for q in test_questions:
print(f"\n处理查询: '{q}'")
cache_result, dist, is_hit, cache_time = evaluator.time_cache_check(q)
if is_hit:
print(f" -> 缓存命中 (距离: {dist:.3f}, 耗时: {cache_time*1000:.1f}ms)")
print(f" 答案:
# 004:衡量缓存效果 📊
在本节课中,我们将学习如何评估你的语义缓存。我们将使用命中率、精确率、召回率和延迟等指标来衡量缓存的实际效果,并理解其性能表现。


## 概述
缓存系统可能以两种方式失效:质量不佳或性能低下。衡量缓存性能的方法与衡量机器学习模型性能的方法类似。我们将通过一系列指标和可视化工具来全面评估缓存系统。
## 缓存性能的核心指标
上一节我们介绍了评估缓存的重要性,本节中我们来看看用于衡量缓存效果的具体指标。这些指标帮助我们理解缓存命中的质量以及系统速度的提升。
以下是衡量缓存性能的四个核心指标:
1. **命中率**:该指标衡量所有到达系统的用户查询中,有多少个查询落在了预设的距离阈值范围内。公式可以表示为:
`命中率 = (落在阈值内的查询数) / (总查询数)`
例如,在5个查询中有3个命中,则命中率为60%。
2. **精确率**:精确率衡量所有落在距离阈值内的缓存条目中,有多少是真正有效的匹配。有效性来源于数据中的标签列。公式为:
`精确率 = (真正例) / (真正例 + 假正例)`
3. **召回率**:召回率衡量所有应该被命中的查询中,实际有多少被成功命中。如果因为距离阈值设置过低而导致应命中的查询未命中,就会降低召回率。公式为:
`召回率 = (真正例) / (真正例 + 假反例)`
4. **延迟**:衡量缓存系统带来的速度提升。我们可以使用以下公式计算带缓存的系统延迟:
`带缓存的延迟 = (平均缓存延迟 * 缓存命中率) + (平均大模型延迟 * (1 - 缓存命中率))`
通过比较缓存前后的延迟,可以计算出性能提升的百分比。
## 混淆矩阵与F1分数
为了更细致地分析缓存表现,我们需要查看混淆矩阵。混淆矩阵展示了真正例、真反例、假正例和假反例的数量。
在理想的混淆矩阵中,值应主要集中在主对角线上(即真正例和真反例)。距离阈值可以帮助我们在精确率和召回率之间进行权衡:
* 降低距离阈值可以提高精确率,但会降低召回率。
* 提高距离阈值可以提高召回率,但会降低精确率。
为了在精确率和召回率之间取得良好平衡,我们可以使用**F1分数**。F1分数是精确率和召回率的调和平均数。我们将通过遍历不同的阈值来优化并找到最适合我们数据的最佳阈值。
## 代码实践:评估缓存性能
现在,让我们在代码中实现所有这些缓存性能的衡量方法。
首先,我们设置环境并加载熟悉的数据集。数据集包含用户查询、对应的缓存命中(或最接近的匹配)、到该匹配的距离以及一个标签列(指示该命中是否正确)。
```python
# 导入必要的库并加载数据
import pandas as pd
# 假设 `faq_dataframe` 是包含查询和缓存条目的数据框
data = pd.read_csv('cache_dataset.csv')
接下来,我们引入一个语义缓存包装器,它封装了我们的缓存并提供了一些辅助函数,用于“填充”缓存或检查缓存。
# 初始化缓存包装器
cache_wrapper = SemanticCacheWrapper()
# 使用辅助函数填充缓存
cache_wrapper.hydrate_cache(faq_dataframe)
然后,我们可以检查缓存。对于一个用户查询,检查缓存会返回一个对象,显示查询内容及其所有匹配项。
# 检查第一个用户查询的缓存
first_query = faq_dataframe['user_query'].iloc[0]
cache_result = cache_wrapper.check_cache(first_query)
print(cache_result)
我们定义一个测试查询列表,并使用check_many辅助函数批量获取匹配结果。我们可以配置不同的阈值或匹配数量。
test_queries = [...] # 用户查询列表
results = cache_wrapper.check_many(test_queries, threshold=0.5, top_k=3)
现在,使用一个名为CacheEvaluator的抽象来进行评估。由于我们的数据集是以特定方式生成的,我们可以自动生成标签。
# 初始化缓存评估器
evaluator = CacheEvaluator(cache_results=results, data_container=data_container)
# 自动生成标签并打印报告
report = evaluator.report_matrix()
print(report)
报告会打印出清晰的混淆矩阵和精确率、召回率等指标。get_matrix函数可以字典格式获取所有指标,并提供一个“混淆掩码”,用于筛选出混淆矩阵中不同类别的具体数据点。
metrics_dict, confusion_mask = evaluator.get_matrix()
# 查看假正例的例子
false_positives = data[confusion_mask['false_positives']]
print(false_positives.head())
例如,在一个假正例中,查询“我可以安排特定的送货时间吗?”被匹配到了缓存条目“我可以更改我的送货地址吗?”。尽管它们听起来相似,但含义不同,模型给出的距离足够低以至于被判定为匹配,但根据我们的标签,这应是一个错误匹配。
评估延迟性能
接下来,我们评估缓存的延迟性能。首先定义一个函数来模拟大语言模型的响应延迟。
import time
import random
def simulate_llm_response():
# 模拟200-500毫秒的延迟
delay = random.uniform(0.2, 0.5)
time.sleep(delay)
return "Simulated LLM response"
然后,我们运行性能对比代码,使用一个名为PerfVow的抽象来同时比较缓存和大模型代码的执行性能。
from perf_tools import PerfVow
perf_evaluator = PerfVow()
metrics = perf_evaluator.compare_performance(cache_wrapper.check_many, simulate_llm_response, test_queries)
print(metrics)
输出的指标字典会给出缓存和大模型的平均延迟。我们还可以可视化这些指标,直观地对比360毫秒的平均大模型延迟与2毫秒左右的平均缓存延迟。
最后,我们使用前面提到的公式,结合一个假设的缓存命中率(例如30%),来计算带缓存的系统整体延迟,并与原始延迟比较,得出速度提升的倍数。
avg_cache_latency = metrics['avg_cache_latency'] # 例如 0.0022 秒
avg_llm_latency = metrics['avg_llm_latency'] # 例如 0.361 秒
cache_hit_ratio = 0.3 # 假设命中率
cached_system_latency = (avg_cache_latency * cache_hit_ratio) + (avg_llm_latency * (1 - cache_hit_ratio))
speedup = avg_llm_latency / cached_system_latency
print(f"系统加速比: {speedup:.2f}x")
使用大语言模型作为评判器
为了自动标注查询-缓存对,我们可以引入大语言模型作为评判器。我们重新开始,用FAQ数据框填充缓存,并进行一次全量检索(设置距离阈值为1,为每个测试查询检索最接近的邻居)。
# 重新填充缓存并进行全量检索
cache_wrapper.clear_cache()
cache_wrapper.hydrate_cache(faq_dataframe)
full_matches = cache_wrapper.check_many(test_queries, threshold=1.0, top_k=1)
然后,我们使用大语言模型来标注所有这些查询-缓存对。首先加载API密钥,然后使用一个名为LMEvaluator的抽象。
from lm_evaluator import LMEvaluator
lm_evaluator = LMEvaluator(api_key=your_api_key, model="gpt-3.5-turbo")
lm_results = lm_evaluator.predict(pairs=list(zip(test_queries, full_matches)), batch_size=10)
lm_results对象有一个dataframe属性,以数据框格式展示结果,包含每对查询是否相似的标签以及大语言模型的推理原因。
现在,我们可以再次使用缓存评估器,但这次使用大语言模型生成的自动标签,而不是数据容器提供的标签。
# 使用大语言模型生成的标签进行评价
auto_labels = lm_results.dataframe['is_similar'].tolist()
evaluator_auto = CacheEvaluator.from_full_retrieval(cache_results=full_matches, labels=auto_labels)
report_auto = evaluator_auto.report_matrix()
print(report_auto)
通过比较可以发现,使用自动标注得到的精确率和召回率(例如75%和90%)与之前手动/半自动标注的结果(79%和79%)接近。这表明该方法可以为我们的评估生成足够好的标签。
总结


本节课中我们一起学习了如何全面评估语义缓存的性能。我们介绍了命中率、精确率、召回率和延迟等核心指标,并通过混淆矩阵和F1分数深入分析了缓存命中的质量。在代码实践中,我们实现了缓存性能的衡量、延迟对比,并探索了使用大语言模型作为自动评判器来生成评估标签的方法。掌握这些评估技术是优化缓存系统的第一步。在下一课中,你将学习如何根据这些评估结果来改进你的缓存。现在,让我们清理缓存并继续下一课。
005:提升缓存效果 🚀
在本节课中,我们将学习几种技术来提升语义缓存的准确性和效率。我们将探讨阈值调优、交叉编码器、大语言模型验证和模糊匹配这四种策略,并通过代码示例展示如何实现它们。
概述


上一节我们介绍了语义缓存的基本概念和评估方法。本节中,我们将深入探讨如何通过多种技术手段来优化缓存性能,包括调整相似度阈值、使用更精细的模型进行重排序、引入大语言模型进行验证,以及在特定场景下应用模糊匹配算法。
阈值调优
第一种策略称为阈值扫描。在此策略中,我们使用不同的相似度阈值来评估缓存,以找到能使F1分数最大化的最佳阈值。
观察图表可以发现,提高阈值会提升召回率,但会降低精确率。由于F1分数是精确率和召回率的调和平均数,它会在一个中间的阈值处达到最大值。这个阈值通常就是我们希望设定的值。
以下是执行阈值扫描的代码示例:
# 假设我们有一个评估器 evaluator
report = evaluator.report_threshold_sweep()
print(report)
交叉编码器
第二种策略是使用交叉编码器作为重排序器。交叉编码器是一种能够同时处理两个句子并捕捉它们之间细微差别的模型,因此表达能力更强。但它不能直接替代嵌入模型,因为它不产生便于快速搜索的向量。
通常,我们在语义缓存系统中的使用方式是:首先为一组用户查询检索出最接近的缓存命中结果,然后将这些结果通过交叉编码器运行,并选择相似度分数最高的缓存命中。
总结来说,交叉编码器可以显著提升精确率,尤其是在使用自有数据对模型进行微调后。当然,这种设置也会增加系统的缓存延迟。
以下是如何在缓存包装器中注册并使用交叉编码器的代码:
# 实例化交叉编码器
cross_encoder = CrossEncoder("model_name")
# 在缓存包装器中注册重排序器
cache_wrapper.register_reranker(cross_encoder.get_reranker_instance())
# 使用带有重排序功能的缓存进行查询
results = cache_wrapper.query(test_queries, distance_threshold=1.0, num_results=10)
大语言模型验证
我们的第三种策略是使用一个大语言模型,直接询问一对句子在语义上是否相似。为了高效工作,我们可以使用一个更廉价的大语言模型来进行验证。验证相似性所需的令牌数远少于生成整个用户查询响应,因此在实践中成本更低。
您也可以将其设置为一个后备机制,用于验证那些具有模糊距离值的缓存命中。这种验证方案可以大幅提高精确率,但与没有大语言模型验证器的系统相比,它会增加延迟和令牌使用量。
以下是如何集成大语言模型验证器的示例:
# 加载OpenAI密钥
import os
os.environ["OPENAI_API_KEY"] = "your_key"
# 实例化LM验证器
lm_validator = LMValidator()
# 在缓存包装器中注册
cache_wrapper.register_reranker(lm_validator)
# 运行查询
results = cache_wrapper.query(test_queries, distance_threshold=optimal_threshold)
模糊匹配
最后,让我们看看模糊匹配。这是一种用于匹配字符串的算法策略。模糊匹配通常测量字符串之间的编辑距离,或者衡量通过删除、替换和插入操作将一个字符串转换为另一个字符串的难易程度。
这种技术在关键词式搜索领域非常有用,可以轻松过滤掉拼写错误。为了正确设置,我们通常将模糊匹配放在语义缓存系统之前。通过设置较高的相似度阈值,它可以将带有拼写错误的查询与之前对应的查询匹配起来,从而绕过整个嵌入生成和向量搜索机制。这通常会降低延迟并提高精确率,但如果设置不当,也可能导致误报。
以下是一个简单的字符串模糊化函数和模糊缓存的使用示例:
def fuzzify_string(input_string, iterations):
"""通过随机交换相邻字符来模糊化字符串。"""
# ... 实现细节
return fuzzified_string
# 实例化模糊缓存
fuzzy_cache = FuzzyCache()
# 使用数据集初始化缓存
fuzzy_cache.hydrate(faq_dataset)
# 查询模糊化的查询
fuzzy_results = fuzzy_cache.check(fuzzy_queries)
代码实践与结果
现在,让我们在代码中查看所有这些技术。我们首先设置环境并初始化缓存包装器和数据容器。使用之前评估的基线指标,我们依次应用阈值扫描、交叉编码器重排序和大语言模型验证,并观察各项指标(精确率、召回率、缓存率、F1分数)的变化。
例如,在应用交叉编码器后,我们可能将精确率从90%提升到94%,同时保持可观的缓存率。而引入大语言模型验证器后,我们甚至可能获得100%的精确率,尽管召回率可能会有所下降。
对于模糊匹配,我们创建了一个专门的FAQ数据集并引入不同程度的拼写错误。通过模糊缓存进行匹配,即使在字符串被严重扭曲的情况下,我们仍能获得高精确率和召回率,展示了该技术在特定场景下的有效性。
总结
本节课中,我们一起学习了四种提升语义缓存效果的核心技术:
- 阈值调优:通过系统性地测试不同阈值来平衡精确率与召回率。
- 交叉编码器:使用更强大的模型对初步检索结果进行重排序,以提升匹配质量。
- 大语言模型验证:利用大语言模型对可疑的缓存命中进行二次验证,确保高精确率。
- 模糊匹配:在语义缓存前处理拼写错误,提高系统在关键词搜索场景下的鲁棒性。

每种技术都在精度、召回、延迟和成本之间提供了不同的权衡。在实际系统中,您可以根据具体需求选择并组合这些策略。下一节课,您将动手实现第一个利用语义缓存来提升速度和效率的AI智能体。
006:构建具备语义缓存的快速AI代理 🚀


在本节课中,我们将学习如何将语义缓存集成到AI代理中,以便复用过去的结果、跳过冗余工作,从而随着时间的推移获得更快的响应速度。
概述
在构建我们自己的代理之前,我们先回顾一下。代理的扩展成本可能很高,因为它们会消耗大量令牌。代理的独特之处在于,在执行过程中,它们会执行多个步骤,并在适当时提供许多重用中间状态的机会。这不仅仅是缓存对原始用户问题的最终响应。在某些情况下这可能有效,但通常代理应用程序的原始输入更为复杂,需要多个步骤才能获得高质量的答案。
例如,代理可以缓存用户配置文件或偏好、工具调用的输出、推理过程,甚至是LLM生成的计划。最终,我们期望看到缓存随着时间的推移而丰富,从而减少处理所需的令牌数量。
让我们看看实际应用。在Redis,我们构建了自己的DataFrame Expr代理,可以回答加载到pandas DataFrame中的数据集相关问题。代理会获得数据模式(schema)和用户问题。这些输入会提示代理生成一些Python代码来分析pandas DataFrame。在整个过程中,代理会缓存多个内容,包括端到端的用户问题和结果答案。这些内容以较低的TTL(生存时间)进行缓存,因为数据可能频繁变化。我们还缓存了来自LLM的生成代码。这样,如果提出类似的问题,我们已经知道可以用来执行以获取响应的代码。此外,我们还缓存了关于解决LLM生成代码中常见错误的指导。所有这些结合在一起,可以产生更高效的代理工作流程。
在上图中,我们在左侧展示了处理三个不同问题的过程,右侧展示了当我们第二次提出一个非常相似的问题时会发生什么。在第二次处理时,缓存已启用,我们可以看到第二次使用的令牌数量少得多。最终,每一个成功的周期都会丰富缓存。后续的执行使用更少的令牌获得更即时的答案。
接下来,我们将在代码中自己实现这一点,并构建我们自己的深度研究代理。
环境设置与依赖导入
首先,我们需要设置环境。在本课中,我们将使用OpenAI来构建我们的代理。首先,设置OpenAI密钥,它应该已经在您的学习环境中。接下来,我们将一次性导入构建代理所需的一系列Python库和依赖项。
从Python依赖项中,您会注意到我们从标准库中引入了几个组件,从LangChain和LangGraph中引入组件,最后从Redis和RedisVL开源SDK中获取进行语义缓存和向量化所需的依赖项。
在构建代理之前,我们还需要连接到运行在localhost:6379上的Redis数据库。让我们ping一下以确保可以通信。一切正常。
我们正在构建的代理是一个客户支持代理。该代理应该能够访问一个内容知识库来回答问题。您可以将其视为来自文档、手册、PDF等的内容。第一步,我们将把原始文档加载到知识库中。这个知识库允许代理查询其从头开始回答问题所需的相关信息。换句话说,这允许代理实现RAG。
让我们继续用所有这些文档加载我们的知识库。这里的每个文档条目都将使用OpenAI嵌入模型进行向量化。
现在我们已经有了一个知识库,基础设施方面我们需要的另一件事是语义缓存。与课程的其他部分类似,我们将从我们一直使用的FAQ数据集中加载语义缓存。在这里,我们用我们一直在处理的FAQ数据来填充语义缓存。很好,您可以看到Redis正在运行且可访问,并且我们已经将8个FAQ条目加载到缓存中。
构建代理工作流程
是时候构建我们的代理了。本课中的代理是使用LangGraph构建的,这使得它变得非常简单。我们将代理的所有部分分解为不同的辅助函数。这个方法只是用其缓存、知识库索引和嵌入模型来初始化代理。
LangGraph允许我们创建传递状态的工作流程。我们将使用一组辅助方法来构建这个工作流程,以添加节点、添加边,并在图的过程中引导流量。例如,这个工作流程有执行查询分解、检查缓存、执行额外研究和评估研究质量的节点。最后,为最终用户合成结果。
我们的工作流程入口点从分解查询步骤开始,我们在所有必要的节点之间设置边,有些是确定性的边,有些是条件边。条件边是基于某些决策在节点之间构建的,决策通常特定于手头的工作流程或应用程序。
在这里,例如,我们在缓存检查之后放置了一个条件边。如果我们需要对其中一个分解后的问题进行额外研究,我们将条目传递给研究节点。否则,如果所有内容都已从缓存中加载,我们可以直接跳到末尾并跳过合成步骤。
我们的代理还在研究和评估质量之间有一个直接的边。因此,在每一个研究步骤之后,我们传递给一个作为“法官”的LLM来评估结果的质量。最后,在质量评估之后,我们要么返回去做更多的研究,要么走到最后一步,为最终用户合成结果。这就是我们工作流程的终点。

让我们继续编译这个代理。LangGraph的另一个优点是我们可以可视化刚刚构建的代理工作流程。正如您所看到的,这是我们在课程介绍中讨论的工作流程,它涉及查询分解、缓存检查、研究、质量评估和最终合成。


代理演示与性能分析
是时候演示代理了。我们将针对三种不同的场景测试我们的代理。




场景一:最终用户正在尝试评估一款软件。他们对不同的功能和不同事物的状态有疑问,需要了解这些信息才能做出购买决定。让我们看看代理如何响应。


第一步是代理将原始问题分解为四个不同的子问题。在这四个需要研究的子问题中,有三个不在缓存中,但其中一个可以直接从缓存中提供。这意味着对于这个特定请求,我们的缓存命中率是25%。现在我们有3个总问题需要研究,因为它们不在缓存中。我们的代理开始迭代并寻找问题答案的过程。最后,一旦它得到一个好的解决方案,我们所有的子问题都被验证并添加到缓存中,以供下一次运行使用。

这个工作流程大约在20秒内完成。此外,我们使用了8次大语言模型调用(2次GPT-4,6次GPT-4-mini)。请求的总延迟大约为20秒。您可以看到研究部分花费了近10秒。这是返回给用户的完整响应,包含了他们需要的所有个性化信息。


场景二:涉及一个不同的用户,他们处于购买该特定软件过程的不同阶段,需要推进实施计划。他们有一些额外的问题要问支持代理。我们将看到这个代理如何响应。



以类似的方式,代理这次将问题分解为四个子问题,其中三个从缓存中找到,只有一个子问题未命中。这没关系,因为我们的代理可以处理那个问题并完成研究。最后,您可以看到我们获得了75%的缓存命中率,四个问题中有三个命中了缓存。相应地,这使我们的LLM调用从上一次的8次减少到仅4次(2次GPT-4,2次GPT-4-mini)。这次运行的总延迟也是13秒,而不是上次的20到25秒左右。这是一个很大的改进。这里您可以看到给最终用户的格式良好的响应。

场景三:这是另一个正在进行购买前竞争性审查的用户。他们处于采购的最后阶段。在进行专业版计划购买之前,他们希望对有疑问的不同事项进行完整验证。您会注意到其中一些主题与其他用户请求中出现的主题相似。让我们看看我们的代理如何处理这个特定请求。
与之前类似,我们的代理通过将问题分解为四个子问题来处理。与上次类似,四个问题中有三个命中了缓存,一个未命中。同样,这不是问题,因为我们的代理仍然可以处理那个任务的研究。最后,您可以看到我们的总延迟大约为18秒,LLM调用这次是6次,不如4次好,但仍然比8次好。这个特定请求的缓存命中率约为75%。

结果对比与总结



现在我们已经让代理运行了不同的场景,您可能想知道它们之间的直接比较如何。让我们看一下。这个analyze_agent_results函数获取我们测试的所有三个场景的结果,并返回一个漂亮的图表和可视化。

在场景一中,我们的缓存命中率为25%,但在接下来的两个场景中,我们的缓存命中率达到了75%。您可以看到随着时间的推移,累积缓存命中率达到了60%。我们期望在一个拥有数百、数千甚至数百万用户的系统中,这种特定的缓存命中率在节省成本方面会非常有效。
让我们看看LLM调用。在我们的第一个场景中,我们为不同的任务使用了8次不同的LLM调用。但在最后两个场景中,我们分别使用了4次和6次。这是一个很大的节省。
对于我们的用户体验来说,最有趣的部分可能是延迟。您可以看到在第一个场景中,我们的总体延迟相当大。而在接下来的两个场景中,端到端延迟大约是总延迟的三分之一。这是一个显著的性能改进。同样,随着时间的推移,我们期望这种趋势会持续下去。
现在,我们将构建一个交互式演示,将所有内容整合在一起。该演示运行在Gradio上,允许我们访问您选择的某个URL的外部网页。我们将从这些页面中提取所有文档和内容,将其加载到您代理的知识库中。该代理将实现RAG,允许您与这些文档聊天,并且随着时间的推移,这个特定的代理将像我们在理论和实践课程中看到的那样建立其语义缓存。
现在我们已经启用了具备语义缓存的深度研究代理。让我们从AT&T国际漫游计划网站提取内容。点击“处理URL”将首先爬取并加载该网站上的所有内容,然后加载到我们的本地知识库中。现在,我们可以对这些特定数据提出不同的问题,实现我们之前看到的完全相同的代理。
尝试提出第一个问题。第一个问题是关于即将到来的游轮旅行。我们想确保我们通过AT&T的手机覆盖在游轮上仍然有效。我们会没事吗?代理目前正在进行研究以找到这个问题的答案。我们可以在性能日志中看到,缓存中没有记录,总共大约进行了4次LLM调用才得到答案。
尝试另一个问题:在国际旅行时,AT&T的覆盖范围在游轮和陆地之间有什么区别吗?好的。在这个问题上,我们实际上能够利用缓存。LLM的一个子问题命中了缓存。我们端到端只使用了2次LLM调用。您在这里可以看到,我们得到了一个给用户的漂亮的个性化答案。
再试一个问题:在西班牙,我会被AT&T覆盖吗?很好。代理告诉我们是的,在西班牙我们仍然可以使用AT&T服务,AT&T提供国际日通行证。在这个特定问题上,我们还使用了缓存中的一个条目,并且只使用了2次LLM调用,大约300个令牌。这很棒。
现在问一个关于我们去西班牙旅行的更复杂的问题。我们即将去西班牙旅行,部分行程在游轮上,另一部分在陆地上,有哪些选项可以让我通过AT&T保持手机覆盖?在这里,我们可以看到代理回复了一个关于我即将到来的西班牙之旅的个性化答案。它注意到我将在陆地和游轮上度过时间。在这个特定问题上,我们也能够利用缓存中的某些内容,因此LLM比我们第一次运行时更高效。
让我们看看缓存中最终有什么。我们可以用距离阈值为1进行粗略的缓存检查,意思是找到任何接近此特定查询的内容。我们可以看到缓存中的条目是“我有一个游轮旅行即将到来,想确保我的AT&T覆盖会有效”。我们有一个被使用的响应,看起来在这个特定代理执行过程中被多次使用。
现在您可以在演示中访问这个代理,尝试另一个URL,尝试另一个网站,提出不同的问题,试验这个代理随着时间的推移表现如何。您在这里拥有了构建一个生产就绪的AI代理所需的一切,该代理在过程中使用了语义缓存。我们希望您学到了很多关于构建和扩展系统的重要基础知识。我们期待看到您用它构建出什么。

总结

在本节课中,我们一起学习了如何将语义缓存集成到AI代理工作流程中。我们回顾了代理缓存的价值不仅在于最终答案,还包括中间状态如子问题、生成代码和解决方案指导。我们使用LangGraph构建了一个具备查询分解、缓存检查、研究循环、质量评估和结果合成等节点的深度研究代理。通过三个场景的演示,我们观察到随着缓存命中率的提高(从25%到75%),LLM调用次数减少(从8次降至4-6次),请求延迟显著降低(从约20秒降至13-18秒),这验证了语义缓存在提升代理效率、降低成本和改善用户体验方面的有效性。最后,我们通过一个交互式Gradio演示展示了如何将理论应用于实践,构建一个能够从网页加载知识并利用缓存不断学习的生产级AI代理。
007:总结 🎯
在本节课中,我们将一起回顾语义缓存如何帮助AI代理提升速度与效率,并总结课程的核心要点。
课程概述
在之前的章节中,我们深入探讨了语义缓存的技术原理与应用。本节作为课程的总结,将系统性地回顾所学内容,并展望其应用前景。
核心内容回顾
语义缓存的核心机制在于,它允许AI代理通过比对当前查询与缓存中语义相似的历史查询,来直接复用已有的计算结果。其核心逻辑可以用以下伪代码表示:
if semantic_similarity(current_query, cached_query) > threshold:
return cached_response
else:
new_response = generate_response(current_query)
cache.add(current_query, new_response)
return new_response
这个过程避免了大量重复计算,从而显著提升了响应速度。
语义缓存的优势
以下是语义缓存为AI代理带来的主要益处:

- 提升速度:对于相似查询,可直接返回缓存结果,大幅减少响应延迟。
- 保证质量:复用的是已验证的高质量结果,确保了输出的一致性。
- 降低成本:减少了对大模型或复杂后端服务的调用次数,节省了计算资源。
- 增强可扩展性:高效的缓存机制使系统能够更好地应对高并发请求。
总结

本节课中我们一起学习了语义缓存的核心价值。我们了解到,通过智能地存储和复用语义相似的查询结果,AI代理能够在保持高质量输出的同时,变得更快、更高效、成本更低。希望本课程的知识能帮助你构建出更强大的AI应用。
我们期待看到你的实践成果。🚀

浙公网安备 33010602011771号