使用langchain与你自己的数据对话(三):检索(Retrieval)

 

之前我已经完成了使用langchain与你自己的数据对话的前两篇博客,还没有阅读这两篇博客的朋友可以先阅读一下:

  1. 使用langchain与你自己的数据对话(一):文档加载与切割
  2. 使用langchain与你自己的数据对话(二):向量存储与嵌入

今天我们来继续讲解deepleaning.AI的在线课程“LangChain: Chat with Your Data”的第四门课:检索(Retrieval)。

Langchain在实现与外部数据对话的功能时需要经历下面的5个阶段,它们分别是:Document Loading->Splitting->Storage->Retrieval->Output,如下图所示:

 在上一篇博客:向量存储与嵌入中我们介绍了嵌入(Embeddings)和开源向量数据库Chroma的基本原理,当文档经过切割(splitting),嵌入(embedding)后存入向量数据库以后,接下来就来到了检索(retrieval)环节:

检索是指根据用户的问题去向量数据库中搜索与问题相关的文档内容,当我们访问和查询向量数据库时可能会运用到如下几种技术:

  • 基本语义相似度(Basic semantic similarity)
  • 最大边际相关性(Maximum marginal relevance,MMR)
  • 过滤元数据
  • LLM辅助检索

在讨论这些新技术之前,想让我们完成一些基础性工作,比如设置一下openai的api key:

import os
import openai
import sys
sys.path.append('../..')
 
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv()) # read local .env file
 
openai.api_key  = os.environ['OPENAI_API_KEY']

基本语义相似度(Basic semantic similarity)

下面我们来实现一下语义的相似度搜索,我们把三句英语的句子存入向量数据库Chroma中,然后我们提出问题让向量数据库根据问题来搜索相关答案:

from langchain.vectorstores import Chroma
from langchain.embeddings.openai import OpenAIEmbeddings

#创建open ai的embedding对象
embedding = OpenAIEmbeddings()

#需要存入数据库的文本
texts = [
    """The Amanita phalloides has a large and imposing epigeous (aboveground) fruiting body (basidiocarp).""",
    """A mushroom with a large fruiting body is the Amanita phalloides. Some varieties are all-white.""",
    """A. phalloides, a.k.a Death Cap, is one of the most poisonous of all known mushrooms.""",
]


#创建向量数据库
smalldb = Chroma.from_texts(texts, embedding=embedding)

下面我们把代码中的三句英语的文本翻译为中文,这样便于大家更好的理解其含义:

  • 1.“鹅膏菌具有巨大而雄伟的地上(地上)子实体(担子果)。”
  • 2.“具有较大子实体的蘑菇是鹅膏菌。有些品种是全白色的。”
  • 3.“鬼笔甲,又名死亡帽,是所有已知蘑菇中毒性最强的一种。”

我们可以看到前两句都是描述的是一种叫“鹅膏菌”的菌类,包括它们的特征:有较大的子实体,第三句描述的是“鬼笔甲”,一种已知的最毒的蘑菇,它的特征就是:含有剧毒。下面我们提出一个问题:“告诉我有关子实体大的全白蘑菇的信息”,然后让向量数据库用相似度(similarity)方法去搜索2个和问题最相关的答案:

#问题:告诉我有关子实体大的全白蘑菇的信息
question = "Tell me about all-white mushrooms with large fruiting bodies"

smalldb.similarity_search(question, k=2)

 我们看到向量数据库返回了2个文档,就是我们存入向量数据库中的第一句和第二句。这里我们很明显的就可以看到chroma的similarity_search方法可以根据问题的语义去数据库中搜索与之相关性最高的文档也就是搜索到了第一句和第二句的文本,但这似乎有存在一些问题,因为第一句和第二句的含义非常接近,他们都是描述“鹅膏菌”及其“子实体”的,所以假如只返回其中的一句就足以满足要求了,如果返回两句含义非常接近的文本感觉是一种资源的浪费,下面我们来看一下max_marginal_relevance_search的搜索结果:

