参考资料:智能体入门 | MetaGPT
Agent = LLM + Observation + Thought + Action + Memory
-大语言模型(LLM):LLM作为智能体的“大脑”部分,使其能够处理信息,从交互中学习,做出决策并执行行动。
-观察:这是智能体的感知机制,使其能够感知其环境。智能体可能会接收来自另一个智能体的文本消息、来自监视摄像头的视觉数据或来自客户服务录音的音频等一系列信号。这些观察构成了所有后续行动的基础。
-思考:思考过程涉及分析观察结果和记忆内容并考虑可能的行动。这是智能体内部的决策过程,其可能由LLM进行驱动。
-行动:这些是智能体对其思考和观察的显式响应。行动可以是利用 LLM 生成代码,或是手动预定义的操作,如阅读本地文件。此外,智能体还可以执行使用工具的操作,智能体还可以执行使用工具的操作,包括在互联网上搜索天气,使用计算器进行数学计算等。
-记忆:智能体的记忆存储过去的经验。这对学习至关重要,因为它允许智能体参考先前的结果并据此调整未来的行动。
多智能体 = Agents + Environment + SOP + Communication + Economy
这些组件各自发挥着重要的作用:
Agents:在上面单独定义的基础上,在多智能体系统中的智能体协同工作,每个智能体都具备独特有的LLM、观察、思考、行动和记忆。
Environment:环境是智能体生存和互动的公共场所。智能体从环境中观察到重要信息,并发布行动的输出结果以供其他智能体使用。
SOP:这些是管理智能体行动和交互的既定程序,确保系统内部的有序和高效运作。例如,在汽车制造的SOP中,一个智能体焊接汽车零件,而另一个安装电缆,保持装配线的有序运作。
Communication:通信是智能体之间信息交流的过程。它对于系统内的协作、谈判和竞争至关重要。
Economy:这指的是多智能体环境中的价值交换系统,决定资源分配和任务优先级。
案例:
-在环境中,存在三个智能体Alice、Bob和Charlie,它们相互作用。
-他们可以将消息或行动的输出结果发布到环境中,同时也会被其他智能体观察到。
-下面将揭示智能体Charlie的内部过程,该过程同样适用于Alice和Bob。
-在内部,智能体Charlie具备我们上述所介绍的部分组件,如LLM、观察、思考、行动。
-Charlie思考和行动的过程可以由LLM驱动,并且还能在行动的过程中使用工具。
-Charlie观察来自Alice的相关文件和来自Bob的需求,获取有帮助的记忆,思考如何编写代码,执行写代码的行动,最终发布结果。
-Charlie通过将结果发布到环境中以通知Bob。Bob在接收后回复了一句赞美的话。
让agent写一个产品需求文档
Role 类是智能体的逻辑抽象。
一个 Role 能执行特定的 Action,拥有记忆、思考并采用各种策略行动。基本上,它充当一个将所有这些组件联系在一起的凝聚实体。
有以下几个属性(具体作用后面会分析):
Role的运行时上下文
拥有environment,memory,state,todo等等
有以下几个属性(具体作用后面会分析):
Action 是动作的逻辑抽象,里面有一个run方法,指具体执行的Action的动作(由于Action是抽象类,所以没有定义具体的Action)
有以下几个属性(具体作用后面会分析):
环境,承载一批角色,角色可以向环境发布消息,可以被其他角色观察到
有以下几个属性(具体作用后面会分析):
ProductManager初始化:
①调用父类Role初始化函数,创建RoleSetting,创建LLM,创建RoleContext
②初始化actions和states(图里是WritePRD构成的单链表)
a. 将actions放到自己的属性里
b. 以key-value对的形式,将actions里的index与action放到states里面
③添加watch Actions,监听对应的Action
①将msg存到memory中
②执行react,获取response
关键变量:state当前的状态,todo需要完成的任务
a.开启循环,当react循环次数达到上限,或者todo为空时,退出循环
b._think:根据角色对应的prompt前缀,以及memory,让大模型给出下一步的Action,并且更新state和todo
构建prompt并发给LLM获取答案:
将RoleContext中的history,在init_actions时构建的states(即Index-Action对)和但当前的状态state等信息发给LLM,让LLM给出下一个state(也即Actions所对应的index)
根据LLM的答案,更新state和todo
c._act:根据todo和memory,执行action,得到结果后,更新msg,更新todo,更新memory
具体action:
其中,prd = await self._aask_v1(prompt, "prd", OUTPUT_MAPPING, format=format)这句,会调用llm_api去询问大模型,得到结果
将结果添加到RoleContext的memory中
d.更新token,循环次数等等
e.进入下一次循环
f.当循环退出时,返回rsp
③将rsp发布到RoleContext的environment中
将msg广播到environment
放到消息队列里面,并且添加到environment的memory中
1初始化Role
1.1创建RoleContext等
1.2初始化actions,初始化states
1.3添加Action监听
2 role.run()
2.1将msg存到memory中
2.2执行react
2.2.1开启循环,当react循环次数达到上限,或者todo为空时,推出循环
2.2.2_think:根据角色对应的prompt前缀,以及memory,让大模型给出下一步的Action,并且更新state和todo
将RoleContext中的history,在init_actions时构建的states(即Index-Action对)和但当前的状态state等信息发给LLM,让LLM给出下一个state(也即Actions所对应的index)
根据LLM的答案,更新state和todo
2.2.3_act:根据todo和memory,执行action,得到结果后,更新msg,更新todo,更新memory
将结果添加到RoleContext的memory中
2.2.4更新token,循环次数等等
2.2.5进入下一次循环
2.2.6当循环退出时,返回rsp
2.3将rsp发布到RoleContext的environment中
用自然语言编写代码,并想让一个agent(假设为SimpleCoder)为我们做这件事。
1)定义一个编写代码的动作
2)为智能体配备这个动作
在这个示例中,我们创建了一个 SimpleCoder,它能够根据人类的自然语言描述编写代码。
步骤如下:
1)我们为其指定一个名称和配置文件。
2)我们使用 self._init_action 函数为其配备期望的动作 SimpleWriteCode。
3)我们覆盖 _act 函数,其中包含智能体具体行动逻辑。我们写入,我们的智能体将从最新的记忆中获取人类指令,运行配备的动作,MetaGPT将其作为待办事项 (self._rc.todo) 在幕后处理,最后返回一个完整的消息。
和上面1使用现成agent的流程一样
①初始化基本信息,创建RoleContext等
②初始化Actions和states
逻辑基本和上面的run一样,但是在_react中,调用_act方法时,会调用我们覆盖后的方法,如下图:
通过SimpleWriteCode().run(),可以调用到SimpleWriteCode这个Action的run方法,它会向LLM提出要求,写python代码,并用正则匹配提取出来
拿到code_text后,构建msg,并返回给外层
和上面1使用现成agent的流程序列基本一样,区别仅仅在于Role的_act()和Action的run()
假设现在我们不仅希望用自然语言编写代码,而且还希望生成的代码立即执行。
一个拥有多个动作的智能体可以满足我们的需求。让我们称之为RunnableCoder,一个既写代码又立即运行的Role。
我们需要两个Action:SimpleWriteCode 和 SimpleRunCode
直接调用命令行去执行python代码
和上面2定义单一动作的智能体差不多
①构造函数
用 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。
和上面1使用现成agent的流程一样
RunnableCoder初始化:
①调用父类Role初始化函数,创建RoleSetting,创建LLM,创建RoleContext
②初始化actions和states
③设置react_mode
React_mode有3中模式,具体的描述如下
这里设置成by_order,也就是让RunnableCoder按照init_actions的顺序执行(也就是先执行SimpleWriteCode,后执行SimpleRunCode)
框架和上面使用现成agent一样
不同之处在于:
在role执行react的时候,由于react_mode是by_order,这里会执行_act_by_order(),如下图
其中order的执行逻辑如下图:
也即按照init_actions的顺序进行_act,注意这里的_set_state,会把rc.todo设置为当前遍历的action
而_act的逻辑如下:
在其中会执行todo.run,也就是action的run方法
也就是会依次执行SimpleWriteCode, SimpleRunCode
执行完成后,返回rsp
和上面1使用现成agent基本一样,区别仅仅在于react_mode不同,进而act的方式不同
创建一名编码人员,一名测试人员,一名审阅人员,来组成一个团队,完成从编码到测试到审阅的过程。
具体的角色与其对应的职责如下:
①Action1:写代码
②Action2:根据代码写k条测试用例
③根据代码和测试用例进行整体评估
①SimpleCoder:和自定义单一动作的Agent中的Role是一样的
②SimpleTester:代码测试人员
③SimpleReviewer:代码审阅人员
现在我们已经定义了三个 Role,接着我们初始化所有角色,设置一个 Team,并hire 它们。构建完成之后,运行team。
拥有一个或多个Role(也就是agent),SOP和消息平台的类。专门用于执行多agent的活动。
有以下几个属性(具体作用后面会分析):
创建一个team对象
注意,这里会创建一个默认的Environment对象,并赋值给environment字段
Team雇佣员工来合作:
具体完成的工作有两步(具体逻辑如下):
①将role的environment设置成team中创建的environment
②将role以role.profile-role的key-value对的形式,放到team所对应的environment的roles中
投资investment的预算max_budget。
其作用为:当我们跑LLM时的cost超过max_budget时,抛出NoMoneyException
这里的investment为我们在main函数中设置的参数
具体的工作:
①把team的investment设置成investment
②把config的最大预算也设置成investment
①设置ieda,这里的idea和investment一样,来自于main函数中的参数
②将idea作为content,把cause_by的Action设置成BossRequirement,构建Message,并发布到environment中
注意这里message会被存在两个集合中,一个是list格式的storage,另一个是dict格式的index(用来根据cause_by来检索message)
①进入循环
②self._check_balance()检查余额,判断当前总的花费是否超过我们的最大预算,如果超过,就抛出异常
③执行self.environment.run()
遍历所有的role,并发执行其中的run方法,并且用futures收集这部分异步任务的结果,等最后所有的role都执行完成之后,返回
由于environment.run()的run()方法
④当environment.run()执行完成后,进入下一次循环退出循环
④返回environment中的history
我们有三个角色:
SimpleCoder会执行SimpleWriteCode的Action,且监视着BossRequirement
SimpleTester会执行SimpleWriteTest的Action,且监视着SimpleWriteCode
SimpleReviewer会执行SimpleWriteReview的Action,且监视着SimpleWriteTest
也就是这个调用顺序会是SimpleCoder->SimpleTester->SimpleReviewer
接下来我们先看role的run()方法,接着,从上面第五步await team.run(n_round=n_round)中的遍历所有的role,并发执行其中的run方法这个部分,来看metaGPT是如何一步一步执行的
由于上面第五步await team.run(n_round=n_round)中的遍历所有的role,并发执行其中的run方法这个部分,其中的run是不带任何参数的,而我们看role的run方法(如下图),如果message为None,是会走到elif not wait self._observe()这里的,这里会执行self._observe(),然后根据其结果来判断是否要执行这一个elif里面的代码块
_observe()
作用:从environment中观察,并获取重要的信息,将其添加到memory中
①env_msgs = self._rc.env.memory.get()
从environment的memory中获取所有的Message,具体参考下面代码:
②observed = self._rc.env.memory.get_by_actions(self._rc.watch)
在environment的memory的index: dict[Type[Action], list[Message]]中,获取由watch的Action所触发的Message,获取完之后,拼接在一起,形成一个list返回
③self._rc.news = self._rc.memory.find_news(observed)
找到在observered中存在,且在rc自己的memory的storage中(注意这里是rc的memory,和上面②中的不一样,不是rc的environment的memory)不存在的Message,并形成list返回,也就是找到之前没有监测到的message,也就是找到新产生的message
并且设置到当前role的context中的news变量中
④for i in env_msgs: self.recv(i)
将environment中所有的message都存到该role的memory中,也就是把environment中的memory的Message信息,添加到rc的memory中
这一步相当于是告诉role,这些message我都已经看过了,下次有类似的message直接跳过,避免产生重复的news
⑤将news的简略信息拼起来,并打印到控制台
⑥返回news的长度
elif not判断
如果没有新的news(也就是news的长度为0),那么就直接返回
如果有新的news就执行react(),并publish到environment中
rsp = await self.react()
和1使用现成agent解释的差不多
self._publish_message(rsp)
和1使用现成agent解释的差不多
我们按照上面分析的role.run()的代码来每个agent执行的动作
SimpleCoder
①_observe()
注意observed = self._rc.env.memory.get_by_actions(self._rc.watch)这一句
因为SimpleCoder添加watch的Action是BossRequirement,所以这一步会过滤出BossRequirement相关的message,如下图:
接着,会把这条消息Human: write a function that calculates the product of a list设置到rc的news中
然后,当运行到最后时,各个变量的状态如下(参考下面红框中的部分):
②elif not判断
由于news有1条message,所以len(self._rc.news)为1,不会进到对应的代码块里面,相应的会执行后面的react()和publish_message()
③self.react()
a._think()
由于当前role里面只配备了一个Action,所以这里直接把当前的todo设置成此Action,也就是SimpleWriteCode,设置完成之后,直接返回
b._act()
会走到我们自定义的_act方法里面去
正常情况下应该是todo.run(),但是官网代码写的是code_text = await SimpleWriteCode().run(msg.content),但是也不影响,因为在这个role里,todo只能是SimpleWriteCode或者None
c. SimpleWriteCode().run()
正常情况下,应该是执行上面这几步,去询问LLM,但我这里连不上open ai的接口,就直接写死了一段代码来测试,如下图
d.回到_act()方法中,构建message
注意content是code_text,cause_by是type(todo)
④self.publish_message(rsp)
向environment中添加消息
SimpleTester
①_observe()
因为SimpleTester添加watch的Action是SimpleWriteCode,所以这里会过滤出SimpleWriteCode相关的且以前没有处理过的新的message,并设置到rc的news中
然后,当运行到最后时,各个变量的状态如下(参考下面红框中的部分):
②elif not判断
由于news有1条message,所以len(self._rc.news)为1,不会进到对应的代码块里面,相应的会执行后面的react()和publish_message()
③self.react()
a._think()
由于当前role里面只配备了一个Action,所以这里直接把当前的todo设置成此Action,也就是SimpleWriteTest,设置完成之后,直接返回
b._act()
会走到我们自定义的_act方法里面去
c. SimpleWriteTest ().run()
正常情况下,应该是执行上面这几步,去询问LLM,但我这里连不上open ai的接口,就直接写死了一段代码来测试,如下图
d.回到_act()方法中,构建message
④self.publish_message(rsp)
向environment中添加消息
SimpleReviewer
①_observe()
因为SimpleReviewer添加watch的Action是SimpleWriteTest,所以这里会过滤出SimpleWriteTest相关的且以前没有处理过的新的message,并设置到rc的news中
然后,当运行到最后时,各个变量的状态如下(参考下面红框中的部分):
②elif not判断
由于news有1条message,所以len(self._rc.news)为1,不会进到对应的代码块里面,相应的会执行后面的react()和publish_message()
③self.react()
a._think()
由于当前role里面只配备了一个Action,所以这里直接把当前的todo设置成此Action,也就是SimpleWriteReview,设置完成之后,直接返回
b._act()
由于我们没有在SimpleReviewer中自定义_act(),因此这里会调用其父类Role的_act方法()
先获取important_memory
然后执行todo.run(),也就是其所持有的SimpleWriteReview的Action对象的run方法
c. SimpleWriteReview.run()
调用open ai的接口去询问LLM
但由于我这里连不上open ai的接口,这里出现了异常……