### **背景**
从23年开年以来,大模型引爆了各行各业。去年比较出圈的是各类文生图的应用,比如 Stable Diffusion。网上可以看到各类解释其背后的原理和应用的文章。另外一条平行线,则是文生文的场景。受限于当时 LLM([大语言模型](https://aws.amazon.com/cn/what-is/large-language-model/?trk=cndc-detail))的能力,其应用场景相当受限。不过,进入到24年以来,Claude 3、LLaMa 3、 Mistral、GPT 4等大模型的能力开始不断增强,将各路榜单分数不断提高。LLM 生态同时也在快速进化,各种智能体(Agent)开始屡见不鲜。
斯坦福大学吴恩达教授也于前不久之前发表了著名的 Agent “四种设计模式”:**反思,工具使用,规划和多 Agent 协作**。在目前大模型的能力前提下,往往利用良好的“流程设计”可以做到“1+1 > 2”的效果。要做到这样效果,有别于传统方法,Agent 工作流不是让 LLM 直接生成最终输出,而是多次提示(prompt)LLM,使其逐步构建更高质量的输出。
本文利用了上述思想,将展示**如何使用 “Agent” 技术,从头开始搭建一个“狼人杀”游戏服务器端。**
![image.png](https://dev-media.amazoncloud.cn/b40c52981cd942daba3191aeb8a23a77_image.png "image.png")
### **狼人杀**
既然是《狼人杀》游戏,我们需要知道 LLM 对它的了解程度如何?这里对它提了几个问题,发现它对于 LLM 是有基本的了解的。这可能是这个游戏已经存在了很多年,在互联网上有大量相关的语料,因此在训练的时候 LLM 对它就有天然的认知。
![image.png](https://dev-media.amazoncloud.cn/ea33b21621074f9686d54321d0751bbe_image.png "image.png")
不过这也带来了新的问题:
* **这么多版本,如何让 LLM 在游戏过程中保持和人类一致的认知?**
* **游戏中每个角色都是有自己的设定和功能的,如何写提示词,才能让LLM代入角色?**
* **游戏中同阵营角色如何进行配合?**
* **作为 LLM 驱动的游戏,它和传统游戏开发有什么不一样?**
* **市面上那么多的 LLM,哪一款能真正地将游戏玩下去?**
* **...**
### **生成式 AI 技术**
下面会介绍一些目前比较火热的生成式 AI 技术,同样也是《狼人杀》游戏用到的技术。由于这块涉及内容很多,受限于篇幅无法全部展开。
#### **LLM Tuning**
对于模型微调手段,通常有如下四种手段:
* 提示词工程(PE):通过提示词模版的方式引导 LLM 的输出。
* 检索式增强(RAG):通过向量数据库来存储私有知识库,减少 LLM 的幻觉。
* 微调(Fine-Tuning):通过向 LLM 训练中增加私有词条,让 LLM 输出带有一定偏向性。
* 预训练(Pre-Traning):从头训练一个新模型。
#### **提示词工程**
常用的提示词工程技术有(这里主要介绍在游戏中使用到的方法):
* **zero-shot/few-shot**:在没有或者少量样本情况下引导 LLM 进行输出。
* **CoT/XoT**:利用大模型的推理能力,一步一步输出中间过程来提高提示词效果。
* **ReAct**:让 LLM 逐步思考观察,并且使用工具来获取额外信息,循环前面过程来引导出可信结果。
* **Reflextion**:它由三个不同的模型组成:Actor、Evaluator 和 Self-Reflection。Actor 模型使用大型语言模型(LLM)来生成文本和动作,并在环境中接收观察结果。Evaluator 模型负责评估 Actor 产生的轨迹的质量,并计算一个奖励分数以反映其性能。Self-Reflection 模型则对反馈内容进行反思,为后续流程提供有价值的反馈信息。这三个模型共同协作,在任务中不断迭代优化,从而提高决策能力。
接下来可以看,如何在《狼人杀》游戏中使用这些提示词技巧。
#### **游戏规则设定**
通过以下的规则设定传入 LLM 后,我们可以限定住游戏的版本,并且将一些常用的策略告知大模型,也能有助于 LLM 进行游戏。
```
## 游戏分坏人和好人两大阵营,
- 坏人阵营只有狼人,好人阵营有女巫,预言家和村民
- 阵营配置:{formation}
- 坏人阵营:消灭所有好人,或者保证坏人数目大于好人数目
- 好人阵营:女巫和预言家要利用自己特殊能力保护村民消灭所有坏人
## 游戏分白天和夜晚两个阶段交替进行:
- 夜晚:所有玩家闭眼,不能发言,行动不会暴露身份
-- 狼人的行动必须统一投票淘汰一名玩家
-- 预言家必须查验一名玩家身份
-- 女巫必须使用一种药水
-- 普通村民晚上无法行动和发言
- 白天:所有玩家睁眼, 分为讨论和投票两环节
-- 讨论环节,每个玩家必须参与讨论发言
-- 投票环节,每个玩家必须投票或者放弃
## 策略:
- 村民:多观察大家发言,说出站队理由,根据场上局势和信息来进行判断
- 女巫:只有一瓶毒药和一瓶解药,可以暗示但需要隐藏自己身份
-- 毒药可以立马淘汰一名玩家
-- 解药可以让淘汰的玩家复活
- 预言家:发言要流利,多说细节。第二天不要跳,第三天可以跳
- 狼人:
-- 悍跳预言家:发言强势,制造混乱,模仿预言家发言
-- 普通狼人:发言精简,根据情况站队,必要时踩自己队友
-- 自刀狼人1:狼人夜晚自刀,骗女巫解药,若女巫不救,则狼人团队减1,若女巫救人,则可坐实好人身份
-- 自刀狼人2:和真预言家一起跳“预言家”时候,自刀让好人相信自己身份
```
#### **Agent 设定**
通用部分, 这里的 game_rule 即为上面的游戏规则部分,commands 会在工具部分介绍:
```
<instructions>
你是资深的社交游戏玩家助手, 熟悉《狼人杀》游戏规则:
<game_rules>
{game_rule}
</game_rules>
熟悉游戏所有命令以及例子:
<commands>
{commands}
</commands>
```
由于采用 Reflextion 架构设计,我们需要对应地设置三个模块(LLM)。
* Self-Reflection(游戏内叫 DoMemory,记忆总结模块)。
```
接下来, 你扮演游戏中“上帝”角色,需要将 <input> 的文字输入进行有效提炼并且输出且满足下面的要求:
- 保持客观冷静, 用自然语言且不能超过{num}个字还原内容
- 不输出无关内容,内容言简意赅,突出重点
- 不需要输出任何中间思考过程
- 不要给任何推理和主观意见
```
* Actor(游戏内叫 DoAction,利用 ReAct 方式实现的推理规划模块)。
```
现在需要你根据,结合观察和推理提取有用的信息,一步一步使用如下格式思考:
观察: 不要编造任何内容.陈述游戏事实(当前时间,上轮决策评分(如果有),本玩家身份,本玩家队友(如果有),物品状态(如果有)等等)
思考: 结合玩家性格特点,进行逐步推测游戏信息(本方游戏赢面,场上玩家身份,是否有矛盾信息等等),注意鉴别历史信息真伪
行动: 必须从 <commands> 中的描述选择合适方法, 只需要输出方法名字即可
参数:
agent=执行玩家,
target=目标玩家(必须是明确的名字),
content=发言内容,多使用专用词汇来发言, 运用辩解,对抗,欺骗,伪装,坦白等技巧包装内容(可选,注意言简意赅,少讲废话, 突出重点.)
决策: 参考 <commands> 中例子部分输出 JSON 格式
注意:
- 决策评分标准:1-3分为差,4-5分一般,6分及格,7分优秀,8分满分.
- 决策评分信息不一定每次都有,需要保证一个6分以上的决策
```
* Evaluator(游戏内叫 DoReflect,设定两个维度对 DoAction 进行打分)。
```
<examples>
{"score": 1}
{"score": 5}
</examples>
接下来, 你需要将玩家<input>的内容进行评分. 请一步一步使用如下格式思考:
观察: 陈述游戏情况和玩家决策
思考:
- 决策对于自己的影响 (1:不利,2:平均,3:有利,4:最佳.如果决策不足够清晰明确,直接1分)
- 决策对于本方的影响 (1:不利,2:平均,3:有利,4:最佳.如果决策不足够清晰明确,直接1分)
- 将以上多个分数相加
评分: 输出参考 <examples> 中例子输出 JSON 格式
```
这样,每次 Agent 需要做决策时候,都会进行至少3次 LLM 的调用来保证输出结果的可靠性。
#### **工具**
在《狼人杀》游戏中,我们需要将每个 Agent 按照其身份对动作进行抽象和归类。这么做的好处有:
* 统一成外部资源,方便扩展。甚至可以利用 RAG 技术将这部分作为专用知识库来存储。
* 提高模版的复用率, 比如狼人的动作就包括 wolf+player,而村名只有 player 可以用。
* 提供 zero-shot/few-shot 来增强 LLM 的认知和输出结果的可控性。
```
<wolf>
- 方法: WolfVote
描述: agent(狼人)夜晚对 target (目标)投票, 并且提醒队友跟随
参数: agent=执行玩家,target=玩家名字
例子: {"action": "WolfVote", "agent": "P1", "target": "Mike"}
</wolf>
<prophet>
- 方法: ProphetCheck
描述: agent(预言家)夜晚查验对 target (目标)身份, 同一个夜晚只能用一次
参数: agent=执行玩家,target=玩家名字
例子: {"action": "ProphetCheck", "agent": "Ben", "target": "John"}
</prophet>
<witch>
- 方法: WitchPoision
描述: agent(女巫)夜晚对 target (目标)使用毒药
参数: agent=执行玩家,target=玩家名字
例子: {"action": "WitchPoision", "agent": "P6", "target": "P2"}
- 方法: WitchAntidote
描述: agent(女巫)夜晚对 target (目标)使用解药
参数: agent=执行玩家,target=死亡玩家
例子: {"action": "WitchAntidote", "agent": "Lisa", "target": "Chirs"}
</witch>
<player>
- 方法: PlayerVote
描述: agent(玩家)白天对 target (目标)进行投票, 票数高的玩家会被淘汰
参数: agent=执行玩家,target=玩家名字
例子: {"action": "PlayerVote", "agent": "P2", "target": "P4"}
- 方法: PlayerDoubt
描述: agent(玩家)白天对 target (目标)进行怀疑,不会计入投票
参数: agent=执行玩家,target=存活玩家,content=思考/理由(可选项)
例子: {"action": "PlayerDoubt", "agent": "P2", "target": "P4", content="我对P4身份存疑,(给出原因)"}
- 方法: Debate
描述: agent(玩家)白天讨论,不会计入投票
参数: agent=执行玩家, content=思考/理由
例子: {"action": "Debate", "agent": "P2", "content": "我跳预言家/女巫,(给出原因)"}
- 方法: DeathWords
描述: agent(玩家)白天死亡遗言
参数: agent=执行玩家, content=思考/理由
例子: {"action": "DeathWords", "agent": "P2", "content": "我今天被刀了,(给出原因)"}
- 方法: Pass
描述: agent(玩家)行动弃权
参数: agent=执行玩家
例子: {"action": "Pass", "agent": "P2"}
</player>
```
#### **输出提取**
当前面的 PE 模版设置好以后,我们可以看到 Agent 的回答基本就符合要求了:
```
观察: 目前是第1天夜晚,我是狼人 Fiona,队友是 Daphne。狼人 Daphne 已经投票淘汰了 Adam。还剩下6名存活玩家。
思考: 作为狼人,我的目标是消灭所有好人玩家。第一天夜晚刚开始,场上信息很少,难以判断其他玩家的身份。不过 Daphne 已经开始行动了,我应该支持她的决策,并利用狼人的优势来混淆视听。
行动: WolfVote
参数:
agent=Fiona
target=Adam
content=我同意狼人 Daphne 的决定,一起刀 Adam 吧。
决策: {"action": "WolfVote", "agent": "Fiona", "target": "Adam"}
这里面还有比较关键的一步,则是提取决策里面的内容。不同的框架会用不同方式来提取,比如 langchain 会用 output parser 来做。这里我们会采用正则的方式提取即可:
def ParseJson(text):
try:
# 使用正则表达式查找 {} 之间的内容
json_pattern = re.compile( r'{[\\s\\S]*?}')
json_strings = re.findall(json_pattern, text)
return json_strings
except Error:
logger.exception("Cannot parse json")
return None
```
#### **上帝**
在《狼人杀》游戏中,“上帝”是一个比较特殊的存在。它的行动不需要太多的发散,甚至“可控”才是它的追求。因为它是游戏的节拍器,用来保证游戏正常运行。所以这个部分我们就采用了程序实现方案。
这里我们采用的技巧是:在每次向 Agent 提问时候会动态替换玩家的身份信息和游戏局面信息。减少 LLM 上下文过长后带来的遗忘问题。比如下面的这段“上帝提问”,它其实是动态生成的:
![image.png](https://dev-media.amazoncloud.cn/15fa1cf400d545cd9ec0451187f9bcbb_image.png "image.png")
#### **Agent**
**“Agent” 技术可以认为是一个或者多项微调技术(PE+RAG+Fine-Tuning 等)的综合实现**。其起源很早,并且有各种版本和模式。但真正让 LLM “出圈”来自 standford town。在小镇项目中,玩家可以以上帝的角色观察里面多个村民进行生活和对话,可以类比为一个“西部世界”。这里重要的一个特质是:**自主思考和行动。**
![image.png](https://dev-media.amazoncloud.cn/d9bba4ea42b44e939b2a7ab9a8a54016_image.png "image.png")
为了实现这个特质,设计者提出了 generative Agent 架构,最重要的三个核心模块:memory stream,reflection 和 planning。
* Memory System:长程记忆模块,即用自然语言记录 Agent 经历的复杂清单。
* Reflection:随着时间推移,将记忆合成提炼出 high-level 的推断,使得 Agent 得出关于自身以及他人的总结,从而更好地指导 Agent 的行为。
* Planning:将这些总结以及当前环境转换为 high-level 的行为计划,然后进行递归为行动和反应的详细行为。
#### **消息系统**
对于游戏环境来说,通常我们会有多个 Agent 进行协作和互动,因此会引入一个“环境(Environments)”的概念。Agent 通过观察环境,思考规划,行动后,会对环境产生一个变化。这个变化可能是:某个“狼人”的伪装,某个“预言家”的查验,某个“玩家”的临终遗言等等。它是一个可以被观察的变化。进一步,我们可以把它抽象成“消息”。因为对于《狼人杀》来说,它就是一个个消息来驱动的游戏。
当然由于游戏的特殊性,“消息”是需要进行分类的。比如“狼人”之间的对话就只有他们之间“共享”;而“预言家”的行为又只能它自己独享;“平民”的动作则是公开透明的。**为此我们需要实现两种消息系统:点对点,多播或者广播。**
![image.png](https://dev-media.amazoncloud.cn/c3989cff524f468e8d259034bb005ced_image.png "image.png")
实现这种消息传递机制并不困难,最简单的方案就是通过一些具有特殊设计的数据结构(比如数组,队列等等)就能完成目的。当然,现在主流的 Multi-Agent 框架也都提供了这种机制。
* MetaGPT:提出了环境概念,并且利用 Agent 之间的订阅消息方法实现了之间的工作流(消息传递)。
* AutoGen:预设了很多工作流(消息传递)的模版,比如 2-Agent、seq chat、group chat、nested chat。
* LangGraph:基于 Langchain 的多 Agent 工作流(消息传递)。
除此之外,它们还有的特性:
#### **人在回路**
有的时候,我们会考虑将玩家的输入也考虑进去,比如让玩家和游戏 Agent 进行对局之类。这个时候我们需要将某些 Agent 替换成人类输入。框架的好处是,它会封装好对应的方法。比如在 MetaGPT 中,我们只需要将 is_human=True 即可。本文实现的狼人杀并没有采用“人在回路”的特性。
```
team.hire(
[
SimpleCoder(),
SimpleTester(),
# SimpleReviewer(), # the original line
SimpleReviewer(is_human=True), # change to this line
]
)
```
#### **“预算”控制**
在 MetaGPT 中设置了 investment 参数,类似的在 AutoGen 有 max_turns,它能保证 Agent 之间的对话在规定的“预算”内完成,从而避免超额使用 token 数。同样,《狼人杀》游戏也延续了这样思路,当一个问题超过三次无法给出“有效”的答案时候,则会自动 PASS 该 Agent 的选择。
本文看来,选择成熟的框架还是自己实现没有绝对的对错。比如 LangGraph 就被人诟病设计得过于臃肿(40w+行的代码,语法糖过多,文档维护差)。AutoGen 对于非 Azure 系的模型支持不够好。MetaGPT 虽然支持更多模型调用,但工作流设计过于简单,并且容错率较低等等。
### **游戏架构**
介绍到现在,我们《狼人杀》的游戏架构图也呼之欲出了。
![image.png](https://dev-media.amazoncloud.cn/58cd8558dc51425bb4873687c22a85e5_image.png "image.png")
**Game** 部分就对应了游戏的“上帝”,它是纯代码实现的。
* 其核心为一个无限循环,每次循环会根据游戏的时间(白天或夜晚)以及关键的事件向 Agent 提问。
* 为了统一控制,所有的问题都是预设好的,只需要动态替换关键信息即可。
* 另外一个重要的工作,就是维护场上玩家的信息:比如存活情况,道具情况等等。
**Player** 部分就是游戏内的“玩家 Agent”,利用 **Reflextion** 的多 LLM 模式来完成游戏推理。
* **DoMemory/DoAnswer/DoReflect**:这些模块分别和 Reflexion 子模块对应,前面已解释。
* **DoValidation** 模块,这个模块的作用是为了验证大模型输出的数据有效性,来解决一些特殊场景下的问题。比如预言家同一个晚上重复检查玩家。
* **DoAction** 模块,对应着真正的玩家动作选择以及对环境产生影响,它会形成一个“可被观察的记忆”。
**Memory** 部分就是通过消息方式实现的共享记忆(狼人)和独享记忆(预言家)。
* 每次记忆提取并不是全部塞到 context 中,而是设定了一个记忆窗口大小,比如过去的20轮。
* 拿到过去的记忆,也不是直接使用,需要通过一个“压缩”过程。
### **模型选择**
模型选择是非常有意思的话题。模型的选择我们一般会考虑模型生成内容的质量、响应速度和性价比。**因此在游戏,综合考虑下我们选择了 Sonnet 模型作为游戏的核心引擎。**
在实际游戏开发中,为了节约成本,降低对模型访问频次的依赖,同时避免单一模型的 Bias。因此在模型行为评估阶段,我们选择了第二种[大语言模型](https://aws.amazon.com/cn/what-is/large-language-model/?trk=cndc-detail)来对推理模型的输出进行打分。而在内容总结方面,我们选择了第三种大语言模型。对于这种目标明确且不复杂的任务,即便是小参数量的大语言模型,也可以非常好地完成任务。
在整个游戏过程中,**选择交替使用3个模型,1个模型保障了每个玩家角色高质量的“发言”,另外2个模型起到辅助作用。**
事实上,我们还可以根据需要,随时切换到 [Amazon Bedrock](https://aws.amazon.com/cn/bedrock/?trk=cndc-detail) 上其他模型。灵活的模型切换这一点,[Amazon Bedrock](https://aws.amazon.com/cn/bedrock/?trk=cndc-detail) 为我们提供了丰富的模型选择,我们完全可以根据业务的需要,去做不同模型之间的切换。
另外 [Amazon Bedrock](https://aws.amazon.com/cn/bedrock/?trk=cndc-detail) 还支持自定义模型的上传,我们完全可以将开源模型上传到 [Amazon Bedrock](https://aws.amazon.com/cn/bedrock/?trk=cndc-detail) 后,然后以按需或者独占算力资源的方式服务我们的应用。这一点上我们完全可以将大模型托管这件事从我们的应用中进行解耦,从而我们可以更加专注于业务逻辑的开发。
### **其他考虑**
#### **游戏复盘**
当游戏参与的 Agent 变多后,一局游戏下来,其产生的内容和 tokens 数是很庞大的。这个时候还需要引入一个 LLM 来帮助我们进行游戏“复盘”,将关键的信息提取出来,方便后续分析。
![image.png](https://dev-media.amazoncloud.cn/c12c51b23e91413e9120347e31db4472_image.png "image.png")
#### **模型问题**
虽然 LLM 拒答问题的情况有所减少,但这并不意味着问题完全解决了。幻觉问题依然存在。比如,LLM 偶尔会虚构不存在的人物或事实,例如某某玩家昨晚表现异常等。另一个问题是,模型的回答往往过于“礼貌”,或者说带有明显的AI味道,缺乏拟人性。诸如此类的问题,光靠提示工程(Prompt Engineering)是无法从根本上解决的。在这种情况下,我们需要借助 Fine-tuning 的方法来优化模型输出,使其更加自然、人性化。
其实开源社区有不少用 Llama 或者 Mistral 作为底模来 Fine-tuning 过的 LLM,比如 Pygmalion-2-13b, 它就很擅长于对话和角色扮演。因此后续会考虑在最终输出层加一个这类型的 LLM。受限于篇幅,这里不再展开。
### **服务器架构**
既然是游戏服务器,最后我们需要考虑的就是怎么对外服务,下面是服务器部分的架构图。
**应用层**包括:
* **[Amazon EC2 ](https://aws.amazon.com/cn/ec2/?trk=cndc-detail)flask** 负责启动游戏进程,监控端口。
* **[Amazon DynamoDB](https://aws.amazon.com/cn/dynamodb/?trk=cndc-detail)** 负责记录游戏的进度和日志。
* **[Amazon S3](https://aws.amazon.com/cn/s3/?trk=cndc-detail) + Amazon Cloudfront** 负责分发游戏的配置。
**模型层**包括:
* **[Amazon Bedrock](https://aws.amazon.com/cn/bedrock/?trk=cndc-detail)**:亚马逊云科技模型商店,为游戏进程统一的调用接口。
### **总结**
总的来说,Agent 是当下比较火热的技术。在本文中,**我们主要聚焦于如何使用大模型来驱动游戏 Agent,并结合提示词工程的一些技巧以及游戏的特性进行架构设计**。可以看到,**LLM 的能力不断增长,超长上下文、响应速度、拒答率等因素不再成为约束,其在”游戏智能 NPC” 的探索和应用会越来越多。借助其优秀的推理能力,游戏的玩法和设计也会变得更加丰富多彩。**
然而,目前我们还看到一些其他的限制:角色的表现力、游戏的沉浸感这块目前属于大模型能力的短板。哪怕我们利用”微调”(fine-tuning)的方法,提升的效果也比较有限。**即使解决了文字的部分,一款游戏的表现力还取决于画面、声音等多维度,这是一个复杂且具有挑战性的工程。**
站在现在的时间点回过头来看,LLM 技术就像90年代初的游戏联网、3D 渲染一样,没有人知道未来这些技术将如何发挥最佳的表现力。未曾想到,现代游戏游戏把这些技术已经运用得淋漓尽致。同样,我们也充满了好奇和信心,LLM 必将在这新一轮“大航海时代”中披荆斩棘。
最后,欢迎大家来尝试和探讨。
> **游戏客户端:**
>
> https\://github.com/yourlin/Werewolf-Unity?trk=cndc-detail
>
> **游戏服务器:**
>
> https\://github.com/xiwan/LLM-Game-Agents?trk=cndc-detail
>
> **游戏展示视频:**
>
> https\://github.com/pgvector/pgvector?trk=cndc-detail
![image.png](https://dev-media.amazoncloud.cn/f6581eea2d944c72988869fdd18823ba_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/97f5feec8cbb433aaf8fd8f8e79299a3_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/4a4278b88e8c4c57aa569167a43a0b41_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/514b7fd0283648acae8c412eb1d2a8f6_image.png "image.png")