通过3.4节的描述,读者了解了什么是提示词和提示词工程。为了学以致用,下面回到百川角色大模型创建虚拟角色的例子,看看能否开发出属于自己的应用,通过这个应用创建一个武学奇才——大头天尊,并与这个NPC进行对话。
接下来的实战演练会模仿百川角色大模型,为游戏定义一些角色。在开始之前,先整理一下思路,如图3-5所示,用户通过填写或者选择的方式生成提示词,这个提示词是让大模型扮演虚拟角色用的。大模型得到提示词之后就会根据其内容扮演对应的虚拟角色,此时用户再向大模型进行提问,就好像是在与创建的虚拟人物对话一样了。
图3-5 定义个性化角色的思路
有了基本思路之后,就需要明确目标,我们的核心目标是模仿“百川大模型虚拟角色”平台开发一款应用,可以用该应用创造能互动的虚拟游戏角色,这个角色将以NPC(非玩家控制)角色的身份存在于游戏的某一特定关卡内。因此,我们需要构建一个具备界面交互能力的大模型,可以通过填写或选择的方式创建虚拟角色,并与角色对话。具体而言,需要详细定义以下需求以确保准确实现设计目标。
初始化角色的基础属性,包括但不限于昵称、性别、年龄及其所属的门派。角色将被赋予独特的性格特征和行为习惯,并且这些特征应与其基本信息相符合。
确定角色存在的关卡并给予详细描述,包括环境布置、关卡故事背景、角色的角色定位及其在关卡内的作用。具体而言,设计了五个关卡,分别是第一关(大闹青城山)、第二关(幽暗水底洞)、第三关(荒漠金字塔)、第四关(云端之城)、第五关(末日火山)。每个关卡都有对应的描述,如第一关的描述如下:
从关卡描述上看,交代了游戏地点、周围的环境、游戏任务等信息。其他关卡与之相似,会在后面代码实时的部分展示。
为避免大模型扮演的NPC角色说出不当或破坏游戏体验的话语,需要为NPC角色的对话系统设定严格的规则约束。确保角色在与玩家交互时保持符合设定的性格和故事背景。因此,设计了以下规则。
从上面的规则可以看出,包含信息安全、伦理道德、公序良俗、法律法规等多方面的考虑,在增强游戏体验的同时,也提升了虚拟角色的安全性。
在完成角色创建之后,需要有一个测试接口证实虚拟角色的互动行为达到了预期。用户可以通过修改角色信息、关卡信息以及规则信息测试虚拟角色。
在实现上述需求时,必须将创意与技术紧密结合,并在开发过程中考虑上述目标。下一步将深入研究和讨论每个需求的技术实施方案。
为了让应用更加具象化,需要将定义虚拟角色的思路转化为交互界面。图3-6所示为虚拟角色项目的界面草图。其中,最左边是定义角色基础信息的界面,这里会定义昵称、性别、年龄等基本信息;右边的最上方是关卡信息,这里可以选择五个关卡中的任意一个;接着是规则信息,这里将准备好的规则放置在这里,同时也支持添加或者修改规则内容。下面是测试功能的部分,可以与创建好的角色进行对话,看看是否达到了要求。
图3-6 将定义个性化角色的思路转化为界面草图
上面只是一个草图,具体的界面设计可以放到代码实时的部分实现。
基于3.5.2小节生成的草图,项目要实现的功能包括定义一个虚拟角色,根据虚拟角色生成提示词,通过提示词让大模型扮演虚拟角色,然后与之对话。同时,还需要对这个角色进行关卡和规则的定义。下面通过Python、LangChain、Streamlit等工具架构尝试写一个应用,以创建虚拟角色的定义器。
首先创建一个虚拟角色的实体类、NPC.py文件和NPC class。然后创建NPC的构造函数,用来初始化角色的基本信息,这与交互界面中的“基础信息”页面对应。下面代码是该实体类的构造函数:
代码描述如下。
(1)代码描述构造函数(__init__方法):该类用于创建并初始化一个游戏中的NPC角色。每个角色实例含有自己的专属属性,并能够生成一段描述其特征的自我介绍文字。使用LangChain中的PromptTemplate类,处理模板文本和生成格式化的字符串。
(2)初始化提示词模板:introduction_template是一个包含占位符的多行字符串,定义了角色自我介绍的结构和内容。占位符如{nickname}将在后续被替换为实际的角色属性值。这个模板文本作为角色描述的框架,在生成角色自我介绍时发挥关键作用。
(3)创建PromptTemplate对象:prompt中存储模板文本和一系列输入变量的名称。在input_variables中定义了introduction_template中对应的占位符所需的变量名,同时还会将introduction_template纳入其中。这样变量和模板都包含到PromptTemplate中了。
(4)生成自我介绍:self.introduction是通过调用self.prompt.format方法生成的,这个方法让实际的变量(如昵称、年龄、性别、年龄)取代了模板中的占位符,用实例中定义的属性值填充它们。另外,age将从数值型转换为字符串,而personality_traits列表被转换成用顿号分隔的字符串。得到的结果是一个描述角色属性和特性的完整段落。
说明
上面的代码引入了LangChain中的PromptTemplate类,这里需要特别说明一下,PromptTemplate类使用提示词模板将提示词中的变量抽取出来作为占位符,然后用程序输入的变量替换占位符,从而生成动态的提示词。
下面通过图3-7来了解PromptTemplate类所完成的工作,按照数字顺序从左往右看:
(1)为虚拟角色生成提示词模板introduction_template。在该模板中将昵称、年龄、性别等信息通过占位符{nickname}、{age}、{gender}实现。因为这些信息可能随着用户的输入而发生变化,所以在定义时无法完全确定,只有等用户输入之后,这些信息才能完全确定。由此看来,提示词模板实际上就是一个带有占位符的字符串。这个字符串信息最终会与PromptTemplate类中的template变量进行关联,为后续生成提示词做准备。
(2)占位符也就是变量的信息,这部分信息实际在提示词模板,即introduction_template中已经定义了,在PromptTemplate类中再次定义是为了将其与角色信息中的实际变量进行映射。因为这些角色信息,如昵称,需要从界面中获得,然后将其赋值到模板中,所以input_variables就充当了占位符与变量之间映射表的角色。
(3)有了提示词模板以及占位符对应的变量信息之后,还需要使用PromptTemplate类中的format方法将两者进行结合,生成最终的提示词。也就是说,是将具体的昵称、年龄、性别等信息提入到提示词模板中,替换掉模板中原来的占位符,让它从模板变成一句完整的提示词,一句能让大模型“听懂的话”。
(4)将生成的最终提示词交给大模型,大模型在理解之后就可以给用户回应了。
图3-7 PromptTemplate类的工作原理
通过NPC类的构造函数可以看出,角色的基本信息都保存在self.introduction中了。接着需要对应“关卡信息”和“规则信息”创建两个函数,主要作用是返回对应的提示词。
以下代码提供了一个灵活的方式来查询和返回游戏关卡的描述信息,使得根据玩家当前的关卡名称能够获得相应的挑战和环境描述。通过组织关卡信息为一个字典并利用get方法返回描述。代码如下:
以上代码的核心是定义了一个名为levels的字典,其中包含各个关卡名称及其对应的描述信息。这个字典涵盖了从“第一关:大闹青城山”到“第五关:末日火山”五个关卡,每个关卡都有详细的描述,介绍了玩家将要遭遇的挑战和风险。
利用rules = """…"""将游戏规则的文本内容赋值给变量rules。这段文本详细列出了游戏的九项基本规则,也就是来自3.5.1小节的“制订规则”的信息,涵盖了信息安全、伦理道德、法律法规、玩家体验等方面。为了不影响阅读体验,这里不展示全部的代码内容。创建这个方法的目的是在初始化前端时,可以将默认的规则填入文本框,同时也支持后期人为修改。
为了生成最终的提示词,下面尝试创建__build_prompt方法。该方法通过接收游戏相关参数和玩家查询,利用预定义的模板和格式化操作,生成一段详细的对话指导文本。这种方法将对话的结构和内容要求明确地展示给虚拟角色,有助于保证虚拟角色的互动质量,同时确保游戏的互动符合既定规则。
代码解释如下:
(1)方法定义与参数。
定义def __build_prompt(self, game_level, rules, query)的方法,接收game_level(游戏关卡)、rules(规则信息)、query(玩家提问)三个参数,用于构建和格式化游戏中虚拟角色对话的提示信息。
(2)多行字符串模板。
利用prompt_template = """…"""创建一个多行字符串,作为对话提示的模板。这个模板通过花括号({})包含的占位符(如{introduction}、{game_level}等),用于后续插入具体的游戏角色介绍、游戏关卡、规则以及玩家的查询。
(3)PromptTemplate对象创建。
通过self.prompt = PromptTemplate(…)语句创建一个PromptTemplate对象,并将其赋值给实例变量self.prompt。这个对象使用了之前定义的prompt_template作为模板,并定义了接收的用户输入变量["introduction", "game_level", "rules", "query"],准备用于格式化文本。
(4)格式化占位符。
通过instruction = self.prompt.format(…)调用format方法,使用实例变量self.introduction、方法参数game_level、rules、query来填充prompt_template中的占位符,生成具体的指令文本。
(5)返回值。
方法通过return instruction返回格式化后的指令文本,这个文本是为虚拟角色准备的,包含角色介绍、游戏关卡、规则及玩家查询,用于引导虚拟角色如何与玩家互动。
同样,这个方法也是前端界面使用的,当用户与界面交互时,需要对最终的模型进行测试。此时用户会将提出的问题交给大模型回答,这个方法就是在调用大模型给用户回应。代码如下:
代码解释如下:
(1)方法定义与参数。
使用def queryLLM(self, game_level, rules, user_query)定义了一个方法,接收game_level(游戏关卡)、rules(规则信息)、user_query(用户查询)三个参数。该方法用于向语言模型提交一个经过构建和格式化的提示,以获取模型的响应。
(2)构建最终提示文本。
通过final_prompt = self.__build_prompt(game_level, rules, user_query)调用__build_prompt方法(该方法在上面已经定义过了)将游戏关卡、规则信息和用户查询作为参数传递,生成最终的提示文本final_prompt,用于后续向语言模型查询。
(3)打印最终提示文本。
使用print(final_prompt)打印最终的提示文本,这一步是为了调试或记录,让开发者或用户可以看到将要发送给大型语言模型的确切提示内容。
(4)创建大型语言模型端点对象。
通过llm = QianfanLLMEndpoint(model="Qianfan-Chinese-Llama-2-7B", temperature=0.8)创建一个名为llm的QianfanLLMEndpoint对象。这里指定了模型名称为Qianfan-Chinese-Llama-2-7B,并设置了温度参数temperature为0.8,这影响了生成文本的变异程度。如果需要使用其他平台的大模型,需要阅读第2章中对应模型应用的相关章节。后面章节案例中的模型使用基本也遵循这个原则,不做特别说明。
(5)调用大型语言模型并获取响应。
通过response = llm(final_prompt)使用之前构建的最终提示文本final_prompt调用大型语言模型对象llm,并将得到的响应赋值给变量response。
(6)返回值。
queryLLM方法通过return response返回大型语言模型的响应,这个响应是基于用户查询以及游戏规则和等级构建的提示文本生成的。
queryLLM方法通过整合游戏关卡、规则信息和用户查询信息来构建一个详细的提示文本,然后使用这个文本向特定的大型语言模型查询,以获取适合的响应。这个过程涉及提示文本的动态构建、模型选择和参数配置,体现了在游戏互动中使用大型语言模型进行智能回答的能力,同时可以确保回答的内容符合游戏的规则和上下文。
至此,NPC类的构建就完成了,下面对这个类做一个总结。
NPC类是为游戏中的虚拟角色设计的实现,它涵盖了角色的创建、自我介绍、与玩家互动等多个方面。具体来说,它实现了以下功能。
(1)角色初始化: 类的构造函数__init__可以创建具有特定属性的虚拟角色,如昵称、性别、年龄、所属派别、等级、自我描述和性格特点。这为角色赋予了丰富的背景信息,使其更具个性和深度。
(2)自我介绍模板的创建和应用: 通过内部使用PromptTemplate对象,结合角色的属性,动态生成角色的自我介绍文本。这种方法既保持了介绍内容的灵活性,也确保了文本的一致性和规范性。
(3)游戏关卡描述: 通过get_level_description方法,根据游戏关卡的名称返回具体的描述信息。这个功能为游戏提供了详细的场景描述,增加了游戏的丰富性和玩家的沉浸感。
(4)游戏规则文本封装: get_rules_text方法封装了游戏内的行为规则,这些规则用于指导玩家与NPC的交互,确保游戏环境的健康和玩家体验的质量。
(5)与语言模型的交互: queryLLM方法展示了如何将游戏关卡、规则信息和用户查询整合成一个提示文本,然后使用这个文本向大型语言模型(如QianfanLLMEndpoint)查询,以获取适应游戏场景和规则的响应。这个过程不仅利用了AI来增强游戏的互动性,还通过精心设计的提示保证了交互的质量和相关性。
(6)动态提示构建: __build_prompt是一个私有方法,用于根据游戏关卡、规则信息和用户查询动态构建与玩家互动时的提示文本。这个方法体现了类设计中的模块化和重用原则,通过参数化的方式生成定制的对话提示,旨在引导NPC如何与玩家进行交互。
NPC类是一个综合性的设计,通过结合模板生成、动态文本格式化和AI语言模型的交互,创建了一个可以根据游戏规则和玩家互动需求进行自适应响应的虚拟角色。这不仅提升了游戏的可玩性和互动性,还为游戏角色的自动化交互提供了一种有效的实现机制。
在成功设计和实现NPC类之后,下一步是创建用户界面,允许用户以交互的方式创建虚拟角色、添加关卡和规则信息。这个界面的草图在3.5.2小节中已经设想过了,当时只是设计了大概的展示位置,对于细节问题还没有深入的研究。这里需要对其进行细化,并落实到代码中。由于用户界面同时也是程序的入口,因此会开发app.py文件,它使用流行的Streamlit库来构建Web应用程序。通过app.py,用户可以通过图形界面与NPC类进行交互,实现定制化的游戏体验。
app.py文件通过引入Streamlit库和前面定义的NPC类,构建了一个Web应用程序,允许用户通过边栏输入虚拟角色的基础信息,如昵称、性别、年龄等。用户可以选择角色所属的门派和武学造诣水平,还可以定义角色的自我描述和性格特点。在应用的主体部分,用户可以选择游戏关卡,查看关卡描述,阅读并编辑游戏规则。最重要的是,app.py还提供了一个测试功能,让用户可以直接与虚拟角色对话,通过单击按钮发送用户输入,并展示虚拟角色的响应。
在技术架构上,app.py和NPC类展现了前后端分离的设计理念。其中,NPC类承担了后端的角色,负责处理业务逻辑,包括虚拟角色的创建与管理、关卡描述的提供、游戏规则的管理以及用户查询的处理。app.py则作为前端,专注于用户交互界面的构建和管理,通过Streamlit库实现了友好的图形界面。这样的分工使得NPC类的复杂逻辑得以在背后高效运行,而app.py则将重点放在提升用户体验和交互的流畅性上。
由于需要利用Streamlit库开发交互界面,因此需要引入Streamlit。同时,引入NPC类作为虚拟角色的实体。在chapter02目录下创建app.py文件,加入以下代码,Streamlit库用于实现应用的前端交互逻辑,而NPC类则提供了后端处理逻辑。
根据3.5.2小节中的界面设计草图,在交互界面的左侧加入角色的“基础信息”。继续在app.py文件中添加代码,使用Streamlit库构建一个Web应用的侧边栏,用于收集用户输入的虚拟角色基础信息。各个部分的功能如下:
代码解释如下:
(1)设置侧边栏标题。
利用st.sidebar.title("基础信息")在应用的侧边栏上设置标题为“基础信息”,这有助于用户理解侧边栏内容的主题。
(2)昵称输入框。
nickname = st.sidebar.text_input("昵称", value="大头天尊")创建了一个文本输入框,让用户输入昵称,默认值为“大头天尊”。这行代码的作用是收集用户的昵称信息。
(3)性别选择。
gender = st.sidebar.radio("性别", ("男", "女"), index=0)创建了一个单选按钮组,让用户选择性别,选项包括“男”和“女”,默认选择“男”。这行代码用于收集用户的性别信息。
(4)年龄输入。
age = st.sidebar.number_input("年龄", min_value=18, max_value=100, value=20)创建了一个数字输入框,限制用户输入的年龄在18到100岁之间,默认值为20岁。这行代码用于收集用户的年龄信息。
(5)门派下拉列表框。
Faction = st.sidebar.selectbox("门派", ("武当", "峨眉", "少林", "天山"), index=0)创建了一个下拉列表框,允许用户从“武当”“峨眉”“少林”“天山”中选择一个门派,默认选择“武当”。这行代码用于收集用户的门派归属信息。
(6)武学造诣下拉列表框。
level = st.sidebar.selectbox("武学造诣", ("初学乍练", "略有小成", "炉火纯青", "登峰造极","一代宗师"), index=4)创建了另一个下拉列表框,让用户选择其武学造诣的级别,默认为“一代宗师”。这行代码用于了解用户的武学水平。
(7)自我描述文本区域。
self_description = st.sidebar.text_area("自我描述", value="武功盖世")创建了一个文本区域,供用户输入自我描述,默认文本为“武功盖世”。这行代码用于收集用户的自我描述信息。
(8)性格特点多选框。
personality_traits = st.sidebar.multiselect("性格特点", ["乐观", "耐心", "诚实", "自律"],default=["乐观", "耐心"])创建了一个多选框,让用户可以选择多个性格特点,默认选项为“乐观”和“耐心”。这行代码用于收集用户的性格特点。
(9)NPC对象创建。
npc = NPC(nickname, gender, age, Faction, level, self_description, personality_traits)使用前面收集的所有用户信息,创建了一个NPC对象。这行代码的目的是根据用户提供的信息实例化一个虚拟角色。
上述代码通过Streamlit的侧边栏组件收集了用户的基础信息,包括昵称、性别、年龄、门派、武学造诣、自我描述和性格特点,然后利用这些信息创建了一个NPC对象,该对象会在下面的用户交互中用到。
创建好的基本信息交互界面如图3-8所示,通过Streamlit只需几行简单的代码就加入了文本输入框、选项区、数字设置控件、下拉列表框控件等。
图3-8 角色基本信息界面
这段代码的主要功能包括让用户选择关卡、查看关卡描述、了解游戏规则并与虚拟角色进行简单的对话交互。通过全局变量npc引用和操作虚拟角色,实现了用户与虚拟角色之间的动态互动。
代码解释如下:
(1)主函数定义和全局变量声明。
使用def main()定义了main函数,作为程序的主入口点。在这个函数内部,通过global npc声明了一个全局变量npc,这意味着npc在函数外部也能被识别和使用,通常用来引用程序中的虚拟角色(NPC)对象。
(2)关卡名称下拉列表框。
level_name = st.selectbox(…)创建了一个下拉列表框,用户可以从五个预定义的关卡中选择一个。这行代码的作用是收集用户想要了解或者玩的关卡名称。
(3)获取并显示关卡描述。
game_level = npc.get_level_description(level_name)调用npc对象的get_level_description方法,传入用户选择的关卡名称level_name,以获取该关卡的描述。然后,使用st.write(game_level)将获取的关卡描述显示在页面上。
(4)规则列表文本区域。
rules_input = st.text_area(…)创建了一个多行文本框,其默认值为通过npc.get_rules_text()获取的游戏规则文本。这个文本区域展示了确保游戏中的虚拟角色遵守的规则列表。
(5)测试功能与用户交互。
通过user_input = st.text_input("与虚拟角色对话", key="user_input")创建了一个文本输入框,允许用户输入想要与虚拟角色对话的内容。if st.button('发送'):检查用户是否单击了“发送”按钮。如果单击了,程序将展示用户输入的内容,并通过npc.queryLLM(game_level,rules_input,user_input)获取虚拟角色的回应,最后将虚拟角色的回应展示在页面上。
界面创建之后如图3-9所示,在该界面中用户可以选择游戏关卡,修改虚拟角色遵守的规则,输入问题与大模型扮演的角色进行对话。
图3-9 角色设置以及对话界面
代码完成之后通过控制台启动程序,并对其进行测试。在app.py文件的目录下执行以下命令:
通过streamlit命令启动app.py文件,由于该文件中存放了Web UI界面的定义,因此在执行程序之前需要启动它。执行streamlit命令之后,可以看到访问入口信息,如图3-10所示,通过http://localhost:8501或者http://192.168.0.104:8501就能够访问Web应用了。
图3-10 启动项目并返回项目访问接口
将上面两个地址中的任意一个复制到浏览器中并按Enter键,就会打开图3-12所示的页面。
由图3-11可以看出,默认信息为大头天尊、男、20岁、武当派、一代宗师等。当然,也可以修改角色的基础信息。
接着看向右边的关卡信息和规则信息,如图3-12所示,可以通过下拉列表框选择关卡,在规则信息部分已经将默认的规则放置到文本框中,用户可以根据不同的情况修改其中的内容。
图3-11 生成虚拟角色
图3-12 设置关卡信息
最后,可以通过提问的方式测试角色。输入:“你是谁?我要了解这个关卡的相关信息”,如图3-13所示。
图3-13 对话虚拟角色
由图3-13可以看出,在测试虚拟角色交互平台时,得到了一段精彩的对话输出,展现了虚拟角色“大头天尊”的丰富背景和当前所处的游戏关卡信息。大头天尊来自武当派,他是一位武学修为达到一代宗师级别的高手。在介绍中得知,当前关卡为青城山脉,目标是揭开山中隐居的古代剑仙的秘密,并找到进入下一关的神秘符印。这个关卡不仅要求玩家解开谜题、击败灵兽,还需要运用智慧和武艺穿越危险的吊桥。
此次测试不仅展示了虚拟角色和关卡信息的动态生成能力,还体现了应用的灵活性和用户友好性。通过简单的输入,用户可以与虚拟角色进行互动,获取关卡的详细介绍和所需的策略信息。这个过程不仅增加了游戏的沉浸感,也提供了一个个性化的体验,让用户可以根据自己的喜好和需求,生成和调整心仪的虚拟角色及游戏经历。笔者建议读者动手实践探索平台的各种可能性,发挥创意,创造出属于自己的虚拟角色和游戏故事。