DataWhale的MetaGPT学习笔记——③

Agent实现

实现一个单动作的Agent

①重写基类函数

我们需要重写Role基类的 _init__act 方法

_init_ 方法中,我们需要声明 Agent 的name(名称)profile(类型)

我们使用 self._init_action 函数为其配备期望的动作 SimpleWriteCode 这个Action 应该能根据我们的需求生成我们期望的代码

_act方法中,我们需要编写智能体具体的行动逻辑,智能体将从最新的记忆中获取人类指令,运行配备的动作,MetaGPT将其作为待办事项 (self``.rc``.todo) 在幕后处理,最后返回一个完整的消息。

②分析我们的需求

要实现一个 SimpleCoder 我们需要分析这个Agent 它需要哪些能力

img编辑

首先我们需要让他接受用户的输入的需求,并记忆我们的需求,接着这个Agent它需要根据自己已知的信息和需求来编写我们需要的代码。

③编写SimpleWriteCode动作

我们可以调用self._aask函数来获取LLM的回应

​
async def _aask(self, prompt: str, system_msgs: Optional[list[str]] = None) -> str:
    """Append default prefix"""
    return await self.llm.aask(prompt, system_msgs)


​

然后就会调用LLM来进行对应的回答

import re
import asyncio
from metagpt.actions import Action
​
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

在我们的场景中,我们定义了一个 SimpleWriteCode 类,它继承自 Action类,我们重写了run 方法,该方法决定了我们对传入的内容到底要做什么样的处理。

我们在name:这一行指定动作的名称

run方法中,我们需要声明当采取这个行动时,我们要对传入的内容做什么样的处理,在 SimpleWriteCode 类中,我们应该传入:“请你帮我写一个XXX的代码” 这样的字符串,也就是用户的输入,run方法需要对它进行处理,把他交给llm,等到llm返回生成结果后,我们再取出其中的代码部分返回。

官方文档写好了一个提示词模板,将用户输入嵌入模板中

也就是说,我们在name里面指定的字符串被作为参数传入

​
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:
    """
prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)


​

然后让大模型生成回答

​
rsp = await self._aask(prompt)


​

生成回答之后,直接用正则匹配来提取code部分返回即可。

正则表达式的提取过程

对应的正则提取内容如下:

parse_code方法使用正则表达式来匹配用户输入的代码文本。它会查找以 ```python 开头且以`````结尾的代码块,并提取其中的代码内容。如果找到匹配的代码块,则返回提取的代码内容;否则,返回原始的用户输入。

​
@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


​

最后将代码内容返回

至此我们就完成了这样一个编写代码的动作。

④设计SimpleCoder角色

在MetaGPT中,Message类是最基本的信息类型

img编辑

​
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._init_actions([SimpleWriteCode])
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: ready to {self.rc.todo}")
        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


​

我们只需要指定name和profile,然后重写Role基类的 _``_``init``_``__act 方法,就可以实现一个最基础的Role了。

__init__ 方法用来初始化这个Action,而 _act 方法决定了当这个角色行动时它的具体行动逻辑

我们在 __init__ 方法中为 Role 配备了我们之前写好的动作 SimpleWriteCode。

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

配置完成之后,我们定义的行动SimpleWriteCode就会被加入到代办self.rc.todo

_act方法中,我们就会要求我们的智能体来执行这个动作,也就是我们需要调用todo.run()方法

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


​

当调用action的时候,用户输入作为instruction传递给action,当用户与Agent交互的时候,所有的内容都会被存在于其Memory中。

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

当需要获取记忆时(获取LLM输入的上下文),我们可以使用self.get_memories。函数定义如下:

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


​

在SimpleCoder中,我们只需要最近的一条记忆,也就是由用户指定的需求,然后传递给action

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



​

⑤运行SimpleCoder角色

我们需要对其进行初始化,用一个起始信息来进行运行操作

import asyncio
​
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())


import re
import asyncio
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
​
class SimpleWriteCode(Action):
​
    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:
    """
​
    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 SimpleCoder(Role):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._init_actions([SimpleWriteCode])
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: ready to {self.rc.todo}")
        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 sum of a list"
    role = SimpleCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)
​
asyncio.run(main())
​

实现多动作Agent

我们可以把多个动作组合起来,完成更复杂的任务。

就比如我们还希望写出来的代码马上执行,那它就是RunnableCoder

我们就需要两个action:SimpleWriteCode和SimpleRunCode

①编写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

②编写SimpleRunCode动作

一个action无需LLM也能运行,也能借助LLM运行,如果是simpleRunCode,就不涉及LLM。我们只需要一个子进程启动来或结果,这里用标准库的subprocess包fork一个子线程,并且运行外部的程序。。

subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。

第一个进程是Python程序本身,它执行了包含 SimpleRunCode 类定义的代码。第二个进程是由 subprocess.run 创建的,它执行了 python3 -c 命令,用于运行 code_text 中包含的Python代码。这两个进程相互独立,通过 subprocess.run 你的Python程序可以启动并与第二个进程进行交互,获取其输出结果。

class SimpleRunCode(Action):
​
    name: str = "SimpleRunCode"
