MetaGPT实践——一个简单的多任务Agent (BY_ORDER)

一、前置知识:

MetaGPT中的智能体采用ReAct(Reason + Act)范式作为基本工作范式。

具体的流程分为三步:

_think(思考,根据“QUERY”选择将执行的action)

_act(行动,执行选择的action)

_observe(观察,获取外部输入的信息,也即{action: action的输出},将其和历史的query一起生成新的query作为智能体的新的输入“QUERY”,进入一个新的循环,直到行动输出正确的结果。

MetaGPT中的类介绍:

  • Action类是动作(action)的逻辑抽象。Action基类中定义了一个_aask函数来获取LLM的回复,从Action类派生出来的类可以直接使用这个函数来调用llm。
async def _aask(self, prompt: str, system_msgs: Optional[list[str]] = None) -> str:
    """Append default prefix"""
    if not system_msgs:
        system_msgs = []
    system_msgs.append(self.prefix)
    return await self.llm.aask(prompt, system_msgs)

  • Message类是最基本的消息类,其代码如下:

    class Message(BaseModel):
        """list[<role>: <content>]"""
        
        id: str = Field(default="", validate_default=True)  # According to Section 2.2.3.1.1 of RFC 135
        content: str
        instruct_content: Optional[BaseModel] = Field(default=None, validate_default=True)
        role: str = "user"  # system / user / assistant
        cause_by: str = Field(default="", validate_default=True)
        sent_from: str = Field(default="", validate_default=True)
        send_to: set[str] = Field(default={MESSAGE_ROUTE_TO_ALL}, validate_default=True)
    

    content:消息的内容。只有这个是必选,其他是optional,或者有缺省值。

    role:发出消息的角色

    cause_by:是哪个动作导致产生这个消息

    遗留问题:send_to的set是个啥类型?

  • Memory类是智能体的记忆的逻辑抽象。当初始化时,Role会初始化一个Memory对象作为self._rc.memory属性,它将在之后的_observe中存储每个Message,以便后续的检索。简而言之,**Role的记忆是一个含有Message的列表。**

  • Role类是智能体的逻辑抽象。每个Role有自己的记忆,能够执行特定的Action。

    实现一个最简单的Role,只需要重写Role基类的 _init__act 方法。

    • __init__ 方法中声明了这个Role的name(昵称)、profile(人设),并初始化Role可以使用的actions。
    • _act方法决定了当这个角色行动时它的具体行动逻辑。

二、复现教材中的智能体:以实现一个SimpleCoder的智能体为例来说明上述过程。以下代码基于metagpt0.8.0。

SimpleCoder只有一个”动作“——WritePythonFunction——的能力,即根据用户的输入要求{instruction}写一个Python函数。

继承Action类,定义WritePythonFunction动作:

import re
import asyncio
from metagpt.actions import Action

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

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    async def run(self, instruction: str):
        prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
        rsp = await self._aask(prompt)
        code_text = WritePythonFunction.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
  • 定义WritePythonFunction的PROMPT_TEMPLATE,含有一个待输入参数instruction,用户的输入会作为这个参数的值;
  • 重写了__init__方法,完成WritePythonFunction的初始化:声明了这个类要使用的llm,这个动作的name名称,以及行动的一些前置知识(context);
  • 重写了run方法,定义WritePythonFunction对传入的参数执行的处理:根据输入的instruction生成prompt,将prompt发送给llm来处理(即要求llm根据instruction写一个python函数),然后用正则表达式提取其中的code部分;
  • parse_code是WritePythonFunction独有的一个方法,使用正则表达式来匹配用户输入的代码文本。它会查找以‘````python’开头, 以‘`````’`结尾的代码块,并提取其中的代码内容。如果找到匹配的代码块,则返回提取的代码内容;否则,返回原始的用户输入。

继承Role类,定义SimpleCoder角色:

from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger

class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"
    
    def __init__(
        self,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.set_actions([WritePythonFunction])

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

        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

  • 实现一个最简单的Role,只需要重写Role基类的 _init__act 方法。
  • 重写__init__方法,完成SimpleCoder的初始化:声明了SimpleCoder的name(昵称),profile(人设),以及为他配备了写好的动作 WritePythonFunction。配备好的动作,会被加入到Role类这两个定义的代办参数self.rc.todo中,可以被SimpleCoder类的对象调用。
  • 重写_act方法,定义要求SimpleCoder完成的动作,此处只要求它执行WritePythonFunction动作,调用的方式是用todo.run()方法——如果你顺着MetGPT的源代码一路找下去,最后实际执行的就是WritePythonFunction.run()。

注意:前方高能!下面这三行代码隐含了Role类的Memory的管理机制,以及Message的传递机制!

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))

第一行:Role初始化时,会初始化一个Memory对象作为self._rc.memory属性,它将在之后的_observe中存储每个Message,以便后续的检索。**Role的记忆是一个含有Message的列表。**此处的self.get_memories(k=1)[0],是获取Role的最近的一条记忆,也就是带有用户下达的需求的那条Message。它的代码如下:

def get_memories(self, k=0) -> list[Message]:
    """A wrapper to return the most recent k memories of this role, return all when k=0"""
    return self._rc.memory.get(k=k)

第二行:将消息的内容传递给Role的Action去执行——等效执行的是WritePythonFunction.run(instruction)。

第三行:将Action执行的结果封装成一条Message。并作为_act的执行结果返回,进入MetaGPT的Message的管理机制。

SimpleCoder(Role)现在拥有了一个WritePythonFunction的行动(Action)能力,可以出去溜达一下了。

import re
import asyncio
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger

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

asyncio.run(main())

深入MetaGPT的源代码,理解MetaGPT内部的执行和调用过程:

SimpleCoder/Role.run(msg)

-规整msg:不论输入的msg是str, Message还是list,同一规整为Message类

-规整的msg放入到队列中,Role.rc.msg_buffer.push(message)

-rsp = Role.react()

—根据Role初始化时action的执行顺序设定执行“行动”,以BY_ORDER为例

—Role._act_by_order()

——start_idx = Role.rc.state:选择开始执行的action

——按执行顺序执行所有的action

Role._act()

—Role._set_state(state=-1): # current reaction is complete, reset state to -1 and todo back to None

-Role.set_todo() : Reset the next action to be taken

-Role.publis_message(rsp): Send the response message to the Environment object to have it relay the message to the subscribers.

最后执行的就是上面重写的SimpleCoder类的_act()方法。

生成的python代码如下:


def generate_sum_function():
    def sum_of_list(lst):
        return sum(lst)
    return sum_of_list

# Test cases
sum_function = generate_sum_function()

# Test case 1
assert sum_function([1, 2, 3, 4, 5]) == 15
print("Test case 1 passed.")

# Test case 2
assert sum_function([-1, 0, 1, -2, 2]) == 0
print("Test case 2 passed.")

目前SimpleCoder只有一个行动能力,太少了,给它加一个。

当前的action是写一个python的function,并写了2个测试case。光写不练可不行。如果让SimpleCoder拥有运行python代码的能力,加上写好的测试用例,就可以验证自己写的代码是否正确了。

继承Action类,定义RunPythonFunction动作:

class RunPythonFunction(Action):	
    name: str ="RunPythonFunction"
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

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

在Python中,我们通过标准库中的subprocess包来fork一个子进程,并运行一个外部的程序。调用 subprocess.run 创建一个新的进程,它执行了 python3 -c 命令,用于运行 code_text 中包含的Python代码。新创建的进程与当前程序的进程相互独立,通过 subprocess.run 你的Python程序可以启动并与第二个进程进行交互,获取其输出结果。

特别说明:使用sys.executable可以确保使用当前运行的Python解释器,这通常是更稳妥的选择。

重新修改SimpleCoder的定义,将新的action — RunPythonFunction加入进去:

class SimpleCoder(Role):
    name: str = "Alice"
    profile: str = "SimpleCoder"
    def __init__(
        self,
        **kwargs,
    ):
        # super().__init__(name, profile, **kwargs)
        super().__init__(**kwargs)

        self.set_actions([WritePythonFunction, RunPythonFunction])
        self._set_react_mode(react_mode="by_order") # 串行执行上面的两个行动

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: ready to {self.rc.todo}")
        # 通过在底层按顺序选择动作
        # todo 首先是 WritePythonFunction() 然后是 RunPythonFunction()
        todo = self.rc.todo

        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))
        self.rc.memory.add(msg)

        return msg

重新溜一下SimpleCoder,代码不变。

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

asyncio.run(main())

SimpleCoder会顺序执行WritePythonFunction和RunPythonFunction两个行动,最后会打印出下面的运行日志,两个测试用例都通过了。

2024-05-17 05:02:38.964 | INFO     | __main__:run:63 - code_result='Test case 1 passed.\\nTest case 2 passed.\\n'
2024-05-17 05:02:38.966 | INFO     | __main__:main:101 - SimpleCoder: Test case 1 passed.
Test case 2 passed.

三、用最简单的方式完成课堂作业——不调用LLM,只实现MetaGPT最基本的行动逻辑。

编写这样一个 agent
- 这个 Agent 拥有三个动作 打印1 打印2 打印3(初始化时 init_action([print,print,print]))
- 重写有关方法(请不要使用act_by_order,我希望你能独立实现)使得 Agent 顺序执行上面三个动作
- 当上述三个动作执行完毕后,为 Agent 生成新的动作 打印4 打印5 打印6 并顺序执行,(之前我们初始化了三个 print 动作,执行完毕后,重新 init_action([...,...,...]),然后顺序执行这个新生成的动作列表)

第一步,定义3个Action:PrintOne,PrintTwo,PrintThree。

只展示PrintOne的代码,其他两个类似。

class PrintOne(Action):
    name: str ="PrintOne"
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
    async def run(self, code_text: str):
        code_text = "print('One - Hello world')"

        import sys
        result = subprocess.run([sys.executable, "-c", code_text], capture_output=True, text=True)
        print_info = result.stdout

        logger.info(f"{print_info=}")
        return print_info

第二步,定义1个Role:TriplePrinter。

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

    def __init__(
        self,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.set_actions([PrintOne, PrintTwo, PrintThree])
        self._set_react_mode(react_mode="by_order")

    async def _act(self) -> Message:
        logger.info(f"{self._setting}: ready to {self.rc.todo}")
        # 通过在底层按顺序选择动作
        # todo的顺序列表: PrintOne, PrintTwo, PrintThree
        todo = self.rc.todo

        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))
        self.rc.memory.add(msg)

        return msg

重写__init__: 初始化action列表,设定action的执行顺序”by_order。

重写_act:实现按照设定的顺序,执行PrintOne, PrintTwo, PrintThree三个动作。由于只验证逻辑,代码中传递的消息只用来验证消息传递机制,实际上没有任何意义。

第三步,写运行的测试函数(几乎都是一样的:))

async def main():
    msg = "a triple printer for test"
    role = TriplePrinter()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)

asyncio.run(main())

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值