Hugging Muti Agent

swallow010 / 2024-03-03 / 原文

一、前期环境准备

相关参考文档:

  • MetaGPT Doc

  • geekan/MetaGPT GitHub

  • Hugging Muti Agent(二月学习)

  • 智谱AI 接口文档

Python3.9 及以上环境

$ python --version
Python 3.10.13

安装 MetaGPT

$ pip install -i https://pypi.tuna.tsinghua.edu.cn/simple metagpt
$ pip list | grep metagpt
metagpt                      0.7.3

配置 MetaGPT

$ metagpt --init-config
$ vim ~/.metagpt/config2.yaml
llm:
  api_type: 'zhipuai'
  api_key: '从https://open.bigmodel.cn/usercenter/apikeys获取的api-key'
  model: 'glm-4'

测试示例

# main.py
import asyncio

from metagpt.actions import Action
from metagpt.environment import Environment
from metagpt.roles import Role
from metagpt.team import Team

action1 = Action(name="AlexSay", instruction="Express your opinion with emotion and don't repeat it")
action2 = Action(name="BobSay", instruction="Express your opinion with emotion and don't repeat it")
alex = Role(name="Alex", profile="Democratic candidate", goal="Win the election", actions=[action1], watch=[action2])
bob = Role(name="Bob", profile="Republican candidate", goal="Win the election", actions=[action2], watch=[action1])
env = Environment(desc="US election live broadcast")
team = Team(investment=10.0, env=env, roles=[alex, bob])

asyncio.run(team.run(idea="Topic: climate change. Under 80 words per message.", send_to="Alex", n_round=5))

二、智能体综述及多智能体框架

2.1 AI Agent 介绍

个人理解,AI Agent 是一种通过各种途径输入信息,并把这些信息进一步分解和规划,最后将结果依次馈入 LLM 大模型。或者说是具有多种不同模式的 LLM 聚合体,

https://e2b.dev/ai-agents

2.2 多智能体框架介绍

MetaGPT 是一种先进的元编程技术,它利用了大规模语言模型(LLM)和多智能体系统来模拟软件工程团队中的协作过程。通过编码标准操作程序(SOP)为提示,MetaGPT能够复制一个软件研发团队的结构和协作方式,以自动化和优化软件开发流程。旨在通过整合人类的程序知识来提高软件的鲁棒性,减少错误,并为复杂任务设计高效的软件解决方案。MetaGPT能够扮演不同的角色,如产品经理、架构师、项目经理和工程师,从而覆盖软件开发过程中的多个方面。只需要一个简单的需求输入,MetaGPT 就能够生成用户故事、竞品分析、需求文档、数据结构、API设计以及其他相关文档。它通过模拟不同角色的职责,高效地组织工作流程,并能够在给定任务的环境下主动观察和检索相关信息。

三、单智能体开发

3.1

v0.7.3 https://github.com/geekan/MetaGPT/blob/v0.7.3/metagpt/roles/role.py#L88

class RoleContext(BaseModel):
    """Role Runtime Context"""

    model_config = ConfigDict(arbitrary_types_allowed=True)

    # # env exclude=True to avoid `RecursionError: maximum recursion depth exceeded in comparison`
    env: "Environment" = Field(default=None, exclude=True)  # # avoid circular import
    # TODO judge if ser&deser
    msg_buffer: MessageQueue = Field(
        default_factory=MessageQueue, exclude=True
    )  # Message Buffer with Asynchronous Updates
    memory: Memory = Field(default_factory=Memory)
    # long_term_memory: LongTermMemory = Field(default_factory=LongTermMemory)
    working_memory: Memory = Field(default_factory=Memory)
    state: int = Field(default=-1)  # -1 indicates initial or termination state where todo is None
    todo: Action = Field(default=None, exclude=True)
    watch: set[str] = Field(default_factory=set)
    news: list[Type[Message]] = Field(default=[], exclude=True)  # TODO not used
    react_mode: RoleReactMode = (
        RoleReactMode.REACT
    )  # see `Role._set_react_mode` for definitions of the following two attributes
    max_react_loop: int = 1

    def check(self, role_id: str):
        # if hasattr(CONFIG, "enable_longterm_memory") and CONFIG.enable_longterm_memory:
        #     self.long_term_memory.recover_memory(role_id, self)
        #     self.memory = self.long_term_memory  # use memory to act as long_term_memory for unify operation
        pass

    @property
    def important_memory(self) -> list[Message]:
        """Retrieve information corresponding to the attention action."""
        return self.memory.get_by_actions(self.watch)

    @property
    def history(self) -> list[Message]:
        return self.memory.get()

    @classmethod
    def model_rebuild(cls, **kwargs):
        from metagpt.environment.base_env import Environment  # noqa: F401

        super().model_rebuild(**kwargs)

