大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。
本文来学习一下MetaGPT的一个实战案例 - 狼人杀游戏,该案例源码已经在 MetaGPT GitHub开源代码 中可以看到。
上次我们拆解了该游戏的整体实现框架(【AI Agent教程】【MetaGPT】案例拆解:使用MetaGPT实现“狼人杀“游戏(1)- 整体框架解析),本文我们从运行流程的角度来进行学习。
0. 从 start_game 开始
运行游戏的入口在 MetaGPT\examples\werewolf_game\start_game.py
中。main
函数为入口,然后调用了 start_game
函数开始游戏。
start_game 代码如下:
async def start_game(
investment: float = 3.0,
n_round: int = 5,
shuffle: bool = True,
add_human: bool = False,
use_reflection: bool = True,
use_experience: bool = False,
use_memory_selection: bool = False,
new_experience_version: str = "",
):
game = WerewolfGame()
game_setup, players = game.env.init_game_setup(
role_uniq_objs=[Villager, Werewolf, Guard, Seer, Witch],
num_werewolf=2,
num_villager=2,
shuffle=shuffle,
add_human=add_human,
use_reflection=use_reflection,
use_experience=use_experience,
use_memory_selection=use_memory_selection,
new_experience_version=new_experience_version,
prepare_human_player=prepare_human_player,
)
logger.info(f"{game_setup}")
players = [Moderator()] + players
game.hire(players)
game.invest(investment)
game.run_project(game_setup)
await game.run(n_round=n_round)
(1)首先,它生成了一个 WerewolfGame 的环境。可以看下这个环境是个我们所熟悉的Team类型:
class WerewolfGame(Team):
"""Use the "software company paradigm" to hold a werewolf game"""
(2)然后,是初始化游戏:game.env.init_game_setup
,分配角色,初始化角色状态
(3)当然不要忘了加上主持人:players = [Moderator()] + players
(4)将所有角色加入到游戏环境中:game.hire(players)
(5)设置预算:game.invest(investment)
,虽然实际游戏没有预算,但是用GPT玩游戏得有预算呀!!!
(6)往环境中放入第一条启动消息:game.run_project(game_setup)
def run_project(self, idea):
"""Run a project from user instruction."""
self.idea = idea
self.env.publish_message(
WwMessage(role="User", content=idea, cause_by=UserRequirement, restricted_to={"Moderator"})
)
这第一条消息是发送给主持人的。
(7)正式开始游戏:await game.run(n_round=n_round)
这部分除了 init_game_setup
步骤,其余是标准的 Team
组件的使用流程,想深入学习的同学可以看下我这篇关于 Team 组件的文章:【AI Agent系列】【MetaGPT多智能体学习】4. 基于MetaGPT的Team组件开发你的第一个智能体团队
1. 主持人发言
游戏开始之后,第一条消息是发给主持人的,所以,主持人先开始动作。
看下主持人的动作集合:InstructSpeak
、ParseSpeak
、AnnounceGameResult
、Speak
-
self.set_actions([InstructSpeak, ParseSpeak, AnnounceGameResult])
-
capable_actions = [Speak] + special_actions
_think函数决定应该执行哪个Action动作:
async def _think(self):
if self.winner:
self.rc.todo = AnnounceGameResult()
return
latest_msg = self.rc.memory.get()[-1]
if latest_msg.role in ["User", "Human", self.profile]:
# 1. 上一轮消息是用户指令,解析用户指令,开始游戏
# 2.1. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说
# 2.2. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令
self.rc.todo = InstructSpeak()
else:
# 上一轮消息是游戏角色的发言,解析角色的发言
self.rc.todo = ParseSpeak()
根据上述代码,理一下主持人刚开始的动作:
(1)刚开始主持人接收到的是 UserRequirement
消息,因此执行的动作是 InstructSpeak
。
(2)在 _act 中执行 InstructSpeak
msg_content, msg_to_send_to, msg_restricted_to = await InstructSpeak().run(
self.step_idx,
living_players=living_players,
werewolf_players=werewolf_players,
player_hunted=player_hunted,
player_current_dead=player_current_dead,
)
注意这里有一个 self.step_idx
参数,这是游戏的第几步。刚开始是第0步。在 InstructSpeak
中,通过这个步骤取一些固定的流程信息:
async def run(self, step_idx, living_players, werewolf_players, player_hunted, player_current_dead):
instruction_info = STEP_INSTRUCTIONS.get(
step_idx, {"content": "Unknown instruction.", "send_to": {}, "restricted_to": {}}
)
STEP_INSTRUCTIONS
为固化的游戏流程信息。
(3)取流程信息时,这时候step_idx
为0,取出的流程信息为:
0: {
"content": "It’s dark, everyone close your eyes. I will talk with you/your team secretly at night.",
"send_to": {RoleType.MODERATOR.value}, # for moderator to continue speaking
"restricted_to": empty_set,
},
注意这里取出的 “send_to
” 为 MODERATOR
,是主持人,因此,这个 InstructSpeak
执行完之后,生成的消息还是发送给主持人,接下来还是主持人进行动作。
(4)动作执行完,还有一步:self.rc.env.step(EnvAction(action_type=EnvActionType.PROGRESS_STEP))
,游戏步骤 +1
.
(5)根据 _think
的判断,上一轮的消息是主持人的动作生成的,所以还是执行 InstructSpeak
动作。注意这时候的 step_idx
为 1
了。取出来的 STEP_INSTRUCTIONS
信息为:
1: {
"content": "Guard, please open your eyes!",
"send_to": {RoleType.MODERATOR.value}, # for moderator to continue speaking
"restricted_to": empty_set,
},
最终 send_to 还是发给了自己,主持人继续发言。
(6)来到了第2步,取出来的 STEP_INSTRUCTIONS
信息为:
2: {
"content": """Guard, now tell me who you protect tonight?
You only choose one from the following living options please: {living_players}.
Or you can pass. For example: Protect ...""",
"send_to": {RoleType.GUARD.value},
"restricted_to": {RoleType.MODERATOR.value, RoleType.GUARD.value},
},
这次主持人根据流程叫到了“Guard”守卫的角色,"send_to": {RoleType.GUARD.value},
。让守卫选择保护的角色。同时,“restricted_to
” 参数设置为 RoleType.MODERATOR.value, RoleType.GUARD.value
,意味着接下来的动作产生的消息只有主持人和守卫知道。
2. 夜晚的发言
来看下守卫的技能列表:["Protect"]
和 [Speak]
_think函数决定应该执行哪个Action动作:
async def _think(self):
news = self.rc.news[0]
assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作
if not news.restricted_to:
# 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言)
self.rc.todo = Speak()
elif self.profile in news.restricted_to:
# FIXME: hard code to split, restricted为"Moderator"或"Moderator, 角色profile"
# Moderator加密发给自己的,意味着要执行角色的特殊动作
self.rc.todo = self.special_actions[0]()
(1)本次发言前收到的消息来自 InstructSpeak
,并且存在 restricted_to
参数,因此执行 self.special_actions[0]()
,也就是 ["Protect"]
动作。
(2)执行 Protect
动作:
class Protect(NighttimeWhispers):
name: str = "Protect"
Protect
动作继承自 NighttimeWhispers
。凡是 NighttimeWhispers
类型的动作,执行完之后,restricted_to
参数都是主持人,也就是动作的结果都是发送给主持人。
async def _act(self):
......
# 根据自己定义的角色Action,对应地去run,run的入参可能不同
if isinstance(todo, Speak):
......
elif isinstance(todo, NighttimeWhispers):
rsp = await todo.run(
profile=self.profile, name=self.name, context=memories, reflection=reflection, experiences=experiences
)
restricted_to = {RoleType.MODERATOR.value, self.profile} # 给Moderator发送使用特殊技能的加密消息
所以接下来又回到主持人发言。
(3)根据主持人的 _think
函数,上次消息来自守卫的Protect
动作,因此执行 ParseSpeak
动作
async def _think(self):
......
if latest_msg.role in ["User", "Human", self.profile]:
......
else:
# 上一轮消息是游戏角色的发言,解析角色的发言
self.rc.todo = ParseSpeak()
(4)因为主持人监听了 ParseSpeak
的动作:self._watch([UserRequirement, InstructSpeak, ParseSpeak])
,因此下一轮还是主持人发言。
(5)根据 _think
函数,下一步主持人需要执行动作是 InstructSpeak
if latest_msg.role in ["User", "Human", self.profile]:
# 1. 上一轮消息是用户指令,解析用户指令,开始游戏
# 2.1. 上一轮消息是Moderator自己的指令,继续发出指令,一个事情可以分几条消息来说
# 2.2. 上一轮消息是Moderator自己的解析消息,一个阶段结束,发出新一个阶段的指令
self.rc.todo = InstructSpeak()
(6)执行 InstructSpeak
动作,这时候来到了第3步(只有主持人的 InstructSpeak
动作会增加这个步数。),取出的指令为:
3: {"content": "Guard, close your eyes", "send_to": {RoleType.MODERATOR.value}, "restricted_to": empty_set},
(7)可以看到还是主持人发言(send_to
),主持人还是继续执行 InstructSpeak
,第4步取出的指令为:
4: {
"content": "Werewolves, please open your eyes!",
"send_to": {RoleType.MODERATOR.value},
"restricted_to": empty_set,
},
(8)第4步的指令还是发给主持人,接着取第5步的指令:
5: {
"content": """Werewolves, I secretly tell you that {werewolf_players} are
all of the {werewolf_num} werewolves! Keep in mind you are teammates. The rest players are not werewolves.
choose one from the following living options please:
{living_players}. For example: Kill ...""",
"send_to": {RoleType.WEREWOLF.value},
"restricted_to": {RoleType.MODERATOR.value, RoleType.WEREWOLF.value},
},
然后就到了 WEREWOLF
发言。
其它角色都类似以上流程,这里不再赘述,大家可以自己类比着去跟踪其它角色的流程。
3. 白天发言
(1)白天到了,主持人 InstructSpeak
取白天的第一个步骤,在代码中是第14步:
14: {
"content": """It's daytime. Everyone woke up except those who had been killed.""",
"send_to": {RoleType.MODERATOR.value},
"restricted_to": empty_set,
},
(2)上一步发送给主持人,下一步还是主持人继续 InstructSpeak
,第15步和第16步:
15: {
"content": "{player_current_dead} was killed last night!",
"send_to": {RoleType.MODERATOR.value},
"restricted_to": empty_set,
},
16: {
"content": """Living players: {living_players}, now freely talk about the current situation based on your observation and
reflection with a few sentences. Decide whether to reveal your identity based on your reflection.""",
"send_to": {MESSAGE_ROUTE_TO_ALL}, # send to all to speak in daytime
"restricted_to": empty_set,
},
(3)根据第16步的说明,下面是所有游戏角色发言:"send_to": {MESSAGE_ROUTE_TO_ALL}
根据 _think
函数,游戏角色应该都执行一次 Speak
动作:
async def _think(self):
news = self.rc.news[0]
assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作
if not news.restricted_to:
# 消息接收范围为全体角色的,做公开发言(发表投票观点也算发言)
self.rc.todo = Speak()
(4)角色的发言 Speak
:
if isinstance(todo, Speak):
rsp = await todo.run(
profile=self.profile,
name=self.name,
context=memories,
latest_instruction=latest_instruction,
reflection=reflection,
experiences=experiences,
)
restricted_to = set()
Speak
之后,restricted_to
为空,也就是所有人都能接收到消息,一轮发言下来,又来到了主持人的 InstructSpeak
。
这里有两个细节:
- 这里怎么完成的所有人轮流发言?环境的run函数,就是循环让每个角色都处理一遍消息。
async def run(self, k=1):
"""处理一次所有信息的运行
Process all Role runs at once
"""
for _ in range(k):
futures = []
for role in self.roles.values():
future = role.run()
futures.append(future)
await asyncio.gather(*futures)
logger.debug(f"is idle: {self.is_idle}")
- 所以角色都
Speak
后,为什么后面又来到了主持人的InstructSpeak
,其它角色就不继续动作了?- 来到主持人是因为上面说的,环境的run在循环每个角色。
- 在
send_to
为all
时,所有角色都接收到了消息,但只有主持人继续InstructSpeak
是因为:
async def _think(self):
news = self.rc.news[0]
assert news.cause_by == any_to_str(InstructSpeak) # 消息为来自Moderator的指令时,才去做动作
(5)InstructSpeak
取第17步的设定:投票环节,所有游戏角色发言:"send_to": {MESSAGE_ROUTE_TO_ALL}
,根据 _think
函数,游戏角色还是都执行一次 Speak
动作。
17: {
"content": """Now vote and tell me who you think is the werewolf. Don’t mention your role.
You only choose one from the following living options please:
{living_players}. Say ONLY: I vote to eliminate ...""",
"send_to": {MESSAGE_ROUTE_TO_ALL},
"restricted_to": empty_set,
},
(6)Speak
之后,restricted_to
为空,也就是所有人都能接收到消息,一轮发言下来,又来到了主持人的 InstructSpeak
。InstructSpeak
取第18步的设定:
18: {
"content": """{player_current_dead} was eliminated.""",
"send_to": {RoleType.MODERATOR.value},
"restricted_to": empty_set,
},
(7)到这里,一轮游戏就走完了。下面 step_idx
又会从0开始新一轮。
4. 游戏结束
主持人每次在思考下一步动作前,都会先检查一下游戏是否已经可以结束:
async def _think(self):
if self.winner:
self.rc.todo = AnnounceGameResult()
return
如果已经有获胜者了,那就执行 AnnounceGameResult 动作。
5. 其它一点细节
在第15-18步的时候会更新游戏状态:
def update_game_states(self):
step_idx = self.step_idx % self.per_round_steps
if step_idx not in [15, 18] or self.step_idx in self.eval_step_idx:
return
else:
self.eval_step_idx.append(self.step_idx) # record evaluation, avoid repetitive evaluation at the same step
if step_idx == 15: # step no
# night ends: after all special roles acted, process the whole night
self.player_current_dead = [] # reset
if self.player_hunted != self.player_protected and not self.is_hunted_player_saved:
self.player_current_dead.append(self.player_hunted)
if self.player_poisoned:
self.player_current_dead.append(self.player_poisoned)
self._update_players_state(self.player_current_dead)
# reset
self.player_hunted = None
self.player_protected = None
self.is_hunted_player_saved = False
self.player_poisoned = None
elif step_idx == 18:
# updated use vote_kill_someone
pass
6. 总结
本文我们从游戏入口函数开始,详细过了一遍这个游戏的执行过程,各个角色之间怎么进行消息传递,怎么限制角色的发言,怎么限制角色的消息接收,怎么指定让角色发言(restricted_to)等。通过本文,你应该对MetaGPT的多智能体间消息的交互有更加深入的了解和认识。
消息传递过程和游戏流程很清晰了,但是里面的游戏环节处理细节很复杂,所以,想自己写出这样一个游戏,还是需要耗费不少精力的,对作者感到深深的佩服。
如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~
- 大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例
- 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
- +v: jasper_8017 一起交流💬,一起进步💪。
- 微信公众号也可搜【同学小张】 🙏
本站文章一览: