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

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

0. 前置推荐阅读

更多 MetaGPT 实战可看这里https://blog.csdn.net/attitude93/category_12546719.html?spm=1001.2014.3001.5482

1. 本文内容

本文是 《MetaGPT智能体开发入门》课程的第5个任务:用ActionNode完成短篇小说写作(爽文/科幻/言情)。

2. 需求分析

2.1 步骤拆解

写小说与写技术文章类似,所以总体思路我们可以参考之前的技术文档助手:【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手,即先生成目录,然后根据目录详细写每一章的内容。

但是小说与技术文章有几个不同点:

  • 小说有故事情节、人物等要素
  • 各章节之间要保持故事的连贯性
  • … 其它可能没想到的

保持连贯性,其实就是在prompt中加入上下文信息,也就是多使用点记忆里的东西。

因为咱也没写过小说,也没用prompt在chatgpt上写过小说,于是我从网上找了一个相对来讲比较系统的写小说的prompt:

ChatGPT魔法课堂进阶:通过Prompt提示模板写本自己的小说

总结出通过Prompt提示写小说的几个步骤:

(1)生成小说的基本要素:标题、主角、反派、情节设置、中心主题等

(2)根据第一步生成的基本要素,生成小说的章节目录

(3)根据小说的章节目录,生成每个章节的故事简介

(4)根据每个章节的故事简介,生成每个章节的详细故事内容

参考以上步骤,我觉得可以将2、3步合并,直接一起生成章节目录和故事简介,即:

(1)生成小说的基本要素:标题、主角、反派、情节设置、中心主题等
(2)根据第一步生成的基本要素,生成小说的章节目录和对应的故事简介
(4)根据每个章节的故事简介,生成每个章节的详细故事内容

2.2 流程设计

简单画了下,凑活看… 解释下:

(1)生成小说基本要素的ActionNode为STRUCT_WRITE

(2)生成小说目录的ActionNode为DIRECTORY_WRITE

(3)这两个ActionNode作为子节点,包裹在父节点 OUTLINE_WRITE_NODES中

(4)父节点OUTLINE_WRITE_NODES包裹在WriteStroyStruct Action中(ActionNode必须在Action中运行)

(5)执行完WriteStroyStruct Action就完成了前两个步骤,得到了章节目录和故事简介。

(6)使用_handle_directory函数分解章节目录和故事简介,生成一系列WriteContent Action

(7)WriteContent Action中使用STORY_WRITE ActionNode来写详细信息

3. 写代码

3.1 生成小说基本要素的ActionNode:STRUCT_WRITE

STRUCT_WRITE_INSTRUCTION = """
您现在是短篇小说领域经验丰富的小说作家, 我们需要您根据给定的小说题材生成故事的基本结构。
按照以下内容输出符合给定题材的小说基本结构:
标题:"小说的标题"
设置:"小说的情景设置细节,包括时间段、地点和所有相关背景信息"
主角:"小说主角的名字、年龄、职业,以及他们的性格和动机、简要的描述"
反派角色:"小说反派角色的名字、年龄、职业,以及他们的性格和动机、简要的描述"
冲突:"小说故事的主要冲突,包括主角面临的问题和涉及的利害关系"
对话:"以对话的形式描述情节,揭示人物,以此提供一些提示给读者"
主题:"小说中心主题,并说明如何在整个情节、角色和背景中展开"
基调:"整体故事的基调,以及保持背景和人物的一致性和适当性的说明"
节奏:"调节故事节奏以建立和释放紧张气氛,推进情节,创造戏剧效果的说明"
其它:"任何额外的细节或对故事的要求,如特定的字数或题材限制"
"""
# ActionNode,写小说基本结构
STRUCT_WRITE = ActionNode(
    # ActionNode的名称
    key="Struct Write",
    # 期望输出的格式
    expected_type=str,
    # 命令文本
    instruction=STRUCT_WRITE_INSTRUCTION,
    # 例子输入,在这里我们可以留空
    example="",
 )

3.2 生成目录的ActionNode:DIRECTORY_WRITE

DIRECTORY_WRITE_INSTRUCTION = """
您现在是短篇小说领域经验丰富的小说作家。我们需要您根据context的内容创作出小说的目录和章节内容概况。
请按照以下要求提供该小说的具体目录和目录中的故事概况:
1. 输出必须严格符合指定语言。
2. 回答必须严格按照字典格式,如: {"title": "xxx", "directory": {"第一章:章节标题": "故事概况", "第二章:章节标题": "故事概况"}} 。
3. 目录应尽可能引人注目和充分,包括一级目录和本章故事概况。
4. 不要有额外的空格或换行符。
"""
# ActionNode,生成小说目录
DIRECTORY_WRITE = ActionNode(
    # ActionNode的名称
    key="Directory Write",
    # 期望输出的格式
    expected_type=str,
    # 命令文本
    instruction=DIRECTORY_WRITE_INSTRUCTION,
    # 例子输入,在这里我们可以留空
    example="",
 )

