目录
- 资料来源
- 安装
- 测试环境
- 学习过程
- 智能体的智能中枢:大模型问答模块
_aask
- 智能体的行动模块:
Action
- 智能体的角色模块:
Role
- 智能体的智能中枢:大模型问答模块
- 作业
资料来源
MetaGPT github:https://github.com/geekan/MetaGPT
MetaGPT 官方文档:https://docs.deepwisdom.ai/main/zh/guide/get_started/introduction.html
MetaGPT 官方教程(飞书):https://deepwisdom.feishu.cn/wiki/KhCcweQKmijXi6kDwnicM0qpnEf
Datawhale 合作课程:https://spvrm23ffj.feishu.cn/docx/RZNpd5uXfoebTPxMXCFcWHdMnZb
安装
本笔记基于目前最新的v0.7.*
版本。
作为一种学习途径,我会尝试根据最新版本修改教程中不兼容的代码。
使用mamba
(conda的加速版)安装依赖:
mamba create -n metagpt
git clone https://github.com/geekan/MetaGPT.git
cd MetaGPT
git checkout v0.7.3 # choose the latest v0.7.*
pip install ./
v0.7
版本变更了配置文件生成方式:
metagpt --init-config
在生成的$HOME/.metagpt/config2.yaml
文件中添加对应大语言模型的API key。
测试环境
目前我使用Jupyter Notebook
进行测试,需要安装jupyterlab
:
#安装jupyterlab
conda install -c conda-forge jupyterlab
#启动jupyterlab
jupyter-lab
学习过程
1. 智能体的智能中枢:与大模型的模块_aask
首先来感受智能体的核心:与大模型交互的模块_aask
:
from typing import Optional
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)
测试该模块,直观感受它的作用:
msg = "生成一段DNA序列的反向互补序列."
myask = Action()
resp01 = await myask._aask(msg)
output:
生成DNA序列的反向互补序列需要遵循几个基本原则:DNA由四种核苷酸组成,分别是腺嘌呤(A)、胸腺嘧啶(T)、胞嘧啶(C)和鸟嘌呤(G)。在DNA的反向互补序列中,A与T互补,C与G互补。生成反向互补序列时,首先需要将原序列反向,然后将每个核苷酸替换为其互补核苷酸。
例如,如果原始DNA序列是:
5'-ATGCTAGC-3'
那么,首先将其反向:
3'-CGATGCAT-5'
然后将每个核苷酸替换为其互补核苷酸:
5'-GCTACGTA-3'
这就是原始DNA序列的反向互补序列。
如果你有一个具体的DNA序列需要转换,请提供该序列
2024-03-02 21:11:44.964 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.009 | Max budget: $10.000 | Current cost: $0.009, prompt_tokens: 33, completion_tokens: 295
,我将为你生成其反向互补序列。
注意,在jupyter
中需要直接使用await获取异步调用结果。
同时可以感受到,_aask
其实就是与指定LLM进行问答的过程。给予提示词,返回大模型推理、思考、组织的结果。
2. 智能体的行动模块:Action
下面再来看一下智能体利用大模型问答能力来实现的动作模块Action
:
from metagpt.actions import Action
import re # 导入正则表达式库
class SimpleWriteCode(Action):
# 定义代码prompt模板
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
name: str = "SimpleWriteCode"
# 定义异步方法run,用于接收指令并运行生成代码的函数
async def run(self, instruction: str):
# 将输入内容整合到Prompt模版中
prompt = self.PROMPT_TEMPLATE.format(instruction=instruction)
# 发送Prompt,向LLM提问,接收答复
rsp = await self._aask(prompt)
# 解析答复文本,提取其中的代码部份
code_text = SimpleWriteCode.parse_code(rsp)
# 返回提取出的代码文本
return code_text
# 定义静态方法parse_code,用于从答复文本中解析提取出代码
@staticmethod
def parse_code(rsp):
# 定义正则表达式匹配模式,用于识别包含在```python```标记中的代码
pattern = r"```python(.*)```"
# 使用正则表达式搜索匹配的代码文本
match = re.search(pattern, rsp, re.DOTALL)
# 如果搜索到匹配,提取第一个捕获组(即我们需要的代码),否则返回完整的响应文本
code_text = match.group(1) if match else rsp
# 返回提取的代码文本
return code_text
先感受下同样的输入指令,SimpleWriteCode
和_aask
输出的不同:
msg = "生成一段DNA序列的反向互补序列."
test = SimpleWriteCode()
resp02 = await test.run(msg)
output:
```python
def reverse_complement_dna(dna_sequence):
complement = {'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C'}
return ''.join([complement[base] for base in reversed(dna_sequence)])
# Test cases
print(reverse_complement_dna("ATCG")) # Expected output: CGAT
print(reverse_complement_dna("AAGCTT")) # Expected output: A
2024-03-02 21:22:32.325 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.004 | Max budget: $10.000 | Current cost: $0.004, prompt_tokens: 73, completion_tokens: 103
AGCTT
```#
可以发现,这次给出的结果不再废话,而是输出纯纯的代码。这得益于以下几个处理:
- PROMPT_TEMPLATE:通过对指令进行提示工程,添加指引、约束和规范,让大模型返回可以方便我们处理的格式
- parse_code(): 格式统一之后,我们就能用通用代码,将其中我们需要的部份提取出来,而将不需要的废话和修饰格式去除。
3. 智能体的角色模块:Role
下面封装一个可以灵活运用诸如上述动作模块的智能体角色:一位生信程序员「马湃森」并让他拥有写python代码的能力。
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
class BioCoder(Role):
name: str = "马湃森"
profile: str = "BioCoder"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_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
注意:0.7版本中,_init_actions
被替换为了set_actions
. _rc
也在先前的版本中改为了rc
.
依然利用相同的提示词来感受下Role的能力:
msg = "生成一段DNA序列的反向互补序列."
role = BioCoder()
resp03 = await role.run(msg)
output:
2024-03-02 21:24:12.863 | INFO | __main__:_act:14 - 马湃森(BioCoder): ready to SimpleWriteCode
```python
def reverse_complement(dna_sequence):
complement = {'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C'}
return ''.join([complement[base] for base in reversed(dna_sequence)])
# Test case 1
print(reverse_complement("ATCG")) # Expected output: CGAT
# Test case 2
print(reverse_complement("GGCCAA")) # Expected output: TTGGCC
```2024-03-02 21:24:16.965 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.004 | Max budget: $10.000 | Current cost: $0.004, prompt_tokens: 87, completion_tokens: 103
我们发现输出内容跟直接调用SimpleWriteCode
动作是几乎相同的。因为在先前的定义中,这个角色只被赋予了SimpleWriteCode
动作,还没有发挥role的全部潜力。
下面我们再‘教’给马派森一个新的动作:运行SimpleWriteCode
给出的代码:
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
code_stderr = result.stderr
logger.info(f"{code_result=}")
if(code_stderr is not None):
logger.info(f"{code_stderr=}")
code_result = code_result + f"{code_stderr=}"
return code_result
# test
tRun = SimpleRunCode()
resp04 = await tRun.run(resp03.content)
output:
2024-03-02 21:35:02.651 | INFO | __main__:run:8 - code_result='CGAT\nTTGGCC\n'
2024-03-02 21:35:02.653 | INFO | __main__:run:10 - code_stderr=''
可以看出,这个步骤很简单,都不需要问LLM, 只需要调用subprocess运行一下即可判断脚本写的正不正确.
接下来重新定义role,正式让「马湃森」的技能升级到lv.2, 增加SimpleRunCode的能力:
from metagpt.roles.role import Role, RoleReactMode
import re
class RunnableBioCoder(Role):
name: str = "马湃森"
profile: str = "RunnableBioCoder"
def __init__(self, **kwargs):
super().__init__( **kwargs)
self.set_actions([SimpleWriteCode, SimpleRunCode]) # 就在这里增加Actions
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})")
# 通过在底层按顺序选择动作
# 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
再次测试效果:
msg = "生成一段DNA序列的反向互补序列."
role = RunnableBioCoder()
resp05 = await role.run(msg)
output:
2024-03-02 21:35:30.025 | INFO | __main__:_act:14 - 马湃森(RunnableBioCoder): to do SimpleWriteCode(SimpleWriteCode)
```python
def reverse_complement(dna_sequence):
complement = {'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C'}
return ''.join([complement[base] for base in reversed(dna_sequence)])
# Test case 1
print(reverse_complement("ATCG")) # Expected output: CGAT
# Test case 2
print(reverse_complement("GGATCC")) # Expected output: GGATCC
```2024-03-02 21:35:36.567 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.004 | Max budget: $10.000 | Current cost: $0.004, prompt_tokens: 88, completion_tokens: 103
2024-03-02 21:35:36.570 | INFO | __main__:_act:14 - 马湃森(RunnableBioCoder): to do SimpleRunCode(SimpleRunCode)
2024-03-02 21:35:37.652 | INFO | __main__:run:8 - code_result='CGAT\nGGATCC\n'
2024-03-02 21:35:37.653 | INFO | __main__:run:10 - code_stderr=''
如上,实现了对生成代码的立刻运行测试。
作业
经过上面的学习,我想你已经对 MetaGPT 的框架有了基本了解,现在我希望你能够自己编写这样一个 agent
- 这个 Agent 拥有三个动作 打印1 打印2 打印3(初始化时 init_action([print,print,print]))
- 重写有关方法(请不要使用act_by_order,我希望你能独立实现)使得 Agent 顺序执行上面三个动作
- 当上述三个动作执行完毕后,为 Agent 生成新的动作 打印4 打印5 打印6 并顺序执行,(之前我们初始化了三个 print 动作,执行完毕后,重新init_action([...,...,...]),然后顺序执行这个新生成的动作列表)
答:
先定义一个Action
:
from metagpt.actions import Action
class print123(Action):
PROMPT_TEMPLATE: str = """
I may say something with explaination and/or description.
Focus on the ojbject I mentioned. It should be a number.
Remember this number, response this number + 1.
Otherwise, if you realize that the object I mentioned is not a number, response NOTHING.
Return ```markdown your_response``` with NO other texts,
I say: {message}
your responce:
"""
name: str = "print123"
prefix: str = "repeat what I say + 1. NO extral word." # aask*时会加上prefix,作为system_message
desc: str = "一个简单的打印动作,你说啥它打印啥+1。" # for skill manager
async def run(self, message: str):
prompt = self.PROMPT_TEMPLATE.format(message=message)
rsp = await self._aask(prompt)
rsp_text = print123.parse_text(rsp)
return rsp_text
@staticmethod
def parse_text(rsp):
pattern = r"```markdown(.*)```"
match = re.search(pattern, rsp, re.DOTALL)
text = match.group(1) if match else rsp
return text
测试:
msg = "My luck item is cake."
msg = "My luck number is 7."
msg = "Today is Sunday."
msg = "This month is September."
msg = "Now it's 5 o'clock"
msg = "Now it's five o'clock"
msg = "I say fourteen."
myprint = print123()
resp123 = await myprint.run(msg)
output:
2024-03-02 16:46:22.940 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.001 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 103, completion_tokens: 6
```markdown
15
```#
再来构建一个将这个打印执行两遍,每遍三次的Role
:
from metagpt.roles import Role
from metagpt.schema import Message
from metagpt.logs import logger
class print126(Role):
name: str = "Printer"
profile: str = "printer"
def __init__(self, **kwargs):
super().__init__(**kwargs)
#self.set_actions([print123,print123,print123])
async def _think(self) -> None:
"""Determine the next action to be taken by the role."""
msg = self.get_memories(k=1)[0] # find the most recent messages
if self.rc.todo is None:
self._set_state(0)
return
if self.rc.state + 1 < len(self.states):
self._set_state(self.rc.state + 1)
else:
self.rc.todo = None
async def _react(self) -> Message:
"""Execute the assistant's think and actions.
Returns:
A message containing the final result of the assistant's actions.
"""
acts = 2
while acts > 0 :
self.set_actions([print123,print123,print123])
while True:
await self._think()
if self.rc.todo is None:
break
resp = await self._act()
acts = acts - 1
return resp
async def _act(self) -> Message:
logger.info(f"{self._setting}: ready to {self.rc.todo}")
todo = self.rc.todo
msg = self.get_memories(k=1)[0] # find the most recent messages
printContent = await todo.run(msg.content)
msg = Message(content=printContent, role=self.profile, cause_by=type(todo))
self.rc.memory.add(msg)
return msg
测试效果:
msg='0'
theRole = print126()
resp = await theRole.run(msg)
output:
2024-03-02 18:16:50.882 | INFO | __main__:_act:44 - Printer(printer): ready to print123
2024-03-02 18:16:53.005 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.001 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
2024-03-02 18:16:53.008 | INFO | __main__:_act:44 - Printer(printer): ready to print123
```markdown
1
```#
2024-03-02 18:16:55.001 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.003 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
2024-03-02 18:16:55.002 | INFO | __main__:_act:44 - Printer(printer): ready to print123
```markdown
2
```#
2024-03-02 18:16:55.914 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.004 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
2024-03-02 18:16:55.917 | INFO | __main__:_act:44 - Printer(printer): ready to print123
```markdown
3
```#
2024-03-02 18:16:56.783 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.005 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
2024-03-02 18:16:56.785 | INFO | __main__:_act:44 - Printer(printer): ready to print123
```markdown
4
```#
2024-03-02 18:16:57.993 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.006 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
2024-03-02 18:16:57.995 | INFO | __main__:_act:44 - Printer(printer): ready to print123
```markdown
5
```#
2024-03-02 18:16:59.073 | INFO | metagpt.utils.cost_manager:update_cost:52 - Total running cost: $0.008 | Max budget: $10.000 | Current cost: $0.001, prompt_tokens: 109, completion_tokens: 6
```markdown
6
```#
这里react的方法很多,可以写循环也可以写条件,多试试可以加深理解。