2026/1/16 0:11:44
网站建设
项目流程
静态html网站打包成exe,城市网站改版建设,如何制作一个个人网页,网页设计实验报告实验1原文#xff1a;towardsdatascience.com/implementing-anthropics-contextual-retrieval-for-powerful-rag-performance-b85173a65b83 检索增强生成 (RAG) 是一种强大的技术#xff0c;它利用大型语言模型 (LLMs) 和向量数据库来创建更准确的用户查询响应。RAG 允许 LLMs 在响…原文towardsdatascience.com/implementing-anthropics-contextual-retrieval-for-powerful-rag-performance-b85173a65b83检索增强生成 (RAG) 是一种强大的技术它利用大型语言模型 (LLMs) 和向量数据库来创建更准确的用户查询响应。RAG 允许 LLMs 在响应用户查询时利用大型知识库从而提高响应的质量。然而RAG 也有一些缺点。一个缺点是 RAG 在检索用于响应用户查询的上下文时使用向量相似度。向量相似度并不总是一致的例如它可能难以处理独特的用户关键词。此外RAG 还面临挑战因为文本被分割成更小的块这阻碍了 LLM 在响应查询时利用文档的完整上下文。Anthropic 的文章关于上下文检索试图通过使用 BM25 索引和向块添加上下文来解决这两个问题。https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/540f2b1ec27f9903ea1187be6fb80d09.png在本篇文章中学习如何实现 Anthropic 的上下文检索 RAG。图片由 ChatGPT 提供。动机我写这篇文章的动机有两个。首先我想测试机器学习中最新的模型和技术。对于任何 ML 工程师和数据科学家来说跟上机器学习中的最新趋势对于最有效地解决他们每天遇到的现实世界问题至关重要。其次我喜欢研究 RAG 系统上下文检索是一个有趣的想法可以提高我的 RAG 系统的性能。我计划在我目前正在工作的一个问题以下章节中解释上实现上下文检索以看看上下文检索如何提高 LLM 响应用户查询的能力。Anthropic 也是一个领先的 AI 公司因此我发现他们的文章特别有趣。这篇文章的主要内容灵感来自 Anthropic 的上下文检索文章。如果你想了解更多关于基本 RAG 的信息你可以阅读我关于这个主题的文章链接如下如何构建 RAG 系统以获得对数据的强大访问权限目录· 动机 · 我将应用上下文检索解决的问题 · 前置条件 · 带有上下文的块创建 ∘ 实用性 ∘ 创建块 ∘ 上传到 Pinecone · BM25 索引 · 结合 BM25 和基于向量的块检索 · 结论我将应用上下文检索解决的问题我目前正在开发一个应用程序允许用户搜索以前的法院判决或裁决。例如这是一个最高法院的判决。在挪威的当前情况下这些判决对公众来说并不容易获取。您主要有两个选择支付昂贵的服务以允许在法院裁决和判决中进行搜索。在这里您可以使用 Lovdata.no单个用户每年费用为 12500 NOK/1169 USD或者 Rettsdata.no该网站没有公开列出其价格。一个免费的选择是 Rettspraksis.no本质上是一个关于挪威法院裁决的 Wiki。不幸的是这里您只能进行直接搜索并且有成千上万的裁决这使得找到相关文献变得困难。因此我的目标是创建一个更便宜的替代方案使用户能够轻松地搜索和发现挪威的相关法院裁决。我之前已经获得了这些裁决的文本。在本文中我旨在使用带有上下文检索的 RAG 系统使这些文本的内容易于获取。前置条件基础 Python 知识访问 OpenAI API 密钥您将使用 GPT-4o-mini这是一个相对便宜的替代品Pinecone 账户您可以使用他们的免费层https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/7574e57b486ba8116de6916a31a400ab.png这就是您将在本文中开发的流程。该流程展示了如何从语料库中创建块并在用户查询时检索这些块。然后从 BM25 和向量数据库中检索到的 Top K 块被组合起来为 GPT 提供上下文以响应用户查询。图片由作者提供。带有上下文的块创建我在一个名为 _extracted_info_pagesfiltered的文件夹中存储了 449 个文本文件这些文本文件是我将通过上下文检索提供的。您可以在 这个 Google Drive 文件夹 中找到我使用的文本。我还有更多文件可用但为了这篇文章我正在使用它们的子集。这些数据是从 Norges Høyesterett 提取的并且在挪威由于是 [公共利益的文件](https://www.kopinor.no/opphavsrett/opphavsrett-p-123#:~:textAv%20hensyn%20til%20allmennhetens%20behov,vern%20(%C3%A5ndsverklovens%20%C2%A7%2014) 而免于版权法这意味着它可以用于商业用途。实用工具我将从一些重要的实用文件开始。一般来说使用这样的文件可以使你的代码更加模块化更容易处理所以我强烈建议在编写自己的代码时创建类似的文件。我创建了一个constants.py文件来存储所有全局常量CHUNK_SIZE500CHUNK_OVERLAP100TEXT_EMBEDDING_MODELtext-embedding-3-smallGPT_MODELgpt-4o-miniINPUT_TOKEN_PRICE0.15/1e6OUTPUT_TOKEN_PRICE0.6/1e6我然后创建了两个实用文件一个用于处理 OpenAI名为 _openaiutility.py:。importosimporttiktokenimportstreamlitasstfromopenaiimportOpenAIfromlangchain_openaiimportChatOpenAIfromopenaiimportOpenAIfromconstantsimportINPUT_TOKEN_PRICE,OUTPUT_TOKEN_PRICEfromconstantsimportTEXT_EMBEDDING_MODEL,GPT_MODEL OPENAI_API_KEYst.secrets[OPENAI_API_KEY]openai_clientOpenAI(api_keyOPENAI_API_KEY)defcount_tokens(text):encodingtiktoken.encoding_for_model(TEXT_EMBEDDING_MODEL)tokensencoding.encode(text)returnlen(tokens)defget_embedding(text,modelTEXT_EMBEDDING_MODEL):returnopenai_client.embeddings.create(input[text],modelmodel).data[0].embeddingdefprompt_gpt(prompt):messages[{role:user,content:prompt}]responseopenai_client.chat.completions.create(modelGPT_MODEL,# Ensure correct model name is usedmessagesmessages,temperature0,)contentresponse.choices[0].message.content prompt_tokensresponse.usage.prompt_tokens# Input tokens (in)completion_tokensresponse.usage.completion_tokens# Output tokens (out)pricecalculate_prompt_cost(prompt_tokens,completion_tokens)returncontent,pricedefcalculate_prompt_cost(input_tokens,output_tokens):returnINPUT_TOKEN_PRICE*input_tokensOUTPUT_TOKEN_PRICE*output_tokens这只是一个发送 GPT 提示、创建嵌入等的标准代码。一般来说我建议有一个这样的文件来轻松地与 OpenAI API 一起工作。请记住将 _OPENAI_APIKEY常量更改为您自己的 API 密钥。我还创建了一个名为 _vector_databaseutility.py:的实用文件来处理向量数据库。fromconstantsimportCHUNK_OVERLAP,CHUNK_SIZE,TEXT_EMBEDDING_MODELimportosfromutility.openai_utilityimportcount_tokensimporttiktokenimportjson tokenizertiktoken.get_encoding(cl100k_base)defsplit_text_into_chunks_with_overlap(text):tokenstokenizer.encode(text)# Tokenize the input textchunks[]# Loop through the tokens, creating chunks with overlapforiinrange(0,len(tokens),CHUNK_SIZE-CHUNK_OVERLAP):chunk_tokenstokens[i:iCHUNK_SIZE]# Include overlap by adjusting start pointchunk_texttokenizer.decode(chunk_tokens)chunks.append(chunk_text)returnchunksdefload_all_chunks_from_folder(folder_path):chunks[]forchunk_filenameinos.listdir(folder_path):withopen(os.path.join(folder_path,chunk_filename),r)asf:datajson.load(f)chunks.append(data)returnchunks创建块从这里开始我在笔记本中创建块并将其上传到 Pinecone。首先导入所需的包importosfromjinja2importEnvironment,FileSystemLoaderfromtqdm.autoimporttqdmfromutility.vector_database_utilityimportsplit_text_into_chunks_with_overlapfromutility.openai_utilityimportprompt_gpt,get_embeddingimportjsonimportstreamlitasst注意我正在使用 Streamlit 来存储我的机密信息因为我可能会稍后将其发布为一个 Streamlit 应用程序。然后我定义了存储和保存信息的超参数。DOCUMENT_FILEPATHSrDatahoyesteretts_dommerfilteredextracted_info_pages_filteredCHUNKS_SAVE_PATHrDatahoyesteretts_dommerfilteredchunksDOCUMENT_TYPEhoyesterettsdommerDOCUMENT_FILEPATHS应该是您从 Google Drive 下载的文档的路径或您自己的文档。CHUNKS_SAVE_PATH是您想要存储块的位置。DOCUMENT_TYPE用于在数据库和桶中进行过滤。然后我设置了一个 Jinja2 环境。Jinja2 是一个用于处理提示的包我在这个项目的工作中对其进行了测试。Jinja2 的主要优势是它允许你将提示存储在单独的文件中并包含逻辑。例如你可以在提示中包含 if 语句比如是否包含提示的某些部分。这在处理更复杂的提示工程时非常有用。# Set up Jinja2 environmentfile_loaderFileSystemLoader(./templates)envEnvironment(loaderfile_loader)defget_add_context_prompt(chunk_text,document_text):templateenv.get_template(create_context_prompt.j2)data{WHOLE_DOCUMENT:document_text,# Leave blank for default or provide a nameCHUNK_CONTENT:chunk_text# You can set any score here}outputtemplate.render(data)returnoutput我的 Jinja2 模板存储提示的文件存储在一个名为templates的文件夹中并保存为名为 _create_contextprompt.j2的文件。文件看起来像这样document{{WHOLE_DOCUMENT}}/documentHereisthe chunk we want to situate within the whole documentchunk{{CHUNK_CONTENT}}/chunkPlease give a short succinct context to situate this chunk within the overall documentforthe purposes of improving search retrieval of the chunk.Answer onlywiththe succinct contextandnothingelse.在这个例子中我在提示中使用了两个变量WHOLE_DOCUMENT 和 CHUNK_CONTENT这些变量将在创建发送给 GPT 的每个提示时被替换。上面定义的函数 _get_add_contextprompt替换了这些变量。然后我使用以下命令创建了一个用于保存块的文件夹os.makedirs(CHUNKS_SAVE_PATH,exist_okTrue)以下代码用于创建和存储块document_filenamesos.listdir(DOCUMENT_FILEPATHS)forfilenameintqdm(document_filenames):withopen(f{DOCUMENT_FILEPATHS}/{filename},r,encodingutf-8)asf:document_textf.read()# now split text into chunksprint(fCurrent tot price: ,tot_price)chunkssplit_text_into_chunks_with_overlap(document_text)foridx,chunkinenumerate(chunks):# store the chunkchunk_save_filenamef{filename.split(.)[0]}_{idx}.jsonchunk_save_pathf{CHUNKS_SAVE_PATH}/{chunk_save_filename}ifos.path.exists(chunk_save_path):continuepromptget_add_context_prompt(chunk,document_text)context,priceprompt_gpt(prompt)tot_priceprice chunk_info{id:f{filename}_{int(idx)},chunk_text:contextnnchunk,chunk_idx:idx,filename:filename,document_type:DOCUMENT_TYPE}withopen(chunk_save_path,w,encodingutf-8)asf:json.dump(chunk_info,f,indent4)代码读取你从 Google Drive 下载的文本文件。它将文本分割成块块的大小和重叠由 constants.py 文件中的常量决定。然后我检查块是否存在如果存在则转到下一个块并提示 GPT-4o-mini 为我添加上下文。然后我将添加了上下文的块以及块索引和文件名作为元数据存储。接下来我取我们创建的块带有上下文为块文本创建嵌入并将其存储在以下文件中# go through each chunk, create an embedding for it, and save it the same folderforchunk_filenameinos.listdir(CHUNKS_SAVE_PATH):# load chunkwithopen(f{CHUNKS_SAVE_PATH}/{chunk_filename},r,encodingutf-8)asf:chunk_infojson.load(f)chunk_textchunk_info[chunk_text]chunk_text_embeddingget_embedding(chunk_text)chunk_info[chunk_embedding]chunk_text_embedding# save chunkwithopen(f{CHUNKS_SAVE_PATH}/{chunk_filename},w,encodingutf-8)asf:json.dump(chunk_info,f,indent4)上传到 Pinecone最后我将块上传到 Pinecone这是一个向量数据库你可以从中检索给定提示的最相关块。我首先导入 Pinecone 和我的 API 密钥。如果你没有 Pinecone 的 API 密钥你可以访问Pinecone 网站并创建一个免费账户。Pinecone 的免费层非常好所以我推荐使用它。frompineconeimportPineconefrompinecone_utilityimportPineconeUtility PINECONE_API_KEYst.secrets[PINECONE_API_KEY]你可以在 Pinecone 网站上创建一个索引。我给我的索引命名为lov-avgjorelser在英语中大致翻译为“法律裁决”。pineconePinecone(api_keyPINECONE_API_KEY)index_namelov-avgjorelserindexpinecone.Index(index_name)接下来你可以将这些块上传到 Pinecone。通常你不应该直接将文本上传到 Pinecone。相反我建议将每个块的文本分别存储例如在 AWS 上的 S3 桶中。然后当你从 Pinecone 检索块时你可以使用块标识符在 S3 桶中找到块文本。然而我这里将简单地将在桶中存储文本。请注意Pinecone 中元数据的最大大小为 40kb因此你直接在 Pinecone 中存储的文本量是有限的。# pinecone expects list of objects with: [{id: id, values: embedding, metadata, metadata}]# upload to pineconeforchunk_filenameinos.listdir(CHUNKS_SAVE_PATH):# load chunkwithopen(f{CHUNKS_SAVE_PATH}/{chunk_filename},r,encodingutf-8)asf:chunk_infojson.load(f)chunk_filenamechunk_info[filename]chunk_idxchunk_info[chunk_idx]chunk_textchunk_info[chunk_text]chunk_info[chunk_embedding]chunk_text_embedding document_typechunk_info[document_type]metadata{filename:chunk_filename,chunk_idx:chunk_idx,chunk_text:chunk_text,document_type:document_type}data_with_metadata[{id:chunk_filename,values:chunk_text_embedding,metadata:metadata}]index.upsert(vectorsdata_with_metadata)恭喜你现在已经通过 Pinecone 使带有上下文的块可访问了BM25 索引BM25 是一种用于索引文档的较老技术它可以帮助在给定查询的情况下在语料库中找到最相关的文档。你可以在这里了解更多关于 BM25 的信息。从Anthropic 关于上下文检索的文章中你可以了解到他们实现 BM25 作为一个独立的块检索算法然后将从 BM25 和向量相似度搜索中检索到的块结合起来。这意味着我们可以将 BM25 作为一个完全独立的检索步骤来实现用于我们制作的块。你可以使用以下命令安装 BM25 包pip install rank_bm25以下代码用于使用 BM25 进行查询fromrank_bm25importBM25Okapifromnltk.tokenizeimportword_tokenizeimportnltkfromutility.vector_database_utilityimportload_all_chunks_from_folder# Download the NLTK tokenizer if you haventnltk.download(punkt)nltk.download(punkt_tab)CHUNK_PATHrDatahoyesteretts_dommerfilteredchunkschunksload_all_chunks_from_folder(CHUNK_PATH)corpus[chunk[chunk_text]forchunkinchunks]# Tokenize each document in the corpustokenized_corpus[word_tokenize(doc.lower())fordocincorpus]# should store this somewhere for easy retrievalbm25BM25Okapi(tokenized_corpus)defretrieve_with_bm25(query:str,corpus:list[str],top_k:int2)-list[str]:tokenized_queryword_tokenize(query.lower())doc_scoresbm25.get_top_n(tokenized_query,corpus,ntop_k)returndoc_scoresif__name____main__:queryfyllekjøringresponseretrieve_with_bm25(query,corpus,top_k10)此代码每次运行函数时都会创建标记化的语料库但更好的解决方案是将标记化的语料库存储在某个地方以便于加载。运行此文件的结果是按相关性从高到低排序的多个文档的文本只显示 top_k 结果。我还想在这里添加一个关于语言的注释。文本嵌入通常是双语的这意味着用户可以用一种语言提示即使块嵌入在另一种语言中也能找到相关的块。然而对于 BM25 来说并非如此因为 BM25 依赖于精确的词匹配。考虑到检索到的块中有一半来自 BM25另一半来自向量数据库这不应该是一个问题。结合 BM25 和基于向量的块检索你可以使用之前编写的代码执行使用上下文检索的完整 RAG 搜索。我创建了一个名为 RagAgent 的新类我使用它来执行 RAG。类的定义如下importstreamlitasstfromopenaiimportOpenAIfrompineconeimportPineconefromtypingimportOptional,Listfromutility.openai_utilityimportprompt_gpt,get_embedding# NOTE this is the file where you made the bm25 indexfrombm25importretrieve_with_bm25 PINECONE_API_KEYst.secrets[PINECONE_API_KEY]pcPinecone(api_keyPINECONE_API_KEY)openai_clientOpenAI()pineconePinecone(api_keyPINECONE_API_KEY)classRagAgent:def__init__(self,index_name):# load pinecone indexself.indexpinecone.Index(index_name)defquery_pinecone(self,query,top_k2,include_metadata:boolTrue):query_embeddingget_embedding(query)query_responseself._query_pinecone_index(query_embedding,top_ktop_k,include_metadatainclude_metadata)returnself._extract_info(query_response)def_query_pinecone_index(self,query_embedding:list,top_k:int2,include_metadata:boolTrue)-dict[str,any]:query_responseself.index.query(vectorquery_embedding,top_ktop_k,include_metadatainclude_metadata,)returnquery_responsedef_extract_info(self,response)-Optional[dict]:extract data from pinecone query response. Returns dict with id, text and chunk idxifresponseisNone:returnNoneres_list[]forrespinresponse[matches]:_idresp[id]res_list.append({id:_id,chunk_text:resp[metadata][chunk_text],chunk_idx:resp[metadata][chunk_idx],})returnres_listdef_combine_chunks(self,chunks_bm25,chunks_vector_db,top_k20):given output from bm25 and vector database, combine them to only include unique chunksretrieved_chunks[]# assume lists are ordered from most relevant docs to least relevantforchunk1,chunk2inzip(chunks_bm25,chunks_vector_db):ifchunk1notinretrieved_chunks:retrieved_chunks.append(chunk1)iflen(retrieved_chunks)top_k:breakifchunk2notinretrieved_chunks:retrieved_chunks.append(chunk2)iflen(retrieved_chunks)top_k:breakreturnretrieved_chunksdefrun_bm25_rag(self,query,top_k2):chunks_bm25retrieve_with_bm25(query,top_k)chunks_vector_dbself.query_pinecone(query,top_k)combined_chunksself._combine_chunks(chunks_bm25,chunks_vector_db)contextn.join([chunk[chunk_text]forchunkincombined_chunks])full_promptfGiven the following emails{context}what is the answer to the question:{query}response,_prompt_gpt(full_prompt)returnresponse类的解释我首先包含相关的导入和参数_query*pinecone*和__query_pinecone*index*函数是基本的 Pinecone 函数你可以在 Pinecone 文档 中了解更多信息。__extract*info*函数从每个块中提取信息到预期的格式。__combine*chunks*函数通过简单地从每个检索到的块列表中选择第一个向量将 BM25 和向量数据库检索到的块结合起来确保没有选择重复的块。_run_bm25*rag*函数运行带有 BM25 和添加了上下文的块的上下文 RAG利用了文章中提到的不同函数。然后你可以使用以下代码运行上下文 RAG。确保更新 index_name 为你的 Pinecone 索引名称。fromrag_agentimportRagAgent index_namelov-avgjorelserrag_agentRagAgent(index_name)queryHva er straffen for fyllekjøring?rag_agent.run_bm25_rag(query,top_k20)GPT 给出了以下响应Straffen for fyllekjøring varierer avhengig av alvorlighetsgraden av overtredelsen og om det foreligger gjentakelse. Generelt kan straffen for fyllekjøring i Norge være bøter, fengsel, eller tap av førerrett. nnFor første gangs overtredelse kan straffen være bøter og/eller fengsel i inntil 6 måneder. Ved gjentakelse eller alvorlige tilfeller, som høy promille eller ulykker, kan straffen bli betydelig strengere, med fengsel i flere år. I tillegg kan det ilegges tap av retten til å føre motorvogn for en periode, avhengig av alvorlighetsgraden av overtredelsen. nnDet er viktig å merke seg at straffene kan variere basert på omstendighetene rundt den spesifikke saken, inkludert om det er registrert tidligere overtredelser.我尚未对上下文检索与标准 RAG 进行定量评估因为这需要大量时间并且很难客观评估语言模型的表现。然而在使用上下文检索模型一段时间后我可以从定性角度说我已经注意到模型响应的改进。首先当利基关键词是用户查询的一部分时上下文 RAG 更能有效地检索相关文档。这是由于 BM25 索引的结果。此外我还注意到LLM 更能有效地利用上下文中的信息。例如在标准 RAG 中模型有时会使用与上下文无关的块中的信息这是有道理的因为模型没有块的上下文。这可能导致模型响应中的幻觉。然而在上下文 RAG 中由于模型被赋予了每个块的上下文幻觉问题显著减少。结论在这篇文章中我讨论了如何为您的 RAG 系统实现上下文检索以及 Anthropic 提出的思想。首先我讨论了我正在解决的问题即使挪威的法院判决更加易于获取。然后我使用了不同法院判决的文本将其分成块并使用 GPT-4o-mini 为每个块添加上下文。接着我为每个块创建了嵌入并将它们存储在 Pinecone 中。此外我还创建了一个 BM25 索引它与向量相似性一起用于检索最相关的块。这些块随后被 GPT 用于回答用户的提示并使用块中的上下文。从定性测试来看上下文检索是标准 RAG 的改进两者都能更好地检索相关文档并能更准确地响应用户查询。