一、前置知识:
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())