首页 > 编程语言 >MetaGPT day04 MetaGPT ActionNode

MetaGPT day04 MetaGPT ActionNode

时间:2024-01-26 17:57:07浏览次数:20  
标签:str self instruction day04 MetaGPT context ActionNode example

ActionNode

说明文档导读

# 什么是ActionNode?
    1.ActionNode是Action的通用化抽象
    2.ActionNode是SOP的最小单元

# ActionNode是Action的通用化抽象:
    反推可得知Action不够通用化?也就是说ActionNode的粒度比action更细?
	Action  -粒度更细-> ActionNode
    
# ActionNode是SOP的最小单元:
    每个SOP都可以看做由ActionNode组成的动作集合,也就是说:
    SOP = {ActionNode1,ActionNode2,...}

# ActionNode构成的动作树能够更有效地模拟语言的复杂结构,包括语法和语义关系,这有助于提升系统处理自然语言的能力。

ActionNode -构成-> 动作树 -模拟-> 语言结构
语言结构 = 语法 + 语义关系 + ...

# ActionNode使得大型语言模型(LLM)能够专注于每个最小单元的填槽任务,为智能体的规划能力提供了一种高效的数据结构,以便快速遍历各种状态和决策点,寻找最优或所需的路径 
    
# 填槽任务:
在多轮对话过程中,系统为完成任务而需要获取的关键信息我们称之为“槽位”。我的理解,填槽任务就是补全prompt。

ActionNode使得LLM能够专注于每个ActionNode的prompt补全任务。ActionNode提供了一种高效的数据结构(树),以便快速遍历各种状态和决策点,寻找最优或所需的路径。

# ActionNode为后续SOP的自动生成尝试提供了可能。

就像在游戏《我的世界》中,不同的技能可以组合成新的技能一样,通过动作树节点的编排,我们可以创造出新的技能,从而更接近自然语言编程的目标。这种统一的抽象和结构化的方法,使得系统能够更好地适应和学习,为智能体的进化提供了坚实的基础

ActionNode功能

在MG中我们希望将llm完成的功能收纳到ActionNode进行实现。(重写已有的一些功能?)
更多详见源码:
https://github.com/geekan/MetaGPT/blob/dev/metagpt/actions/action_node.py

# ActionNode功能说明:
fill / review / revise / plan / cache / with_docs 

# fill: 
填槽。实现执行传入的prompt并获取结果返回,并将结果存储在对象自身中。

# review: 
审查填槽效果。评价填槽完成度与问题。

# revise: 
修正。基于review的建议和结果进行优化。

# plan: 
规划。生成新的child槽,可以只有instruction。

# cache: 
支持程序记忆。ActionNode在做具体的动作:比如fill时,需要有对应的“程序记忆”,用来缓存、重放、参考、调用。
如果遇到了历史的query,有限调用缓存来解决。
   例子1:生成产品文档
      1. 第一次fill:2048 game的产品文档 -> PRD:实际的2048产品文档
        1. 生成了实际的2048产品文档
        2. GPT-4 review分数满分
        3. 缓存:因为review满分,所以这个pair被缓存到本地
      2. 第二次fill:2048 game的产品文档 -> PRD:实际的2048产品文档
        1. 重放:使用了之前缓存的结果,直接给出了PRD
      3. 第三次fill:4096 game的产品文档 -> PRD:4096的产品文档
        1. 参考:检索了2048 game的文档,用来做参考
        2. 生成了实际的4096产品文档(实际单独生成难度比较高,这里相当于降低了难度)
   例子2:生成代码(以下简写)
      1. 第一次fill:生成2048 move函数,成功缓存
      2. 第二次fill:生成2048 move函数,复用缓存
      3. 第三次fill:生成8*8格子的move函数,参考缓存
      4. 第四次fill:需要写一个新函数,调用已有的k个函数,而这些函数存在于memory之中
        
# ith_docs:
用于实现RAG(索引增强生成)。即用外部文档做检索,检索出优质内容再传入llm增强生成效果。(这部分代码将在0.7版本后支持)

ActionNode参数结构

schema: str  # 数据的结构。比如以下三种: raw/json/markdown, 默认: ""

