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

2024-01-23 06:36

本文主要是介绍【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 0. 前置推荐阅读
    • 1. 重写WriteDirectory Action
      • 1.1 实现WriteDirectory的ActionNode:DIRECTORY_WRITE
      • 1.2 将 DIRECTORY_WRITE 包进 WriteDirectory中
    • 2. 重写WriteContent Action
      • 2.1 思考重写方案
      • 2.2 实现WriteContent的ActionNode
      • 2.3 改写WriteContent Action
    • 3. 改写TutorialAssistant Role
    • 4. 完整代码及执行结果

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

0. 前置推荐阅读

  • 【AI的未来 - AI Agent系列】【MetaGPT】5. 更复杂的Agent实战 - 实现技术文档助手
  • 【AI的未来 - AI Agent系列】【MetaGPT】4. ActionNode从理论到实战
  • 【AI的未来 - AI Agent系列】【MetaGPT】4.1 细说我在ActionNode实战中踩的那些坑

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 = languageasync 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.contentreturn 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 = languageself.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( ## 每个章节初始化一个ActionNodekey=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 = Nonereturn 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 Dictfrom 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 = languageasync 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.contentreturn 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 = languageself.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_contentclass 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 = languageasync 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)returnif self.rc.state + 1 < len(self.states):self._set_state(self.rc.state + 1)else:self.rc.todo = Noneasync 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 = Nonereturn 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.todoif type(todo) is WriteDirectory:msg = self.rc.memory.get(k=1)[0]self.topic = msg.contentresp = 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 += respreturn 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:breakmsg = 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 msgasync 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写一篇小说

这篇关于【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/635513

相关文章

三频BE12000国补到手2549元! ROG 魔盒Pro WIFI7电竞AI路由器上架

《三频BE12000国补到手2549元!ROG魔盒ProWIFI7电竞AI路由器上架》近日,华硕带来了ROG魔盒ProWIFI7电竞AI路由器(ROGSTRIXGR7Pro),目前新... 华硕推出了ROG 魔盒Pro WIFI7电竞AI路由器(ROG STRIX GR7 Phttp://www.cppcn

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.

Java操作Word文档的全面指南

《Java操作Word文档的全面指南》在Java开发中,操作Word文档是常见的业务需求,广泛应用于合同生成、报表输出、通知发布、法律文书生成、病历模板填写等场景,本文将全面介绍Java操作Word文... 目录简介段落页头与页脚页码表格图片批注文本框目录图表简介Word编程最重要的类是org.apach

Python Pillow 库详解文档(最新推荐)

《PythonPillow库详解文档(最新推荐)》Pillow是Python中最流行的图像处理库,它是PythonImagingLibrary(PIL)的现代分支和继承者,本文给大家介绍Pytho... 目录python Pillow 库详解文档简介安装核心模块架构Image 模块 - 核心图像处理基本导入

Qt如何实现文本编辑器光标高亮技术

《Qt如何实现文本编辑器光标高亮技术》这篇文章主要为大家详细介绍了Qt如何实现文本编辑器光标高亮技术,文中的示例代码讲解详细,具有一定的借鉴价值,有需要的小伙伴可以了解下... 目录实现代码函数作用概述代码详解 + 注释使用 QTextEdit 的高亮技术(重点)总结用到的关键技术点应用场景举例示例优化建议

Nginx 重写与重定向配置方法

《Nginx重写与重定向配置方法》Nginx重写与重定向区别:重写修改路径(客户端无感知),重定向跳转新URL(客户端感知),try_files检查文件/目录存在性,return301直接返回永久重... 目录一.try_files指令二.return指令三.rewrite指令区分重写与重定向重写: 请求

C#实现将Office文档(Word/Excel/PDF/PPT)转为Markdown格式

《C#实现将Office文档(Word/Excel/PDF/PPT)转为Markdown格式》Markdown凭借简洁的语法、优良的可读性,以及对版本控制系统的高度兼容性,逐渐成为最受欢迎的文档格式... 目录为什么要将文档转换为 Markdown 格式使用工具将 Word 文档转换为 Markdown(.

详解如何使用Python构建从数据到文档的自动化工作流

《详解如何使用Python构建从数据到文档的自动化工作流》这篇文章将通过真实工作场景拆解,为大家展示如何用Python构建自动化工作流,让工具代替人力完成这些数字苦力活,感兴趣的小伙伴可以跟随小编一起... 目录一、Excel处理:从数据搬运工到智能分析师二、PDF处理:文档工厂的智能生产线三、邮件自动化:

Python实现自动化Word文档样式复制与内容生成

《Python实现自动化Word文档样式复制与内容生成》在办公自动化领域,高效处理Word文档的样式和内容复制是一个常见需求,本文将展示如何利用Python的python-docx库实现... 目录一、为什么需要自动化 Word 文档处理二、核心功能实现:样式与表格的深度复制1. 表格复制(含样式与内容)2

Java中的登录技术保姆级详细教程

《Java中的登录技术保姆级详细教程》:本文主要介绍Java中登录技术保姆级详细教程的相关资料,在Java中我们可以使用各种技术和框架来实现这些功能,文中通过代码介绍的非常详细,需要的朋友可以参考... 目录1.登录思路2.登录标记1.会话技术2.会话跟踪1.Cookie技术2.Session技术3.令牌技