借助开源大语言模型,无需联网在本机实现和游戏 NPC 自由对话 [译]

作者:

Theia Vogel

引言

关于 GPT4、Claude 等模型,有很多相关讨论,它们非常出色,我也经常使用,但在某些情况下可能不是最佳选择。比如,在制作游戏时,如果你希望游戏中的 NPC 能够与玩家进行动态对话,通过服务器来回传递信息可能会有显著的延迟,这不仅降低游戏体验,还可能因为依赖于 OpenAI 或其他公司的服务器而带来风险(比如服务器故障导致游戏无法进行)。此外,这些模型的使用成本可能会随着玩家数量的增加而变得高昂,对于成本较低或玩家游玩次数过多的游戏来说,这并不经济。为了节约成本,你可能会尽量减少游戏中基于大语言模型的内容,因为你需要为每个 Token 支付费用,很快费用就上去了。

我使用的工具

我在制作中使用了 llama.cpp 和 Mistral7b 来生成对话,并用 StyleTTS2 来产生配音,同时利用 Unreal Engine 5 进行渲染。我原本尝试将 llama.cpp 作为 DLL 文件集成到 Unreal 中,但最终遇到了难题。因此,我采取了一个临时的解决方案,使用了 Node 脚本。我利用了 mrfakename's 提供的 StyleTTS2 演示 Docker 镜像 来生成语音,通过 Gradio API 接口包进行交互。理想情况下我不想使用 Docker 镜像,但由于无法在我电脑上直接运行 StyleTTS2 模型,只能采取这种方法。

我在家用的 PC 上进行这些操作,配置如下:

  • HDD: Samsung 980 PRO M.2 1TB
  • CPU: Intel Core i5 12600KF
  • Memory: 64GB DDR5
  • GPU: GeForce RTX 4070 12GB

如何工作

这个 Node(节点)脚本是通过 Unreal 引擎中的 FInteractiveProcess(交互式处理)类来启动的。它把先前的对话历史作为命令行参数传递给脚本,然后脚本逐句输出 NPC 的对话。为了提高效率,我们选择逐行处理——而不是等待整个对话生成完成。这样,我们就可以在 NPC 正在说当前句子时,就生成并播放下一句。输出的内容是一个 JSON(JavaScript 对象表示法)对象,其中包含字幕文本和对应该句的音频文件位置,之后 Unreal 会将其解析为结构体并进行播放。

性能

性能方面出乎意料地好。在生成新对话行时会稍微有点卡顿,但影响不大。StyleTTS2(文本转语音系统)需要占用约 14GB 的 RAM,而基于 llama.cpp 运行的服务器占用 3GB,因此运行这一系统需要较大的 RAM 容量。不过我相信 StyleTTS 还有进一步优化的空间。从视频中可以看出,对帧率影响不大,游戏画面依然能够保持流畅的 60 帧每秒。

生成一句新对话的时间大约为 2-3 秒,我认为这已经相当不错了。可能可以通过播放某种动画来弥补生成对话时的等待时间,使其显得不那么突兀。如果搭配使用 Whisper 技术,可以在生成回应的同时,延迟显示玩家语音转录的文字,这是人们在使用类似 Siri 这样的语音助手时已经习惯的体验。

缺点

尽管 Mistral 模型的处理速度更快,但主要缺陷在于其一致性不如 GPT3.5。它容易偏离主题,并且不太能坚持事实。例如,在我的一次测试中,它指引玩家前往几英里外的一个村庄,但实际上玩家已经在那个村庄里了。还有,在视频中可以看到,它提到了玩家名为 John,但这个名字并未在对话中提及;它还提到了 Angers 这个地名,但这个名字直到几百年后才会被使用。

此外,Mistral 在判断游戏世界中哪些事情可能发生哪些不可能方面也不是很准确。比如,在演示中出现的关于训练村民的任务,在游戏中实际上是无法实现的,因为游戏中根本没有相关机制。我认为可以通过一种叫做“检索式增强生成”(Retrieval-Augmented Generation,RAG)的方法来解决这个问题(详见结论部分)。