​
    async def run(self, code_text: str):
        # 在Windows环境下,result可能无法正确返回生成结果,在windows中在终端中输入python3可能会导致打开微软商店
        result = subprocess.run(["python3", "-c", code_text], capture_output=True, text=True)
        # 采用下面的可选代码来替换上面的代码
        # result = subprocess.run(["python", "-c", code_text], capture_output=True, text=True)
        # import sys
        # 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

③定义RunnableCoder角色

  1. self._init_actions 初始化所有 Action

  2. 指定每次 Role 会选择哪个 Action。我们将 react_mode 设置为 "by_order",这意味着 Role 将按照 self._init_actions 中指定的顺序执行其能够执行的 Action。在这种情况下,当 Role 执行 _act 时,self.rc.todo 将首先是 SimpleWriteCode,然后是 SimpleRunCode

  3. 覆盖 _act 函数。Role 从上一轮的人类输入或动作输出中检索消息,用适当的 Message 内容提供当前的 Action (self.rc.todo),最后返回由当前 Action 输出组成的 Message

这里我们用Role类的 _set_react_mode 方法来设定我们action执行的先后顺序。

class RunnableCoder(Role):
​
    name: str = "Alice"
    profile: str = "RunnableCoder"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._init_actions([SimpleWriteCode, SimpleRunCode])
        self._set_react_mode(react_mode="by_order")
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: 准备 {self.rc.todo}")
        # 通过在底层按顺序选择动作
        # todo 首先是 SimpleWriteCode() 然后是 SimpleRunCode()
        todo = self.rc.todo
​
        msg = self.get_memories(k=1)[0] # 得到最相似的 k 条消息
        result = await todo.run(msg.content)
​
        msg = Message(content=result, role=self.profile, cause_by=type(todo))
        self.rc.memory.add(msg)
        return msg

④运行RunnableCoder角色

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

总结一下,下面就是完整的整合的多动作Agent代码

import os
import re
import subprocess
import asyncio
​
import fire
import sys
from metagpt.llm import LLM
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
​
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([sys.executable, "-c", code_text], capture_output=True, text=True)
        code_result = result.stdout
        logger.info(f"{code_result=}")
        return code_result
​
class RunnableCoder(Role):
​
    name: str = "Alice"
    profile: str = "RunnableCoder"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._init_actions([SimpleWriteCode, SimpleRunCode])
        self._set_react_mode(react_mode="by_order")
​
    async def _act(self) -> Message:
        logger.info(f"{self._setting}: ready to {self.rc.todo}")
        # 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 messagesA
        result = await todo.run(msg.content)
​
        msg = Message(content=result, role=self.profile, cause_by=type(todo))
        self.rc.memory.add(msg)
        return msg
​
async def main():
    msg = "write a function that calculates the sum of a list"
    role = RunnableCoder()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)
​
asyncio.run(main())

四.我的Agent作业

我们这里遇到了一个问题,没有打印后续的456.这是因为在 _act 方法中重新初始化了动作列表,但没有继续执行新的动作。我们需要确保在重新初始化新动作列表后继续顺序执行新动作。

以下是修正的代码

import asyncio
import logging
from typing import List
​
from metagpt.actions import Action
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
​
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
​
# 定义打印动作类
class PrintAction(Action):
    def __init__(self, name: str, message: str):
        super().__init__(name=name)
        self.message = message
​
    async def run(self):
        logger.info(self.message)
        return self.message
​
# 定义具有顺序执行能力的 Agent
class SequentialPrinter(Role):
​
    name: str = "SequentialPrinter"
    profile: str = "Printer"
​
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # 初始化动作列表
        self.init_actions([PrintAction("Print1", "打印1"), PrintAction("Print2", "打印2"), PrintAction("Print3", "打印3")])
        self.current_action_index = 0
        self.round = 0  # 轮次计数器
​
    def init_actions(self, actions: List[Action]):
        self.actions = actions
        self.current_action_index = 0
​
    async def _act(self) -> Message:
        if self.current_action_index < len(self.actions):
            # 执行当前动作
            action = self.actions[self.current_action_index]
            result = await action.run()
            self.current_action_index += 1
            msg = Message(content=result, role=self.profile, cause_by=type(action))
            self.rc.memory.add(msg)
            return msg
        else:
            # 增加轮次计数器
            self.round += 1
            if self.round == 1:
                # 动作列表执行完毕后重新初始化新的动作
                self.init_actions([PrintAction("Print4", "打印4"), PrintAction("Print5", "打印5"), PrintAction("Print6", "打印6")])
                self.current_action_index = 0
                return await self._act()
            else:
                # 所有动作完成后退出
                return Message(content="所有动作完成", role=self.profile, cause_by=None)
​
    async def run(self, initial_message: str):
        # 为了与现有框架兼容,将初始消息保存到记忆中
        self.rc.memory.add(Message(content=initial_message, role="User"))
        # 开始执行动作
        result_messages = []
        while True:
            msg = await self._act()
            if msg.content == "所有动作完成":
                break
            result_messages.append(msg.content)
        return result_messages
​
# 主函数
async def main():
    role = SequentialPrinter()
    initial_message = "开始执行动作"
    logger.info(initial_message)
    results = await role.run(initial_message)
    for result in results:
        logger.info(result)
​
# 运行主函数
asyncio.run(main())
​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值