# 动作上下文
context: str  # 所有上下文,包括所有必要信息
llm: BaseLLM  # 具有 aask 接口的 LLM
children: dict[str, "ActionNode"]  # 子节点的字典,键为字符串,值为 "ActionNode" 类型的对象

# 动作输入
key: str  # 产品需求 / 文件列表 / 代码
expected_type: Type  # 例如 str / int / float 等
# context: str  # 历史中的所有内容
instruction: str  # 应该遵循的指导
example: Any  # 上下文学习的示例

# 动作输出
content: str
instruct_content: BaseModel

快速掌握ActionNode的用法:打印斐波那契数列

目标:打印前10个斐波那契数列的数字,并且,LLM要能以特定的可解析的格式来返回斐波那契数列,并通过格式解析实现逐个打印数字的效果。

image-20240124155404955
import asyncio
import re

from metagpt.actions.action import Action
from metagpt.actions.action_node import ActionNode
from metagpt.logs import logger
from metagpt.roles import Role
from metagpt.schema import Message

# 将思考斐波那契数列的10个数字作为prompt输入,在这里我们将“思考需要生成的数字列表”作为命令(instruction)写入
# 将期望返回格式(expected_type)设置为str,无需设置例子(example)
SIMPLE_THINK_NODE = ActionNode(
    key="Simple Think Node",
    expected_type=str,
    instruction="""
            Think about what list of numbers you need to generate
            """,
    example=""
)

# 在这里通过命令(instruction)来规定需要生成的数字列表格式,提供例子(example)来帮助LLM理解
SIMPLE_CHECK_NODE = ActionNode(
    key="Simple CHECK Node",
    expected_type=str,
    instruction="""
            Please provide the number list for me, strictly following the following requirements:
            1. Answer strictly in the list format like [1,2,3,4]
            2. Do not have extra spaces or line breaks.
            Return the list here:
            """,
    example="[1,2,3,4]"
            "[4,5,6]",
)


class THINK_NODES(ActionNode):
    def __init__(self, name="Think Nodes", expected_type=str, instruction="", example=""):
        super().__init__(key=name, expected_type=str, instruction=instruction, example=example)
        self.add_children([SIMPLE_THINK_NODE, SIMPLE_CHECK_NODE])  # 初始化过程,将上面实现的两个子节点加入作为THINK_NODES类的子节点

    async def fill(self, context, llm, to="raw", mode="auto", strgy="complex"):
        self.set_llm(llm)
        self.set_context(context)
        if hasattr(self, 'to'):
            to = self.to

        if strgy == "simple":
            return await self.simple_fill(to=to, mode=mode)
        elif strgy == "complex":
            # 这里隐式假设了拥有children
            child_context = context  # 输入context作为第一个子节点的context
            for _, i in self.children.items():
                i.set_context(child_context)  # 为子节点设置context
                child = await i.simple_fill(to=to, mode=mode)
                child_context = child.content  # 将返回内容(child.content)作为下一个子节点的context

            self.content = child_context  # 最后一个子节点返回的内容设置为父节点返回内容(self.content)
            return self


class SimplePrint(Action):
    """
    Action that print the num inputted
    """
    input_num = 0

    def __init__(self, name="SimplePrint", input_num: int = 0):
        super().__init__()

        self.input_num = input_num

    async def run(self, **kwargs):
        print(str(self.input_num) + "\n")
        return 0


class ThinkAction(Action):
    """
    Action that think
    """

    def __init__(self, name="ThinkAction", context=None, llm=None):
        super().__init__()
        self.node = THINK_NODES()  # 初始化Action时,初始化一个THINK_NODE实例并赋值给self.node

    async def run(self, instruction) -> list:
        PROMPT = """
            You are now a number list generator, follow the instruction {instruction} and 
            generate a number list to be printed please.
            """

        prompt = PROMPT.format(instruction=instruction)
        rsp_node = await self.node.fill(context=prompt, llm=self.llm, to="raw",
                                        strgy="complex")  # 运行子节点,获取返回(返回格式为ActionNode)(注意设置 schema="raw" )
        rsp = rsp_node.content  # 获取返回的文本内容

        rsp_match = self.find_in_brackets(rsp)  # 按列表格式解析返回的文本内容,定位“[”与“]”之间的内容

        try:
            rsp_list = list(map(int, rsp_match[0].split(',')))  # 按列表格式解析返回的文本内容,按“,”对内容进行分割,并形成一个python语法中的列表

            return rsp_list
        except:
            return []

    @staticmethod
    def find_in_brackets(s):
        pattern = r'\[(.*?)\]'
        match = re.findall(pattern, s)
        return match