smalldb.max_marginal_relevance_search(question,k=2, fetch_k=3)

这里我们看到 max_marginal_relevance_search返回了第二和第三句的文本,尽管第三句与我们的问题的相关性不太高,但是这样的结果其实应该是更加的合理,因为第一句和第二句文本本来就有着相似的含义,所以只需要返回其中的一句就可以了,另外再返回一个与问题相关性弱一点的答案(第三句文本),这样似乎增强了答案的多样性,相信用户也会更加偏爱max_marginal_relevance_search 的结果,因为它兼顾了答案的相关性和多样性,下面我们就来讨论“最大边际相关性(Maximum marginal relevance ,MMR)”的问题。

最大边际相关性(Maximum marginal relevance ,MMR)

最大边际相关性(Maximum marginal relevance ,MMR)在论文:《The Use of MMR, Diversity-Based Reranking for Reordering Documents and Production Summaries》中有详细的介绍,MMR 尝试减少结果的冗余,同时保持结果与查询条件相关性和多样性的平衡(。作者在论文中提出了如下的公式:

 其中:

  • Q: 查询条件
  • R: 与查询Q相关的所有文档集
  • D: 与查询条件相关的文档集
  • S: R中已选择的文档子集
  • R\S: R中未选择的文档子集
  • λ:[0–1] 范围内的常数,用于结果的多样化
  • Sim1,Sim2: 用来度量查询Q与文档相似度的指标,如余弦相似度等。

有兴趣的读者可以仔细研读该篇论文,幸运的是我们不需要手动去实现MMR算法,Langchain的内置方法max_marginal_relevance_search已经帮我们首先了该算法,在执行max_marginal_relevance_search方法时,我们需要设置fetch_k参数,用来告诉向量数据库我们最终需要k个结果,向量数据库在搜索时会获取一个和问题相关的文档集,该文档集中的文档数量大于k,然后从中过滤出k个具有相关性同时兼顾多样性的文档。

下面我们来测试一下max_marginal_relevance_search方法,还记得在上一篇博客(向量存储与嵌入)中我们介绍了两种向量数据在查询时的失败场景吗?当向量数据库中存在相同的文档时,而当用户的问题又与这些重复的文档高度相关时,向量数据库会出现返回重复的文档情况,现在我们就可以运用Langchain的max_marginal_relevance_search来解决这个问题,不过首先我们需要先加载一下上一篇博客中保存在本地的关于吴恩达老师的机器学习课程cs229课程讲义的向量数据库:

#向量数据库地址
persist_directory = 'docs/chroma/'

embedding = OpenAIEmbeddings()

vectordb = Chroma(
    persist_directory=persist_directory,
    embedding_function=embedding
)

#打印向量数据库中的文档数量
print(vectordb._collection.count())

 

 这里我们加载了之前保存在本地的向量数据库,并查询了数据库中的文档数量为209,这与我们之前创建该数据库时候的文档数量是一致的,接下来我们提出了和上篇博客中相同的问题,然后先用similarity_search方法来查询一下,它应该会返回两篇相同的文档:

question = "what did they say about matlab?"
docs_ss = vectordb.similarity_search(question,k=3)

docs_ss 

 这里我们看到向量数据库返回了两篇相同的文档,这是因为在上一篇博客中我们在创建向量数据库时加载了两篇相同的文档(Lecture01.pdf),所以这回通过similarity_search方法搜索相似文档时它们被同时搜索到并返回给了用户。下面我们使用max_marginal_relevance_search方法来搜索:

docs_mmr = vectordb.max_marginal_relevance_search(question,k=3)

docs_mmr

这里我们看到向量数据库返回了3篇完全不同的文档,这是因为我们使用的是MMR搜索,它把搜索结果中相似度很高的文档做了过滤,所以它保留了结果的相关性又同时兼顾了结果的多样性。

过滤元数据

 在上一篇博客中的“失败的应用场景”的章节中我们还提出了一个问题:我们要求向量数据库在第三篇原始文档(Lecture03.pdf)中搜索相关答案,结果向量数据库的返回结果中除了第三篇文档的结果以外还包含了第一篇(Lecture01.pdf)和第二篇文档(Lecture02.pdf)的内容,这是我们所不希望看到的结果,之所以产生这样的结果是因为当我们向向量数据库提出问题时,数据库并没有很好的理解问题的语义,所以返回的结果不如预期,要解决这个问题,我们可以通过过滤元数据的方式来实现精准搜索,当前很多向量数据库都支持对元数据的操作:

question = "what did they say about regression in the third lecture?"

docs = vectordb.similarity_search(
    question,
    k=3,
    filter={"source":"docs/cs229_lectures/MachineLearning-Lecture03.pdf"}
)

for d in docs:
    print(d.metadata)

 这里我们可以看到通过我们在代码中设置了filter过滤条件后,向量数据库返回的3个答案呢都是基于第三篇文档的(Lecture03.pdf),这就符合我们的要求了,通过在similarity_search方法中加入filter参数,就可以指定需要搜索的原始文档,因为所有原始文档的文件名信息都保存在切割后文档的元数据信息中,通过过滤元数据信息就可以使向量数据库在指定的文档中搜索和问题相关的答案了。

LLM辅助检索

上面我们使用的是手动设置元数据的过滤参数filter来实现过滤指定文档的功能,其实这也不是很方便,因为我们每次都需要手动去设置过滤条件,这会非常的麻烦。有没有方法可以准确识别问题中的语义(因为在用户的问题中已经指出了需要过滤的文档)从而自动实现元数据过滤呢?也就是说是否有一种方法可以从用户问题的语义中自动推断出需要过滤的元数据信息呢?Langchain为我们提供了这样的方法,我们可以使用 SelfQueryRetriever,它使用 LLM 从用户原始问题中抽取取:

  • 用于向量搜索的查询字符串(search term)
  • 用于过滤元数据的信息(Filter)

其原理如下图所示:

 下面我们就来实现一下LLM辅助检索:

from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

#定义元数据的过滤条件
metadata_field_info = [
    AttributeInfo(
        name="source",
        description="The lecture the chunk is from, should be one of `docs/cs229_lectures/MachineLearning-Lecture01.pdf`, `docs/cs229_lectures/MachineLearning-Lecture02.pdf`, or `docs/cs229_lectures/MachineLearning-Lecture03.pdf`",
        type="string",
    ),
    AttributeInfo(
        name="page",
        description="The page from the lecture",
        type="integer",
    ),
]

#创建SelfQueryRetriever
document_content_description = "Lecture notes"
llm = OpenAI(temperature=0)
retriever = SelfQueryRetriever.from_llm(
    llm,
    vectordb,
    document_content_description,
    metadata_field_info,
    verbose=True
)

这里我们首先定义了metadata_field_info ,它包含了元数据的过滤条件source和page, 其中source的作用是告诉LLM我们想要的数据来自于哪里,page告诉LLM我们还需要提取相关的内容在原始文档的第几页。有了metadata_field_info信息后,LLM会自动从用户的问题中提取出上图中的Filter和Search term两项,然后向量数据库基于这两项去搜索相关的内容。下面我们看一下查询结果:

#问题
question = "what did they say about regression in the third lecture?"

#搜索相关文档
docs = retriever.get_relevant_documents(question)

#打印结果中的元数据信息
for d in docs:
    print(d.metadata)

 由于我们在定义SelfQueryRetriever时设置了verbose=True,所以我们可以看到一些SelfQueryRetriever执行的中间结果,从上面的返回结果中我们看到query和filter这两项,其中query=‘regression’就是从用户问题中提取出来的搜索项,搜索内容为“regression”, 而filter项是LLM根据metadata_field_info中定义的信息对向量数据库中的文档块的元数据进行过滤的条件。最后我们看到返回结果均来自于第三个文档(Lecture03.pdf),完全符合我们的要求。

附加技巧:压缩(compression)

所谓压缩是指提高文档检索质量的一种方法,当我们根据用户的问题使用检索器(Retriever)去检索向量数据库时,向量数据库一般会返回与问题相关文档块(chunks)中的所有内容,即把整个文档块的内容全部输出,这就可能会产生一些资源浪费的情况,因为和问题相关的文档内容可能只占该文档块的一小部分,可我们却输出了整个文档块的全部内容,这样会增加token成本,因为我们使用的openai的LLM不是免费的,它是根据token数量来收费的,因此输出整个文档块的内容其实是一种资源的浪费,幸运的是Langchain为我们提供给了ContextualCompressionRetriever,其原理如下图所示:

 从上图中我们看到,当向量数据库返回了所有与问题相关的所有文档块的全部内容后,会有一个Compression LLM来负责对这些返回的文档块的内容进行压缩,所谓压缩是指仅从文档块中提取出和用户问题相关的内容,并舍弃掉那些不相关的内容。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor

def pretty_print_docs(docs):
    print(f"\n{'-' * 100}\n".join([f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]))
    
# Wrap our vectorstore
llm = OpenAI(temperature=0)
compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vectordb.as_retriever()
)

