使用Langchain、OpenAI、gTTS和Streamlit创建双聊天机器人语言学习应用的逐步教程
当我开始学习一门新语言时,我喜欢买那些“会话对话”书籍。我发现这些书籍非常有用,因为它们帮助我理解语言是如何运作的——不仅是语法和词汇,还有人们在日常生活中如何实际使用它。
现在随着大型语言模型(LLM)的兴起,我想到一个想法:我能否以更互动、动态和可扩展的形式复制这些语言学习书籍?我能否利用LLM创建一个工具,为语言学习者生成新鲜的、按需的对话?
这个想法启发了我今天想与大家分享的项目——一个AI驱动的语言学习应用,在这个应用中,学习者可以观察和学习两个AI聊天机器人进行用户定义的对话或辩论。
关于所采用的技术栈,我使用了Langchain、OpenAI API、gTTS和Streamlit创建了这个应用程序,用户可以定义角色、情节或辩论主题,让AI生成内容。
开发的语言学习应用的演示。(图作者)
如果你想知道它是如何工作的,那么请和我一起逐步了解构建这个交互式双聊天机器人系统的过程🗺️📍🚶♀️。
你可以在这里找到完整的源代码💻。在本文中,我们还将通过关键的代码片段来解释这些想法。
有了这个想法,让我们开始吧!
目录 · 1. 项目概述 · 2. 先决条件 ∘ 2.1 LangChain ∘ 2.2 ConversationChain · 3. 项目设计 ∘ 3.1 开发单个聊天机器人∘ 3.2 开发双聊天机器人系统 · 4. 使用Streamlit设计应用界面 · 5. 学习和未来扩展 · 6. 结论
1. 项目概述
如前所述,我们的目标是创建一个双聊天机器人驱动的独特语言学习应用程序。这个应用程序的创新之处在于,这些聊天机器人互相交互,在目标语言中创建逼真的对话。用户可以观察这些AI驱动的对话,将其用作语言学习资源,并了解他们选择的语言的实际用法。
在我们的应用程序中,用户应该可以根据自己的需求自定义他们的学习体验。他们可以调整多个设置,包括目标语言、学习模式、会话长度和熟练水平。
目标语言🔤
用户可以选择他们想学习的语言。这个选择指导了聊天机器人在互动中使用的语言。目前,我已经支持了英语—‘en’、德语—‘de’、西班牙语—‘es’和法语—‘fr’,但只要GPT模型有足够的知识,添加更多的语言就是微不足道的。
学习模式📖
这个设置让用户选择聊天机器人之间的对话风格。在“对话”模式下,用户可以为每个机器人定义角色(例如顾客和服务员)和动作(点餐和接受订单),并指定一个情节(在餐厅),机器人将模拟逼真的对话。在“辩论”模式下,用户被提示输入一个辩论主题(我们应该采用核能吗?),然后机器人就会在提供的主题上进行生动的辩论。
应用程序的界面应该是响应式的,并根据用户选择的学习模式进行动态调整,提供无缝的用户体验。
会话时长 ⏰
会话时长设置为用户控制每个聊天机器人对话或辩论的持续时间。这意味着他们可以根据自己的偏好进行短暂快速的对话或更长更深入的讨论。
熟练水平 🏆
此设置将聊天机器人对话的复杂性调整到用户的语言技能水平。初学者可能更喜欢简单的对话,而更高级的学习者可以处理复杂的辩论或讨论。
一旦用户指定了这些设置,他们就可以启动会话并观察AI聊天机器人根据用户的偏好进行动态和交互式的对话。我们的整体工作流程可以如下图所示:
2. 先决条件
在我们深入了解应用程序的开发之前,让我们熟悉一下我们将要使用的工具。在本节中,我们将简要介绍LangChain库,特别是ConversationChain
模块,它是我们应用程序的核心。
2.1 LangChain
构建由大型语言模型(LLM)驱动的应用程序涉及许多复杂性。您需要通过API调用与语言模型提供商进行接口,将这些模型连接到各种数据源,处理用户交互的历史记录,并设计执行复杂任务的管道。这就是LangChain库发挥作用的地方。
LangChain是一个专门用于简化LLM驱动应用程序开发的框架。它提供了广泛的组件,解决了上述常见痛点。无论是管理与语言模型提供商的交互、协调数据连接、维护历史交互的内存,还是定义复杂任务管道,LangChain都有所涉及。
LangChain引入的一个关键概念是“链”。实质上,链允许我们将多个组件组合在一起,创建一个单一的、连贯的应用程序。例如,在LangChain中的一个基本链类型是LLMChain。
它创建了一个管道,首先使用用户提供的输入关键值格式化提示模板,然后将格式化的指令传递给LLM,最后返回LLM输出。
LangChain提供了各种链类型,包括RetrievalQAChain,
用于对文档进行问答,SummarizationChain,
用于总结多个文档,当然,我们今天的重点是ConversationChain。
2.2 ConversationChain
ConversationChain
用于通过提供交换消息和存储会话历史的框架来促进交互式对话。以下是一个示例代码片段,用于说明其使用方法:
from langchain.chains import ConversationChain# 创建会话链conversation = ConversationChain(memory, prompt, llm)# 运行会话链conversation.predict(input="Hi there!")# 获取LLM的响应:“Hello! How can I assist you today?"# 我们可以不断调用会话链conversation.predict(input="I'm doing well! Just having a conversation with an AI.")# 获取LLM的响应:“That sounds like fun! I'm happy to chat with you. Is there anything specific you'd like to talk about?"
在这个例子中,ConversationChain
需要三个输入:memory,一个LangChain组件,用于保存交互历史记录;prompt,LLM的输入;和llm,核心大型语言模型(例如GPT-3.5-Turbo等)。
一旦实例化了ConversationChain
对象,我们就可以简单地使用用户输入调用conversation.predict()
来获得LLM的响应。使用ConversationChain
的便利之处在于,我们实际上可以多次调用conversation.predict()
,它会在幕后自动记录消息历史记录。
在下一节中,我们将利用 ConversationChain
的强大功能来创建我们的聊天机器人,并深入探讨如何定义和利用记忆、提示模板和LLM。
如果您想了解更多关于LangChain的信息,请查看他们的官方文档。此外,这个YouTube播放列表也提供了全面、实用的介绍。
3. 项目设计
现在我们已经清楚地了解了我们想要构建的东西以及构建它的工具,是时候动起手来深入代码了!在本节中,我们将重点讲解创建我们的双聊天机器人交互的具体细节。首先,我们将探讨单个聊天机器人的类定义,然后扩展到创建双聊天机器人类,使我们的两个聊天机器人能够互动。我们将在第四节中使用Streamlit保存应用程序界面的设计。
3.1 开发单个聊天机器人
在本小节中,我们将一起开发一个单个聊天机器人,稍后将其集成到双聊天机器人系统中。让我们从整体类设计开始,然后将注意力转向提示工程。
🏗️ 类设计
我们的聊天机器人类应该能够管理一个单独的聊天机器人。这涉及到使用用户指定的LLM作为其骨干,根据用户的意图提供指令,并促进交互式的多轮对话。有了这个想法,让我们开始编码。
首先,导入必要的库:
import osimport openaifrom langchain.prompts import ( ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate, HumanMessagePromptTemplate)from langchain.prompts import PromptTemplatefrom langchain.chains import LLMChainfrom langchain.chains import ConversationChainfrom langchain.chat_models import ChatOpenAIfrom langchain.memory import ConversationBufferMemory
接下来,我们定义类构造函数:
class Chatbot: """用LangChain创建具有内存的单个聊天机器人的类定义。""" def __init__(self, engine): """选择背景大语言模型,并实例化 用于在LangChain中创建语言链的内存。 """ # 实例化LLM if engine == 'OpenAI': # 提醒:需要设置openAI API密钥 # (例如,通过环境变量OPENAI_API_KEY) self.llm = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0.7 ) else: raise KeyError("目前不支持的聊天模型类型!") # 实例化内存 self.memory = ConversationBufferMemory(return_messages=True)
目前,您只能选择使用本机的OpenAI API。不过,由于LangChain支持各种类型的后端LLM(例如Azure OpenAI端点、Anthropic聊天模型、Google Vertex AI上的PaLM API等),因此添加更多后端LLM很简单。
除了LLM之外,我们需要实例化的另一个重要组件是内存,用于跟踪对话历史记录。在这里,我们使用 ConversationBufferMemory
来实现这一目的,它简单地将几个最近的输入/输出添加到聊天机器人的当前输入之前。这是LangChain提供的最简单的内存类型,足以满足我们当前的目的。
有关其他类型的内存的完整概述,请参阅官方文档。
接下来,我们需要一个类方法,允许我们向聊天机器人发出指令并与其交谈。这就是 self.instruct()
的作用:
def instruct(self, role, oppo_role, language, scenario, session_length, proficiency_level, learning_mode, starter=False): """确定聊天机器人交互的上下文。 """ # 定义语言设置 self.role = role self.oppo_role = oppo_role self.language = language self.scenario = scenario self.session_length = session_length self.proficiency_level = proficiency_level self.learning_mode = learning_mode self.starter = starter # 定义提示模板 prompt = ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(self._specify_system_message()), MessagesPlaceholder(variable_name="history"), HumanMessagePromptTemplate.from_template("{input}") ]) # 创建对话链 self.conversation = ConversationChain(memory=self.memory, prompt=prompt, llm=self.llm, verbose=False)
- 我们定义了一些设置,允许用户定制他们的学习体验。
除了在“第1部分项目概述”中提到的内容外,我们还有四个新属性:
self.role/self.oppo_role:
这个属性采用字典形式记录角色名称和相应的操作。例如:
self.role = {'name': '顾客', 'action': '点餐'}
self.oppo_role
表示与当前聊天机器人进行对话的另一个聊天机器人的角色。这是必要的,因为当前聊天机器人需要了解它正在与谁进行通信,提供必要的上下文信息。
self.scenario
设定了对话的舞台。对于“对话”学习模式,self.scenario
表示对话发生的地点;对于“辩论”模式,self.scenario
表示辩论主题。
最后,self.starter
只是一个布尔标记,用于指示当前聊天机器人是否会启动对话。
- 我们构建聊天机器人的提示方式。
在OpenAI中,聊天模型通常将消息列表作为输入,并返回模型生成的消息作为输出。LangChain支持 SystemMessage
,AIMessage
,HumanMessage
: SystemMessage
帮助设置聊天机器人的行为,AIMessage
存储以前的聊天机器人的响应,HumanMessage
提供请求或评论,供聊天机器人回应。
LangChain方便地提供了PromptTemplate
来简化提示生成和摄取。对于聊天机器人应用程序,我们需要为所有三种消息类型指定PromptTemplate
。最重要的部分是设置SystemMessage
,它控制聊天机器人的行为。我们有一个单独的方法,self._specify_system_message()
,来处理这个问题,我们将在后面详细讨论。
- 最后,我们将所有部分结合起来构建一个
ConversationChain。
🖋️ 提示设计
我们现在的重点是指导聊天机器人参与用户所期望的对话。为此,我们有self._specify_system_message()
方法。这个方法的签名如下:
def _specify_system_message(self): """指定聊天机器人的行为,包括以下方面: - 一般上下文:在给定情景下进行对话/辩论 - 使用的语言 - 模拟对话/辩论的目的 - 语言复杂度要求 - 交换长度要求 - 其他细微差别的限制 输出: -------- prompt:聊天机器人的指令。 """
基本上,这个方法编译一个字符串,然后将其馈入SystemMessagePromptTemplate.from_template()
中,以指导聊天机器人,如上面self.instruct()
方法的定义所示。我们将在以下部分解剖这个“长字符串”,以理解如何将每个语言学习要求合并到提示中。
1️⃣ 会话长度
会话长度由直接指定在一个会话中可能发生的最大交换数来控制。这些数字目前是硬编码的。
# 确定两个聊天机器人之间的交换数量exchange_counts_dict = { '短': {'对话': 8,'辩论': 4}, '长': {'对话': 16,'辩论': 8}}exchange_counts = exchange_counts_dict[self.session_length][self.learning_mode]
2️⃣ 聊天机器人在一个交换中可以说的句子数量
除了限制允许的总交换数外,限制聊天机器人在一个交换中可以说多少话,或者等价地,限制句子的数量也是有益的。在我的实验中,通常不需要在“对话”模式下限制这一点,因为聊天机器人模仿现实对话,往往会说出合理的长度。但是,在“辩论”模式下,有必要进行限制。否则,聊天机器人可能会继续讲话,最终生成一篇“论文”😆。
与限制会话长度类似,限制发言长度的数字也是硬编码的,并与用户在目标语言中的熟练程度相对应:
# 确定辩论中的句子数argument_num_dict = {'初学者': 4,'中级': 6,'高级': 8}
3️⃣ 确定语言复杂度
在这里,我们调节聊天机器人可以使用的语言复杂度:
if self.proficiency_level == '初学者': lang_requirement = """尽可能使用基本和简单的词汇和句子结构。必须避免习语、俚语和复杂的语法结构。"""elif self.proficiency_level == '中级': lang_requirement = """使用更广泛的词汇和各种句子结构。您可以包含一些习语和口语表达,但避免使用高度技术性的语言或复杂的文学表达。"""elif self.proficiency_level == '高级': lang_requirement = """使用复杂的词汇、复杂的句子结构、习语、口语表达和适当的技术语言。"""else: raise KeyError('当前不支持的熟练程度!')
4️⃣ 将所有内容组合在一起!
以下是不同学习模式的指令:
# 编译机器人指令if self.learning_mode == 'Conversation': prompt = f"""您是一台擅长角色扮演的人工智能。您正在模拟一次典型的{self.scenario}发生的对话。在这个场景中,您扮演一个{self.role['name']}{self.role['action']},与一个{self.oppo_role['name']}{self.oppo_role['action']}交谈。您的对话应仅使用{self.language}进行。不要翻译。这种模拟的{self.learning_mode}是为{self.language}语言学习者设计的,以学习{self.language}的真实对话。您应假设学习者在{self.language}的熟练程度为{self.proficiency_level}。因此,您应{lang_requirement}。您应在{exchange_counts}次与{self.oppo_role['name']}交换中完成对话。与{self.oppo_role['name']}的对话应在考虑到{self.language}文化的情况下自然而典型地进行。"""elif self.learning_mode == 'Debate': prompt = f"""您是一台擅长辩论的人工智能。您现在正在进行以下主题的辩论:{self.scenario}。在这个辩论中,您扮演一个{self.role['name']}的角色。始终记住您在辩论中的立场。您的辩论应仅使用{self.language}进行。不要翻译。这种模拟辩论是为{self.language}语言学习者设计的,以学习{self.language}。您应假设学习者在{self.language}的熟练程度为{self.proficiency_level}。因此,您应{lang_requirement}。您将与另一台人工智能(扮演{self.oppo_role['name']}角色的人工智能)交换意见{exchange_counts}次。每次发言时,您最多只能讲{argument_num_dict[self.proficiency_level]}个句子。"""else: raise KeyError('当前不支持的学习模式!')
5️⃣ 谁先说话?
最后,我们指示聊天机器人是否应该首先发言还是等待对手人工智能的回复:
# 给机器人指令if self.starter: # 如果当前机器人是第一个发言的 prompt += f"您正在领导{self.learning_mode}。\n"else: # 如果当前机器人是第二个发言的 prompt += f"等待{self.oppo_role['name']}的陈述。"
现在我们已完成提示设计🎉。简要总结一下,这是我们迄今为止开发的内容:
3.2 开发双聊天机器人系统
现在我们来到了令人兴奋的部分!在这个小节中,我们将开发一个双聊天机器人类,让两个聊天机器人互相交互💬💬。
🏗️ 类设计
感谢先前开发的单一 Chatbot 类,我们可以轻松地在类构造函数中实例化两个 chatbot:
class DualChatbot: """定义双聊天机器人交互系统的类,使用 LangChain 创建。""" def __init__(self, engine, role_dict, language, scenario, proficiency_level, learning_mode, session_length): # 实例化两个 chatbot self.engine = engine self.proficiency_level = proficiency_level self.language = language self.chatbots = role_dict for k in role_dict.keys(): self.chatbots[k].update({'chatbot': Chatbot(engine)}) # 分配两个 chatbot 的角色 self.chatbots['role1']['chatbot'].instruct(role=self.chatbots['role1'], oppo_role=self.chatbots['role2'], language=language, scenario=scenario, session_length=session_length, proficiency_level=proficiency_level, learning_mode=learning_mode, starter=True) self.chatbots['role2']['chatbot'].instruct(role=self.chatbots['role2'], oppo_role=self.chatbots['role1'], language=language, scenario=scenario, session_length=session_length, proficiency_level=proficiency_level, learning_mode=learning_mode, starter=False) # 添加会话时长 self.session_length = session_length # 准备对话 self._reset_conversation_history()
self.chatbots
是一个字典,用于存储与两个机器人相关的信息:
# 对于“交谈”模式self.chatbots= { 'role1': {'name': '顾客', 'action': '点餐', 'chatbot': Chatbot()}, 'role2': {'name': '服务员', 'action': '记录订单', 'chatbot': Chatbot()} }# 对于“辩论”模式self.chatbots= { 'role1': {'name': '支持者', 'chatbot': Chatbot()}, 'role2': {'name': '反对者', 'chatbot': Chatbot()} }
self._reset_conversation_history
用于初始化全新的对话历史记录并向 chatbot 提供初始指令:
def _reset_conversation_history(self): """重置对话历史记录。 """ # 对话历史记录的占位符 self.conversation_history = [] # 两个 chatbot 的输入 self.input1 = "开始对话。" self.input2 = ""
为了方便两个 chatbot 之间的交互,我们使用了 self.step()
方法。该方法允许两个 chatbot 之间进行一轮交互:
def step(self): """在两个 chatbot 之间进行一轮交流。 """ # Chatbot1 发言 output1 = self.chatbots['role1']['chatbot'].conversation.predict(input=self.input1) self.conversation_history.append({"bot": self.chatbots['role1']['name'], "text": output1}) # 将 chatbot1 的输出作为 chatbot2 的输入 self.input2 = output1 # Chatbot2 发言 output2 = self.chatbots['role2']['chatbot'].conversation.predict(input=self.input2) self.conversation_history.append({"bot": self.chatbots['role2']['name'], "text": output2}) # 将 chatbot2 的输出作为 chatbot1 的输入 self.input1 = output2 # 翻译响应 translate1 = self.translate(output1) translate2 = self.translate(output2) return output1, output2, translate1, translate2
请注意,我们嵌入了一个名为 self.translate()
的方法。该方法的目的是将脚本翻译成英语。对于语言学习者来说,这种功能可能很有用,因为他们可以理解目标语言生成的对话的含义。
为了实现翻译功能,我们可以使用基本的 LLMChain
,它需要一个后端 LLM 模型和一个指令提示:
def translate(self, message): """将生成的脚本翻译成英语。 """ if self.language == '英语': # 没有执行翻译 translation = '翻译:' + message else: # 实例化翻译器 if self.engine == 'OpenAI': # 提醒:需要设置 openAI API 密钥 # (例如通过环境变量 OPENAI_API_KEY) self.translator = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0.7 ) else: raise KeyError("当前不支持的翻译模型类型!") # 指定指令 instruction = """将以下句子从 {src_lang}(源语言)翻译为 {trg_lang} (目标语言)。 这是源语言的句子:\n {src_input}。""" prompt = PromptTemplate( input_variables=["src_lang", "trg_lang", "src_input"], template=instruction, ) # 创建一个语言链 translator_chain = LLMChain(llm=self.translator, prompt=prompt) translation = translator_chain.predict(src_lang=self.language, trg_lang="英语", src_input=message) return translation
最后,为语言学习者提供生成的对话脚本的关键语言学习要点的摘要可能是有益的,无论是重点词汇、语法要点还是功能短语。为此,我们可以包括一个self.summary()
方法:
def summary(self, script): """从生成的脚本中提取关键语言学习要点。 """ # 实例化摘要机器人 if self.engine == 'OpenAI': # 提醒:需要设置 OpenAI API 密钥 #(例如通过环境变量 OPENAI_API_KEY) self.summary_bot = ChatOpenAI( model_name="gpt-3.5-turbo", temperature=0.7 ) else: raise KeyError("当前不支持的摘要模型类型!") # 指定说明 instruction = """以下文本是模拟的对话,使用{src_lang}。这段文本的目标是帮助{src_lang}学习者学习{src_lang}的实际使用。因此,你的任务是根据给定的文本总结关键词汇、语法要点和功能短语,这些对于学习{src_lang}的学生可能非常重要。你的摘要应该用英语进行,但在适当的情况下使用原语言的文本示例。请记住,你的目标学生在{src_lang}中的熟练程度为{proficiency}级别。你的摘要必须与他们的熟练程度相匹配。 对话如下:\n {script}。""" prompt = PromptTemplate( input_variables=["src_lang", "proficiency", "script"], template=instruction, ) # 创建语言链 summary_chain = LLMChain(llm=self.summary_bot, prompt=prompt) summary = summary_chain.predict(src_lang=self.language, proficiency=self.proficiency_level, script=script) return summary
与self.translate()
方法类似,我们使用了基本的LLMChain
来执行所需的任务。请注意,我们明确要求语言模型根据用户的熟练程度总结关键语言学习要点。
有了这个,我们已经完成了双聊天机器人类的开发🥂。简单总结一下,到目前为止我们已经开发了:
4. 使用Streamlit进行应用程序界面设计
现在我们已经准备好开发用户界面了🖥️。对于这个项目,我们将使用Streamlit库来构建前端。
如果你不熟悉,Streamlit是一个开源的Python库,专门用于创建以数据科学和机器学习为重点的交互式Web应用程序。它通过提供易于使用的API、实时代码重新加载以进行即时更新、支持用户输入的交互式小部件、支持数据可视化库以及集成丰富媒体的功能,简化了构建和部署应用程序的过程。
让我们从一个新的Python脚本app.py开始,导入必要的库:
import streamlit as stfrom streamlit_chat import messagefrom chatbot import DualChatbotimport timefrom gtts import gTTSfrom io import BytesIO
除了主要的streamlit
库之外,我们还导入了streamlit_chat
库,这是一个由社区构建的Streamlit组件,专门用于创建聊天机器人UI。我们之前开发的DualChatbot
类存储在chatbot.py文件中,所以我们也需要导入它。最后,我们导入gTTS
,它代表Google的文本到语音,以在此项目中为机器人生成的对话脚本添加音频。
在配置Streamlit界面之前,让我们首先定义语言学习设置:
# 定义语言学习设置LANGUAGES = ['英语', '德语', '西班牙语', '法语']SESSION_LENGTHS = ['短', '长']PROFICIENCY_LEVELS = ['初学者', '中级', '高级']MAX_EXCHANGE_COUNTS = { '短': {'Conversation': 8, 'Debate': 4}, '长': {'Conversation': 16, 'Debate': 8}}AUDIO_SPEECH = { '英语': 'en', '德语': 'de', '西班牙语': 'es', '法语': 'fr'}AVATAR_SEED = [123, 42]# 定义主干llmengine = 'OpenAI'
AVATAR_SEED
用于为不同的聊天机器人生成不同的头像图标。
首先,我们设置用户界面的基本布局并建立用户选择的选项:
# 设置应用程序的标题st.title('Language Learning App 🌍📖🎓')# 设置应用程序的描述st.markdown("""该应用程序生成对话或辩论脚本,以辅助语言学习 🎯 选择您想要的设置,然后点击“生成”开始 🚀""")# 添加用于学习模式的下拉框learning_mode = st.sidebar.selectbox('学习模式 📖', ('对话', '辩论'))if learning_mode == '对话': role1 = st.sidebar.text_input('角色1 🎭') action1 = st.sidebar.text_input('动作1 🗣️') role2 = st.sidebar.text_input('角色2 🎭') action2 = st.sidebar.text_input('动作2 🗣️') scenario = st.sidebar.text_input('情景 🎥') time_delay = 2 # 配置角色字典 role_dict = { 'role1': {'name': role1, 'action': action1}, 'role2': {'name': role2, 'action': action2} }else: scenario = st.sidebar.text_input('辩论话题 💬') # 配置角色字典 role_dict = { 'role1': {'name': '支持者'}, 'role2': {'name': '反对者'} } time_delay = 5language = st.sidebar.selectbox('目标语言 🔤', LANGUAGES)session_length = st.sidebar.selectbox('会话长度 ⏰', SESSION_LENGTHS)proficiency_level = st.sidebar.selectbox('熟练程度 🏆', PROFICIENCY_LEVELS)
注意引入了time_delay
变量。它用于指定显示两个连续消息之间的等待时间。如果将此延迟设置为零,则两个聊天机器人之间生成的交换将迅速出现在应用程序中(仅受OpenAI的响应时间限制)。但是,为了用户体验,允许用户在下一个交换出现之前有足够的时间阅读生成的消息可能是有益的。
接下来,我们初始化Streamlit会话状态,以在Streamlit应用程序中存储特定于用户的会话数据:
if "bot1_mesg" not in st.session_state: st.session_state["bot1_mesg"] = []if "bot2_mesg" not in st.session_state: st.session_state["bot2_mesg"] = []if 'batch_flag' not in st.session_state: st.session_state["batch_flag"] = Falseif 'translate_flag' not in st.session_state: st.session_state["translate_flag"] = Falseif 'audio_flag' not in st.session_state: st.session_state["audio_flag"] = Falseif 'message_counter' not in st.session_state: st.session_state["message_counter"] = 0
这里我们回答了两个问题:
1️⃣ 首先,为什么我们需要“session_state”?
在Streamlit中,每当用户与应用程序交互时,Streamlit会从头到尾重新运行整个脚本,相应地更新应用程序的输出。然而,Streamlit的这种反应性可能会在您想要在应用程序中维护特定于用户的数据或在不同交互或页面之间保留状态时带来挑战。由于Streamlit在每次用户交互时重新加载脚本,常规Python变量将失去其值,应用程序将重置为其初始状态。
这就是session_state的作用。Streamlit中的会话状态提供了一种存储和检索数据的方式,该数据在用户会话期间持久存在,即使在重新加载应用程序或用户在应用程序中导航到不同的组件或页面时也是如此。它允许您维护有状态的信息并保存每个用户的应用程序上下文。
2️⃣ 其次,会话状态中存储的那些变量是什么?
“ bot1_mesg ”是一个列表,其中列表的每个元素都是一个字典,该字典保存第一个聊天机器人说的消息。它具有以下键:“role”、“content”和“translation”。 “ bot2_mesg ”的定义与此相同。
“ batch_flag ”是一个布尔标志,用于指示是否一次性显示对话交换还是有时间延迟。在当前设计中,当两个聊天机器人的对话首次生成时,它们之间的聊天将以时间延迟的方式出现。之后,用户可能希望查看或添加生成的会话的翻译(在“ bot1_mesg ”和“ bot2_mesg ”中存储的会话消息将一次性显示出来)。这是有益的,因为我们不需要再次调用OpenAI API以减少成本和延迟。
“translate_flag”和“audio_flag”用于指示翻译和/或音频是否将显示在原始对话旁边。
“message_counter”是一个计数器,每当来自chabot的消息被显示时,它就会增加一个。想法是将消息ID与此计数器分配,因为Streamlit要求每个UI组件都需要具有唯一的ID。
现在我们可以介绍让两个聊天机器人相互交互并生成对话的逻辑:
if 'dual_chatbots' not in st.session_state: if st.sidebar.button('生成'): # 添加标志以指示这是第一次运行脚本 st.session_state["first_time_exec"] = True with conversation_container: if learning_mode == 'Conversation': st.write(f"""#### 以下对话发生在{role1}和{role2}之间{scenario}🎭""") else: st.write(f"""#### 辩论💬:{scenario}""") # 实例化双聊天机器人系统 dual_chatbots = DualChatbot(engine, role_dict, language, scenario, proficiency_level, learning_mode, session_length) st.session_state['dual_chatbots'] = dual_chatbots # 开始交流 for _ in range(MAX_EXCHANGE_COUNTS[session_length][learning_mode]): output1, output2, translate1, translate2 = dual_chatbots.step() mesg_1 = {"role": dual_chatbots.chatbots['role1']['name'], "content": output1, "translation": translate1} mesg_2 = {"role": dual_chatbots.chatbots['role2']['name'], "content": output2, "translation": translate2} new_count = show_messages(mesg_1, mesg_2, st.session_state["message_counter"], time_delay=time_delay, batch=False, audio=False, translation=False) st.session_state["message_counter"] = new_count # 更新会话状态 st.session_state.bot1_mesg.append(mesg_1) st.session_state.bot2_mesg.append(mesg_2)
第一次运行脚本时,会话状态中不会存储“dual_chatbots”键(因为尚未创建双聊天机器人)。因此,当用户单击侧边栏上的“生成”按钮时,上面显示的代码片段将被执行。两个聊天机器人将交替聊天一定次数,并将所有对话消息记录在会话状态中。 show_message()
函数是一个辅助函数,旨在成为样式消息显示的唯一接口。我们将在本节末回到它。
现在,如果用户与应用程序交互并更改了一些设置,则Streamlit将从顶部重新运行整个脚本。由于我们已经生成了所需的对话脚本,因此无需再次调用OpenAI API。相反,我们可以简单地检索存储的信息:
if 'dual_chatbots' in st.session_state: # 显示翻译 if translate_col.button('翻译成英语'): st.session_state['translate_flag'] = True st.session_state['batch_flag'] = True # 显示原始文本 if original_col.button('显示原始文本'): st.session_state['translate_flag'] = False st.session_state['batch_flag'] = True # 添加音频 if audio_col.button('播放音频'): st.session_state['audio_flag'] = True st.session_state['batch_flag'] = True # 恢复生成的对话和聊天机器人 mesg1_list = st.session_state.bot1_mesg mesg2_list = st.session_state.bot2_mesg dual_chatbots = st.session_state['dual_chatbots'] # 控制消息外观 if st.session_state["first_time_exec"]: st.session_state['first_time_exec'] = False else: # 显示完整信息 with conversation_container: if learning_mode == 'Conversation': st.write(f"""#### {role1}和{role2}{scenario}🎭""") else: st.write(f"""#### 辩论💬:{scenario}""") for mesg_1, mesg_2 in zip(mesg1_list, mesg2_list): new_count = show_messages(mesg_1, mesg_2, st.session_state["message_counter"], time_delay=time_delay, batch=st.session_state['batch_flag'], audio=st.session_state['audio_flag'], translation=st.session_state['translate_flag']) st.session_state["message_counter"] = new_count
请注意,会话状态中还有另一个名为“first_time_exec” 的标记。这是用于指示是否已在应用上显示最初生成的脚本。如果我们删除此检查,则在第一次运行应用程序时将出现相同的消息两次。
唯一剩下的就是在 UI 中包含关键学习要点的摘要。为此,我们可以使用 st.expander
。在 Streamlit 中,当我们有大量内容或信息需要以紧凑形式呈现时,st.expander
很有用,最初将其隐藏起来。当用户单击扩展器时,其中的内容将展开或折叠,从而显示或隐藏其他详细信息。
# 创建关键学习要点的摘要 summary_expander = st.expander('关键学习要点') scripts = [] for mesg_1, mesg_2 in zip(mesg1_list, mesg2_list): for i, mesg in enumerate([mesg_1, mesg_2]): scripts.append(mesg['role'] + ': ' + mesg['content']) # 编译摘要 if "summary" not in st.session_state: summary = dual_chatbots.summary(scripts) st.session_state["summary"] = summary else: summary = st.session_state["summary"] with summary_expander: st.markdown(f"**这是学习摘要:**") st.write(summary)
由于关键学习要点的摘要也是通过调用 OpenAI API 生成的,因此我们可以将生成的摘要保存到会话状态中,以便在再次运行脚本时可以检索内容。
最后,让我们使用辅助函数 show_message
完成 Streamlit UI 设计:
def show_messages(mesg_1, mesg_2, message_counter, time_delay, batch=False, audio=False, translation=False): """显示对话交换。此辅助函数支持显示原始文本、翻译文本和音频语音。 输出: ------- message_counter: 更新后的 ID 键计数器 """ for i, mesg in enumerate([mesg_1, mesg_2]): # 显示原始交换 () message(f"{mesg['content']}", is_user=i==1, avatar_style="bottts", seed=AVATAR_SEED[i], key=message_counter) message_counter += 1 # 模拟对话之间的时间间隔 # (仅在第一次生成对话脚本时出现此时间延迟) if not batch: time.sleep(time_delay) # 显示翻译交换 if translation: message(f"{mesg['translation']}", is_user=i==1, avatar_style="bottts", seed=AVATAR_SEED[i], key=message_counter) message_counter += 1 # 将音频附加到交换 if audio: tts = gTTS(text=mesg['content'], lang=AUDIO_SPEECH[language]) sound_file = BytesIO() tts.write_to_fp(sound_file) st.audio(sound_file) return message_counter
有几点需要进一步解释:
1️⃣ message()
对象
这是 streamlit_chat
库的一部分,用于显示消息。在其最简单的形式中,我们有:
import streamlit as stfrom streamlit_chat import messagemessage("Hellp, I am a Chatbot, how may I help you?") message("Hey, what's a chatbot", is_user=True)
其中参数 is_user
确定消息是否应左对齐或右对齐。在我们的 show_message
代码片段中,我们还指定了 avatar_style
和 seed
来设置两个聊天机器人的头像图标。 key
参数仅用于为每条消息分配唯一 ID,如 Streamlit 所需。
2️⃣ 文字转语音
在这里,我们使用 gTTS 库根据生成的脚本创建目标语言的音频语音。这个库很容易使用,但它确实有一个限制:你只能有一个声音。生成音频对象后,我们可以使用 st.audio
为应用程序中的每条消息创建音频播放器。
太棒了!我们现在已经完成了UI设计:) 在终端中输入以下命令:
streamlit run app.py
您应该可以在浏览器中看到应用程序并与之交互。做得好!
5. 学习和未来扩展
在我们结束之前,我想与您分享这个项目的一些关键学习和未来增强的潜在方向。
1️⃣ 如何停止对话?
如果您想做得正确,这个问题实际上比看起来更难。理想情况下,我们希望对话自然结束。然而,在我的一些实验中,我注意到聊天机器人在对话结束时只会互相说“谢谢”或“再见”,这会不必要地延长对话。解决此问题的几种潜在方法包括:
- 交流回合的硬性限制:这可能是最简单的解决方案,也是我们在这个项目中采用的方案。但是,它可能并不总是理想的,因为它可能会导致对话过早地终止。作为解决方法,我们已经指示
SystemMessage
中的机器人在一定数量的交换内结束对话。 - 使用“信号词”:聊天机器人可以被编程为在它认为对话自然结束时说特定的“信号词”(如“会话结束”)。然后可以实施逻辑来检测这些“信号词”,并相应地结束循环。
- 对话后处理:一旦聊天机器人生成了对话,就可以部署另一个LLM作为“编辑器”来修剪对话。这可能是一种有效的方法。然而,它的缺点可能包括设计额外的提示,从再次调用OpenAI API中产生额外的成本以及增加延迟。
2️⃣ 如何控制语言的复杂性?
在我的经验中,已开发的聊天机器人似乎在关于聊天中使用的语言复杂性方面有困难:有时候“中级”语言的使用水平会出现,即使熟练程度被设置为“初学者”。一个原因可能是当前的提示设计不足以指定不同复杂性水平之间的细微差别。
有几种方法来解决这个问题:首先,我们可以进行上下文学习。也就是说,我们为聊天机器人提供示例,并向它们展示我们希望不同复杂性水平使用何种语言。另一种方法类似于我们上面讨论的方法:我们可以使用另一个LLM来调整对话的复杂度。实质上,这个额外的LLM可以将生成的脚本作为起点,并重写一个新的脚本以匹配用户所需的熟练程度。
3️⃣ 更好的文本转语音库?
当前项目仅使用简单的gTTS库来合成声音,还有改进的空间。更先进的库提供多语言支持、多扬声器支持和更自然的语音。其中一些库包括:pyttsx3、Amazon Polly、IBM Watson TTS、Microsoft Azure Cognitive Services TTS、Coqui.ai-TTS,以及Meta的最新发布Voicebox。
4️⃣ 使用不同场景进行更多测试?
由于时间限制,我只测试了一些场景,以确定聊天机器人是否能够生成有意义的对话。这些测试确定了初始提示设计中的问题,提供了完善的机会。额外的情景测试可能会揭示被忽视的领域,并建议增强提示的方法。我已经编制了一个典型的“交谈”情景和“辩论”话题的全面列表。随时尝试并评估当前提示设计的性能。
5️⃣ 包括其他形式的生成AI?
这个项目主要探索了文本到文本(聊天机器人)和文本到语音生成功能。我们可以通过利用其他形式的生成AI来进一步增强用户体验,例如文本到图像或文本到视频。
- 文本到图像: 对于每个用户输入的情景,我们可以使用文本到图像模型创建相应的图形。在生成的对话旁边显示这些图形可以提供视觉上下文,并增强语言学习的参与度。像StableDiffusion、Midjourney和DALL-E这样的模型可以用于此目的。
- 文本到视频: 为了使应用程序更多媒体化,我们可以根据输入情景生成视频。像RunwayML这样的工具可以帮助实现这一点。此外,我们甚至可以尝试创建数字人物来呈现对话,如果正确执行,这将极大地增强用户体验。Synthesia可能是这个目的的合适工具。
6️⃣ 更多语言学习设置?
目前,我们的应用主要关注“对话”和“辩论”学习模式。但是,增长潜力是巨大的。例如,我们可以引入其他学习模式,如“讲故事”和“文化学习”。此外,我们可以扩展聊天机器人的互动,以满足更多的专业和技术场景。这些可能包括像会议、谈判或销售和营销、法律、工程等行业。这样的功能对于旨在提高专业语言能力的语言学习者可能会有所帮助。
6. 结论
哇,这是一次很棒的旅程!非常感谢您一直陪伴着我 🤗 从设计提示到创建聊天机器人,我们确实涵盖了很多内容。使用LangChain和Streamlit,我们构建了一个可以用于学习语言的功能性双聊天机器人系统,不错!
我希望我们的冒险激发了您的好奇心并启发了您的思考。让我们一起继续探索、创新和学习。快乐编码!