class Printer(Role):

    def __init__(self, name="Jerry", profile="Printer", goal="Print the number", constraints=""):
        super().__init__()

        self._init_actions([ThinkAction])
        # self.num_list = list()

    async def _think(self) -> None:
        """Determine the action"""
        # logger.info(self._rc.state)

        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 _prepare_print(self, num_list: list) -> Message:
        """Add actions"""
        actions = list()

        for num in num_list:
            actions.append(SimplePrint(input_num=num))

        self._init_actions(actions)
        self._rc.todo = None
        return Message(content=str(num_list))

    async def _act(self) -> Message:
        """Action"""
        todo = self._rc.todo

        if type(todo) is ThinkAction:
            msg = self._rc.memory.get(k=1)[0]
            self.goal = msg.content
            resp = await todo.run(instruction=self.goal)
            # logger.info(resp)

            return await self._prepare_print(resp)

        resp = await todo.run()
        # logger.info(resp)

        return Message(content=resp, role=self.profile)

    async def _react(self) -> Message:
        """"""
        while True:
            await self._think()

            if self._rc.todo is None:
                break
            msg = await self._act()

        return msg


async def main():
    msg = "Provide the first 10 numbers of the Fibonacci series"
    role = Printer()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)


if __name__ == '__main__':
    asyncio.run(main())

prompt示例:

## context

            You are now a number list generator, follow the instruction Provide the first 10 numbers of the Fibonacci series and 
            generate a number list to be printed please.
            

-----

## format example
[CONTENT]
{'Simple Think Node': ''}
[/CONTENT]

## nodes: "<node>: <type>  # <comment>"
- Simple Think Node: <class 'str'>  # 
            Think about what list of numbers you need to generate
            


## constraint

- Language: Please use the same language as the user input.
- Format: output wrapped inside [CONTENT][/CONTENT] as format example, nothing else.


## action
Fill in the above nodes based on the format example.

ActionNode示例

如下,将四个子节点,通过from_children方法,添加到一个父节点中。在父节点使用compile编译,这将根据所有子节点和一个模板拼接成一个prompt。然后我们会将这个prompt使用fill方法传给llm,得到返回结构,最终存储到ActionNode对象自身。

import asyncio
from metagpt.actions.action_node import ActionNode
from metagpt.logs import logger
from metagpt.llm import LLM

LANGUAGE = ActionNode(
    key="语言",
    expected_type=str,
    instruction="提供项目中使用的语言,通常应与用户的需求语言相匹配。",
    example="en_us",
)

PROGRAMMING_LANGUAGE = ActionNode(
    key="编程语言",
    expected_type=str,
    instruction="Python/JavaScript或其他主流编程语言。",
    example="Python",
)

ORIGINAL_REQUIREMENTS = ActionNode(
    key="原始需求",
    expected_type=str,
    instruction="将原始的用户需求放在这里。",
    example="创建2048游戏",
)

PROJECT_NAME = ActionNode(
    key="项目名称",
    expected_type=str,
    instruction="根据“原始需求”的内容,使用蛇形命名风格为项目命名,例如 'game_2048' 或 'simple_crm'。",
    example="game_2048",
)

NODES = [
    LANGUAGE,
    PROGRAMMING_LANGUAGE,
    ORIGINAL_REQUIREMENTS,
    PROJECT_NAME,
]

WRITE_PRD_NODE = ActionNode.from_children("WritePRD", NODES)


async def main():
    prompt = WRITE_PRD_NODE.compile(context="你是一个产品经理,你需要为游戏幻兽帕鲁写需求文档", to='markdown', mode='auto')
    logger.info(prompt)
    respone = await LLM().aask(prompt)
    logger.info(respone)


