一、背景
LlamaIndex提供2種交互引擎:查詢引擎和聊天引擎。(詳情請看這里)查詢引擎默認沒有上下文信息,也就是說默認是單輪對話。
在RAG系統中,單輪對話/單次查詢的場景較少,而多輪對話則是最常見的場景,若使用查詢引擎,則需要手動將歷史對話拼接后,傳給查詢引擎。這樣做有很多弊端,
(1)手動拼接歷史消息容易出錯,也不夠優雅;
(2)歷史消息不可能一直無限制的拼接,若超過token最大長度,該如何處理?
(3)如何歷史消息通過接口傳遞,不夠安全;
(4)太多的歷史消息,會導致查詢的準確度降低等。
二、對話引擎
查詢引擎的一種實際應用的場景是每次都對數據與知識提出獨立的問題以獲得答案,不考慮歷史對話記錄,另一種實際應用的場景是需要通過多次對話來滿足使用者的需求,比如客戶連續的產品問答與咨詢。在這種場景中,需要跟蹤過去對話的上下文,以更好地理解與回答當前的問題。由于大模型本質上都是無狀態服務形式的,多次對話是通過攜帶歷史對話記錄來完成的。
?1.使用索引快速構造對話引擎
chat_engine = index.as_chat_engine(chat_mode="condense_question")print(chat_engine.chat('陳平安是誰?'))print("--"*40)print(chat_engine.chat('他幾歲了?'))
結果如下:
陳平安是一個自幼失去父母的少年,生活在一個以瓷器聞名的小鎮。他從小就在當地的窯廠做學徒,跟隨一位脾氣不太好的師傅學習燒瓷技藝。盡管生活艱辛,他仍然堅持下來并逐漸掌握了燒瓷的技術。然而,隨著小鎮失去官窯的地位,窯廠被迫關閉,他也失去了生計,只能回到破舊的老宅生活。故事中的他坐在臺階上仰望星空,展現出一種在困境中依然保持希望的形象。
--------------------------------------------------------------------------------
陳平安失去窯廠工作回到老宅時,年齡是十六歲。
從結果上看,“他”被正確的指向到“陳平安”;也就說有上下文了。
2、使用底層 API 組合構造對話引擎
如果需要更精確地控制對話引擎的構造,就要使用底層 API 組合構造。
不同模式的查詢引擎是通過輸入不同的響應生成器來構造的,而不同模式的對話引擎則是直接通過構造不同類型的對話引擎組件完成的。
查詢引擎所依賴的底層組件是檢索器與響應生成器,而對話引擎則通常需要在查詢引擎的基礎上增加記憶等能力而構造。
下面的例子演示了如何構造一個 condense 類型的對話引擎:
custom_prompt = PromptTemplate("""請根據以下的歷史對話記錄和新的輸入問題,重寫一個新的問題,使其能夠捕捉對話中的所有相關上下文。<Chat History> {chat_history} <Follow Up Message> {question} <Standalone question> """)# 歷史對話記錄custom_chat_history = [ChatMessage( role=MessageRole.USER, content="陳平安是誰?"),ChatMessage(role=MessageRole.ASSISTANT, content="陳平安是一個自幼失去父母的少年,生活在一個以瓷器聞名的小鎮。"),]#先構造查詢引擎, 這里省略了構造 vector_index 對象query_engine = index.as_query_engine()#再構造對話引擎chat_engine = CondenseQuestionChatEngine.from_defaults(query_engine=query_engine, #對話引擎基于查詢引擎構造condense_question_prompt=custom_prompt, #設置重寫問題的Prompt模板chat_history=custom_chat_history, #攜帶歷史對話記錄verbose=True,) resp=chat_engine.chat('他幾歲了?')print(resp)
可以看到,這里的構造方法與使用底層 API 組合構造查詢引擎的方法完全
不同,這里構造的是一種叫 condense_question模式的對話引擎(CondenseQuestionChatEngine)。
這種引擎會查看歷史對話記錄,并將最新的用戶問題重寫成新的、具有更完整語義的問題,然后把這個問題輸入查詢引擎獲得答案。因此可以看到,這種類型的引擎所依賴的組件包括查詢引擎、重寫問題的 Prompt 模板、歷史對話記錄,即from_defaults 方法的參數。
LlamaIndex 框架中,不同類型的對話引擎與其他組件之間的關系如圖所示。?
總的來說,對話引擎在底層所依賴的組件主要有以下 3 種。
(1)LLM:大模型。大模型在對話引擎中的作用并不限于最后輸出問題的答案。比如,在Agent類型的對話引擎中,大模型需要根據歷史對話記錄和任務來規劃與推理出使用的工具;在 Condense類型的對話引擎中,大模型需要根據歷史對話記錄和當前問題來重寫輸入的問題。
(2)Query Engine 或者Retriever:查詢引擎或檢索器。由于查詢引擎本身包含了檢索器與多種不同響應生成模式的響應生成器,因此兩者的區別是,只使用檢索器的對話引擎更簡單,而依賴查詢引擎的對話引擎則支持更加多樣的底層響應生成模式。
(3)Memory:記憶體。這是對話引擎區別于查詢引擎的一個顯著特征。由于對話是一種有“狀態”的服務,因此為了保持這種狀態,需要有相應的組件來記錄與維持狀態信息,也就是歷史對話記錄,而Memory組件就是用于實現這個目的的。
對話模式 | 引擎類型 | 依賴的主要組件 |
---|---|---|
simple | SimpleChatEngine | LLM |
condense_question | CondenseQuestionChatEngine | QueryEngine, LLM |
context | ContextChatEngine | Retriever, LLM |
condense_plus_context | CondensePlusContextChatEngine | Retriever, LLM |
react | ReActAgent | [Tool], LLM |
openai | OpenAIAgent | [Tool], LLM |
best | ReActAgent 或 OpenAIAgent | [Tool], LLM |
3、整合Memory組件
Memory使用的說明請看這篇文章。? 對話引擎可以直接使用Memory 。
def complex_memory_cit_test():"""使用帶有記憶功能的對話歷史和自定義 Prompt,結合引用型查詢引擎(CitationQueryEngine),實現帶來源追溯的上下文對話測試。輸出模型回答及其相關片段和元數據,便于溯源。"""# 歷史對話記錄memory= memory_init()memory.put_messages([ChatMessage(role=MessageRole.USER, content="報告期內,公司實現營業收入 63,609.19 萬元,同比增長 4.40%;"),# ChatMessage(role=MessageRole.ASSISTANT,# content="陳平安是一個自幼失去父母的少年,生活在一個以瓷器聞名的小鎮。他從小就開始在窯場做粗活,跟隨一位脾氣不太好的師傅學習燒瓷技藝。盡管生活艱辛,他仍然堅持努力,逐漸掌握了燒瓷的一些技巧。然而,隨著小鎮失去官窯的地位,窯場被迫關閉,他也失去了生計,只能回到破舊的老宅獨自生活。他性格堅韌,面對困境依然保持著對生活的希望。"),])#先構造查詢引擎citation_query_engine = CitationQueryEngine.from_args(index,similarity_top_k=3,citation_chunk_size=512)#再構造對話引擎chat_engine = CondenseQuestionChatEngine.from_defaults(query_engine=citation_query_engine,condense_question_prompt=custom_prompt,memory=memory,verbose=True,)print("--"*40)resp=chat_engine.chat('請將該數值轉成以元單位') #print(resp.response) # LLM 輸出回答print("------來源---------------")for node in resp.source_nodes:print("相關片段:", node.text)print("片段分數:", node.score)print("片段元數據:", node.metadata)# print(f"node.metadata:{node.metadata}")print("=" * 40)if __name__ == "__main__":complex_memory_cit_test()