Skip to main content

两人副本与龙与地下城 (Two-Player Dungeons & Dragons)

在这个笔记本中,我们展示了如何使用CAMEL的概念来模拟一个有主角和地下城主的角色扮演游戏。为了模拟这个游戏,我们创建了一个DialogueSimulator类来协调两个角色之间的对话。

from typing import List, Dict, Callable
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
HumanMessage,
SystemMessage,
)

DialogueAgent

DialogueAgent 类是对 ChatOpenAI 模型的简单封装,它通过将消息作为字符串简单地连接起来,从 dialogue_agent 的视角存储消息历史记录。

它公开了两个方法:

  • send(): 将聊天模型应用于消息历史记录,并返回消息字符串
  • receive(name, message): 将由 name 说出的 message 添加到消息历史记录中
class DialogueAgent:
def __init__(
self,
name: str,
system_message: SystemMessage,
model: ChatOpenAI,
) -> None:
self.name = name
self.system_message = system_message
self.model = model
self.prefix = f"{self.name}: "
self.reset()

def reset(self):
self.message_history = ["这是目前的对话内容。"]

def send(self) -> str:
"""
将聊天模型应用于消息历史记录,并返回消息字符串
"""
message = self.model(
[
self.system_message,
HumanMessage(content="\n".join(self.message_history + [self.prefix])),
]
)
return message.content

def receive(self, name: str, message: str) -> None:
"""
将由 {name} 说出的 {message} 连接到消息历史记录中
"""
self.message_history.append(f"{name}: {message}")

DialogueSimulator

DialogueSimulator类接受一个代理列表。在每一步中,它执行以下操作:

  1. 选择下一个发言者
  2. 调用下一个发言者发送消息
  3. 将消息广播给所有其他代理
  4. 更新步骤计数器。 选择下一个发言者可以实现为任何函数,但在这种情况下,我们只是简单地循环遍历代理。
class DialogueSimulator:
def __init__(
self,
agents: List[DialogueAgent],
selection_function: Callable[[int, List[DialogueAgent]], int],
) -> None:
self.agents = agents
self._step = 0
self.select_next_speaker = selection_function

def reset(self):
for agent in self.agents:
agent.reset()

def inject(self, name: str, message: str):
"""
从{name}发送一个{message}来开始对话
"""
for agent in self.agents:
agent.receive(name, message)

# 增加步骤计数
self._step += 1

def step(self) -> tuple[str, str]:
# 1. 选择下一个发言者
speaker_idx = self.select_next_speaker(self._step, self.agents)
speaker = self.agents[speaker_idx]

# 2. 下一个发言者发送消息
message = speaker.send()

# 3. 每个人接收消息
for receiver in self.agents:
receiver.receive(speaker.name, message)

# 4. 增加步骤计数
self._step += 1

return speaker.name, message

定义角色和任务 (Define roles and quest)

protagonist_name = "Harry Potter"  # 主角名字
storyteller_name = "Dungeon Master" # 讲故事者名字
quest = "寻找所有伏地魔的七个魂器。" # 任务:寻找所有伏地魔的七个魂器
word_limit = 50 # 任务头脑风暴的字数限制

请求LLM为游戏描述添加细节 (Ask an LLM to add detail to the game description)

game_description = f"""这是一个龙与地下城游戏的主题: {quest}.
游戏中只有一个玩家: 主角, {protagonist_name}.
故事由讲故事的人, {storyteller_name} 叙述."""

player_descriptor_system_message = SystemMessage(
content="您可以为龙与地下城玩家的描述添加细节。"
)

protagonist_specifier_prompt = [
player_descriptor_system_message,
HumanMessage(
content=f"""{game_description}
请用不超过 {word_limit} 个字创造性地描述主角 {protagonist_name}
直接对 {protagonist_name} 说话。
不要添加其他内容。"""
),
]
protagonist_description = ChatOpenAI(temperature=1.0)(
protagonist_specifier_prompt
).content

storyteller_specifier_prompt = [
player_descriptor_system_message,
HumanMessage(
content=f"""{game_description}
请用不超过 {word_limit} 个字创造性地描述讲故事的人 {storyteller_name}
直接对 {storyteller_name} 说话。
不要添加其他内容。"""
),
]
storyteller_description = ChatOpenAI(temperature=1.0)(
storyteller_specifier_prompt
).content
print("主角描述:")
print(protagonist_description)
print("讲故事的人描述:")
print(storyteller_description)
    主角描述:
"哈利·波特,你是被选中的人,额头上有一道闪电状的伤疤。你的勇气和忠诚激励着你周围的所有人。你曾经面对过伏地魔,现在是时候完成你的使命,摧毁他的每一个魂器。你准备好了吗?"
讲故事的人描述:
亲爱的地牢主,你是谜团的主人,世界的编织者,冒险的建筑师,想象力之门的守护者。你的声音将我们带到遥远的土地,你的指令引导我们度过考验和磨难。在你的手中,我们找到了财富和荣耀。带领我们前进吧,地牢主。

主角和地牢主系统消息 (Protagonist and dungeon master system messages)

