# MetaGPT智能体学习 | 第四章:多智能体

作业:你画我猜

目录:

  • 初次尝试版本
  • 改进版本
    #解决消息传递机制
  • 实测案例

参考资料:

MetaGPT github:https://github.com/geekan/MetaGPT
MetaGPT 官方文档:https://docs.deepwisdom.ai/main/zh/guide/get_started/introduction.html
MetaGPT 官方教程(飞书):https://deepwisdom.feishu.cn/wiki/KhCcweQKmijXi6kDwnicM0qpnEf
Datawhale 合作课程:https://spvrm23ffj.feishu.cn/docx/RZNpd5uXfoebTPxMXCFcWHdMnZb

初次尝试版本

设计逻辑:
Human: 出题
Drawer: 接收Human消息, 根据题目比划
Guesser:接收Drawer消息, 根据比划猜题
Drawer: 接收Guesser消息, 对比答案是否正确。若不正确,继续比划。
Guesser【重复】

加载需要模块:


import asyncio
import platform
from typing import Any

import fire

from metagpt.actions import Action, UserRequirement
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.team import Team

参考辩论的写法,Action设计:

class Draw(Action):
    """Action: By given an object, describe it without saying the name itself."""

    PROMPT_TEMPLATE: str = """
    ## BACKGROUND
    Suppose you are playing the game "You draw, I Guess". You are the actor to 'draw'. 
    ### The question
    {question} 
    ## Previous guess
    {context}
    ## YOUR TURN
    Now it's your turn, observe previous round. If the latest answer is EXACTLY the same as the question, you win and response "WIN". 
    Otherwise, describe the question without saying the word itself. Try new aspects other then previous rounds. 
    DO NOT mention the question itself in your response!
    If the question is a combination of multiple single words. Those words are alos not allowed present in your response.
    Your will respone in Chinese:
    """
    name: str = "draw"

    async def run(self, context: str, question:str, topic: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context, question=question, topic=topic)
        # logger.info(prompt)

        rsp = await self._aask(prompt)

        return rsp

class Guess(Action):
    """Action: By given a descriptin, guess the word or phase."""

    PROMPT_TEMPLATE: str = """
    ## BACKGROUND
    Suppose you are playing the game "You draw, I Guess". You are the actor to 'guess'. 
    ### The topic
    {topic}
    ## The description
    {context}
    ## YOUR TURN
    Now it's your turn, observe the description. Guess what it is, wihtout any other words. 
    Your respone:
    """
    name: str = "Guesser"

    async def run(self, context: str, topic: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context, topic=topic)
        # logger.info(prompt)

        rsp = await self._aask(prompt)

        return rsp

Roles 设计:

class Drawer(Role):
    name: str = "Drawer"
    profile: str = "Drawer"
    topic: str = ""

    def __init__(self, **data: Any):
        super().__init__(**data)
        self.set_actions([Draw])
        self._watch([UserRequirement, Guess])

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo 

        memories = self.get_memories()
        context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        question = ""
        for msg in memories:
            if msg.role == "Human":
                question = f"{msg.content}"
        rsp = await todo.run(context=context, topic=self.topic, question=question)

        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
        )
        self.rc.memory.add(msg)

        return msg

class Guesser(Role):
    name: str = "Guesser"
    profile: str = "Guesser"
    topic: str = ""

    def __init__(self, **data: Any):
        super().__init__(**data)
        self.set_actions([Guess])
        self._watch([Draw])

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo 

        memories = self.get_memories(k=1)
        context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        # print(context)

        rsp = await todo.run(context=context, topic=self.topic)

        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
        )
        self.rc.memory.add(msg)

        return msg

最后team封装:

async def drawAguess(idea: str, topic: str, investment: float = 3.0, n_round: int = 5):
    """Run a team of presidents and watch they quarrel. :)"""
    team = Team()
    team.hire([
        Drawer(topic=topic) ,
        Guesser(topic=topic)])
    team.invest(investment)
    team.run_project(idea)
    await team.run(n_round=n_round)


idea= "冰球"
topic = "运动"
investment = 3.0
n_round = 5

asyncio.run(drawAguess(idea, topic, investment, n_round))