env:Environment 对象,当在 Environment 添加 Role 时会同时设置 Role 对 Environment 的引用。
msg_buffer:一个 MessageQueue 对象,该对象是对 asyncio 的 Queue 进行简单封装,主要是提供了非阻塞的 pop / push 方法。Role 通过该对象与环境中的其他 Role 进行信息交互。
memory:记忆对象。当 Role 执行 _act 时,会将执行得到的响应转换为 Message 对象放入 memory 中。另外当 Role 执行 _observe 时,会把 msg_buffer 的所有消息转移到 memory 中。
state:记录 Role 的执行状态。初始状态值为 -1,当全部 Action 执行完成之后也会被重置为 -1。
todo:下一个待执行的 Action。当 state >= 0 时会指向最后一个 Action。
watch:用 str 表示的当前 Role 观察的 Action 列表,目前用在 _observe 获取 news 时进行消息过滤。
news:存储那些在本次执行 _observe 时读取到的与当前 Role 上下游相关的消息。
react_mode:ReAct 循环的模式,目前支持 REACT、BY_ORDER、PLAN_AND_ACT 3种模式,默认使用 REACT 模式。在 _set_react_mode 方法中有相关说明。简单来说,BY_ORDER 模式按照指定的 Action 顺序执行。PLAN_AND_ACT 则为一次思考后执行多个动作,即 _think -> _act -> act -> ...,而 REACT 模式按照 ReAct 论文中的思考——行动循环来执行,即 _think -> _act -> _think -> _act -> ...。
max_react_loop:在 react_mode 为 REACT 模式时生效,用于设置最大的思考-循环次数,超过后会停止 _react 执行。

class Role(SerializationMixin, ContextMixin, BaseModel):
    """Role/Agent"""

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")

    name: str = ""
    profile: str = ""
    goal: str = ""
    constraints: str = ""
    desc: str = ""
    is_human: bool = False

    role_id: str = ""
    states: list[str] = []

    # scenarios to set action system_prompt:
    #   1. `__init__` while using Role(actions=[...])
    #   2. add action to role while using `role.set_action(action)`
    #   3. set_todo while using `role.set_todo(action)`
    #   4. when role.system_prompt is being updated (e.g. by `role.system_prompt = "..."`)
    # Additional, if llm is not set, we will use role's llm
    actions: list[SerializeAsAny[Action]] = Field(default=[], validate_default=True)
    rc: RoleContext = Field(default_factory=RoleContext)
    addresses: set[str] = set()
    planner: Planner = Field(default_factory=Planner)

    # builtin variables
    recovered: bool = False  # to tag if a recovered role
    latest_observed_msg: Optional[Message] = None  # record the latest observed message when interrupted

    __hash__ = object.__hash__  # support Role as hashable type in `Environment.members`
    
    # ......

3.2 最简单的Agent