在上面的代码中我们定义了一个LLMChainExtractor,它是一个压缩器,它负责从向量数据库返回的文档块中提取相关信息,然后我们还定义了ContextualCompressionRetriever,它有两个参数:base_compressor和base_retriever,其中base_compressor为我们前面定义的LLMChainExtractor的实例,base_retriever为早前定义的vectordb产生的检索器。下面我们来看一下执行结果:

question = "what did they say about matlab?"
compressed_docs = compression_retriever.get_relevant_documents(question)
pretty_print_docs(compressed_docs)

从上面的返回结果中我们看到,返回结果并不是整个文档块的内容,而只是文档块中的部分内容,这些内容与用户的问题是直接相关的,文档块中与问题不相关的内容都已经被舍弃了,这样就大大节省了使用LLM的成本。

其他类型的检索

值得注意的是,vectordb 并不是Langchain中唯一的一种检索器 。LangChain还提供了其他检索文档的方式,例如TF-IDF或SVM。

from langchain.retrievers import SVMRetriever
from langchain.retrievers import TFIDFRetriever
from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter


# Load PDF
loader = PyPDFLoader("docs/cs229_lectures/MachineLearning-Lecture01.pdf")
pages = loader.load()
all_page_text=[p.page_content for p in pages]
joined_page_text=" ".join(all_page_text)

# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size = 1500,chunk_overlap = 150)
splits = text_splitter.split_text(joined_page_text)


# Retrieve
svm_retriever = SVMRetriever.from_texts(splits,embedding)
tfidf_retriever = TFIDFRetriever.from_texts(splits)

这里我们定义了SVMRetriever,和TFIDFRetriever两个检索器,下面我们使用这两个检索器来检索向量数据库:

question = "What are major topics for this class?"
docs_svm=svm_retriever.get_relevant_documents(question)
docs_svm[0]

question = "what did they say about matlab?"
docs_tfidf=tfidf_retriever.get_relevant_documents(question)
docs_tfidf[0]

关于SVM和TF-IDF的相关技术不会在本文中讨论,读者可以执行查阅相关技术资料。

总结

今天我们学习了很多有用的新技术比如最大边际相关性(MMR),MMR可以让搜索结果保持相关性和多样性,这样可以避免出现冗余的搜索结果,除此之外我们可以定义元数据的过滤条件,这样向量数据库可以更加精准的搜索和问题相关的答案。另外我们还学习了LLM辅助检索,通过定义元数据字段信息metadata_field_info ,我们可以使用SelfQueryRetriever自动推断出用户问题中的搜索项和过滤项,这样可以避免手动设置元数据过滤条件带来的麻烦,后面我们还介绍了内容压缩检索器ContextualCompressionRetriever,通过ContextualCompressionRetriever我们可以从返回的整个文档块抽取和用户问题相关的内容,抛弃掉和问题无关的内容,这样可以大大节省LLM的使用成本。最后我们还简单介绍了SVM和TF-IDF检索器的使用方法,希望今天的内容对大家有所帮助!