注意:这里我们规定了返回的内容必须按照字典格式,否则校验不通过,程序报错,这是为了后面的_handle_directory处理。

3.3 生成章节详细内容的ActionNode:STORY_WRITE

# ActionNode,写小说章节内容
STORY_WRITE = ActionNode(
    # ActionNode的名称
    key="Story Write",
    # 期望输出的格式
    expected_type=str,
    # 命令文本
    instruction="您现在是短篇小说领域经验丰富的小说作家。我们需要您创作出小说详细内容。",
    # 例子输入,在这里我们可以留空
    example="",
 )

3.4 父节点:OUTLINE_WRITE_NODES

class OUTLINE_WRITE_NODES(ActionNode):
    def __init__(self, name="Outline Nodes", expected_type=str, instruction="", example=""):
        super().__init__(key=name, expected_type=expected_type, instruction=instruction, example=example)
        self.add_children([STRUCT_WRITE, DIRECTORY_WRITE])    # 初始化过程,将上面实现的两个子节点加入作为OUTLINE_WRITE_NODES类的子节点
    async def fill(self, context, llm, schema="raw", mode="auto", strgy="complex"):
        self.set_llm(llm)
        self.set_context(context)
        if self.schema:
            schema = self.schema
        if strgy == "simple":
            return await self.simple_fill(schema=schema, 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(schema=schema, mode=mode)
                child_context = child.content    # 将返回内容(child.content)作为下一个子节点的context
            self.content = child_context    # 最后一个子节点返回的内容设置为父节点返回内容(self.content)
            return self

3.5 父节点:CONTENT_WRITE_NODES

class CONTENT_WRITE_NODES(ActionNode):
    def __init__(self, name="Content Nodes", expected_type=str, instruction="", example=""):
        super().__init__(key=name, expected_type=expected_type, instruction=instruction, example=example)
        self.add_children([STORY_WRITE]) ## 这里只初始化了一个子节点
        
    async def fill(self, context, llm, schema="raw", mode="auto", strgy="complex"):
        self.set_llm(llm)
        self.set_context(context)
        if self.schema:
            schema = self.schema
            
        if strgy == "simple":
            return await self.simple_fill(schema=schema, 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(schema=schema, mode=mode)
                child_context = child.content    # 将返回内容(child.content)作为下一个子节点的context
            self.content = child_context    # 最后一个子节点返回的内容设置为父节点返回内容(self.content)
            return self  

3.6 小说要素和目录生成Action:WriteStoryStruct

class WriteStoryStruct(Action):
    
    language: str = "Chinese"
    
    def __init__(self, name: str = "", language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        self.node = OUTLINE_WRITE_NODES() ## 包裹住ActionNodes
        
    async def run(self, topic: str, *args, **kwargs) -> Dict:
        DIRECTORY_PROMPT = """
        小说的题材是{topic}。请严格使用{language}语言,并按照instruction中的说明内容输出内容
        """
        
        # 我们设置好prompt,作为ActionNode的输入
        prompt = DIRECTORY_PROMPT.format(topic=topic, language=self.language)
        # 该方法会返回self,也就是一个ActionNode对象
        resp_node = await self.node.fill(context=prompt, llm=self.llm, schema="raw")
        # 选取ActionNode.content,获得我们期望的返回信息
        resp = resp_node.content
        # return Message(content=resp) # 返回Message对象后,会在resp前面加上 user: 字样,会破坏Dict结构
        return OutputParser.extract_struct(resp, dict)

3.7 小说章节详细内容生成Action:WriteContent

class WriteContent(Action):
    language: str = "Chinese"
    directory: str = ""
    content: str = ""
    
    def __init__(self, name: str = "", directory: str = "", content: str = "", language: str = "Chinese", *args, **kwargs):
        super().__init__()
        self.language = language
        self.directory = directory
        self.content = content
        self.node = CONTENT_WRITE_NODES()
    async def run(self, topic: str, *args, **kwargs) -> str:
        CONTENT_PROMPT = """
        您现在是短篇小说领域经验丰富的小说作家。请以引人入胜的风格,深入细致地按照故事概况"{content}"写出故事内容,注意与上一章故事内容的衔接。
        上一章故事内容为{topic}。
        """
        n = 0
        ## 这里是因为大模型经常返回“抱歉,无法满足您的要求”,不给生成内容...... 
        ## 所以加了个重试,有效果,但效果不佳。我白嫖的3.5,用更好的大模型可能没有这个问题......
        while n < 5: 
            prompt = CONTENT_PROMPT.format(
                topic=topic, language=self.language, content=self.content)
            resp_node = await self.node.fill(context=prompt, schema="raw", llm=self.llm)
            resp = resp_node.content
            
            n += 1
            if (resp.find("无法满足") == -1 or resp.find("无法完成") == -1):
                break
        resp = f"## {self.directory}\n\n{resp}" ## 返回时要加上标题
        return resp

3.8 小说助手Role:StoryAssistant

class StoryAssistant(Role):
    
    topic: str = ""
    total_content: str = ""
    language: str = "Chinese"
    main_title: str = ""
    def __init__(
        self,
        name: str = "Story Assistant",
        profile: str = "Story Assistant",
        language: str = "Chinese",
    ):
        super().__init__()
        self._init_actions([WriteStoryStruct(language=language)]) ## 先加入WriteStoryStruct Action
        self.language = language
    async def _think(self) -> None:
        """Determine the next action to be taken by the role."""
        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 _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}"
        actions = list()
        total_dir = titles.get("directory")
        for first_dir in total_dir:
            directory += f"- {first_dir}\n"
            dir_content = total_dir[first_dir]
            actions.append(WriteContent(
                language=self.language, directory=first_dir, content=dir_content)) ## 根据每章节目录,生成一系列WriteContent Action
        self._init_actions(actions)
        self.rc.todo = None
        return Message(content=directory)
    content_count: int = 0
    async def _act(self) -> Message:
        """Perform an action as determined by the role.
        Returns:
            A message containing the result of the action.
        """
        time.sleep(20)
        todo = self.rc.todo
        if type(todo) is WriteStoryStruct:
            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)
        self.content_count += 1
        msg = self.rc.memory.get(k=1)[0] ## 获取上章的内容,为prompt提供上下文信息,使章节之间的故事有连贯性
        if self.content_count > 1:
            if len(msg.content) > 100:
                resp = await todo.run(topic=msg.content[-100]) ## 我这里取的是上章内容最后100个字符
            else:
                resp = await todo.run(topic=msg.content)
        else:
            resp = await todo.run(topic=self.main_title) ## 第一章内容生成之前,没有上文,所以这里传个小说题目进去,当作上下文
 
        if self.total_content != "":
            self.total_content += "\n\n\n"
        self.total_content += resp ## 组装结果
        msg_last = Message(content=str(resp), role=self.profile)
        self.rc.memory.add(msg_last) ## 将本章内容加入记忆中,以便下章作为上下文使用,可以在一定程度上保证上下文连贯性
        return msg_last
    async def _react(self) -> Message:
        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

3.9 完整代码,运行

上文中的代码基本就全了,再加上引入包和main函数,就能跑了。模拟用户输入的小说题材是“科幻”。

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
........ 把上面的代码依次放在这
async def main():
    msg = "科幻"
    role = StoryAssistant()
    logger.info(msg)
    result = await role.run(msg)
    logger.info(result)
asyncio.run(main())

4. 运行结果及不足(优化点)

4.1 运行结果

4.2 不足及优化点

(1)结果的不足

仔细读一下它生成的小说,可以发现是具有上下文连贯性的,但是还是比较差,不够顺滑。要想获得更好的结果,需要继续优化Prompt和传入的上下文等。或者我觉得还可以将以上文本不按章节分块,然后在对每块内容丢给大模型进行语言优化,包括上下文连贯性、去掉总结性的语句等。

(2)看过 【AI的未来 - AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手 的同学可能注意到了,我这里WriteContent结构用的是下图这种,其实没有体现出ActionNode的作用:

可以将其优化成上篇文章中的结构:

参考着改起来比较简单,我就不再赘述了。有兴趣的同学可以去尝试改一改。

4.3 其它思考

通过这个小说助手的实战,感觉MetaGPT的使用都不是最大的障碍了,最大的障碍是写小说的Prompt和标准流程SOP…

知道了SOP,也就知道了整体步骤。其它的无非就是优化prompt,该给大模型什么样的输入,返回的结果要不要处理,要不要给下个动作使用等。

你觉得呢?

相关文章
|
21天前
|
存储 人工智能 分布式计算
Parquet 文件格式详解与实战 | AI应用开发
Parquet 是一种列式存储文件格式,专为大规模数据处理设计,广泛应用于 Hadoop 生态系统及其他大数据平台。本文介绍 Parquet 的特点和作用,并演示如何在 Python 中使用 Pandas 库生成和读取 Parquet 文件,包括环境准备、生成和读取文件的具体步骤。【10月更文挑战第13天】
162 60
|
4天前
|
存储 人工智能 自然语言处理
AI经营|多Agent择优生成商品标题
商品标题中关键词的好坏是商品能否被主搜检索到的关键因素,使用大模型自动优化标题成为【AI经营】中的核心能力之一,本文讲述大模型如何帮助商家优化商品素材,提升商品竞争力。
AI经营|多Agent择优生成商品标题
|
5天前
|
人工智能 算法 搜索推荐
清华校友用AI破解162个高数定理,智能体LeanAgent攻克困扰陶哲轩难题!
清华校友开发的LeanAgent智能体在数学推理领域取得重大突破,成功证明了162个未被人类证明的高等数学定理,涵盖抽象代数、代数拓扑等领域。LeanAgent采用“持续学习”框架,通过课程学习、动态数据库和渐进式训练,显著提升了数学定理证明的能力,为数学研究和教育提供了新的思路和方法。
15 3
|
6天前
|
人工智能 自然语言处理 算法
企业内训|AI/大模型/智能体的测评/评估技术-某电信运营商互联网研发中心
本课程是TsingtaoAI专为某电信运营商的互联网研发中心的AI算法工程师设计,已于近日在广州对客户团队完成交付。课程聚焦AI算法工程师在AI、大模型和智能体的测评/评估技术中的关键能力建设,深入探讨如何基于当前先进的AI、大模型与智能体技术,构建符合实际场景需求的科学测评体系。课程内容涵盖大模型及智能体的基础理论、测评集构建、评分标准、自动化与人工测评方法,以及特定垂直场景下的测评实战等方面。
33 4
|
19天前
|
人工智能 API 决策智能
swarm Agent框架入门指南:构建与编排多智能体系统的利器 | AI应用开发
Swarm是OpenAI在2024年10月12日宣布开源的一个实验性质的多智能体编排框架。其核心目标是让智能体之间的协调和执行变得更轻量级、更容易控制和测试。Swarm框架的主要特性包括轻量化、易于使用和高度可定制性,非常适合处理大量独立的功能和指令。【10月更文挑战第15天】
129 6
|
20天前
|
人工智能 资源调度 数据可视化
【AI应用落地实战】智能文档处理本地部署——可视化文档解析前端TextIn ParseX实践
2024长沙·中国1024程序员节以“智能应用新生态”为主题,吸引了众多技术大咖。合合信息展示了“智能文档处理百宝箱”的三大工具:可视化文档解析前端TextIn ParseX、向量化acge-embedding模型和文档解析测评工具markdown_tester,助力智能文档处理与知识管理。
|
1月前
|
人工智能 算法 决策智能
面向软件工程的AI智能体最新进展,复旦、南洋理工、UIUC联合发布全面综述
【10月更文挑战第9天】近年来,基于大型语言模型(LLM)的智能体在软件工程领域展现出显著成效。复旦大学、南洋理工大学和伊利诺伊大学厄巴纳-香槟分校的研究人员联合发布综述,分析了106篇论文,探讨了这些智能体在需求工程、代码生成、静态代码检查、测试、调试及端到端软件开发中的应用。尽管表现出色,但这些智能体仍面临复杂性、性能瓶颈和人机协作等挑战。
74 1
|
11天前
|
机器学习/深度学习 人工智能 算法
AI赋能大学计划·大模型技术与应用实战学生训练营——吉林大学站圆满结营
10月30日,由中国软件行业校园招聘与实习公共服务平台携手魔搭社区共同举办的AI赋能大学计划·大模型技术与产业趋势高校行AIGC项目实战营·吉林大学站圆满结营。
|
1月前
|
机器学习/深度学习 数据采集 人工智能
【紧跟AI浪潮】深度剖析:如何在大模型时代精准捕获用户心声——提高召回率的实战秘籍
【10月更文挑战第5天】在深度学习领域,大型模型常面临召回率不足的问题,尤其在信息检索和推荐系统中尤为关键。本文通过具体代码示例,介绍如何提升大模型召回率。首先,利用Pandas进行数据预处理,如清洗和特征工程;其次,选择合适的模型架构,如使用PyTorch构建推荐系统;再者,优化训练策略,采用合适的损失函数及正则化技术;此外,选择恰当的评估指标,如召回率和F1分数;最后,通过后处理优化结果展示。以上方法不仅提升召回率,还增强了模型整体性能。
71 0
|
3月前
|
存储 人工智能