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

AI小说 3个月前 lida
18 0 0

大家好,我是【同学小张】。持续学习,持续干货输出,关注我,跟我一起学AI大模型技术!公众号内文章一览

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

有任何问题,欢迎+vx:jasper_8017,期待与志同道合的朋友一起讨论,共同进步!

0. 前置推荐阅读

了解什么是ActionNode:【AI Agent系列】【MetaGPT】4. ActionNode从理论到实战

了解如何用MetaGPT写一篇完整的文档:

【AI Agent系列】【MetaGPT】5. 更复杂的Agent实战 – 实现技术文档助手

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

更多 MetaGPT 实战可看本公众号系列文章。

1. 本文内容

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

2. 需求分析

2.1 步骤拆解

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

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

小说有故事情节、人物等要素

各章节之间要保持故事的连贯性

…… 其它可能没想到的

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

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

ChatGPT魔法课堂进阶:通过Prompt提示模板写本自己的小说https://zhuanlan.zhihu.com/p/619307788

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

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

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

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

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

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

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

2.2 流程设计

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

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

(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 运行结果

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

4.2 不足及优化点

(1)结果的不足

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

(2)看过 【AI Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手

 的同学可能注意到了,我这里WriteContent结构用的是下图这种,其实没有体现出ActionNode的作用:

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

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

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

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

4.3 其它思考

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

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

你觉得呢?

大家好,我是【同学小张】。持续学习,持续干货输出,关注我,跟我一起学AI大模型技术!公众号内文章一览

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

有任何问题,欢迎+vx:jasper_8017,期待与志同道合的朋友一起讨论,共同进步!

相关文章