引言
为了克制这个疑问,检索增强生成(RAG)处置方案越来越受欢迎。RAG的重要思维是将外部文档整合到大型言语模型中,并指点其行为仅从外部常识库中回答疑问。详细地说,这是经过将文档分块为更小的块,计算每个块的嵌入(数值表示),而后将嵌入作为索引存储在专门的向量数据库中来成功的。
RAG上班流程示用意——查问被转换为嵌入,经过检索模型与向量数据库婚配,并与检索到的数据相结合,最终经过大型言语模型发生照应。
高低文检索RAG
将用户的查问与向量数据库中的小块启动婚配的环节通常效果良好;但是,它还存在以下疑问:
为了应答这些疑问,Anthropic公司最近引入了 一种向每个块增加高低文的方法 ;与原始RAG相比,该方法的性能有了清楚提高。在将文档拆分为块后,该方法首先将块与整个文档作为高低文一同发送到LLM,为每个块调配一个冗长的高低文。随后,高低文附加的块被保留到向量数据库中。它们进一步经常使用 bm25检索器 将高低文分块与最佳婚配相结合,该检索器经常使用bm25方法搜查文档,并经常使用一个从新排序模型,该模型依据相关性为每个检索到的块调配评分。
具备高低文检索的多模态RAG
虽然性能有了清楚提高,但Anthropic公司仅证实了这些方法对文本类型数据的实用性。但当今环球中,许多文档中丰盛的消息的来源包括图像(图形、图形)和复杂的表格,等等。假设咱们只解析文档中的文本,咱们将不可深化了解文档中的其余形式。因此,蕴含图像和复杂表格的文档须要高效的解析方法,这不只须要从文档中正确提取它们,还须要了解它们。
经常使用Anthropic公司的最新模型(claude-3-5-connect-20240620)为文档中的每个块调配高低文在大型文档的状况下或者会触及高老本,由于它触及将整个文档与每个块一同发送。虽然 Claude模型的揭示缓存技术 可以经过在API调用之间缓存频繁经常使用的高低文来清楚降落这一老本,但其老本仍远高于OpenAI公司的老本高效模型,如gpt-4o-mini。
本文旨在讨论针对上述Anthropic公司方法的进一步扩展,如下所示:
在了解了Anthropic公司关于高低文检索的 博客文章 之后,我在 GitHub链接 上找到了OpenAI公司的局部成功。但是,它经常使用传统的分块和LlamaParse方法,没有最近推出的 初级形式 。我发现Llamaparse的初级形式在提取文档中的不同结构方面十分有效。
Anthropic公司的高低文检索成功也可以在GitHub上找到,它经常使用了LlamaIdex形象;但是,它没有成功多模态解析。在撰写本文时,LlamaIdex提供了一个降级的 成功 ,它经常使用了多模态解析和高低文检索。该成功经常使用了Anthropic公司的LLM(claude-3–5-connect-2024062)和Voyage公司的嵌入模型()。但是,它们并没有像Anthropic公司的博客文章中提到的那样探求BM25(Best Matching 25)排序算法和重排序(Reranking)技术。
本文讨论的高低文检索成功是一种低老本、多模态的RAG处置方案,经过BM25搜查和从新排序提高了检索性能。还将这种基于高低文检索的多模态RAG(CMRAG)的性能与基本RAG和LlamaIdex的高低文检索成功启动了比拟。
上方4个链接中从新经常使用了这其中的一些配置,并启动了必要的修正。
此成功的源代码可在上取得。
本文中用于成功基于高低文检索的多模态RAG(以下简称“CMRAG”)的总体方法示用意如下所示:
解析后的节点在保留到向量数据库之前会被调配高低文。高低文检索触及结合嵌入(语义搜查)和TF-IDF向量(最佳婚配搜查),而后经过从新排序器模型启动从新排序,最后由LLM生成照应。
接上去,让咱们深化钻研一下CMRAG的分步成功。
多模态解析
首先,须要装置以下依赖库才干运转本文中讨论的代码。
!pip install llama-index ipython cohere rank-bm25 pydantic nest-asyncio python-dotenv openai llama-parse
GitHub笔记本文件中也提到了一切须要导入才干运转整个代码的依赖库。在这篇文章中,我经常使用了 芬兰移民关键数据 (依据CC By 4.0容许,准许重复经常使用),其中蕴含几个图表、图像和文本数据。
LlamaParse经常使用商业性质的多模态模型(如gpt-4o)提供 多模态解析 来处置文档提取。
parser = LlamaParse(use_vendor_multimodal_model=Truevendor_multimodal_model_name="openai-gpt-4o"vendor_multimodal_api_key=sk-proj-xxxxxx)
在这种形式下,会对文档的每一页启动截图,而后将截图发送到多模态模型,并附上提取标志的指令。每页的标志结果被兼并到最终输入中。
最近的 LlamaParse初级形式 提供了先进的多模态文档解析支持,能够将文本、表格和图像提取到结构良好的标志中,同时清楚缩小了缺失的内容和幻觉。它可以经过在 Llama云平台 创立一个收费账号并取得API密钥来经常使用。收费方案提供每天解析1000个页面。
LlamaParse初级形式的经常使用形式如下:
from llama_parse import LlamaParseimport os# 此函数担任从指定目录下读取一切文件def read_docs(data_dir) -> List[str]:files = []for f in os.listdir(data_dir):fname = os.path.join(data_dir, f)if os.path.isfile(fname):files.append(fname)return filesparser = LlamaParse(result_type="markdown",premium_mode=True,api_key=os.getenv("LLAMA_CLOUD_API_KEY"))files = read_docs(data_dir =>
在上述代码中,咱们首先从指定目录读取文档,经常使用解析器的get_json_result()方法解析文档,并经常使用解析器的get_images()方法失掉图像字典。随后,提取节点并将其发送到LLM,以经常使用retrieve_nodes()方法依据整个文档调配高低文。解析这份文档(60页),包括失掉图像词典等外容,合计耗时5分34秒(一次性性环节)。
print("Parsing...")json_results = parser.get_json_result(files)print("Getting image dictionaries...")images = parser.get_images(json_results, download_path=image_dir)print("Retrieving nodes...")
json_results[0]["pages"][3]
报告中的第四页由JSON结果的第一个节点表示(按作者陈列的图像)
高低文检索
经过retrieve_nodes()函数从解析的josn_results中提取单个节点和相关图像(屏幕截图)。每个节点与一切节点(以下代码中的doc变量)一同被发送到_assign_context()函数。_assign_context()函数经常使用揭示模板 context_prompt_TMPL (来自链接,并经过修正后驳回)为每个节点增加繁复的高低文。经过这种形式,咱们将元数据、标志文本、高低文和原始文本集成到节点中。
以下代码显示了retrieve_nodes()函数的成功。两个辅佐函数_get_sorted_image_files()和get_img_page_number()区分按页面和图像的页码失掉排序后的图像文件。总体指标不是像便捷的RAG那样仅依赖原始文原本生成最终答案,而是思考元数据、标志文本、高低文和原始文本,以及检索到的节点的整个图像(屏幕截图)(节点元数据中的图像链接)来生成最终照应。
# 针对文件名经常使用正则表白式失掉图像所在的页码def get_img_page_number(file_name):match = re.search(r"-page-(\d+)\.jpg$", str(file_name))if match:return int(match.group(1))return 0#失掉按页排序的图像文件def _get_sorted_image_files(image_dir):raw_files = [f for f in list(Path(image_dir).iterdir()) if f.is_file()]sorted_files = sorted(raw_files, key=get_img_page_number)return sorted_files#针对高低文块的高低文揭示模板CONTEXT_PROMPT_TMPL = """You are an AI assistant specializing in document analysis. Your task is to provide brief, relevant context for a chunk of text from the given document.Here is the document:<document>{document}</document>Here is the chunk we want to situate within the whole document:<chunk>{chunk}</chunk>Provide a concise context (2-3 sentences) for this chunk, considering the following guidelines:1. Identify the main topic or concept discussed in the chunk.2. Mention any relevant information or comparisons from the broader document context.3. If applicable, note how this information relates to the overall theme or purpose of the document.4. Include any key figures, dates, or percentages that provide important context.5. Do not use phrases like "This chunk discusses" or "This section provides". Instead, directly state the context.Please give a short succinct context to situate this chunk within the overall document to improve search retrieval of the chunk.Answer only with the succinct context and nothing else.Context:"""CONTEXT_PROMPT = PromptTemplate(CONTEXT_PROMPT_TMPL)#上方的函数针对每一个块生成高低文def _assign_context(document: str, chunk: str, llm) -> str:prompt = CONTEXT_PROMPT.format(document=document, chunk=chunk)response = llm.complete(prompt)context = response.text.strip()return context#上方函数经常使用高低文生成文本节点def retrieve_nodes(json_results, image_dir, llm) -> List[TextNode]:nodes = []for result in json_results:json_dicts = result["pages"]document_name = result["file_path"].split('/')[-1]docs = [doc["md"] for doc in json_dicts]# 提取文字消息image_files = _get_sorted_image_files(image_dir)#提取图像消息# 衔接一切文档以创立完整的文件文字内容document_text = "\n\n".join(docs)for idx, doc in enumerate(docs):# 针对每个块(页)生成高低文context = _assign_context(document_text, doc, llm)# 把文档内容与初始块结合到一同contextualized_content = f"{context}\n\n{doc}"# 经常使用高低文明后的内容生成文本节点chunk_metadata = {"page_num": idx + 1}chunk_metadata["image_path"] = str(image_files[idx])chunk_metadata["parsed_text_markdown"] = docs[idx]node = TextNode(text=contextualized_content,metadata=chunk_metadata,)nodes.append(node)return nodes#取得文本节点text_node_with_context = retrieve_nodes(json_results, image_dir, llm)First page of the report (image by author)First page of the report (image by author)
增加了高低文和元数据的节点(图片由作者提供)
用BM25增强高低文检索并从新排序
一切具备元数据、原始文本、标志文本和高低文消息的节点都被索引到向量数据库中。节点的BM25索引被创立并保留在pickle文件中,用于查问推理。处置后的节点也会被保留,以供经常使用(text_node_with_context.pkl)。
# 创立向量存储牵引index = VectorStoreIndex(text_node_with_context, embed_model=embed_model)index.storage_context.persist(persist_dir=output_dir)# 构建BM25索引documents = [node.text for node in text_node_with_context]tokenized_documents = [doc.split() for doc in documents]bm25 = BM25Okapi(tokenized_documents)# 保留bm25和text_node_with_contextwith open(os.path.join(output_dir, 'tokenized_documents.pkl'), 'wb') as f:pickle.dump(tokenized_documents, f)with open(os.path.join(output_dir, 'text_node_with_context.pkl'), 'wb') as f:pickle.dump(text_node_with_context, f)
如今,咱们可以初始化一个查问引擎,经常使用以下管道启动查问。但在此之前,设置以下揭示以指点LLM生成最终照应的行为。初始化多模态LLM(gpt-4o-mini)以生成最终照应。此揭示可依据须要启动调整。
# 定义QA 揭示模板RAG_PROMPT = """\Below we give parsed text from documents in two different formats, as well as the image.---------------------{context_str}---------------------Given the context information and not prior knowledge, answer the query. Generate the answer by analyzing parsed markdown, raw text and the relatedimage. Especially, carefully analyze the images to look for the required information.Format the answer in proper format as deems suitable (bulleted lists, sections/sub-sections, tables, etc.)Give the page's number and the document name where you find the response based on the Context.Query: {query_str}Answer: """PROMPT = PromptTemplate(RAG_PROMPT)#初始化多模态LLMMM_LLM = OpenAIMultiModal(model="gpt-4o-mini", temperature=0.0, max_tokens=16000)
在查问引擎中集成整个管道流程
本节中要引见的QueryEngine类成功了上述完整的上班流程。BM25搜查中的节点数量(top_n_BM25)和从新排序重视新排序的结果数量(top_name)可以依据须要启动调整。经过切换GitHub代码中的best_match_25和re_ranking变量,可以选用或敞开选用BM25搜查和重排序。
上方给出的是QueryEngine类成功的全体上班流程:
1.查找查问嵌入。
2.经常使用基于向量的检索从向量数据库中检索节点。
3.经常使用BM25搜查检索节点(假设选用经常使用该方法的话)。
4.结合BM25和基于向量的检索中的节点。查找节点的惟一数量(删除重复的节点)。
5.运行重排序对组合结果启动重排序(假设选中该方法的话)。在这里,咱们经常使用Cohere公司的rerank-english-v2.0从新排序模型。您可以在Cohere公司的 网站 上创立一个账号,以取得试用版API密钥。
6.从与节点关联的图像创立图像节点。
7.依据解析的markdown文本创立高低文字符串。
8.将节点图像发送到多模态LLM启动解释。
9.经过将文本节点、图像节点形容和元数据发送到LLM来生成最终照应。
#定义类QueryEngine,把一切方法集成到一同class QueryEngine(CustomQueryEngine):# 公共属性qa_prompt: PromptTemplatemulti_modal_llm: OpenAIMultiModalnode_postprocessors: Optional[List[BaseNodePostprocessor]] = None# 经常使用PrivateAttr定义的私有属性_bm25: BM25Okapi = PrivateAttr()_llm: OpenAI = PrivateAttr()_text_node_with_context: List[TextNode] = PrivateAttr()_vector_index: VectorStoreIndex = PrivateAttr()def __init__(self,qa_prompt: PromptTemplate,bm25: BM25Okapi,multi_modal_llm: OpenAIMultiModal,vector_index: VectorStoreIndex,node_postprocessors: Optional[List[BaseNodePostprocessor]] = None,llm: OpenAI = None,text_node_with_context: List[TextNode] = None,):super().__init__(qa_prompt=qa_prompt,retriever=None,multi_modal_llm=multi_modal_llm,node_postprocessors=node_postprocessors)self._bm25 = bm25self._llm = llmself._text_node_with_context = text_node_with_contextself._vector_index = vector_indexdef custom_query(self, query_str: str):# 预备查问bundlequery_bundle = QueryBundle(query_str)bm25_nodes = []if best_match_25 == 1:#假设选用经常使用BM25搜查方法# 经常使用BM25方法检索节点query_tokens = query_str.split()bm25_scores = self._bm25.get_scores(query_tokens)top_n_bm25 = 5#调整要检索的顶节点的数目# 取得顶部BM25分数对应的索引值top_indices_bm25 = bm25_scores.argsort()[-top_n_bm25:][::-1]bm25_nodes = [self._text_node_with_context[i] for i in top_indices_bm25]logging.info(f"BM25 nodes retrieved: {len(bm25_nodes)}")else:logging.info("BM25 not selected.")#从向量存储中经常使用基于向量的检索技术启动节点检索vector_retriever = self._vector_index.as_query_engine().retrievervector_nodes_with_scores = vector_retriever.retrieve(query_bundle)# 指定你想要的顶部向量的数量top_n_vectors = 5# 依据须要调整这个值# 仅取得顶部的'n'个节点top_vector_nodes_with_scores = vector_nodes_with_scores[:top_n_vectors]vector_nodes = [node.node for node in top_vector_nodes_with_scores]logging.info(f"Vector nodes retrieved: {len(vector_nodes)}")# 把节点组合起来,并删除重复的节点all_nodes = vector_nodes + bm25_nodesunique_nodes_dict = {node.node_id: node for node in all_nodes}unique_nodes = list(unique_nodes_dict.values())logging.info(f"Unique nodes after deduplication: {len(unique_nodes)}")nodes = unique_nodesif re_ranking == 1:#假设选用经常使用重排序算法# 经常使用Cohere公司的重排序算法对组合后的结果启动重排序documents = [node.get_content() for node in nodes]max_retries = 3for attempt in range(max_retries):try:reranked = cohere_client.rerank(model="rerank-english-v2.0",query=query_str,documents=documents,top_n=3# top-3 个重排序节点)breakexcept CohereError as e:if attempt < max_retries - 1:logging.warning(f"Error occurred: {str(e)}. Waiting for 60 seconds before retry {attempt + 1}/{max_retries}")time.sleep(60)#重试前须要期待else:logging.error("Error occurred. Max retries reached. Proceeding without re-ranking.")reranked = Nonebreakif reranked:reranked_indices = [result.index for result in reranked.results]nodes = [nodes[i] for i in reranked_indices]else:nodes = nodes[:3]#回退到顶部的3个节点logging.info(f"Nodes after re-ranking: {len(nodes)}")else:logging.info("Re-ranking not selected.")# 针对高低文字符串限度并过滤节点内容max_context_length = 16000# 依据须要启动调整current_length = 0filtered_nodes = []#分词器初始化from transformers import GPT2TokenizerFasttokenizer = GPT2TokenizerFast.from_pretrained("gpt2")for node in nodes:content = node.get_content(metadata_mode=MetadataMode.LLM).strip()node_length = len(tokenizer.encode(content))logging.info(f"Node ID: {node.node_id}, Content Length (tokens): {node_length}")if not content:logging.warning(f"Node ID: {node.node_id} has empty content. Skipping.")continueif current_length + node_length <= max_context_length:filtered_nodes.append(node)current_length += node_lengthelse:logging.info(f"Reached max context length with Node ID: {node.node_id}")breaklogging.info(f"Filtered nodes for context: {len(filtered_nodes)}")#创立高低文字符串ctx_str = "\n\n".join([n.get_content(metadata_mode=MetadataMode.LLM).strip() for n in filtered_nodes])# 依据与图像关联的节点创立图像节点image_nodes = []for n in filtered_nodes:if "image_path" in n.metadata:image_nodes.append(NodeWithScore(node=ImageNode(image_path=n.metadata["image_path"])))else:logging.warning(f"Node ID: {n.node_id} lacks 'image_path' metadata.")logging.info(f"Image nodes created: {len(image_nodes)}")# 为LLM预备揭示符fmt_prompt = self.qa_prompt.format(context_str=ctx_str, query_str=query_str)# 经常使用多模态LLM解释图像并生成照应llm_response = self.multi_modal_llm.complete(prompt=fmt_prompt,image_documents=[image_node.node for image_node in image_nodes],max_tokens=16000)logging.info(f"LLM response generated.")#前往结果照应值return Response(response=str(llm_response),source_nodes=filtered_nodes,metadata={"text_node_with_context": self._text_node_with_context,"image_nodes": image_nodes,},)#经常使用BM25方法、Cohere的Re-ranking算法和查问扩展初始化查问引擎query_engine = QueryEngine(qa_prompt=PROMPT,bm25=bm25,multi_modal_llm=MM_LLM,vector_index=index,node_postprocessors=[],llm=llm,text_node_with_context=text_node_with_context)print("All done")
经常使用OpenAI公司提供的模型,特意是gpt-4o-mini的一个好处是高低文调配和查问推理运转的老本要低得多,高低文调配期间也要短得多。虽然OpenAI公司和Anthropic公司的基本层确实很快到达API调用的最大速率限度,但Anthropc公司的基本层中的重试期间各不相反,或者太长。经常使用claude-3–5-connect-20240620对本文档的前20页启动高低文调配环节,经常使用揭示缓存大概须要170秒,老本为20美分(输入+输入词元)。但是,与Claude 3.5 Sonnet相比,gpt-4o-mini的输入词元大概廉价20倍,输入词元大概廉价25倍。OpenAI公司宣称为重复内容成功了揭示缓存,这对一切API调用都智能起作用。
相比之下,经过gpt-4o-mini向整个文档(60页)中的节点调配高低文大概在193秒内成功,没有任何重试恳求。
成功QueryEngine类后,咱们可以按如下形式运转查问推理:
original_query = """What are the top countries to whose citizens the Finnish Immigration Service issued the highest number of first residence permits in 2023?Which of these countries received the highest number of first residence permits?"""response = query_engine.query(original_query)display(Markdown(str(response)))
这是对此查问的markdown照应。
对查问的照应(图片由作者提供)
查问照应中援用的页面如下:
如今,让咱们比拟一下基于gpt-4o-mini模型的RAG(LlamaParse初级形式+高低文检索+BM25+重排序)和基于Claude模型的RAG。我还成功了一个便捷的基础级别的RAG,可以在GitHub的笔记本中找到。以下是要比拟的三个RAG。
1.LlamaIndex中的便捷RAG经常使用SentenceSplitter将文档宰割成块(chunk_size=800,chunk_overlap=),创立向量索引和向量检索。
2.CMRAG(claude-3–5-connect-20240620,voya-3)——LlamaParse初级形式+高低文检索。
3.CMRAG(gpt-4o-mini,text-embedding-3-small)——LlamaParse初级形式+高低文检索+BM25+重排序。
以下是对每个疑问的三个RAG的回答。
基本RAG、基于Claude模型的CMRAG和基于gpt-4o-mini模型的CMRAG的比拟(图片由作者提供)
可以看出,RAG2的体现十分好。关于第一个疑问,RAG0提供了失误的答案,由于该疑问是从图像中提出的。RAG1和RAG2都提供了这个疑问的正确答案。关于另外两个疑问,RAG0不可提供任何答案。但是,RAG1和RAG2都为这些疑问提供了正确的答案。
总结
总体而言,由于集成了BM25方法、重排序和更好的揭示,RAG2的性能在许多状况下与RAG1相当,甚至更好。它为高低文、多模态RAG提供了一种经济高效的处置方案。该管道方案中或者的集成技术包括假定的文档嵌入(简称“HyDE”)或查问扩展等。雷同,也可以探求开源嵌入模型(如all-MiniLM-L6-v2模型)和/或轻量级的LLM(如gemma2或phi3-small),使其更具老本效益。
无关本文示例中完整的源代码参考,请检查我的github代码仓库:
译者引见
朱先忠,社区编辑,专家博客、讲师,潍坊一所高校计算机老师,自在编程界老兵一枚。
原文题目: Integrating Multimodal> 来源: 内容精选