要自己实现一个最简单的Role,只需要重写Role基类的 _init_ 与 _act 方法。在 _init_ 方法中,我们需要声明 Agent 的name(名称)profile(类型)
我们使用 self._init_action 函数为其配备期望的动作 SimpleWriteCode 这个Action 应该能根据我们的需求生成我们期望的代码在 _act方法中,我们需要编写智能体具体的行动逻辑,智能体将从最新的记忆中获取人类指令,运行配备的动作,MetaGPT将其作为待办事项 (self.rc.todo) 在幕后处理,最后返回一个完整的消息。

实现 SimpleCoder

import asyncio
import re
import subprocess
import fire
from metagpt.actions import Action
from metagpt.logs import logger
from metagpt.roles.role import Role, RoleReactMode
from metagpt.schema import Message


# 编写SimpleWriteCode动作
class SimpleWriteCode(Action):
    PROMPT_TEMPLATE: str = """
    Write a python function that can {instruction} and provide two runnnable test cases.
    Return ```python your_code_here ``` with NO other texts,
    your code:
    """

    name: str = "SimpleWriteCode"

    async def run(self, instruction: str):
        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)

        rsp = await self._aask(prompt)

        code_text = SimpleWriteCode.parse_code(rsp)

        return code_text

    @staticmethod
    def parse_code(rsp):
        pattern = r"```python(.*)```"
        match = re.search(pattern, rsp, re.DOTALL)
        code_text = match.group(1) if match else rsp
        return code_text


class SimpleRunCode(Action):
    name: str = "SimpleRunCode"

    async def run(self, code_text: str):
        result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True)
        code_result = result.stdout
        logger.info(f"{code_result=}")
        return code_result


# 设计SimpleCoder角色
class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode])

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

        msg = self.get_memories(k=1)[0]  # find the most recent messages
        code_text = await todo.run(msg.content)
        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))

        return msg


async def main():
    msg="write a function that calculates the product of a list and run it"
    role = SimpleCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == "__main__":
    asyncio.run(main())

3.3 实现一个多动作Agent

import asyncio
import re
import subprocess
from metagpt.actions import Action
from metagpt.logs import logger
from metagpt.roles.role import Role, RoleReactMode
from metagpt.schema import Message


class SimpleWriteCode(Action):
    PROMPT_TEMPLATE: str = """
    Write a python function that can {instruction} and provide two runnnable test cases.
    Return ```python your_code_here ``` with NO other texts,
    your code:
    """

    name: str = "SimpleWriteCode"

    async def run(self, instruction: str):
        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)

        rsp = await self._aask(prompt)

        code_text = SimpleWriteCode.parse_code(rsp)

        return code_text

    @staticmethod
    def parse_code(rsp):
        pattern = r"```python(.*)```"
        match = re.search(pattern, rsp, re.DOTALL)
        code_text = match.group(1) if match else rsp
        return code_text


class SimpleRunCode(Action):
    name: str = "SimpleRunCode"

    async def run(self, code_text: str):
        result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True)
        code_result = result.stdout
        logger.info(f"{code_result=}")
        return code_result


class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode])

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

        msg = self.get_memories(k=1)[0]  # find the most recent messages
        code_text = await todo.run(msg.content)
        msg = Message(content=code_text, role=self.profile, cause_by=type(todo))

        return msg


class RunnableCoder(Role):
    name: str = "Alice"
    profile: str = "RunnableCoder"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.set_actions([SimpleWriteCode, SimpleRunCode])
        self._set_react_mode(react_mode=RoleReactMode.BY_ORDER.value)

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: to do {self.rc.todo}({self.rc.todo.name})")
        # By choosing the Action by order under the hood
        # todo will be first SimpleWriteCode() then SimpleRunCode()
        todo = self.rc.todo

        msg = self.get_memories(k=1)[0]  # find the most k recent messages
        result = await todo.run(msg.content)

        msg = Message(content=result, role=self.profile, cause_by=type(todo))
        self.rc.memory.add(msg)
        return msg


# 运行SimpleCoder角色
async def main():
    msg = "write a function that calculates the product of a list and run it"
    role = RunnableCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == "__main__":
    asyncio.run(main())