一、RAG簡介
大型語言模型(LLM)已經取得了顯著的成功,盡管它們仍然面臨重大的限制,特別是在特定領域或知識密集型任務中,尤其是在處理超出其訓練數據或需要當前信息的查詢時,常會產生“幻覺”現象。為了克服這些挑戰,檢索增強生成(RAG)通過從外部知識庫檢索相關文檔chunk并進行語義相似度計算,增強了LLM的功能。通過引用外部知識,RAG有效地減少了生成事實不正確內容的問題。RAG目前是基于LLM系統中最受歡迎的架構,有許多產品基于RAG構建,使RAG成為推動聊天機器人發展和增強LLM在現實世界應用適用性的關鍵技術。
二、RAG架構
2.1 RAG實現過程
RAG在問答系統中的一個典型應用主要包括三個步驟:
Indexing(索引):將文檔分割成chunk,編碼成向量,并存儲在向量數據庫中。
Retrieval(檢索):根據語義相似度檢索與問題最相關的前k個chunk。
Generation(生成):將原始問題和檢索到的chunk一起輸入到LLM中,生成最終答案。
2.2 RAG在線檢索架構
三、RAG流程
接下來,我們將深入探討RAG各個流程,并為RAG構建技術路線圖。
3.1 索引
索引是將文本分解成可管理的chunk的過程,是組織系統的關鍵步驟,面臨三個主要挑戰:
不完整的內容表示:chunk的語義信息受到分割方法的影響,導致在更長的上下文中重要信息的丟失或隱藏。
不準確的chunk相似性搜索:隨著數據量的增加,檢索中的噪聲增多,導致頻繁與錯誤數據匹配,使檢索系統變得脆弱和不可靠。
不明確的引用軌跡:檢索到的chunk可能來源于任何文檔,缺乏引用路徑,可能導致存在來自多個不同文檔的chunk,盡管這些chunk在語義上相似,但包含的內容完全不同的主題。
3.1.1 Chunking
Transformer模型有固定的輸入序列長度,即使輸入上下文窗口很大,一個句子或幾個句子的向量也比幾頁文本的平均向量更能代表它們的語義意義。所以我們需要對數據進行分塊,將初始文檔分割成一定大小的chunk,同時不丟失它們的意義(將文本分割成句子或段落,而不是將一個句子分成兩部分)。
有多種文本切分策略能夠完成這項任務,我們在實踐中采用了以下3種策略:
直接分段:將文本按照一定的規則進行分段處理后,轉成可以進行語義搜索的格式。這里不需要調用模型進行額外處理,成本低,適合絕大多數應用場景。
生成問答對:根據一定的規則,將文本拆成一段主題文本,調用LLM為該段主題文本生成問答對。這種處理方式有非常高的檢索精度,但是會丟失部分文本細節,需要特別留意。
增強信息:通過子索引以及調用LLM生成相關問題和摘要,來增加chunk的語義豐富度,更加有利于后面的檢索。不過需要消耗更多的存儲空間和增加LLM調用開銷。
chunk的大小是一個需要重點考慮的參數,它取決于我們使用的Embedding模型及其token的容量。標準的Transformer編碼器模型,如基于BERT的Sentence Transformer最多處理512個token,而OpenAI的text-embedding-3-small能夠處理更長的序列(8191個token)。
為了給LLM提供足夠的上下文以進行推理,同時給搜索提供足夠具體的文本嵌入,我們需要一些折衷策略。較大的chunk可以捕獲更多的上下文,但它們也會產生更多的噪音,需要更長的處理時間和更高的成本。而較小的chunk可能無法完全傳達必要的上下文,但它們的噪音較少。
以網頁https://www.openim.io/en的文本內容為輸入,按照上面3種策略進行文本分割。
1. 直接分段:
切分后的chunk信息,總共10個chunk:
defsplit_long_section(section, max_length=1300):lines = section.split('\n')current_section =""? ? result = []forlineinlines:# Add 1 for newline character when checking the lengthiflen(current_section) + len(line) +1> max_length:ifcurrent_section:? ? ? ? ? ? ? ? result.append(current_section)current_section = line# Start a new paragraphelse:# If a single line exceeds max length, treat it as its own paragraph? ? ? ? ? ? ? ? result.append(line)else:ifcurrent_section:current_section +='\n'+ lineelse:? ? ? ? ? ? ? ? current_section = line
2. 生成問答對:
切分后的chunk信息,總共28個chunk,每個chunk包含一對問答:
切分后的某個chunk的問答對信息:
3. 增強信息:
切分后的chunk信息,總共6個chunk,每個chunk都包含一批數據索引信息:
切分后的某個chunk的數據索引信息:
3.1.1.1 滑動窗口
平衡這些需求的一種簡單方法是使用重疊的chunk。通過使用滑動窗口,可以增強語義過渡。然而,也存在一些限制,包括對上下文大小的控制不精確、有截斷單詞或句子的風險,以及缺乏語義考慮。
final_result = []ast_lines =""forsectioninresult:lines = section.split('\n')last_two_lines ="\n".join(lines[-2:])# Extract the last two linescombined_section = last_lines +"\n"+ sectioniflast_lineselsesection? ? final_result.append(combined_section)? ? last_lines = last_two_lines
3.1.1.2 上下文豐富化
這里的概念是為了獲得更好的搜索質量而檢索較小的chunk,并添加周圍的上下文供LLM進行推理。
有兩個選項:通過在較小的檢索chunk周圍添加句子來擴展上下文,或者將文檔遞歸地分成多個較大的父chunk,其中包含較小的子chunk。
句子窗口檢索
在這個方案中,文檔中的每個句子都被單獨嵌入,這提供了查詢與上下文余弦距離搜索的高準確性。
為了在獲取到最相關的單個句子后更好地推理出找到的上下文,我們通過在檢索到的句子之前和之后添加k個句子來擴展上下文窗口,然后將這個擴展后的上下文發送給LLM。
fromllama_index import ServiceContext, VectorStoreIndex, StorageContextfromllama_index.node_parser import SentenceWindowNodeParserdefbuild_sentence_window_index(document,llm, vector_store, embed_model="local:BAAI/bge-small-en-v1.5"):? ? # create the sentence window node parser w/ default settingsnode_parser=SentenceWindowNodeParser.from_defaults(window_size=3,window_metadata_key="window",original_text_metadata_key="original_text",)sentence_context=ServiceContext.from_defaults(llm=llm,embed_model=embed_model,node_parser=node_parser)storage_context=StorageContext.from_defaults(vector_store=vector_store)sentence_index=VectorStoreIndex.from_documents([document],service_context=sentence_context, storage_context=storage_context)returnsentence_index
父文檔檢索器
文檔被分割成一個層次結構的chunk,然后最小的葉子chunk被發送到索引中。在檢索時,我們檢索k個葉子chunk,如果有n個chunk引用同一個父chunk,我們將它們替換為該父chunk并將其發送給LLM進行答案生成。
關鍵思想是將用于檢索的chunk與用于合成的chunk分開。使用較小的chunk可以提高檢索的準確性,而較大的chunk可以提供更多的上下文信息。具體來說,一種方法可以涉及檢索較小的chunk,然后引用父ID以返回較大的chunk。或者,可以檢索單個句子,并返回該句子周圍的文本窗口。
sub_chunk_sizes = [128,256,512]sub_node_parsers = [SimpleNodeParser.from_defaults(chunk_size=c)forcinsub_chunk_sizes]all_nodes = []forbase_nodeinbase_nodes:forninsub_node_parsers:? ? ? ? sub_nodes = n.get_nodes_from_documents([base_node])? ? ? ? sub_inodes = [IndexNode.from_text_node(sn, base_node.node_id)forsninsub_nodes? ? ? ? ]? ? ? ? all_nodes.extend(sub_inodes)? ? # also add original node to nodeoriginal_node =IndexNode.from_text_node(base_node, base_node.node_id)? ? all_nodes.append(original_node)all_nodes_dict = {n.node_id: nforninall_nodes}
3.1.1.3 元數據附加
可以使用元數據信息對chunk進行豐富,例如URL、文件名、作者、時間戳、摘要,或者chunk可以回答的問題。隨后,可以根據這些元數據對檢索進行篩選,限制搜索范圍。
asyncdefaadd_content_embedding(self, data):foritemindata:documents_to_add = []# Initialize a list to hold all document parts for batch processing.? ? ? ? timestamp = int(time.time())? ? ? ? doc_id, url, chunk_text_vec = itempart_index =0forpart_contentinchunk_text_vec:# Process each part of the content.# Construct metadata for each document part, ensuring each part has a unique ID.metadata = {"source": url,"id":f"{doc_id}-part{part_index}"}# Create a Document object with the part content and metadata.? ? ? ? ? ? doc = Document(page_content=part_content, metadata=metadata)# Add the document part to the list for batch addition.? ? ? ? ? ? documents_to_add.append(doc)part_index +=1# Check if there are document parts to add.ifdocuments_to_add:# Add all document parts to Chroma in a single batch operation.embedding_id_vec =awaitself.chroma_obj.aadd_documents(documents_to_add)logger.info(f"[DOC_EMBEDDING] doc_id={doc_id}, url={url}added{len(documents_to_add)}document parts to Chroma., embedding_id_vec={embedding_id_vec}")
3.1.2 向量化
在構建RAG應用程序時,“使用哪個Embedding模型”沒有一個適用于所有情況的標準答案。實踐中,我們選擇的是OpenAI的text-embedding-3-small來計算chunk的向量。
fromlangchain_openai import OpenAIEmbeddingsOPENAI_EMBEDDING_MODEL_NAME=os.getenv('OPENAI_EMBEDDING_MODEL_NAME', 'text-embedding-3-small')# Initialize OpenAI embeddings with the specified modelg_embeddings=OpenAIEmbeddings(model=OPENAI_EMBEDDING_MODEL_NAME,openai_api_key=OPENAI_API_KEY)
3.1.3 搜索索引
3.1.3.1 向量存儲索引
RAG流程中的關鍵部分是搜索索引,存儲chunk的向量化內容。最簡單的實現使用平面索引,在查詢向量和所有chunk向量之間進行距離計算。
一個優秀的搜索索引,需要確保在大規模元素上的檢索效率,一般使用某種近似最近鄰實現,如聚類、樹或HNSW算法。
NOTE:
如果使用Chroma作為向量存儲DB,需要留意hnsw:space的默認值是l2,實踐中建議調整為cosine。
修改Chrome的距離函數:
create_collection接受一個可選的metadata參數,該參數可以用來通過設置hnsw:space?的值來自定義嵌入空間的距離方法。
classDocumentEmbedder:def__init__(self, collection_name, embedding_function, persist_directory):logger.info(f"[DOC_EMBEDDING] init, collection_name:'{collection_name}', persist_directory:{persist_directory}")collection_metadata = {"hnsw:space":"cosine"}? ? ? ? self.chroma_obj = Chroma(? ? ? ? ? ? ? ? collection_name=collection_name,? ? ? ? ? ? ? ? embedding_function=embedding_function,? ? ? ? ? ? ? ? persist_directory=persist_directory,? ? ? ? ? ? ? ? collection_metadata=collection_metadata)
除了向量索引,還可以考慮支持其它更簡單的索引實現,如列表索引、樹索引和關鍵詞表索引。
3.1.3.2 層級索引
如果需要從許多文檔中檢索信息,我們需要能夠高效地在其中搜索,找到相關信息并將其合成一個帶有來源引用的單一答案。對于大型數據庫,一種有效的方法是創建兩個索引,一個由摘要組成,另一個由文檔chunk組成,并進行兩步搜索,首先通過摘要篩選出相關文檔,然后僅在這個相關組內進行搜索。
層級索引的應用實例:
LSM樹(Log-Structured Merge-tree):LevelDB、RocksDB、Cassandra、HBase。
HNSW(Hierarchical Navigable Small World):Faiss、NMSlib、Annoy。
3.2 Preprocess Query
RAG的一個主要挑戰是它直接依賴用戶的原始查詢作為檢索的基礎。制定一個精確和清晰的問題很難,而輕率的查詢結果會導致檢索效果不佳。
這一階段的主要挑戰包括:
措辭不當的查詢。問題本身復雜,語言組織不良。
語言復雜性和歧義。語言模型在處理專業詞匯或含義多義的模糊縮寫時往往會遇到困難。
3.2.1 Query Expansion
將單一查詢擴展為多個查詢可以豐富查詢的內容,提供更多的上下文來解決缺乏特定細微差別的問題,從而確保生成答案的最佳相關性。
多查詢
通過Prompt工程來擴展查詢,這些查詢可以并行執行。查詢的擴展不是隨機的,而是經過精心設計的。這種設計的兩個關鍵標準是查詢的多樣性和覆蓋范圍。使用多個查詢的一個挑戰是可能稀釋用戶原始意圖的風險。為了緩解這一問題,我們可以指導模型在Prompt工程中給予原始查詢更大的權重。
子查詢
子問題規劃過程代表了生成必要的子問題,當結合起來時,這些子問題可以幫助完全回答原始問題。從原理上講,這個過程與查詢擴展類似。具體來說,一個復雜的問題可以使用從簡到繁的提示方法分解為一系列更簡單的子問題。
3.2.2 Query Transformation
查詢轉換是一系列技術,使用LLM作為推理引擎修改用戶輸入以提高檢索質量。有幾種不同的方法可以實現這一點。
如果查詢很復雜,LLM可以將其分解為幾個子查詢。例如,如果你問:
“在Github上,private-gpt和rag-gpt哪個項目的star更多?”,由于我們不太可能在語料庫中的某些文本中找到直接的比較,因此將這個問題分解為兩個子查詢是有意義的,這些子查詢假定了更簡單和更具體的信息檢索:
“private-gpt在Github上有多少star?”
“rag-gpt在Github上有多少star?”
這些查詢將并行執行,然后將檢索到的上下文合并在一個提示中,供LLM合成對初始查詢的最終答案。
退后式提示,使用LLM生成一個更一般的查詢,為其檢索我們獲得的更一般或高層次的上下文,有助于支撐我們對原始查詢的回答。也會對原始查詢執行檢索,兩種上下文都會在最終答案生成步驟中輸入到LLM中。
查詢重寫,原始查詢并不總是最適合LLM檢索的,特別是在現實世界的場景中。因此,我們可以提示LLM重寫查詢。
OpenIM文檔網站使用rag-gpt搭建了網站智能客服,可以快速驗證查詢重寫策略的效果。
在沒有查詢重寫策略時,如果用戶輸入"如何部署",召回的文檔的相關性分數都小于0.5,會都被過濾掉,最后GPT無法獲得足夠的上下文信息,無法回答。
增加查詢重寫策略時,如果用戶輸入"如何部署",query會被改寫為"如何部署\tOpenIM",此時召回的5篇文檔的相關性分數都是大于0.5的,可以作為上下文傳給GPT,最終GPT給出響應的答案。
defpreprocess_query(query, site_title):# Convert to lowercase for case-insensitive comparison? ? query_lower = query.lower()? ? site_title_lower = site_title.lower()# Check if the site title is already included in the queryifsite_title_lowernotinquery_lower:adjust_query =f"{query}\t{site_title}"logger.warning(f"adjust_query:'{adjust_query}'")returnadjust_queryreturnquery
3.2.3 Query Construction
將用戶查詢轉換成其他查詢語言以訪問替代數據源。常見的方法包括:
文本轉Cypher
文本轉SQL
在許多場景中,結構化查詢語言(例如,SQL、Cypher)常與語義信息和元數據結合使用,以構建更復雜的查詢。
3.2.4 Query Routing
查詢路由是一個基于LLM的決策制定步驟,針對用戶的查詢決定接下來要做什么。通常的選項包括進行總結、對某些數據索引執行搜索,或嘗試多種不同的路由然后將它們的輸出合成一個答案。
查詢路由器也用于選擇一個索引,或者更廣泛地說,一個數據存儲位置,來發送用戶查詢。無論是你擁有多個數據源,例如經典的向量存儲、圖數據庫或關系數據庫,還是你擁有一個索引層級。對于多文檔存儲來說,一個典型的案例可能是一個摘要索引和另一個文檔chunk向量索引。
定義查詢路由器包括設置它可以做出的選擇。路由選項的選擇是通過LLM調用執行的,其結果以預定義格式返回,用于將查詢路由到給定的索引,或者,如果我們談論的是族群行為,路由到子鏈或甚至如下所示的多文檔代理方案中的其他代理。
fromlangchain.utils.mathimportcosine_similarityfromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.promptsimportPromptTemplatefromlangchain_core.runnablesimportRunnableLambda, RunnablePassthroughfromlangchain_openaiimportOpenAIEmbeddingsphysics_template =f"""You are a very smart physics professor.You are great at answering questions about physics in a concise and easy to understand manner.When you don't know the answer to a question you admit that you don't know.Here is a question:{query}"""math_template =f"""You are a very good mathematician. You are great at answering math questions.You are so good because you are able to break down hard problems into their component parts,answer the component parts, and then put them together to answer the broader question.Here is a question:{query}"""embeddings = OpenAIEmbeddings()prompt_templates = [physics_template, math_template]prompt_embeddings = embeddings.embed_documents(prompt_templates)defprompt_router(input):query_embedding = embeddings.embed_query(input["query"])similarity = cosine_similarity([query_embedding], prompt_embeddings)[0]? ? most_similar = prompt_templates[similarity.argmax()]print("Using MATH"ifmost_similar == math_templateelse"Using PHYSICS")returnPromptTemplate.from_template(most_similar)chain = ({"query": RunnablePassthrough()}? ? | RunnableLambda(prompt_router)| ChatAnthropic(model_name="claude-3-haiku-20240307")? ? | StrOutputParser())
3.3 檢索
在RAG中,檢索過程扮演著至關重要的角色。利用預訓練語言模型可以有效地表示查詢和文本在潛在空間中,從而促進問題和文檔之間的語義相似性建立,以支持檢索。
需要考慮以下三個主要因素:
檢索效率:檢索過程應該高效,能夠快速地返回相關的文檔或答案。這要求在檢索過程中采用高效的算法和數據結構,以及優化的索引和查詢方法。
嵌入質量:嵌入向量應該能夠準確地捕捉文本的語義信息。這要求使用高質量的預訓練語言模型,以確保生成的嵌入向量能夠在潛在空間中準確地表示文本的語義相似性。
任務、數據和模型的對齊:檢索過程需要根據具體的任務需求和可用的數據來選擇合適的模型和方法。任務、數據和模型之間的對齊是關鍵,可以通過合理的數據預處理、模型選擇和訓練來提高檢索的效果。
3.3.1 稀疏檢索器
雖然稀疏編碼模型可能被認為是一種有些過時的技術,通常基于統計方法,如詞頻統計,但由于其更高的編碼效率和穩定性,它們仍然占有一席之地。常見的系數編碼模型包括BM25和TF-IDF。
3.3.2 密集檢索器
基于神經網絡的密集編碼模型包括幾種類型:
基于BERT架構構建的編碼器-解碼器語言模型,如ColBERT。
綜合多任務微調模型,如BGE文本嵌入。
基于云API的模型,如OpenAI-Ada-002和Cohere Embedding。
3.3.3 混合檢索
兩種嵌入方法捕獲不同的相關性特征,并且通過利用互補的相關性信息,可以相互受益。例如,稀疏檢索模型可以用來提供訓練密集檢索模型的初步搜索結果。此外,預訓練語言模型可以用來學習術語權重以增強稀疏檢索。具體來說,它還表明稀疏檢索模型可以增強密集檢索模型的零樣本檢索能力,并幫助密集檢索器處理包含罕見實體的查詢,從而提高魯棒性。
3.4 Post-Retrieval
直接檢索整個文檔chunk并將它們直接輸入到LLM的上下文環境中并非最佳選擇。后處理文檔可以幫助LLM更好地利用上下文信息。
主要挑戰包括:
丟失中間部分。像人類一樣,LLM傾向于只記住長文本的開始和結束部分,而忘記中間部分。
噪聲/反事實chunk。檢索到的噪聲多或事實上矛盾的文檔可能會影響最終的檢索生成。
上下文窗口。盡管檢索到了大量相關內容,但大型模型中對上下文信息長度的限制阻止了包含所有這些內容。
3.4.1 Reranking
不改變內容或長度的情況下,對檢索到的文檔chunk進行重新排序,以增強對LLM更為關鍵的文檔chunk的可見性。具體來說:
基于規則的重新排序
根據特定規則,計算度量來重新排序chunk。常見的度量包括:
多樣性
相關性
最大邊際相關性(Maximal Marginal Relevance)
MMR的背后思想是減少冗余并增加結果的多樣性,它用于文本摘要。MMR根據查詢相關性和信息新穎性的組合標準選擇最終關鍵短語列表中的短語。
Model-base Rerank
使用語言模型對文檔chunk進行重新排序,可選方案包括:
來自BERT系列的編解碼器模型,如SpanBERT。
專門的重新排序模型,如Cohere rerank或bge-reranker-large。
通用大型語言模型,如GPT-4。
3.4.2 過濾
在RAG過程中,一個常見的誤解是認為檢索盡可能多的相關文檔并將它們連接起來形成一個冗長的檢索提示是有益的。然而,過多的上下文可能會引入更多噪聲,削弱LLM對關鍵信息的感知,導致如“中間丟失”等問題。
我們可以基于相似度分數、關鍵詞、元數據過濾結果,將一些低質的文檔過來到,不傳遞到LLM的上下文中。
這是在將我們檢索到的上下文輸入LLM以獲取最終答案之前的最后一步。
defsearch_document(self, query, k):# Return docs and relevance scores in the range [0, 1].# 0 is dissimilar, 1 is most similar.returnself.chroma_obj.similarity_search_with_relevance_scores(query, k=k)
使用similarity_search_with_relevance_scores(query: str, k: int = 4, **kwargs: Any) → List[Tuple[Document, float]]來從Chroma中召回文檔,并返回relevance score,可以根據業務具體需求場景和測試日志,設置MIN_RELEVANCE_SCORE,過濾掉低質的文檔。
# Build the context for promptrecall_domain_set =set()? ? filtered_results = []fordoc, scoreinresults:ifscore > MIN_RELEVANCE_SCORE:? ? ? ? ? ? filtered_results.append((doc, score))domain= urlparse(doc.metadata['source']).netlocrecall_domain_set.add(domain)iffiltered_results:filtered_context ="\n--------------------\n".join([f"URL: {doc.metadata['source']}\nRevelance score: {score}\nContent: {doc.page_content}"fordoc, scoreinfiltered_results? ? ? ? ])context= f"""Documents size: {len(filtered_results)}Documents(Sort by Relevance Score from high to low):{filtered_context}"""else:context= f"""Documents size: 0No documents found directly related to the current query.If the query is similar to greeting phrases like ['Hi', 'Hello', 'Who are you?'], including greetings in other languages such as Chinese, Russian, French, Spanish, German, Japanese, Arabic, Hindi, Portuguese, Korean, etc. The bot will offer a friendly standard response, guiding users to seek information or services detailed on the `{site_title}` website.For other queries, please give the answer "Unfortunately, thereisnoinformation available about'{query}'onthe`{site_title}`website. I'm here to assist you with information related to the `{site_title}` website. If you have any specific queries about our services or need help, feel free to ask, and I'lldomy besttoprovide youwithaccurateandrelevant answers.""""
3.4.3 參考引用
這個更多是一種工具而非檢索改進技術,盡管它非常重要。
如果我們使用了多個來源來生成答案,可能是由于初始查詢的復雜性(我們不得不執行多個子查詢,然后將檢索到的上下文合并成一個答案),或者因為我們在不同的文檔中找到了單個查詢的相關上下文,那么就會出現一個問題,即我們是否能準確地反向引用我們的來源。
有幾種方法可以做到這一點:
將這個引用任務插入到我們的提示中,并要求LLM提及所使用來源的ID。
將生成的響應的部分與我們索引中的原始文本chunk進行匹配。
prompt = f"""Given the documents listed below and the user's query history, please provide accurate and detailed answers in the query's language. The response should be in JSON,with'answer'and'source'fields. Answers must be basedonthese documentsanddirectly relevanttothe`{site_title}`website.Ifthequeryisunrelatedtothe documents, inform theuserthat answers cannot be provided.UserID:"{user_id}"Query:"{query}"UserQueryHistory(Sortbyrequesttimefrommost recenttooldest):{history_context}{context}Response Requirements:-Ifunsure about the answer, proactively seek clarification.- Referonlytoknowledge relatedto`{site_title}`website's content.- Inform users that queries unrelated to `{site_title}` website'scontentcannot be answeredandencourage themtoask site-related queries.- Ensure answersareconsistentwithinformationonthe`{site_title}`website.-UseMarkdown syntaxtoformatthe answerforreadability.- Responses must be craftedinthe samelanguageasthe query.The most important instruction: Pleasedonotprovideanyanswers unrelatedtothe`{site_title}`website! Regardlessofwhether the bot know the answerornot! This instruction must be followed!Pleaseformatyour responseasfollows:{{"answer":"A detailed and specific answer, crafted in the query's language.","source": ["Unique document URLs directly related to the answer. If no documents are referenced, use an empty list []."]}}"""
3.4.5 會話歷史
關于構建一個能夠針對單一查詢多次工作的優秀RAG系統的下一個重大事項是聊天邏輯,這一點與前LLM時代經典聊天機器人中的對話上下文相同。
這是為了支持后續問題、指代或與先前對話上下文相關的任意用戶命令所必需的。這通過查詢壓縮技術來解決,同時考慮聊天上下文和用戶查詢。
關于上述上下文壓縮,總有幾種方法。
一個流行且相對簡單的是ContextChatEngine,首先檢索與用戶查詢相關的上下文,然后將其連同聊天歷史記錄從內存緩沖區發送給LLM,以便LLM在生成下一個答案時了解之前的上下文。
更復雜的案例是CondensePlusContextMode,在每次互動中,聊天歷史和最后一條消息被壓縮成新的查詢,然后這個查詢進入索引,檢索到的上下文連同原始用戶消息一起傳遞給LLM以生成答案。
defget_user_query_history(user_id):history_key =f"open_kf:query_history:{user_id}"history_items = g_diskcache_client.get_list(history_key)[::-1]history = [json.loads(item)foriteminhistory_items]returnhistory# Get history session from Cachehistory_session = get_user_query_history(user_id)# Build the history_context for prompthistory_context ="\n--------------------\n".join([f"Previous Query:{item['query']}\nPrevious Answer:{item['answer']}"foriteminhistory_session])
3.5 生成
利用LLM根據用戶的查詢和檢索到的上下文信息生成答案。
3.5.1 LLM模式
根據場景的不同,LLM的選擇可以分為以下兩種類型:
云API基礎生成器。通過調用第三方LLM的API來使用,如OpenAI的gpt-3.5-turbo、gpt-4-turbo和百度的ERNIE-Bot-turbo等。
優點包括:
沒有服務器壓力
高并發性
能夠使用更強大的模型
缺點包括:
數據通過第三方,可能引起數據隱私問題
無法調整模型(在絕大多數情況下)
本地部署
本地部署開源或自行開發的LLM,如Llama系列、GLM等。其優點和缺點與基于云API的模型相反。本地部署的模型提供了更大的靈活性和更好的隱私保護,但需要更高的計算資源。
3.5.2 響應合成器
這是RAG流程的最后一步,根據我們仔細檢索的所有上下文和初始用戶查詢生成答案。
最簡單的方法就是將所有獲取的上下文(超過某個相關性閾值)與查詢一起連接起來,并一次性輸入到LLM中。
然而,像往常一樣,還有其他更復雜的選項,涉及多次LLM調用來優化檢索到的上下文并生成更好的答案。
響應綜合的主要方法有:
通過將檢索到的上下文逐chunk送入LLM,迭代地完善答案。
將檢索到的上下文進行總結,以適應提示。
基于不同的上下文chunk生成多個答案,然后將它們連接或進行總結。
3.5.3 Postprocess Response
如果不是采用流式輸出,在獲取到LLM生成的結果后,我們可以根據具體業務場景,進行最后干預。
比如OpenIM文檔網站的網站客服機器人,只需要回答和OpenIM網站相關的問題,如果用戶咨詢其它問題或者LLM給出了和網站無關的答案,都需要進行結果干預。
defpostprocess_llm_response(query, answer_json, site_title, recall_domain_set):ifanswer_json['source']:answer_json['source'] = list(dict.fromkeys(answer_json['source']))is_adjusted =False? ? adjust_source = []forurlinanswer_json['source']:? ? ? ? domain = urlparse(url).netlocifdomaininrecall_domain_set:? ? ? ? ? ? adjust_source.append(url)else:logger.warning(f"url:'{url}' is not in{recall_domain_set}, it should not be returned!")ifnotis_adjusted:is_adjusted =Trueifis_adjusted:answer_json['source'] = adjust_sourcelogger.warning(f"adjust_source:{adjust_source}")ifnotadjust_source:ifsite_titlenotinanswer_json['answer']:adjust_answer =f"Unfortunately, there is no information available about '{query}' on the `{site_title}` website. I'm here to assist you with information related to the `{site_title}` website. If you have any specific queries about our services or need help, feel free to ask, and I'll do my best to provide you with accurate and relevant answers."logger.warning(f"adjust_answer:'{adjust_answer}'")ifnotis_adjusted:is_adjusted =Trueanswer_json['answer'] = adjust_answerreturnis_adjusted
四、RAG評估
RAG在自然語言處理領域的快速進展和日益普及,將RAG模型的評估推到了LLM社區研究的前沿。評估RAG模型的主要目標是理解和優化其在不同應用場景下的性能。這里主要介紹RAG的主要下游任務、數據集以及如何評估RAG系統。
4.1 下游任務
RAG的核心任務仍然是問答(QA),包括傳統的單跳/多跳QA、多項選擇題、領域特定的QA以及適用于RAG的長篇場景。除了QA,RAG還不斷擴展到多個下游任務,如信息抽取、對話生成、代碼搜索等。
4.2 評估目標
目前對RAG模型的評估主要集中在其在特定下游任務中的執行上。這些評估使用適合具體任務的已建立的評估指標。例如,問答任務的評估可能依賴于EM和F1分數,而事實核查任務通常以準確度為主要指標。
主要的評估目標包括:
檢索質量:評估檢索質量對于確定檢索組件提取的上下文的有效性至關重要。使用來自搜索引擎、推薦系統和信息檢索系統領域的標準指標來衡量RAG檢索模塊的性能。常用的指標包括命中率、MRR和NDCG等。
生成質量:生成質量的評估關注生成器從檢索到的上下文中合成連貫和相關的答案的能力。根據內容的目標,這種評估可以分為無標簽內容和有標簽內容兩種。對于無標簽內容,評估包括生成答案的忠實度、相關性和無害性。相反,對于有標簽內容,重點是模型生成的信息準確性。此外,檢索和生成質量的評估可以通過人工或自動評估方法進行。
綜上所述,RAG模型的評估涉及檢索質量和生成質量的評估,并使用特定任務的評估指標進行衡量。評估可以采用手動或自動評估方法。
4.3 評估方面
RAG模型的評估實踐強調三個主要的質量得分和四個基本能力,共同為RAG模型的兩個主要目標進行評估:檢索和生成。
質量得分:質量得分包括上下文相關性、答案忠實度和答案相關性。這些質量得分從不同的角度評估RAG模型在信息檢索和生成過程中的效率。
上下文相關性評估檢索到的上下文的精確性和特異性,確保相關性并減少與無關內容相關的處理成本。
答案忠實度確保生成的答案與檢索到的上下文保持一致,避免矛盾。
答案相關性要求生成的答案與提出的問題直接相關,有效回答核心問題。
所需能力:RAG的評估還涵蓋了四個能力,這些能力表明其適應性和效率:噪聲魯棒性、負樣本拒絕、信息整合和反事實魯棒性。這些能力對于模型在各種挑戰和復雜場景下的性能至關重要,影響著質量得分。
噪聲魯棒性評估模型處理與問題相關但缺乏實質信息的噪聲文檔的能力。
負樣本拒絕評估模型在檢索到的文檔中不包含回答問題所需知識時的判斷能力,避免回答無法回答的問題。
信息整合評估模型從多個文檔中合成信息以回答復雜問題的能力。
反事實魯棒性測試模型在文檔中識別和忽略已知的不準確信息的能力,即使在知道存在潛在錯誤信息的情況下也能正確處理。
上下文相關性和噪聲魯棒性對于評估檢索的質量非常重要,而答案忠實度、答案相關性、負樣本拒絕、信息整合和反事實魯棒性對于評估生成的質量非常重要。
需要注意的是,這些指標是從相關工作中得出的傳統度量,尚不能代表一種成熟或標準化的方法來量化RAG評估方面。一些評估研究還開發了針對RAG模型的定制指標,盡管這些指標在此處未包含在內。
4.4 評估基準和工具
有幾種用于評估RAG系統性能的框架,它們分享了使用幾個獨立指標的思想,例如整體答案相關性、答案忠實度、準確性和檢索到的上下文相關性。
Ragas,使用忠實度和答案相關性作為生成答案質量的指標,而對于RAG模型的檢索部分,則使用上下文的精確率和召回率。
Truelens,提出了RAG三元組:
檢索到的上下文與查詢的相關性。
忠實度(LLM答案在提供的上下文中的支持程度)。
答案與查詢的相關性。
RAG流程中的關鍵且最可控的指標是檢索到的上下文相關性,而響應合成器和LLM微調則專注于答案相關性和忠實度。
五、總結與展望
目前RAG技術取得了較大進展,主要體現在以下幾個方面:
增強的數據獲取:RAG已經超越了傳統的非結構化數據,現在包括半結構化和結構化數據,重點是對結構化數據進行預處理,以改善檢索并減少模型對外部知識源的依賴。
整合的技術:RAG正在與其它技術整合,包括使用微調、適配器模塊和強化學習來增強檢索能力。
可調適的檢索過程:檢索過程已經發展到支持多輪檢索增強,利用檢索內容指導生成過程,反之亦然。此外,自主判斷和LLM的使用通過確定是否需要檢索,提高了回答問題的效率。
在生產環境中,除了答案相關性和忠實度之外,RAG系統面臨的主要挑戰是響應速度和健壯性。
RAG的應用范圍正在擴展到多模態領域,將其原理應用于解釋和處理圖片、視頻和代碼等多種數據形式。這一拓展突顯了RAG在人工智能部署中的重要實際意義,吸引了學術界和工業界的興趣。以RAG為中心的人工智能應用和支持工具的不斷發展證明了RAG生態系統的壯大。隨著RAG應用領域的擴大,有必要完善評估方法學,以跟上其發展的步伐。確保準確而具有代表性的性能評估對于充分捕捉RAG對人工智能研究與開發社區的貢獻至關重要。