基于LangChain自查问检索器的RAG系统开发实战
想了解更多AIGC的内容,请访问:
AI.x社区
最近,我在阅读Max.com网站时想找一部电影看。通常,这个环节包括阅读系统出现给我的各种列表,阅读一些关系形容,而后筛选一些看起来幽默的电影。假设我知道我想看的电影的片名或我青睐的演员的名字,我通常只会点击搜查性能。否则,搜查就没有多大用途了。
如今,我突然想到了一个新的想法:为什么我不能用人造言语来查找一部电影,更多地基于电影的气氛或实质,而不只仅是题目或演员呢?例如,为什么我不能启动Max、Netflix或Hulu等流媒体播放平台,并在搜查栏中键入相似于以下查问之一呢:
这种方法的美妙之处超出了更人造的电影搜查模式,还包全了用户的隐衷。该系统基本不会经常使用用户数据,不是开掘用户的行为、青睐和不青睐来提供应介绍系统。惟一须要的就是一个查问。
为此,我开发了本文中要展现给大家的一个电影搜查程序。这是一个基于RAG(检索增强生成)的系统,它可以接受用户的查问,嵌入查问,并启动相似性搜查,以找到相似的电影。不过,这个程序逾越了个别的RAG系统。这个系统经常使用了所谓的自查问检索器。该技术准许在启动相似性搜查之前,依据电影的元数据对其启动过滤。因此,假设用户有一个相似“介绍1980年后拍摄的以少量爆炸为特色的恐惧电影”的查问,搜查算法将首先过滤掉一切不是“1980年后制造的恐惧片”的电影,而后再对“以少量爆炸为主”的电影启动相似性搜查。
在本文中,我将提供一个关于我如何创立此系统的总体概述。假设您想深化了解这个程序,完整的源代码将在文后的链接参考处提供。
接上去,让咱们继续作深化引见。
检索数据
首先,该名目的数据来自电影数据库(TMDB:,并失掉了一切者的容许。他们的API经常使用繁难,保养良好,并且没有严厉的费率限度。我从他们的API中提取了以下电影属性:
以下是如何经常使用TMDB API和Python的照应库提取数据的片段:
def get_data(API_key, Movie_ID, max_retries=5):"""函数以JSON格局提取感兴味的电影的详细信息。parameters:API_key (str): Your API key for TMBDMovie_ID (str): TMDB id for film of interestreturns:dict: JSON格局的字典,蕴含您的电影的一切细节兴味"""query = 'https://api.themoviedb.org/3/movie/' + Movie_ID + \'?api_key='+API_key + '&append_to_response=keywords,' + \'watch/providers,credits'for i in range(max_retries):response = requests.get(query)if response.status_code == 429:# If the response was a 429, wait and then try againprint(f"Request limit reached. Waiting and retrying ({i+1}/{max_retries})")time.sleep(2 ** i)# Exponential backoffelse:dict = response.json()return dict
请留意,该查问须要电影ID(也是经常使用TMDB取得的)以及append_to_response,这准许我提取几种类型的数据,例如关键字、影片提供商、演员(导演和演员)以及无关电影的一些基本信息。还有一些基本的框架类代码,以防我到达速率限度,虽然我留意到从未出现这种状况。
而后,咱们必定解析JSON照应。以下的代码片段展现了如何解析电影中的演员和导演:
credits = dict['credits']actor_list, director_list = [], []# 剖析演员表cast = credits['cast']NUM_ACTORS = 5for member in cast[:NUM_ACTORS]:actor_list.append(member["name"])# 剖析剧组crew = credits['crew']for member in crew:if member['job'] == 'Director':director_list.append(member["name"])actor_str = ', '.join(list(set(actor_list)))director_str = ', '.join(list(set(director_list)))
请留意,我将演员数量限度在一部电影的前五名。我还必定说明,我只对导演感兴味,由于系统的照应还包括其他类型的剧组成员,如编辑、服装设计师等。
一切这些数据随后被编译成CSV文件。上方列出的每个属性都被转换成了一列,如今每一行都代表一部特定的电影。以下是经环节序创立的2008_movie_collection_data.csv文件中的短片。在这个名目中,我取得了大概100部1920年至2023年的顶级电影。
用于演示目的的电影数据片段(作者自己提供)
信不信由你,我还没看过《功夫熊猫》。兴许我必定成功这个名目。
将文档上载到pinecone网站
接上去,我必定将csv数据上行到网站([译者注]。Pinecone是一个非开源型的向量数据库。Pinecone支持在大规模向量集上启动极速且实时的搜查,具备亚秒级的查问照应时期,适用于须要高性能和实时性的大型运行,特意适宜于构建实时介绍系统、电商搜查引擎和社交媒体内容过滤等)。通常,分块在RAG系统中很关键,但这里每个“文档”(CSV文件的行)都很短,所以分块不是一个疑问。我首先必定将每个CSV文件转换为LangChain文档,而后指定哪些字段应该是关键内容,哪些字段应该作为元数据。
以下是用于构建这些文档的代码片段:
# 从一切csv文件加载数据loader = DirectoryLoader(path="./data",glob="*.csv",loader_cls=CSVLoader,show_progress=True)docs = loader.load()metadata_field_info = [AttributeInfo(name="Title", description="The title of the movie", type="string"),AttributeInfo(name="Runtime (minutes)",description="The runtime of the movie in minutes", type="integer"),AttributeInfo(name="Language",description="The language of the movie", type="string"),...]for doc in docs:#将page_content字符串解析到字典中page_content_dict = dict(line.split(": ", 1)for line in doc.page_content.split("\n") if ": " in line)doc.page_content = 'Overview: ' + page_content_dict.get('Overview') + '. Keywords: ' + page_content_dict.get('Keywords')doc.metadata = {field.name: page_content_dict.get(field.name) for field in metadata_field_info}#将字段从字符串转换为字符串列表for field in fields_to_convert_list:convert_to_list(doc, field)# 将字段从字符串转换为整数for field in fields_to_convert_int:convert_to_int(doc, field)
LangChain的DirectoryLoader担任将一切csv文件加载到文档中。而后,我须要指定什么应该是page_content,什么应该是metadata;这是一个关键的选择。page_content将在检索阶段嵌入并用于相似性搜查。在启动相似性搜查之前,metadata将仅用于过滤目的。我选择驳回overview和keywords属性并嵌入它们,其他的属性将是元数据。应该做进一步的调整,看看title能否也应该包括在page_content中,但我发现这种性能对大少数用户查问都很有效。
接上去,文件必定上行到pinecone网站。这是一个相当繁难的环节:
# 假设尚未创立索引,则敞开注释pc.create_index(name=PINECONE_INDEX_NAME,dimension=1536,metric="cosine",spec=PodSpec(environment="gcp-starter"))# 目的索引和审核形态pc_index = pc.Index(PINECONE_INDEX_NAME)print(pc_index.describe_index_stats())embeddings = OpenAIEmbeddings(model='text-embedding-ada-002')vectorstore = PineconeVectorStore(pc_index, embeddings)# 创立记载治理器namespace = f"pinecone/{PINECONE_INDEX_NAME}"record_manager = SQLRecordManager(namespace, db_url="sqlite:///record_manager_cache.sql")record_manager.create_schema()# 将文档上载到松果网站index(docs, record_manager, vectorstore,cleanup="full", source_id_key="Website")
我只想在这里强调几个事件:
创立自查问检索器
自查问检索器将准许咱们经过咱们之前定义的元数据来过滤RAG时期检索到的电影。这将大大提高咱们电影介绍人的适用性。
在选用矢量存储时,一个关键的思考起因是确保它支持按元数据过滤,由于并非一切数据库都支持这种技术。链接处提供了LangChain支持自查问检索的数据库列表。另一个关键的思考起因是关于每个矢量存储准许什么类型的比拟器。比拟器是咱们经过元数据启动过滤的方法。例如,咱们可以经常使用eq比拟器来确保咱们的电影属于科幻类型:eq('Genre', 'Science Fiction')。并非一切矢量存储都准许一切比拟器。举个例子,有兴味的读者可以观察一下开源的嵌入式数据库Chroma中支持的比拟器(,以及它们与Pinecone网站中支持的比拟器(有何不同。咱们须要通知模型准许经常使用哪些比拟器,以防止它异常地写入制止的查问。
除了通知模型存在哪些比拟器之外,咱们还可以提供用户查问和相应过滤器的模型示例。这被称为小样本学习(Few-shot Learning),这对指点您的模型是十分贵重的。
要详细地了解这一技巧有何协助,您可以尝试检查以下两个用户查问:
我的元数据过滤模型很容易为这些示例中的每一个编写相反的过滤查问,虽然我宿愿对它们启动不同的处置。第一部应该只介绍兰蒂莫斯执导的电影,而第二部应该介绍与兰蒂莫斯电影有相似气氛的电影。为了确保这种行为,我一点点粗疏地提供了我想要的行为的模型示例。言语模型的美妙之处在于,它们可以应用自己的“推理”才干和环球常识,将这些小样本学习示例推行到其他用户查问中。
document_content_description = "Brief overview of a movie, along with keywords"# 定义准许的比拟器列表allowed_comparators = ["$eq",# Equal to (number, string, boolean)"$ne",# Not equal to (number, string, boolean)"$gt",# Greater than (number)"$gte",# Greater than or equal to (number)"$lt",# Less than (number)"$lte",# Less than or equal to (number)"$in",# In array (string or number)"$nin",# Not in array (string or number)"$exists", # Has the specified metadata field (boolean)]examples = [("Recommend some films by Yorgos Lanthimos.",{"query": "Yorgos Lanthimos","filter": 'in("Directors", ["Yorgos Lanthimos]")',},),("Films similar to Yorgos Lanthmios movies.",{"query": "Dark comedy, absurd, Greek Weird Wave","filter": 'NO_FILTER',},),...]metadata_field_info = [AttributeInfo(name="Title", description="The title of the movie", type="string"),AttributeInfo(name="Runtime (minutes)",description="The runtime of the movie in minutes", type="integer"),AttributeInfo(name="Language",description="The language of the movie", type="string"),...]constructor_prompt = get_query_constructor_prompt(document_content_description,metadata_field_info,allowed_comparators=allowed_comparators,examples=examples,)output_parser = StructuredQueryOutputParser.from_components()query_constructor = constructor_prompt | query_model | output_parserretriever = SelfQueryRetriever(query_constructor=query_constructor,vectorstore=vectorstore,structured_query_translator=PineconeTranslator(),search_kwargs={'k': 10})
除了示例之外,模型还必定知道每个元数据字段的形容。这有助于它了解什么是元数据过滤。
最后,咱们来构建咱们的链。这里的query_model是经常使用OpenAI API的GPT-4 Turbo的一个实例。我倡导经常使用GPT-4而不是3.5来编写这些元数据过滤器查问,由于这是一个关键步骤,理由是3.5会更频繁地出错。search_kwargs={'k':10}通知检索器依据用户查问找出十部最相似的电影。
创立聊天模型
最后,在构建了自查问检索器之后,咱们可以在此基础上构建规范的RAG模型。咱们首先定义咱们的聊天模型。这就是我所说的摘要模型,由于它驳回高低文(检索到的电影+系统信息),并以每个介绍的摘要作为照应。假设你想降落老本,这个模型可以是GPT-3.5 Turbo;当然,假设你想取得相对最佳的结果,这个模型也可以是GPT-4 Turbo。
在系统信息中,我通知机器人它的目的是什么,并提供了一系列倡导和限度,其中最关键的是不要介绍自我查问检索器没有提供应它的电影。在测试中,当用户查问没有从数据库中失掉电影时,我遇到了疑问。例如,查问“介绍一些由韦斯·安德森执导的马特·达蒙主演的1980年之前拍摄的恐惧电影”会造成自我查问检索器无法检索就任何电影(由于虽然听起来很棒,但这部电影并不存在)。在没有电影数据的状况下,该模型会经常使用自己的(失误的)内存来尝试介绍一些电影。这是不好的行为。我不宿愿Netflix的介绍人讨论数据库中没有的电影。上方的系统信息成功阻止了此行为。我确实留意到GPT-4比GPT-3.5更擅长遵照指令,这是在预料之中的事件。
chat_model = ChatOpenAI(model=SUMMARY_MODEL_NAME,temperature=0,streaming=True,)prompt = ChatPromptTemplate.from_messages([('system',"""Your goal is to recommend films to users based on theirquery and the retrieved context. If a retrieved film doesn't seemrelevant, omit it from your response. If your context is emptyor none of the retrieved films are relevant, do not recommend films, but instead tell the user you couldn't find any filmsthat match their query. Aim for three to five film recommendations,as long as the films are relevant. You cannot recommend more thanfive films. Your recommendation should be relevant, original, andat least two to three sentences long.YOU CANNOT RECOMMEND A FILM IF IT DOES NOT APPEAR IN YOURCONTEXT.# TEMPLATE FOR OUTPUT- **Title of Film**:- Runtime:- Release Year:- Streaming:- (Your reasoning for recommending this film)Question: {question}Context: {context}"""),])def format_docs(docs):return "\n\n".join(f"{doc.page_content}\n\nMetadata: {doc.metadata}" for doc in docs)# Create a chatbot Question & Answer chain from the retrieverrag_chain_from_docs = (RunnablePassthrough.assign(context=(lambda x: format_docs(x["context"])))| prompt| chat_model| StrOutputParser())rag_chain_with_source = RunnableParallel({"context": retriever, "question": RunnablePassthrough()}).assign(answer=rag_chain_from_docs)
上述代码中,formatdocs用于格局化提供应模型的信息,使其易于了解和解析。咱们向模型提供page_content(概述和关键字)以及元数据(一切其他电影属性);任何它或许须要用来更好地向用户介绍电影的信息。
rag_chain_from_docs是一个链,它失掉检索到的文档,并经常使用format_docs对其启动格局化,而后将格局化的文档馈送到模型用来回答疑问的高低文中。最后,咱们创立了rag_chain_with_source,这是一个RunnableParallel,望文生义,它并行运转两个操作:自查问检索器启动以检索相似的文档,而查问只是经过RunnablePassthrough()函数传递给模型。而后未来自这两个并行组件的结果启动组合,并经常使用rag_chain_from_docs生成答案。这里的source指的是检索器,它可以访问一切的“source”文档。
由于我宿愿答案是流式的(例如,像ChatGPT这样一块一块地出现给用户),所以咱们经常使用了以下代码:
for chunk in rag_chain_with_source.stream(query):for key in chunk:if key == 'answer':yield chunk[key]
程序展现
如今进入幽默的局部:与模型一同玩。Streamlit软件是一个用于创立前端和托管运行程序的低劣工具。当然,我不会在本文中讨论所开发软件的用户界面关系的代码;无关此用户界面成功的详细信息,请参阅文后所附的原始代码。当然,这些代码也相当繁难,Streamlit网站(上还有很多其他的例子可供参考。
电影搜查实例程序的用户界面(作者自己提供图片)
您可以经常使用软件中提供的好几个方面的倡导,但首先让咱们尝试经常使用自己的查问:
示例查问和模型照应状况(作者自己提供图片)
在底层的代码成功中,这个自我查问的检索器确保过滤掉任何不是法语的电影。而后,它对“生长故事”启动了相似性搜查,得出了十部在此背景下的电影。最后,机器人选用了五部电影启动介绍。请留意倡导的电影范围:有些电影的上映日期最早在1959年,最晚在2012年。为了繁难起见,我确保机器人提供的信息中蕴含电影的运转时期、上映年份、流媒体提供商以及机器人手工制造的冗长介绍。
(旁注:假设你还没有看过《拳》( The Blows:,请中止你正在做的任何事件,立刻去看一看吧。)
值得留意的是,以前在大型言语模型中通常被视为负面的性质,例如其照应的不确定性,如今被系统以为是侧面的性质。向模型提出雷同的疑问两次,你或许会失掉稍微不同的倡导。
关键的是,要留意实施的一些局限性:
最后,您或许对本文名目作出的改良之一是,在检索后对文档(启动从新排序。另外,提供一个聊天模型也或许很幽默,由于你可以在多回合的对话中与之交谈,而不只仅是一个QA机器人。此外,你还可以创立一个介绍器代理(,以便在查问不清楚的状况下向用户揭示一个明晰的疑问。
最后,祝您的电影搜查玩得开心!
链接参考
译者引见
朱先忠,社区编辑,专家博客、讲师,潍坊一所高校计算机老师,自在编程界老兵一枚。
原文题目:How to Build a RAG System with a Self-Querying Retriever in LangChain,作者:Ed Izaguirre
链接:
想了解更多AIGC的内容,请访问:
AI.x社区