问题:

  • 辩论例子中的_observe模块困惑了我一整天,它的写法会导致Drawer接收不到消息,导致程序直接结束。也没有报错,太难排查了,没时间进一步研究了。
  • Drawer没法很好区分人类的题目和Guesser的答案,导致有概率出现一上来就说答对了的情况。后续有待研究改进。

改进版本

经过研究,已经能够非常好的完成你画我猜了。

  • 环境中的消息通过send_to能够正确传递了
  • 增加了描述技巧和猜题技巧的提示词
  • 增加了GPT和GLM分别扮演“画”和“猜”角色的能力,看看谁比较厉害

有待改进的地方:

  • 一次输入多个词,让智能体自动完成多轮游戏
    • 策略1: 增加问题派发员
    • 策略2:在策略1的基础上,让派发员自己生成题目

完整测试代码:


import asyncio
import platform
from typing import Any

import fire

from metagpt.actions import Action, UserRequirement
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.team import Team

from metagpt.config2 import Config
gpt4 = Config.default()
glm4 = Config.from_home("zhipu-glm4.yaml")




class Draw(Action):
    """Action: By given an object, describe it without saying the name itself."""

    PROMPT_TEMPLATE: str = """
    ## BACKGROUND
    Suppose you are playing the game "You draw, I Guess". You are the actor to 'draw'. 
    ### The question
    {question} 
    ## Previous guess
    {previous}
    ## YOUR TURN
    Now it's your turn, Describe the question without saying the word itself. 
    If "Previous guess" have content, try something new other then previous used. 
    DO NOT mention the question itself in your response!
    If the question is a combination of multiple words, ANY SINGLE word arr NOT allowed be present in your response.
    技巧:
    1. 如果意思正确但是字数不同,请描述正确答案的文字长度,如“三个字”,“四个字”
    2. 如果是数字表示方式不同,请描述正确答案的数字类型,如“中文数字”,“阿拉伯数字“
    3. 如果给出的答案意思正确但是语言不匹配时,请描述正确答案的语言,如“改用中文”,“答案是英文”
    注意,不要强调规则,请直接说出描述本身。
    Your will respone in Chinese:
    """
    name: str = "draw"

    async def run(self, previous: str, question:str, topic: str):
        prompt = self.PROMPT_TEMPLATE.format(previous=previous, question=question, topic=topic)
        # logger.info(prompt)

        rsp = await self._aask(prompt)

        return rsp

class Guess(Action):
    """Action: By given a descriptin, guess the word or phase."""

    PROMPT_TEMPLATE: str = """
    ## BACKGROUND
    Suppose you are playing the game "You draw, I Guess". You are the actor to 'guess'. 
    ### The topic
    {topic}
    ## The description
    {context}
    ## YOUR TURN
    Now it's your turn, observe the description. Guess what it is, wihtout any other characters. 
    Use the same language as the description.
    再次强调,请直接说答案,不要带有任何其他描述;也不要带有任何标点符号
    Your respone:
    """
    name: str = "Guesser"

    async def run(self, context: str, topic: str):
        prompt = self.PROMPT_TEMPLATE.format(context=context, topic=topic)
        # logger.info(prompt)

        rsp = await self._aask(prompt)

        return rsp


class Drawer(Role):
    name: str = "Drawer"
    profile: str = "Drawer"
    topic: str = ""

    def __init__(self, **data: Any):
        super().__init__(**data)
        self.set_actions([Draw])
        self._watch([UserRequirement, Guess])

    async def _observe(self) -> int:
        await super()._observe()
        # accept messages sent (from opponent) to self, disregard own messages from the last round
        self.rc.news = [msg for msg in self.rc.news if msg.send_to == {self.name}]
        return len(self.rc.news)


    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo 

        memories = self.get_memories()
        mem0 = memories.pop(0)
        if mem0.role == "Human":
            question = f"{mem0.content}" 
        else:
            raise ValueError(f"Question not found: {mem0}")
        
        if len(memories) > 0:
            mem9 = memories[-1]
            latestAnswer = f"{mem9.content}"
            previousTry = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        else:
            latestAnswer = ""
            previousTry = ""
        
        if latestAnswer == question:
            totalTries = len([msg for msg in memories if msg.role == "Guesser"])
            rsp = f"Win after {totalTries} tries!"
            logger.info(rsp)

            next_to = "Human"
        else:
            rsp = await todo.run(previous=previousTry, topic=self.topic, question=question)
            next_to = "Guesser"

        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
            send_to=next_to,
        )
        self.rc.memory.add(msg)

        return msg