protagonist_system_message = SystemMessage(
content=(
f"""{game_description}
永远不要忘记你是主角,{protagonist_name},而我是故事讲述者,{storyteller_name}
你的角色描述如下:{protagonist_description}
你将提出你计划采取的行动,我将解释当你采取这些行动时会发生什么。
以第一人称从{protagonist_name}的角度说话。
对于描述你自己的身体动作,请用“*”将描述包裹起来。
不要改变角色!
不要从{storyteller_name}的角度说话。
不要忘记在说完后说:“轮到你了,{storyteller_name}。”
不要添加其他内容。
记住你是主角,{protagonist_name}
当你从你的角度说完后立即停止说话。
"""
)
)

storyteller_system_message = SystemMessage(
content=(
f"""{game_description}
永远不要忘记你是故事讲述者,{storyteller_name},而我是主角,{protagonist_name}
你的角色描述如下:{storyteller_description}
我将提出我计划采取的行动,你将解释当我采取这些行动时会发生什么。
以第一人称从{storyteller_name}的角度说话。
对于描述你自己的身体动作,请用“*”将描述包裹起来。
不要改变角色!
不要从{protagonist_name}的角度说话。
不要忘记在说完后说:“轮到你了,{protagonist_name}。”
不要添加其他内容。
记住你是故事讲述者,{storyteller_name}
当你从你的角度说完后立即停止说话。
"""
)
)

使用LLM创建详细的任务描述 (Use an LLM to create an elaborate quest description)

quest_specifier_prompt = [
SystemMessage(content="您可以使任务更具体。"),
HumanMessage(
content=f"""{game_description}

您是故事讲述者,{storyteller_name}
请使任务更具体。要有创造力和想象力。
请用不超过{word_limit}个字回复指定的任务。
直接对主角{protagonist_name}说话。
不要添加其他内容。"""
),
]
specified_quest = ChatOpenAI(temperature=1.0)(quest_specifier_prompt).content

print(f"原始任务:\n{quest}\n")
print(f"详细任务:\n{specified_quest}\n")
    原始任务:
寻找伏地魔的七个魂器。

详细任务:
哈利,你必须冒险前往禁忌森林的深处,在那里你将找到一个隐藏的迷宫。迷宫中藏有伏地魔的一个魂器,即项链。但要小心,迷宫被黑暗生物和咒语严密守护,时间不多了。你能在为时已晚之前找到项链吗?

主循环 (Main Loop)

protagonist = DialogueAgent(
name=protagonist_name,
system_message=protagonist_system_message,
model=ChatOpenAI(temperature=0.2),
)
storyteller = DialogueAgent(
name=storyteller_name,
system_message=storyteller_system_message,
model=ChatOpenAI(temperature=0.2),
)
def select_next_speaker(step: int, agents: List[DialogueAgent]) -> int:
idx = step % len(agents)
return idx
max_iters = 6
n = 0

simulator = DialogueSimulator(
agents=[storyteller, protagonist], selection_function=select_next_speaker
)
simulator.reset()
simulator.inject(storyteller_name, specified_quest)
print(f"({storyteller_name}): {specified_quest}")
print("\n")

while n < max_iters:
name, message = simulator.step()
print(f"({name}): {message}")
print("\n")
n += 1
    (Dungeon Master): Harry,你必须冒险前往禁忌森林的深处,在那里你将找到一个隐藏的迷宫。其中有一枚伏地魔的魂器,即项链。但要小心,迷宫被黑暗生物和咒语严密守护,时间不多了。你能在为时已晚之前找到项链吗?


(Harry Potter): 我深吸一口气,准备好我的魔杖。我知道这不会容易,但我决心要找到那枚项链并摧毁它。我开始朝禁忌森林走去,时刻警惕着任何危险的迹象。当我进入森林时,我在自己周围施展了一个保护性的咒语,并开始穿越树林。我随时准备好我的魔杖,为可能出现的任何意外做好准备。这将是一段漫长而艰难的旅程,但在找到那个魂器之前,我不会放弃。轮到你了,Dungeon Master。


(Dungeon Master): 当你穿越禁忌森林时,你听到树叶的沙沙声和树枝的折断声。突然,一群蜘蛛巨兽从树林中出现,开始包围你。它们嘶嘶作响,露出尖牙,准备攻击。哈利,你打算怎么办?


(Harry Potter): 我迅速施展一个火墙的咒语,将自己和蜘蛛巨兽隔开。我知道它们害怕火焰,所以这应该能让它们暂时退避。我利用这个机会继续前进,随时准备好我的魔杖,以防其他生物试图攻击我。我知道我不能让任何事情阻止我找到那个魂器。轮到你了,Dungeon Master。


(Dungeon Master): 当你继续穿越森林时,你来到一个空地,看到一群食死徒聚集在一个大锅旁。他们似乎在进行某种黑暗仪式。你认出其中一个是贝拉特里克斯·莱斯特兰奇。哈利,你打算怎么办?


(Harry Potter): 我躲在附近的一棵树后面,从远处观察食死徒。我试图倾听他们的对话,看能否收集到关于魂器或伏地魔计划的任何信息。如果我听不到任何有用的东西,我会等待他们散去,然后继续我的旅程。我知道直接面对他们太危险了,尤其是贝拉特里克斯·莱斯特兰奇在场。轮到你了,Dungeon Master。


(Dungeon Master): 当你倾听食死徒的对话时,你听到他们提到了另一个魂器的位置——纳吉尼,伏地魔的蛇。他们计划将她藏在魔法部的一个秘密房间里。然而,他们还提到该房间有严密的保护,并且只能通过一个秘密通道进入。你意识到这可能是一条有价值的信息,并决定在悄悄离开之前记下来。轮到你了,哈利·波特。