# MetaGPT智能体学习 | 第三章复习

目录

  • 资料来源
  • 安装
  • 测试环境
  • 学习过程
    1. 智能体的智能中枢:大模型问答模块_aask
    2. 智能体的行动模块:Action
    3. 智能体的角色模块: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
```#           

可以发现,这次给出的结果不再废话,而是输出纯纯的代码。这得益于以下几个处理:

  1. PROMPT_TEMPLATE:通过对指令进行提示工程,添加指引、约束和规范,让大模型返回可以方便我们处理的格式
  2. 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的方法很多,可以写循环也可以写条件,多试试可以加深理解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容