if __name__ == '__main__':
    asyncio.run(main())

如下是prompt,我需要说明的是:

  1. 大模型的返回结构,可以通过compile的to参数(老版本0.4是schema)配置,可以返回markdown或者json。
  2. constraint、action是模板中的固定部分。
## context
你是一个产品经理,你需要为游戏幻兽帕鲁写需求文档

-----

## format example
[CONTENT]
- 语言: en_us
- 编程语言: Python
- 原始需求: 创建2048游戏
- 项目名称: game_2048

[/CONTENT]

## nodes: "<node>: <type>  # <comment>"
- 语言: <class 'str'>  # 提供项目中使用的语言,通常应与用户的需求语言相匹配。
- 编程语言: <class 'str'>  # Python/JavaScript或其他主流编程语言。
- 原始需求: <class 'str'>  # 将原始的用户需求放在这里。
- 项目名称: <class 'str'>  # 根据“原始需求”的内容,使用蛇形命名风格为项目命名,例如 'game_2048' 或 'simple_crm'。


## constraint
- 语言:请使用与用户输入相同的语言。
- 格式:输出请使用 [CONTENT][/CONTENT] 进行包装,如格式示例所示,不要添加其他内容。


## action
Fill in the above nodes based on the format example.

从prompt的角度来看。ActionNode就是映射了一个prompt中的参数(槽位)。让我们可以一次次的询问llm,得到每一个槽位的值。

LANGUAGE = ActionNode(
    key="语言",  # 这对应了llm返回字典的键值
    expected_type=str,  # 期待llm返回的这个槽位是str类型
    instruction="提供项目中使用的语言,通常应与用户的需求语言相匹配。",  # 指导llm该如何填槽,如何返回这个参数
    example="en_us",  # 提示的例子,也就是正确返回的示例
)

ActionNode.add_children

# 做了什么事?
批量增加子节点(ActionNode),将这些子节点添加到一个children属性(字典)中。字典的键值是子节点的key属性。

举例添加两个子节点:
SIMPLE_THINK_NODE = ActionNode(
    key="Simple Think Node",
)

SIMPLE_CHECK_NODE = ActionNode(
    key="Simple CHECK Node",
)
	
就会得到类似:
	self.children = {'Simple Think Node':SIMPLE_THINK_NODE_obj,'Simple CHECK Node':SIMPLE_CHECK_NODE_obj,...}
	
class THINK_NODES(ActionNode):
    def __init__(self, name="Think Nodes", expected_type=str, instruction="", example=""):
        super().__init__(key=name, expected_type=str, instruction=instruction, example=example)
        self.add_children([SIMPLE_THINK_NODE, SIMPLE_CHECK_NODE])
        
def add_children(self, nodes: List["ActionNode"]):
    """批量增加子ActionNode"""
    for node in nodes:
        self.add_child(node)

def add_child(self, node: "ActionNode"):
	"""增加子ActionNode"""
    self.children[node.key] = node
        

ActionNode.set_llm

# 做了什么事?
1.给父节点设置llm属性
2.递归的给子节点设置llm属性(即子节点还有子节点,也会设置)

这里的llm是,具有预定义系统消息的大型语言模型。
def set_llm(self, llm):
    self.set_recursive("llm", llm)
    
def set_recursive(self, name, value):
    setattr(self, name, value)
    for _, i in self.children.items():
        i.set_recursive(name, value)
        
self.children = {'Simple Think Node':SIMPLE_THINK_NODE_obj,'Simple CHECK Node':SIMPLE_CHECK_NODE_obj,...}

ActionNode.set_context

# 做了什么事?
同上,递归的给所有节点设置上下文属性。
def set_context(self, context):
    self.set_recursive("context", context)

ActionNode.compile

# 做了什么事?
版本为0.5.2,只实现了children模式。

