大家好,我是【同学小张】。持续学习,持续干货输出,关注我,跟我一起学AI大模型技术!公众号内文章一览
有任何问题,欢迎+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 流程设计
简单画了下,凑活看… 解释下:
(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 Agent系列】【MetaGPT】6. 用ActionNode重写技术文档助手
的同学可能注意到了,我这里WriteContent结构用的是下图这种,其实没有体现出ActionNode的作用:
可以将其优化成上篇文章中的结构:
参考着改起来比较简单,我就不再赘述了。有兴趣的同学可以去尝试改一改。
4.3 其它思考
通过这个小说助手的实战,感觉MetaGPT的使用都不是最大的障碍了,最大的障碍是写小说的Prompt和标准流程SOP…..
知道了SOP,也就知道了整体步骤。其它的无非就是优化prompt,该给大模型什么样的输入,返回的结果要不要处理,要不要给下个动作使用等。
你觉得呢?
大家好,我是【同学小张】。持续学习,持续干货输出,关注我,跟我一起学AI大模型技术!公众号内文章一览
有任何问题,欢迎+vx:jasper_8017,期待与志同道合的朋友一起讨论,共同进步!
转载请注明:【AI Agent系列】【MetaGPT】7. 实战:只用两个字,让MetaGPT写一篇小说 | ChatGPT资源导航