参考资料

《The Use of MMR, Diversity-Based Reranking for Reordering Documents and Production Summaries》

Vector stores | 🦜️🔗 Langchain

Self-querying | 🦜️🔗 Langchain

Contextual compression | 🦜️🔗 Langchain

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/49454.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue2+wangEditor5富文本编辑器(图片视频自定义上传七牛云/服务器)

1、安装使用 安装 yarn add wangeditor/editor # 或者 npm install wangeditor/editor --save yarn add wangeditor/editor-for-vue # 或者 npm install wangeditor/editor-for-vue --save在main.js中引入样式 import wangeditor/editor/dist/css/style.css在使用编辑器的页…

java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法

文章目录 一、记录文件相关操作方法二、代码1.读取路径返回List\<File>2.读取路径返回List\<String>3.删除文件夹4.删除文件 一、记录文件相关操作方法 二、代码 1.读取路径返回List<File> import org.slf4j.LoggerFactory; import org.slf4j.Logger;impo…

【Rust笔记】意译解构 Object Safety for trait

意译解构Object Safety for trait 借助【虚表vtable】对被调用成员函数【运行时内存寻址】的作法允许系统编程语言Rust模仿出OOP高级计算机语言才具备的【专用多态Ad-hoc Polymorphism】特性。 计算机高级语言中的“多态”术语是一个泛指。它通常可被细化为 基于继承关系的“子…

QPoint、QLine、QSize、QRect

QPoint、QLine、QSize、QRect QPointQLineQSizeQRect QPoint // 构造函数 // 构造一个坐标原点, 即(0, 0) QPoint::QPoint(); // 参数为 x轴坐标, y轴坐标 QPoint::QPoint(int xpos, int ypos);// 设置x轴坐标 void QPoint::setX(int x); // 设置y轴坐标 void QPoint::setY(in…

性能测试 Linux 环境下模拟延时和丢包实现

在性能测试过程中&#xff0c;我们还需要模拟网络异常的情况下&#xff0c;是否会出现一些异常数据。最常见的就是写库操作&#xff0c;比如说我们下单的场景&#xff0c;如果出现网络异常的时候是否会出现数据对不上这种情况。 如我们JMeter发送成功的请求数量和最终数据库表…

向量vector与erase()

运行代码&#xff1a; //向量vector与erase() #include"std_lib_facilities.h" //声明Item类 struct Item {string name;int iid;double value;Item():name(" "),iid(0),value(0.0){}Item(string ss,int ii,double vv):name(ss),iid(ii),value(vv){}frien…

将AWS S3大文件文件上传相关的API集成为js文件,功能包括 多文件并行上传、文件分片上传、断点续传、文件分片合成、上传暂停、取消上传、文件上传进度条显示

地址 https://github.com/gk-1213/easy-s3/tree/main easy-s3 将AWS S3大文件文件上传相关的API集成为js文件&#xff0c;功能包括多文件并行上传、文件分片上传、断点续传、文件分片合成、上传暂停、取消上传、文件上传进度条显示。 暂时不包括文件分片下载相关功能&#…

【论文阅读】利用道路目标特征的多期车载激光点云配准

目录 引 言1 道路场景点云特征2 配准方法2.1 配准基元获取2.2 特征点提取2.3 两期道路场景车载点云的配准 2.3.1 基于特征点的4PCS 粗配准 3 实验与分析4 结论5 参考文献 摘 要 针对车载移动测量系统获取的城市道路点云场景巨大、目标复杂多样&#xff0c;多期道路场景重访车载…

redis(10):spring+redis+mysql缓存实现

1 新建spring项目 2 修改pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="ht…

振弦采集仪及在线监测系统完整链条的岩土工程隧道安全监测