class Guesser(Role):
    name: str = "Guesser"
    profile: str = "Guesser"
    topic: str = ""

    def __init__(self, **data: Any):
        super().__init__(**data)
        self.set_actions([Guess])
        self._watch([Draw])

    async def _observe(self) -> int:
        await super()._observe()
        # accept messages sent (from opponent) to self, disregard own messages from the last round
        self.rc.news = [msg for msg in self.rc.news if msg.send_to == {self.name}]
        return len(self.rc.news)
    
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        todo = self.rc.todo 

        memories = self.get_memories(k=1)
        context = "\n".join(f"{msg.sent_from}: {msg.content}" for msg in memories)
        # print(context)

        rsp = await todo.run(context=context, topic=self.topic)

        msg = Message(
            content=rsp,
            role=self.profile,
            cause_by=type(todo),
            sent_from=self.name,
            send_to="Drawer"
        )
        self.rc.memory.add(msg)

        return msg

async def drawAguess(idea: str, topic: str, investment: float = 3.0, n_round: int = 5):
    """Run a team of presidents and watch they quarrel. :)"""
    team = Team()
    team.hire([
        Drawer(topic=topic,config=glm4) ,
        Guesser(topic=topic,config=gpt4)
        ])
    team.invest(investment)
    team.run_project(idea, send_to="Drawer")
    await team.run(n_round=n_round)

def main(q: str, topic:str, investment: float = 3.0, n_round: int = 10):
    """
    :param topic: Guess topic, such as "Sports/Movie/Star"
    :param q(uestion): A word, phase to play. Should be a subject belong to the topic.
    :param investment: contribute a certain dollar amount to watch the debate
    :param n_round: maximum rounds of the debate
    :return:
    """
    if platform.system() == "Windows":
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    asyncio.run(drawAguess(q, topic, investment, n_round))


if __name__ == "__main__":
    fire.Fire(main)

实测案例

python learn/drawAguess.py --q "深圳" --topic "地名"

INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是中国一个著名的经济特区,位于广东省,毗邻香港,是一个年轻而充满活力的城市。描述:这是一个由两个汉字组成的城市名,具有改革开放的重要意义。
IINFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
深圳
INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
INFO     | __main__:_act:119 - Win after 1 tries!

尝试了很多,大多数都能像这样一次答对,总体还是不错的。

python learn/drawAguess.py --q "百元之恋" --topic "电影"

NFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是一部讲述拳击和人生挑战的日本电影,描述了一个女性从自暴自弃到自我救赎的过程。这部电影的名字由三个字组成,其中包含了货币单位和一种情感。
INFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
百円の恋
INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是一部以拳击和自我成长为主题的日本电影,描绘了主角从失意沉沦到重燃斗志的心路历程。电影的名字由三个字组成,反映了金钱与爱情的双重含义,其中“元”字代表了货币单位,“恋”字则表达了情感主题。名字中的第一个字是中文数字。
INFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
千与千寻
INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是一部日本电影,讲述了主角通过拳击运动找回自我和爱情的故事。影片的名字由三个汉字组成,表达了金钱与情感的关系,开头的字是中文数字“百”,而结尾的字代表了爱情。名字中既体现了影片中金钱的元素,也暗示了主角在经历挑战后对生活的热爱。
INFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
百万爱情宣言
INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是一部描绘女性通过拳击找回生活意义和爱情的日本电影。片名由三个汉字组成,反映了影片中金钱与情感交织的主题,开头的字使用了中文数字,最后一个字代表了爱情。
INFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
一亿兆恋情
INFO     | __main__:_act:98 - Drawer(Drawer): to do Draw(Draw)
这是一部以拳击为背景,讲述女性从低谷中站起,重新找回生活热情和爱情的日本电影。影片的名字由三个汉字组成,开头的字是中文数字,代表着货币的价值,而最后一个字则是对爱的简称。这个名字既体现了电影中金钱的元素,也隐喻了主人公情感的转变。
INFO     | __main__:_act:154 - Guesser(Guesser): to do Guess(Guess)
百万爱

这个实例,其实第一轮已经猜出来了,但是因为语言没有匹配上,所以进入了下一轮。但是下一轮中并没有及时给予语言上的提示,导致越来越离谱。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容