StyleTTS2 的语音合成效果也不够自然,还带有些许机械感。对于它不熟悉的词汇,它的发音不太准确,或者会根据上下文错误地发音(例如,Angers 这个城市的发音与动词“to anger”发音不同)。

未来改进

这只是一个初步的概念验证,如果真要在游戏中应用,还有许多方面可以进行改进。

  • 我首先会尝试让 llama.cpp 作为动态链接库(DLL)在 Unreal 中运行。这样可以避免使用 Node 脚本,从而使分发过程更为简便。
  • Mistral 有时候会话题跑偏,因此最好对其进行微调(fine-tune),使其更符合游戏环境。例如,通过提示语似乎无法完全避免它产生时代错误,当你的 12 世纪 NPC 能够解释如何使用 AWS 的时候,玩家会感觉很突兀。
  • 同样可以微调它,在文本输出中添加情感标记。通过使用 StyleTTS2,你可以传递语音克隆(voice cloning)片段,因此让配音演员(VA)用不同情感读出示例语句,就可以根据这个特定示例来调用。目前我只有一个语音克隆示例,但在完整的游戏中,你会希望为每个 NPC 的每种情感都有一套完整的示例。
  • 我还会尝试找到一种方法,让 StyleTTS2 更紧密地与 Unreal 结合。我不确定如何实现,但如果能不依赖特定的 Python 版本和特定的 Python 包,那会非常理想。
  • StyleTTS2 还可能需要微调,让它在对话中更像一个人,而不是像在叙述一本书。它也倾向于听起来过于美式。此外,它一次处理一行文本的方式使得朗读显得有些断断续续,可以通过一次性读完整个内容来解决,但这样会在朗读大量信息时耗时较长。
  • 集成 Whisper 会是一个不错的选择——与 NPC 对话并得到回答会感觉更自然,尽管我认为出于几个原因(如避免让感到尴尬的人或打扰家人、室友),保留一个额外的文本字段是必要的。Whisper.cpp 也运行效率很高,在 LLM/StyleTTS 之前 运行不会对游戏性能产生额外影响。
  • 我还打算探索一种方法,让玩家在 NPC 讲话时可以打断它,并且让 NPC 对此做出反应。我认为这将是一个很酷的功能,能让 NPC 显得更加生动。

在演示方面,有许多其他的相关方面也可以带来改善,尽管这些并不是我们的重点。比如,NPC 在说话时嘴巴不动,这一点明显是不理想的。我们可以采用像 audio2face 这样的技术来解决,而且我认为这对性能的影响应该不大。此外,我们还可以让大语言模型 (LLM) 控制 NPC 的肢体语言,比如通过返回一个包含动画指令的 JSON 文件来实现。

结论

从长远来看,我想创建一个数据库,每当玩家开始对话时就进行更新。这个数据库会包含玩家、NPC(包括他们的背景和目标)、世界等方面的信息,以确保整个游戏环境的一致性。你可以创建一个详尽的文档,其中包括玩家完成的所有任务(只要是这个 NPC 知道的)、玩家身上的装备、当前的天气、NPC 与其他 NPC 的互动、他们与玩家的聊天记录(包括日期、时间等,以便推断玩家所提及的是过去的事情)等等,然后在提交给大语言模型之前,先进行自然语言查询。

当然,最理想的情况是手动设计任务。目前我们还没有达到让大语言模型自主设计整个任务系列的水平——如果那样做,可能会出现类似于《上古卷轴》系列中“辐射任务”的情况,那种任务通常只是简单的“去某地做某事”。玩家对此并不感兴趣,我认为主要是因为这些任务缺乏人性化的设计。

主要的挑战在于对大语言模型进行适当的训练,让它保持主题不偏离,并拒绝执行它无法完成的任务。可以给它设定一系列允许的功能(比如给予物品、拿走物品、开启任务等),并让它拒绝执行其他操作。

所有这些改进都是为了在完整游戏中实现这一效果所必需的。但本文的要点是展示了在本地完整运行一个 NPC 是可行的,而且实现起来并不困难。我对这一成果感到非常满意,并期待看到更多人的尝试和创新。

相关源码:https://github.com/joe-gibbs/local-llms-ue5