Multi-Agent实践第6期:面向智能体编程:狼人杀在AgentScope

简介: 本期文章,我们会介绍一下AgentScope的一个设计哲学(Agent-oriented programming)

1. 前言

在前几期的文章中,我们带大家了解了怎么去利用AgentScope去编写一些简单应用程序(可以@人的群聊五子棋)和一些对agent非常有用的技术向的应用(ReAct和RAG)。本期文章,我们会介绍一下AgentScope的一个设计哲学(Agent-oriented programming),然后和大家一起看看在这样的思想指导下设计的AgentScope能如何用比较少的代码量就开发出一个让智能体玩狼人杀游戏的应用。“狼人杀游戏完整的代码可以在链接中找到。”


欢迎大家关注AgentScope,在github上为我们star 🌟。在接下来几天,我们会继续推出教程,带领大家搭建出有趣的多智能体应用!



2. 面向智能体编程(Agent-oriented programming)

在现在最广为接受的编程语言中,大家比较熟悉的范式包括函数式编程、面向过程编程和面向对象编程。这些编程范式都有自己擅长的领域,给广大开发者带来各种应用开发上的便利。在多智能体应用中,原生的、最重要的元素是智能体(这里我们特指基于大模型的智能体),单个智能体对应的开发需求是智能体本身的功能开发;其次,在多智能体应用中,大家免不了还需要设计、实现智能体之间的合作机制。虽然广义来说,智能体也可以被理解成一种特殊的对象(object),但从开发者的角度来看,多智能体编程有一些自己的正面或负面的特性需要额外考虑。具体来说,这些特性可以提炼成两个方面:

  1. 多智能体能完成任务的多样性:除了简单的智能体-用户这种对话场景,多智能体会被通过各种编排去解决各种更复杂的任务,处理各种模态的数据,甚至可能需要大规模部署或者帮助处理一些敏感信息。
  2. 大模型生成内容的创造性和不确定性:就大语言模型而言,硬币的正面是生成的内容因为随机性会显示出一定的创造性,但硬币的反面则是有时大模型不能很好的服从指令或者产生幻觉;同时,有了大模型加成的智能体,可以去调用各种本地或远程的工具去解决问题,但这也需要应用开发者去提炼接口,以及考虑如何处理工具调用失败的情况。


所以我们在设计AgentScope这个多智能体编程框架的时候,就考虑到给我们的开发者们提供一个“面向智能体编程”的环境。从完成任务多样性的角度出发,AgentScope提供了各种便捷的适合智能体合作的编程接口以及原生支持的多模态数据处理,也提供了非常方便的“一行代码转换成分布式”的功能去支持大规模部署和分布式私域部署;为了最大化大模型的创造力和最小化大模型回答的不可靠性,我们提供了各种层面上的信息提取机制和容错机制。


在今天这篇文章里,我们想通过狼人杀游戏,来体现我们AgentScope面向对象编程的一些特性,包括如何便捷生成多个智能体,如何通过极少代码去组织智能体群聊,以及如何规范大模型回答让整个应用流程更流畅。



狼人杀游戏

狼人杀作为剧本杀形式的游戏,非常适合使用多智能体来模拟。下面我们展示一下用AgentScope实现6人局狼人杀的主逻辑;6人局的角色包括2个狼人、2个村民、一个女巫、一个预言家。


2.1 需要多种智能体合作?一行代码生成N种角色!

在狼人杀这个样例中,我们需要给每个智能体一些设定,包括使用的模型和sys_prompt。sys_promt里包含了描述狼人杀的具体游戏规则,以及该agent的名字。

