【AI Agent教程】【MetaGPT】案例拆解:使用MetaGPT实现“狼人杀“游戏(2)- 整体流程解析中再看多智能体消息交互通路

大家好,我是 同学小张,持续学习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. 主持人发言

游戏开始之后,第一条消息是发给主持人的,所以,主持人先开始动作。

看下主持人的动作集合:InstructSpeakParseSpeakAnnounceGameResultSpeak

  • 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_idx1 了。取出来的 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_toall时,所有角色都接收到了消息,但只有主持人继续 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为空,也就是所有人都能接收到消息,一轮发言下来,又来到了主持人的 InstructSpeakInstructSpeak 取第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 一起交流💬,一起进步💪。
  • 微信公众号也可搜同学小张 🙏

本站文章一览:

在这里插入图片描述

  • 37
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

同学小张

如果觉得有帮助,欢迎给我鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值