振弦采集仪及在线监测系统完整链条的岩土工程隧道安全监测 近年来&#xff0c;随着城市化的不断推进和基础设施建设的不断发展&#xff0c;隧道建设也日益成为城市交通发展的必需品。然而&#xff0c;隧道建设中存在着一定的安全隐患&#xff0c;如地质灾害、地下水涌流等&…

使用贝叶斯算法完成文档分类问题

贝叶斯原理 贝叶斯原理&#xff08;Bayes theorem&#xff09;是一种用于计算条件概率的数学公式。它是以18世纪英国数学家托马斯贝叶斯&#xff08;Thomas Bayes&#xff09;的名字命名的。贝叶斯原理表达了在已知某个事件发生的情况下&#xff0c;另一个事件发生的概率。具体…

持续部署CICD

目录 &#xff08;1&#xff09;CICD的开展场景 &#xff08;2&#xff09;项目实际应用 CICD 是持续集成&#xff08;Continuous Integration&#xff09;和持续部署&#xff08;Continuous Deployment&#xff09;简称。指在研发过程中自动执行一系列脚本来降低开发引入 bug…

后台管理系统中刷新业务功能的实现

实现 下载vueuse npm i vueuse/core在header组件中引入并给全屏按钮绑定点击事件 <el-button type"default" click"toggle" icon"FullScreen" circle></el-button>import { useFullscreen } from vueuse/coreconst { toggle } u…

electron的electron-packager打包运行和electron-builder生产安装包过程,学透 Electron 自定义 Dock 图标

electron的electron-packager打包运行和electron-builder生产安装包过程 开发electron客户端程序&#xff0c;打包是绕不开的问题。 macOS 应用构建&#xff0c;看似近在咫尺&#xff0c;实则坑坑致命。 场景&#xff1a;mac笔记本打包&#xff0c;以及生产出可交付的软件安装…

queue

文章目录 定义分类链式队列静态队列循环队列静态队列为什么必须是循环队列&#xff1f;循环队列需要几个参数&#xff1f;循环队列入队伪代码循环队列出队伪代码判断循环队列是否为空判断循环队列是否已满 循环队列的代码实现队列的应用 定义 一种可以实现“先进先出”的存储结…

探索Java API学习路线:从基础到高级的全面指南

文章目录 第一阶段&#xff1a;入门基础1. 环境准备2. 学习Java基础 第二阶段&#xff1a;熟悉常用的Java API1. Java标准库2. Java API文档 第三阶段&#xff1a;深入学习特定领域的Java API1. Java GUI API2. Java数据库连接&#xff08;JDBC&#xff09;API3. Java多线程API…

苍穹外卖day08——地址簿+用户下单+订单支付(做不了)

导入地址簿——需求分析与设计 产品原型 接口设计 数据库设计 导入地址簿——代码导入 导入地址簿——功能测试 没有问题 用户下单——需求分析与设计 业务说明 业务流程 接口设计 数据库设计 用户下单——代码开发 DTO设计和VO设计 Controller层中 RequestMapping(&q…

堆喷射的小例子

引自&#xff1a;https://blog.csdn.net/lixiangminghate/article/details/53413863 照着作者的意思&#xff0c;自己的测试代码&#xff1a; #include <iostream> #include <windows.h> #include <stdio.h>class base {char m_buf[8]; public:virtual int…

CAN学习笔记1:计算机网络

计算机网络 1 概述 计算机网络就是把多种形式的计算机用通信线路连接起来&#xff0c;并使其能够互相进行交换的系统。实际上&#xff0c;计算机网络包括了计算机、各种硬件、各种软件、组成网络的体系结构、网络传输介质和网络通信计数。因此&#xff0c;计算机网络是计算机…

阿里Java开发手册~集合处理

1. 【强制】关于 hashCode 和 equals 的处理&#xff0c;遵循如下规则&#xff1a; 1 &#xff09; 只要重写 equals &#xff0c;就必须重写 hashCode 。 2 &#xff09; 因为 Set 存储的是不重复的对象&#xff0c;依据 hashCode 和 equals 进行判断&#xff…