# 具体参见https://github.com/modelscope/agentscope/blob/main/examples/game_werewolf/configs/agent_configs.json
agent_configs = [
    {
        "class": "DictDialogAgent",
        "args": {
            "name": "Player1",
            # 包含狼人杀的具体规则
            "sys_prompt": "Act as a player in a werewolf game. You are Player1 ...",
            "model": "gpt-4",
            "use_memory": True
        }
    },
    ...
    
    {
        "class": "DictDialogAgent",
        "args": {
            "name": "Player6",
            # 包含狼人杀的具体规则
            "sys_prompt": "Act as a player in a werewolf game. You are Player6 ...",
            "model": "gpt-4",
            "use_memory": True
        }
    }
]


有了这些智能体的配置(模型配置可以参考我们前几期文章),在python程序中,我们可以通过简单agentscope.init一个函数调用,生成6个不同的智能体。

# 主持人
HostMsg = partial(Msg, name="Moderator", echo=True)
# 女巫在开始的时候有解药和毒药
healing, poison = True, True
# 狼人夜间最多讨论回合数
MAX_WEREWOLF_DISCUSSION_ROUND = 3
# 游戏进行的最多天数(防止大模型陷入投票得不到结果或者其他原因导致游戏无法结束的情况)
MAX_GAME_ROUND = 6
# 初始化agent
survivors = agentscope.init(
        model_configs="./configs/model_configs.json",
        agent_configs="./configs/agent_configs.json",
)
# 6个角色
roles = ["werewolf", "werewolf", "villager", "villager", "seer", "witch"]
# 给每个agent按照顺序分配角色
wolves, witch, seer = survivors[:2], survivors[-1], survivors[-2]


2.2 需要频繁的让智能体讨论?message hub和pipeline帮你编排!

狼人杀游戏的主逻辑会循环每一天的流程,从狼人晚上讨论开始到白天大家投票结束。在这之中,有许多需要多智能体讨论的场景。在这些场景中,我们用上了AgentScope为这些多智能体合作设计的message hub模块(msghub)和pipline。msghub可以大大节约编写多智能体“群聊”需要的代码量:只需要把智能体放进msghub,他们就能默认地去广播消息给所有在里面的其他智能体。除此之外,我们也提供了各种编排多智能体执行的pipeline(比如这里用到的sequentialpipeline,更多逻辑pipeline可以参考我们的教程),方便用户编排智能体在游戏中的发言流程。

for i in range(1, MAX_GAME_ROUND + 1):
    # 夜晚来临,狼人首先讨论
    hint = HostMsg(content=Prompts.to_wolves.format(n2s(wolves)))
    with msghub(wolves, announcement=hint) as hub:
        for _ in range(MAX_WEREWOLF_DISCUSSION_ROUND):
            x = sequentialpipeline(wolves)
            if x.get("agreement", False):
                break
    
        # 狼人讨论结束,进行投票
        hint = HostMsg(content=Prompts.to_wolves_vote)
        votes = [extract_name_and_id(wolf(hint).content)[0] for wolf in wolves]
        # 主持人对狼人公布投票结果
        dead_player = [majority_vote(votes)]
        hub.broadcast(
            HostMsg(content=Prompts.to_wolves_res.format(dead_player[0])),
        )
    # 女巫阶段,是否使用解药和毒药(一个晚上最多使用一瓶)
    healing_used_tonight = False
    if witch in survivors:
        if healing:
            hint = HostMsg(
                content=Prompts.to_witch_resurrect.format_map(
                    {"witch_name": witch.name, "dead_name": dead_player[0]},
                ),
            )
            if witch(hint).get("resurrect", False):
                healing_used_tonight = True
                dead_player.pop()
                healing = False
    
        if poison and not healing_used_tonight:
            x = witch(HostMsg(content=Prompts.to_witch_poison))
            if x.get("eliminate", True):
                dead_player.append(extract_name_and_id(x.content)[0])
                poison = False
    # 预言家验人环节
    if seer in survivors:
        hint = HostMsg(
            content=Prompts.to_seer.format(seer.name, n2s(survivors)),
        )
        x = seer(hint)
    
        player, idx = extract_name_and_id(x.content)
        role = "werewolf" if roles[idx] == "werewolf" else "villager"
        hint = HostMsg(content=Prompts.to_seer_result.format(player, role))
        # 预言家获得验人结果
        seer.observe(hint)
    # 根据夜晚发生的情况,更新目前的存活情况并判断游戏是否结束
    survivors, wolves = update_alive_players(survivors, wolves, dead_player)
    if check_winning(survivors, wolves, "Moderator"):
        break
    
    # 根据是否平安夜决定主持人白天的开场白
    content = (
        Prompts.to_all_danger.format(n2s(dead_player))
        if dead_player
        else Prompts.to_all_peace
    )
    hints = [
        HostMsg(content=content),
        HostMsg(content=Prompts.to_all_discuss.format(n2s(survivors))),
    ]
    with msghub(survivors, announcement=hints) as hub:
        # 白天讨论环节
        x = sequentialpipeline(survivors)
        # 白天投票环节
        hint = HostMsg(content=Prompts.to_all_vote.format(n2s(survivors)))
        votes = [extract_name_and_id(_(hint).content)[0] for _ in survivors]
        vote_res = majority_vote(votes)
        # 公布投票结果
        result = HostMsg(content=Prompts.to_all_res.format(vote_res))
        hub.broadcast(result)
        # 更新存活情况并判断游戏是否结束
        survivors, wolves = update_alive_players(survivors, wolves, vote_res)
        if check_winning(survivors, wolves, "Moderator"):
            break
        # 有序继续,新的夜晚来临
        hub.broadcast(HostMsg(content=Prompts.to_all_continue))


2.3 规范大模型回答:DictDialogAgent

在狼人杀这个例子中,所有智能体都是基于DictDialogAgent创建的。DictDialogAgent一个特性是它返回的内容都是python中字典格式(dict或者json),这也是为了在游戏主逻辑中我们可以根据智能体回答为推进游戏做出一些确定性的判断,比如在狼人讨论之后判断当晚需要🔪哪个玩家。具体来说,我们的DictDialogAgent在调用大模型的时候,也会调用一个parse_func去解析大模型的回答,同时fault_handler提供了当应用中大模型回答无法被parse时的一个默认返回。

def parse_dict(response: ModelResponse) -> ModelResponse:
    """Parse function for DictDialogAgent"""
    try:
        response_dict = json.loads(response.text)
    except json.decoder.JSONDecodeError:
        response_dict = json.loads(response.text.replace("'", '"'))
    return ModelResponse(raw=response_dict)
def default_response(response: ModelResponse) -> ModelResponse:
    """The default response of fault_handler"""
    return ModelResponse(raw={"speak": response.text})
class DictDialogAgent(AgentBase):
    
    def reply(self, x: dict = None) -> dict:
        # 省略具体代码...
        # call llm
        response = self.model(
            prompt,
            parse_func=self.parse_func,
            fault_handler=self.fault_handler,
            max_retries=self.max_retries,
        ).raw
        # 省略代码...
        return msg


2.4 效果图展示

像之前的应用一样,大家可以通过as_studio来运行一个带网页UI的狼人杀应用。下面是我们在游戏中的一些截图(样例中大模型使用的是gpt4)。

  1. 狼人 Player1 和 Player2 夜间讨论

image.png

  1. 预言家 Player5 验人以及白天公布结果

image.png


  1. 白天讨论

image.png


2.5 总结

上面只是一个简单的例子,大家如果有兴趣可以去尝试一下比如8人局或者12人局的狼人杀的实现。完整的代码,包括游戏中用到的提示词,都可以在一下链接中找到。


同时,AgentScope提供的面向智能体编程还有更多的特性等待大家去探索,比如体现对智能体赋能的工具调用和RAG(前两期内容)以及分布式(敬请期待下一期的分享)。


延伸阅读和资源




比赛

看到这里,如果你有好玩的想法,不如实践起来,还可以顺便参加下面的比赛~

image.png


点击即可跳转~

Create @ AI 创客松第四季 (aliyun.com)


作者介绍
目录