【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手

简介: 【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手

前文【【AI的未来 - AI Agent系列】【MetaGPT】5. 更复杂的Agent实战 - 实现技术文档助手】我们用Action实现了一个技术文档助手,在学习了ActionNode技术之后,我们用ActionNode来尝试重写一下这个技术文档助手。

0. 前置推荐阅读

1. 重写WriteDirectory Action

根据我们之前的需求,WriteDirectory Action实现的其实就是根据用户输入的内容,直接去询问大模型,然后生成一份技术文档大纲目录。

1.1 实现WriteDirectory的ActionNode:DIRECTORY_WRITE

# 命令文本
DIRECTORY_STRUCTION = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial".
    您现在是互联网领域的经验丰富的技术专业人员。
    我们需要您撰写一个技术教程。
    """
# 实例化一个ActionNode,输入对应的参数
DIRECTORY_WRITE = ActionNode(
    # ActionNode的名称
    key="Directory Write",
    # 期望输出的格式
    expected_type=str,
    # 命令文本
    instruction=DIRECTORY_STRUCTION,
    # 例子输入,在这里我们可以留空
    example="",
 )

1.2 将 DIRECTORY_WRITE 包进 WriteDirectory中

class WriteDirectory(Action):
    
    language: str = "Chinese"
    
    def __init__(self, name: str = "", language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        
    async def run(self, topic: str, *args, **kwargs) -> Dict:
        DIRECTORY_PROMPT = """
        The topic of tutorial is {topic}. Please provide the specific table of contents for this tutorial, strictly following the following requirements:
        1. The output must be strictly in the specified language, {language}.
        2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
        3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
        4. Do not have extra spaces or line breaks.
        5. Each directory title has practical significance.
        教程的主题是{topic}。请按照以下要求提供本教程的具体目录:
        1. 输出必须严格符合指定语言,{language}。
        2. 回答必须严格按照字典格式,如{{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}。
        3. 目录应尽可能具体和充分,包括一级和二级目录。二级目录在数组中。
        4. 不要有额外的空格或换行符。
        5. 每个目录标题都具有实际意义。
        """
        
        # 我们设置好prompt,作为ActionNode的输入
        prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
        # resp = await self._aask(prompt=prompt)
        # 直接调用ActionNode.fill方法,注意输入llm
        # 该方法会返回self,也就是一个ActionNode对象
        resp_node = await DIRECTORY_WRITE.fill(context=prompt, llm=self.llm, schema="raw")
        # 选取ActionNode.content,获得我们期望的返回信息
        resp = resp_node.content
        return OutputParser.extract_struct(resp, dict)

重点是这一句 resp_node = await DIRECTORY_WRITE.fill(context=prompt, llm=self.llm, schema="raw"),将原来的直接拿Prompt询问大模型获取结果,变成了使用ActionNode的fill函数,去内部询问大模型并获取结果。

2. 重写WriteContent Action

2.1 思考重写方案

WriteContent的目的是根据目录标题询问大模型,生成具体的技术文档内容。

最直观的重写方法:每个WriteContent包一个ActionNode,像WriteDirectory一样,如下图:

像不用ActionNode一样,每个WriteContent执行完毕返回结果到Role中进行处理和组装,然后执行下一个WriteContent Action。可能你也看出来了,这种重写方法其实就是将WriteContent直接调用大模型改成了使用ActionNode调用大模型,其它都没变。我认为这种重写方法的意义不大,没体现出ActionNode的作用和价值。

于是我想到了第二种重写方法,如下图:

将每一个章节内容的书写作为一个ActionNode,一起放到WriteContent动作里执行,这样外部Role只需执行一次WriteContent动作,所有内容就都完成了,可以实现ActionNode设计的初衷:突破需要在Role的_react内循环执行的限制,达到更好的CoT效果。

2.2 实现WriteContent的ActionNode

CONTENT_WRITE = ActionNode(
    key="Content Write",
    expected_type=str,
    instruction="",
    example="",
)

这里可以将instruction放空,后面用context设置prompt可以实现相同的效果。

2.3 改写WriteContent Action

主要修改点:

(1)初始化时接收一个ActionNode List,使用这个List初始化 self.node,作为父节点

(2)run方法中不再直接调用大模型,而是依次执行子节点的simple_fill函数获取结果

(3)在调用子节点的simple_fill函数前,记得更新prompt

(4)子节点返回的内容进行组装

(5)最后返回组装后的结果

更多代码细节注释请看下面:

class WriteContent(Action):
    """Action class for writing tutorial content.
    Args:
        name: The name of the action.
        directory: The content to write.
        language: The language to output, default is "Chinese".
    """
    language: str = "Chinese"
    directory: str = ""
    total_content: str = "" ## 组装所有子节点的输出
    
    def __init__(self, name: str = "", action_nodes: list = [], language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        self.node = ActionNode.from_children("WRITE_CONTENT_NODES", action_nodes) ## 根据传入的action_nodes列表,生成一个父节点
    async def run(self, topic: str, *args, **kwargs) -> str:
        COMMON_PROMPT = """
        You are now a seasoned technical professional in the field of the internet. 
        We need you to write a technical tutorial with the topic "{topic}".
        """
        CONTENT_PROMPT = COMMON_PROMPT + """
        Now I will give you the module directory titles for the topic. 
        Please output the detailed principle content of this title in detail. 
        If there are code examples, please provide them according to standard code specifications. 
        Without a code example, it is not necessary.
        The module directory titles for the topic is as follows:
        {directory}
        Strictly limit output according to the following requirements:
        1. Follow the Markdown syntax format for layout.
        2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
        3. The output must be strictly in the specified language, {language}.
        4. Do not have redundant output, including concluding remarks.
        5. Strict requirement not to output the topic "{topic}".
        """
        
        for _, i in self.node.children.items():
            time.sleep(20) ## 避免OpenAI的API调用频率过高
            prompt = CONTENT_PROMPT.format(
                topic=topic, language=self.language, directory=i.key)
            i.set_llm(self.llm) ## 这里要设置一下llm,即使设置为None,也可以正常工作,但不设置就没法正常工作
            ## 为子节点设置context,也就是Prompt,ActionNode中我们将instruction放空,instruction和context都会作为prompt给大模型
            ## 所以两者有一个为空也没关系,只要prompt完整就行
            i.set_context(prompt)
            child = await i.simple_fill(schema="raw", mode="auto") ## 这里的schema注意写"raw"
            self.total_content += child.content ## 组装所有子节点的输出
        logger.info("writecontent:", self.total_content)
        return self.total_content

3. 改写TutorialAssistant Role

TutorialAssistant Role的作用是执行以上两个Action,输出最终结果。改写内容如下:

(1)将原本的生成Action List改为生成ActionNode List

  • 注意细节:生成的ActionNode的key为每个章节的目录标题,在WriteContent中更新每个node的prompt时使用了

(2)将ActionNode List传给WriteContent Action进行WriteContent Action的初始化

(3)将WriteContent初始化到Role的动作中

  • 注意细节:这里不再是之前每个first_dir创建一个WriteContent了,而是最后只初始化一个。

更多代码细节注释请看下面:

async def _handle_directory(self, titles: Dict) -> Message:
        self.main_title = titles.get("title")
        directory = f"{self.main_title}\n"
        self.total_content += f"# {self.main_title}"
        action_nodes = list()
        for first_dir in titles.get("directory"):
            logger.info(f"================== {first_dir}")
            action_nodes.append(ActionNode( ## 每个章节初始化一个ActionNode
                key=f"{first_dir}",  ## 注意key为本章目录标题
                expected_type=str,
                instruction="",
                example=""))
            key = list(first_dir.keys())[0]
            directory += f"- {key}\n"
            for second_dir in first_dir[key]:
                directory += f"  - {second_dir}\n"
        
        self._init_actions([WriteContent(language=self.language, action_nodes=action_nodes)]) ## 初始化一个WriteContent Action,不是多个了
        self.rc.todo = None
        return Message(content=directory)

4. 完整代码及执行结果

# 加载 .env 到环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())
import asyncio
import re
import time
from typing import Dict
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
from metagpt.utils.common import OutputParser
from metagpt.const import TUTORIAL_PATH
from datetime import datetime
from metagpt.utils.file import File
# 命令文本
DIRECTORY_STRUCTION = """
    You are now a seasoned technical professional in the field of the internet. 
    We need you to write a technical tutorial".
    您现在是互联网领域的经验丰富的技术专业人员。
    我们需要您撰写一个技术教程。
    """
# 实例化一个ActionNode,输入对应的参数
DIRECTORY_WRITE = ActionNode(
    # ActionNode的名称
    key="Directory Write",
    # 期望输出的格式
    expected_type=str,
    # 命令文本
    instruction=DIRECTORY_STRUCTION,
    # 例子输入,在这里我们可以留空
    example="",
 )
CONTENT_WRITE = ActionNode(
    key="Content Write",
    expected_type=str,
    instruction="",
    example="",
)
class WriteDirectory(Action):
    
    language: str = "Chinese"
    
    def __init__(self, name: str = "", language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        
    async def run(self, topic: str, *args, **kwargs) -> Dict:
        DIRECTORY_PROMPT = """
        The topic of tutorial is {topic}. Please provide the specific table of contents for this tutorial, strictly following the following requirements:
        1. The output must be strictly in the specified language, {language}.
        2. Answer strictly in the dictionary format like {{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}.
        3. The directory should be as specific and sufficient as possible, with a primary and secondary directory.The secondary directory is in the array.
        4. Do not have extra spaces or line breaks.
        5. Each directory title has practical significance.
        教程的主题是{topic}。请按照以下要求提供本教程的具体目录:
        1. 输出必须严格符合指定语言,{language}。
        2. 回答必须严格按照字典格式,如{{"title": "xxx", "directory": [{{"dir 1": ["sub dir 1", "sub dir 2"]}}, {{"dir 2": ["sub dir 3", "sub dir 4"]}}]}}。
        3. 目录应尽可能具体和充分,包括一级和二级目录。二级目录在数组中。
        4. 不要有额外的空格或换行符。
        5. 每个目录标题都具有实际意义。
        """
        
        # 我们设置好prompt,作为ActionNode的输入
        prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
        # resp = await self._aask(prompt=prompt)
        # 直接调用ActionNode.fill方法,注意输入llm
        # 该方法会返回self,也就是一个ActionNode对象
        resp_node = await DIRECTORY_WRITE.fill(context=prompt, llm=self.llm, schema="raw")
        # 选取ActionNode.content,获得我们期望的返回信息
        resp = resp_node.content
        return OutputParser.extract_struct(resp, dict)
class WriteContent(Action):
    """Action class for writing tutorial content.
    Args:
        name: The name of the action.
        directory: The content to write.
        language: The language to output, default is "Chinese".
    """
    language: str = "Chinese"
    directory: str = ""
    total_content: str = "" ## 组装所有子节点的输出
    
    def __init__(self, name: str = "", action_nodes: list = [], language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        self.node = ActionNode.from_children("WRITE_CONTENT_NODES", action_nodes) ## 根据传入的action_nodes列表,生成一个父节点
    async def run(self, topic: str, *args, **kwargs) -> str:
        COMMON_PROMPT = """
        You are now a seasoned technical professional in the field of the internet. 
        We need you to write a technical tutorial with the topic "{topic}".
        """
        CONTENT_PROMPT = COMMON_PROMPT + """
        Now I will give you the module directory titles for the topic. 
        Please output the detailed principle content of this title in detail. 
        If there are code examples, please provide them according to standard code specifications. 
        Without a code example, it is not necessary.
        The module directory titles for the topic is as follows:
        {directory}
        Strictly limit output according to the following requirements:
        1. Follow the Markdown syntax format for layout.
        2. If there are code examples, they must follow standard syntax specifications, have document annotations, and be displayed in code blocks.
        3. The output must be strictly in the specified language, {language}.
        4. Do not have redundant output, including concluding remarks.
        5. Strict requirement not to output the topic "{topic}".
        """
        
        for _, i in self.node.children.items():
            time.sleep(20) ## 避免OpenAI的API调用频率过高
            prompt = CONTENT_PROMPT.format(
                topic=topic, language=self.language, directory=i.key)
            i.set_llm(self.llm) ## 这里要设置一下llm,即使设置为None,也可以正常工作,但不设置就没法正常工作
            ## 为子节点设置context,也就是Prompt,ActionNode中我们将instruction放空,instruction和context都会作为prompt给大模型
            ## 所以两者有一个为空也没关系,只要prompt完整就行
            i.set_context(prompt)
            child = await i.simple_fill(schema="raw", mode="auto") ## 这里的schema注意写"raw"
            self.total_content += child.content ## 组装所有子节点的输出
        logger.info("writecontent:", self.total_content)
        return self.total_content
class TutorialAssistant(Role):
    
    topic: str = ""
    main_title: str = ""
    total_content: str = ""
    language: str = "Chinese"
    def __init__(
        self,
        name: str = "Stitch",
        profile: str = "Tutorial Assistant",
        goal: str = "Generate tutorial documents",
        constraints: str = "Strictly follow Markdown's syntax, with neat and standardized layout",
        language: str = "Chinese",
    ):
        super().__init__()
        self._init_actions([WriteDirectory(language=language)])
        self.language = language
    async def _think(self) -> None:
        """Determine the next action to be taken by the role."""
        logger.info(self.rc.state)
        # logger.info(self,)
        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 _handle_directory(self, titles: Dict) -> Message:
        self.main_title = titles.get("title")
        directory = f"{self.main_title}\n"
        self.total_content += f"# {self.main_title}"
        action_nodes = list()
        # actions = list()
        for first_dir in titles.get("directory"):
            logger.info(f"================== {first_dir}")
            action_nodes.append(ActionNode(
                key=f"{first_dir}",
                expected_type=str,
                instruction="",
                example=""))
            key = list(first_dir.keys())[0]
            directory += f"- {key}\n"
            for second_dir in first_dir[key]:
                directory += f"  - {second_dir}\n"
        
        self._init_actions([WriteContent(language=self.language, action_nodes=action_nodes)])
        self.rc.todo = None
        return Message(content=directory)
    async def _act(self) -> Message:
        """Perform an action as determined by the role.
        Returns:
            A message containing the result of the action.
        """
        todo = self.rc.todo
        if type(todo) is WriteDirectory:
            msg = self.rc.memory.get(k=1)[0]
            self.topic = msg.content
            resp = await todo.run(topic=self.topic)
            logger.info(resp)
            return await self._handle_directory(resp)
        resp = await todo.run(topic=self.topic)
        logger.info(resp)
        if self.total_content != "":
            self.total_content += "\n\n\n"
        self.total_content += resp
        return Message(content=resp, role=self.profile)
    async def _react(self) -> Message:
        """Execute the assistant's think and actions.
        Returns:
            A message containing the final result of the assistant's actions.
        """
        while True:
            await self._think()
            if self.rc.todo is None:
                break
            msg = await self._act()
        root_path = TUTORIAL_PATH / datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
        logger.info(f"Write tutorial to {root_path}")
        await File.write(root_path, f"{self.main_title}.md", self.total_content.encode('utf-8'))
        return msg
async def main():
    msg = "Git 教程"
    role = TutorialAssistant()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)
asyncio.run(main())
  • 执行结果

下一篇继续实战ActionNode:【AI Agent系列】【MetaGPT】7. 实战:只用两个字,让MetaGPT写一篇小说

相关文章
|
2天前
|
机器学习/深度学习 人工智能 算法
FinRobot:开源的金融专业 AI Agent,提供市场预测、报告分析和交易策略等金融解决方案
FinRobot 是一个开源的 AI Agent 平台,专注于金融领域的应用,通过大型语言模型(LLMs)构建复杂的金融分析和决策工具,提供市场预测、文档分析和交易策略等多种功能。
42 13
FinRobot:开源的金融专业 AI Agent,提供市场预测、报告分析和交易策略等金融解决方案
|
4天前
|
人工智能 开发框架 算法
Qwen-Agent:阿里通义开源 AI Agent 应用开发框架,支持构建多智能体,具备自动记忆上下文等能力
Qwen-Agent 是阿里通义开源的一个基于 Qwen 模型的 Agent 应用开发框架,支持指令遵循、工具使用、规划和记忆能力,适用于构建复杂的智能代理应用。
59 10
Qwen-Agent:阿里通义开源 AI Agent 应用开发框架,支持构建多智能体,具备自动记忆上下文等能力
|
22天前
|
机器学习/深度学习 人工智能 自然语言处理
Gemini 2.0:谷歌推出的原生多模态输入输出 + Agent 为核心的 AI 模型
谷歌最新推出的Gemini 2.0是一款原生多模态输入输出的AI模型,以Agent技术为核心,支持多种数据类型的输入与输出,具备强大的性能和多语言音频输出能力。本文将详细介绍Gemini 2.0的主要功能、技术原理及其在多个领域的应用场景。
130 20
Gemini 2.0:谷歌推出的原生多模态输入输出 + Agent 为核心的 AI 模型
|
8天前
|
存储 人工智能 人机交互
PC Agent:开源 AI 电脑智能体,自动收集人机交互数据,模拟认知过程实现办公自动化
PC Agent 是上海交通大学与 GAIR 实验室联合推出的智能 AI 系统,能够模拟人类认知过程,自动化执行复杂的数字任务,如组织研究材料、起草报告等,展现了卓越的数据效率和实际应用潜力。
78 1
PC Agent:开源 AI 电脑智能体,自动收集人机交互数据,模拟认知过程实现办公自动化
|
22天前
|
人工智能 API 语音技术
TEN Agent:开源的实时多模态 AI 代理框架,支持语音、文本和图像的实时通信交互
TEN Agent 是一个开源的实时多模态 AI 代理框架,集成了 OpenAI Realtime API 和 RTC 技术,支持语音、文本和图像的多模态交互,具备实时通信、模块化设计和多语言支持等功能,适用于智能客服、实时语音助手等多种场景。
123 15
TEN Agent:开源的实时多模态 AI 代理框架,支持语音、文本和图像的实时通信交互
|
23天前
|
人工智能 自然语言处理 前端开发
Director:构建视频智能体的 AI 框架,用自然语言执行搜索、编辑、合成和生成等复杂视频任务
Director 是一个构建视频智能体的 AI 框架,用户可以通过自然语言命令执行复杂的视频任务,如搜索、编辑、合成和生成视频内容。该框架基于 VideoDB 的“视频即数据”基础设施,集成了多个预构建的视频代理和 AI API,支持高度定制化,适用于开发者和创作者。
81 9
Director:构建视频智能体的 AI 框架,用自然语言执行搜索、编辑、合成和生成等复杂视频任务
|
19天前
|
机器学习/深度学习 人工智能 算法
Meta Motivo:Meta 推出能够控制数字智能体动作的 AI 模型,提升元宇宙互动体验的真实性
Meta Motivo 是 Meta 公司推出的 AI 模型,旨在控制数字智能体的全身动作,提升元宇宙体验的真实性。该模型通过无监督强化学习算法,能够实现零样本学习、行为模仿与生成、多任务泛化等功能,适用于机器人控制、虚拟助手、游戏角色动画等多个应用场景。
50 4
Meta Motivo:Meta 推出能够控制数字智能体动作的 AI 模型,提升元宇宙互动体验的真实性
|
4天前
|
人工智能 自然语言处理 前端开发
三大行业案例:AI大模型+Agent实践全景
本文将从AI Agent和大模型的发展背景切入,结合51Talk、哈啰出行以及B站三个各具特色的行业案例,带你一窥事件驱动架构、RAG技术、人机协作流程,以及一整套行之有效的实操方法。具体包含内容有:51Talk如何让智能客服“主动进攻”,带来约课率、出席率双提升;哈啰出行如何由Copilot模式升级为Agent模式,并应用到客服、营销策略生成等多个业务场景;B站又是如何借力大模型与RAG方法,引爆了平台的高效内容检索和强互动用户体验。
70 5
|
1天前
|
存储 人工智能 开发框架
Eliza:TypeScript 版开源 AI Agent 开发框架,快速搭建智能、个性的 Agents 系统
Eliza 是一个开源的多代理模拟框架,支持多平台连接、多模型集成,能够快速构建智能、高效的AI系统。
24 8
Eliza:TypeScript 版开源 AI Agent 开发框架,快速搭建智能、个性的 Agents 系统
|
1月前
|
人工智能 自然语言处理 JavaScript
Agent-E:基于 AutoGen 代理框架构建的 AI 浏览器自动化系统
Agent-E 是一个基于 AutoGen 代理框架构建的智能自动化系统,专注于浏览器内的自动化操作。它能够执行多种复杂任务,如填写表单、搜索和排序电商产品、定位网页内容等,从而提高在线效率,减少重复劳动。本文将详细介绍 Agent-E 的功能、技术原理以及如何运行该系统。
84 5
Agent-E:基于 AutoGen 代理框架构建的 AI 浏览器自动化系统