def compile(self, context, to="json", mode="children", template=SIMPLE_TEMPLATE) -> str:
    """
    mode: all/root/children
        mode="children": 编译所有子节点为一个统一模板,包括instruction与example
        mode="all": NotImplemented
        mode="root": NotImplemented
    """

    # FIXME: json instruction会带来格式问题,如:"Project name": "web_2048  # 项目名称使用下划线",
    self.instruction = self.compile_instruction(to="markdown", mode=mode)
    self.example = self.compile_example(to=to, tag="CONTENT", mode=mode)
    prompt = template.format(
        context=context, example=self.example, instruction=self.instruction, constraint=CONSTRAINT
    )
    return prompt

标签:str,self,instruction,day04,MetaGPT,context,ActionNode,example
From: https://www.cnblogs.com/passion2021/p/17990169

相关文章

  • MetaGPT day03 MetaGPT 订阅智能体
    订阅智能体基本介绍#什么是订阅智能体MetaGPT提供了Agent的开发能力,Agent可以协助我们解决日常的一些问题,其中一个应用场景就是我们可以让Agent为我们关注某些想关注的信息,当有我们关注的事件发生时,Agent获取信息并进行处理,然后通过一些如邮件、微信、discord等通知渠道将处理......
  • MetaGPT day02: MetaGPT Role源码分析
    MetaGPT源码分析思维导图MetaGPT版本为v0.4.0,如下是frommetagpt.rolesimportRole,Role类执行Role.run时的思维导图:概述其中最重要的部分是_react,里面包含了一个循环,在循环中交替执行_think和_act,也就是让llm先思考再行动。_think中决定了llm下一个执行的动作是什么,这个动作......
  • day04 dos
    dos原理相对路径和绝对路径dos常见指令echo/type:创建有内容/空文件......
  • MetaGPT day01: MetaGPT作者代码走读、软件公司初始示例
    LLM发展历史-2013年word2vec提出,但效果不好-2017年Transformer结构提出,降低网络复杂度-2018年BERT预训练语言模型效果显著提升-2019年GPT-3推出,采用大规模预训练-2020年InstructionTuning提出,实现零样本学习-2022年InstructGPT解决模型毒性问题-当前GPT-4成本......
  • 代码随想录day04 两两交换链表中的节点 删除链表的倒数第N个节点 链表相交 环形链表
    两两交换链表中的节点题目:这题画一下链表会比较清晰写写画画指针位置很快就可以写出来一开始以为一个tmp就够用了写着写着发现需要多一个代码:删除链表的倒数第N个节点:没什么思路只好先看看视频思路视频思路很简单也很清晰只需要两个指针一快一慢两指针的间......
  • day04 进制和编码
    day04进制和编码1.pythont代码的运行方式脚本时交互式2.进制二进制(字符串)八进制(整型)十进制(字符串)十六进制(字符串)2.1进制转换v1=bin(35)#十进制转换成二进制v2=oct(35)#十进制转换成八进制v3=hex(35)#十进制转换成十六进制print(v1,v2,v3)结果输......
  • Day04运算符
    基本运算符//前四个,必须掌握!!!!!!!!!!!!!!!算木运算符:+,-,*,/,%,++,--赋值运算符=关系运算符:>,<,>=,<=,==,!=,instanceof运算符:&&,||,!//下面的,了解就行--------------------位运算符:&,|,^,~,>>、<<,>>>......
  • Day04常量
    常量常量(Constant):初始化(initialize)后不能再改変的値!不会変动的値。所谓常量可以理解成一种特殊的変量,它的値被定后,在程序运行程中不允许被改変。//finalfinal常量名=値;finaldoublePI=3.14;System.out.println("==============================");public......
  • Day04类型转换
    类型转换注意点:1.不能对布尔值进行转换2.不能把对象类型转换为不相干的类型3.在把高容量转换到低容量的时候,强制转换4.转换的时候可能存在内存溢出,或者精度问题!高转低,强制转换;低转高,自动转换低------------------------------------------->高byte,short,char->int-......
  • Day04
    Day04注释注释的分类单行注释//注释信息多行注释/*注释信息*/文档注释/**注释信息**/注释的注意事项注释的内容不参与编译也不参与运行的多行注释会以最近的"*/"作为注释的结尾,不管是单行注释还是多行注释,在书写的时候都不要嵌套关键字关键字的特点......