一、背景
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()