Agent实现
实现一个单动作的Agent
①重写基类函数
我们需要重写Role基类的 _init_
与 _act
方法
在 _init_
方法中,我们需要声明 Agent 的name
(名称)profile
(类型)
我们使用 self._init_action
函数为其配备期望的动作 SimpleWriteCode
这个Action 应该能根据我们的需求生成我们期望的代码
在_act
方法中,我们需要编写智能体具体的行动逻辑,智能体将从最新的记忆中获取人类指令,运行配备的动作,MetaGPT将其作为待办事项 (self``.rc``.todo
) 在幕后处理,最后返回一个完整的消息。
②分析我们的需求
要实现一个 SimpleCoder
我们需要分析这个Agent 它需要哪些能力
编辑
首先我们需要让他接受用户的输入的需求,并记忆我们的需求,接着这个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类是最基本的信息类型
编辑
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角色
-
用
self._init_actions
初始化所有Action
-
指定每次
Role
会选择哪个Action
。我们将react_mode
设置为 "by_order",这意味着Role
将按照self._init_actions
中指定的顺序执行其能够执行的Action
。在这种情况下,当Role
执行_act
时,self.rc.todo
将首先是SimpleWriteCode
,然后是SimpleRunCode
。 -
覆盖
_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())