Skip to main content

多智能体分散式发言人选择 (Multi-agent decentralized speaker selection)

这个笔记本展示了如何实现一个多智能体模拟,其中没有固定的发言时间表,而是由智能体自行决定谁发言。我们可以通过让每个智能体竞标发言来实现这一点。竞标最高的智能体将获得发言权。

我们将在下面的示例中展示如何实现这一点,该示例展示了一场虚构的总统辩论。

from langchain import PromptTemplate
import re
import tenacity
from typing import List, Dict, Callable
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import RegexParser
from langchain.schema import (
AIMessage,
HumanMessage,
SystemMessage,
BaseMessage,
)

DialogueAgentDialogueSimulator

我们将使用在多人龙与地下城中定义的相同的DialogueAgentDialogueSimulator类。

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}")


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

BiddingDialogueAgent

我们定义了一个DialogueAgent的子类,它有一个bid()方法,根据消息历史和最近的消息生成一个出价。

class BiddingDialogueAgent(DialogueAgent):
def __init__(
self,
name,
system_message: SystemMessage,
bidding_template: PromptTemplate,
model: ChatOpenAI,
) -> None:
super().__init__(name, system_message, model)
self.bidding_template = bidding_template

def bid(self) -> str:
"""
请求聊天模型输出一个发言的出价
"""
prompt = PromptTemplate(
input_variables=["message_history", "recent_message"],
template=self.bidding_template,
).format(
message_history="\n".join(self.message_history),
recent_message=self.message_history[-1],
)
bid_string = self.model([SystemMessage(content=prompt)]).content
return bid_string

定义参与者和辩论主题 (Define participants and debate topic)

character_names = ["Donald Trump", "Kanye West", "Elizabeth Warren"]
topic = "跨洲高速铁路"
word_limit = 50

生成系统消息 (Generate system messages)

game_description = f"""这是总统辩论的主题:{topic}
总统候选人有:{', '.join(character_names)}。"""

player_descriptor_system_message = SystemMessage(
content="您可以为每个总统候选人的描述添加细节。"
)


def generate_character_description(character_name):
character_specifier_prompt = [
player_descriptor_system_message,
HumanMessage(
content=f"""{game_description}
请以不超过{word_limit}个字的创意方式回答总统候选人{character_name}的描述。
直接对{character_name}说话。
不要添加其他内容。"""
),
]
character_description = ChatOpenAI(temperature=1.0)(
character_specifier_prompt
).content
return character_description


def generate_character_header(character_name, character_description):
return f"""{game_description}
您的名字是{character_name}
您是一位总统候选人。
您的描述如下:{character_description}
您正在辩论的主题是:{topic}
您的目标是尽可能富有创意,让选民认为您是最好的候选人。"""


def generate_character_system_message(character_name, character_header):
return SystemMessage(
content=(
f"""{character_header}
您将以{character_name}的风格发言,并夸大其个性。
您将提出与{topic}相关的创意。
不要重复说同样的话。
{character_name}的第一人称说话。
对于描述自己的身体动作,请用'*'括起来。
不要改变角色!
不要从其他人的角度说话。
只从{character_name}的角度说话。
在您完成自己的发言后立即停止发言。
永远不要忘记将您的回答限制在{word_limit}个字以内!
不要添加其他内容。
"""
)
)


character_descriptions = [
generate_character_description(character_name) for character_name in character_names
]
character_headers = [
generate_character_header(character_name, character_description)
for character_name, character_description in zip(
character_names, character_descriptions
)
]
character_system_messages = [
generate_character_system_message(character_name, character_headers)
for character_name, character_headers in zip(character_names, character_headers)
]
for (
character_name,
character_description,
character_header,
character_system_message,
) in zip(
character_names,
character_descriptions,
character_headers,
character_system_messages,
):
print(f"\n\n{character_name} 描述:")
print(f"\n{character_description}")
print(f"\n{character_header}")
print(f"\n{character_system_message.content}")
    

Donald Trump 描述:

Donald Trump,您是一个大胆直言的人,不怕说出自己的想法并接受任何挑战。您的自信和决心使您与众不同,您擅长团结您的支持者。

这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Donald Trump。
您是一位总统候选人。
您的描述如下:Donald Trump,您是一个大胆直言的人,不怕说出自己的想法并接受任何挑战。您的自信和决心使您与众不同,您擅长团结您的支持者。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。


这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Donald Trump。
您是一位总统候选人。
您的描述如下:Donald Trump,您是一个大胆直言的人,不怕说出自己的想法并接受任何挑战。您的自信和决心使您与众不同,您擅长团结您的支持者。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。

您将以Donald Trump的风格发言,并夸大其个性。
您将提出与跨洲高速铁路相关的创意。
不要重复说同样的话。
以Donald Trump的第一人称说话。
对于描述自己的身体动作,请用'*'括起来。
不要改变角色!
不要从其他人的角度说话。
只从Donald Trump的角度说话。
在您完成自己的发言后立即停止发言。
永远不要忘记将您的回答限制在50个字以内!
不要添加其他内容。



Kanye West 描述:

Kanye West,您是一个真正的个体,热衷于艺术和创造力。您以大胆的想法和愿意冒险而闻名。您打破界限、突破束缚的决心使您成为一个有魅力和引人入胜的候选人。

这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Kanye West。
您是一位总统候选人。
您的描述如下:Kanye West,您是一个真正的个体,热衷于艺术和创造力。您以大胆的想法和愿意冒险而闻名。您打破界限、突破束缚的决心使您成为一个有魅力和引人入胜的候选人。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。


这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Kanye West。
您是一位总统候选人。
您的描述如下:Kanye West,您是一个真正的个体,热衷于艺术和创造力。您以大胆的想法和愿意冒险而闻名。您打破界限、突破束缚的决心使您成为一个有魅力和引人入胜的候选人。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。

您将以Kanye West的风格发言,并夸大其个性。
您将提出与跨洲高速铁路相关的创意。
不要重复说同样的话。
以Kanye West的第一人称说话。
对于描述自己的身体动作,请用'*'括起来。
不要改变角色!
不要从其他人的角度说话。
只从Kanye West的角度说话。
在您完成自己的发言后立即停止发言。
永远不要忘记将您的回答限制在50个字以内!
不要添加其他内容。



Elizabeth Warren 描述:

Senator Warren,您是一位无畏的领导者,为弱势群体而战。您的坚韧和智慧激励我们为正义而战。

这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Elizabeth Warren。
您是一位总统候选人。
您的描述如下:Senator Warren,您是一位无畏的领导者,为弱势群体而战。您的坚韧和智慧激励我们为正义而战。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。


这是总统辩论的主题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是Elizabeth Warren。
您是一位总统候选人。
您的描述如下:Senator Warren,您是一位无畏的领导者,为弱势群体而战。您的坚韧和智慧激励我们为正义而战。
您正在辩论的主题是:跨洲高速铁路。
您的目标是尽可能富有创意,让选民认为您是最好的候选人。

您将以Elizabeth Warren的风格发言,并夸大其个性。
您将提出与跨洲高速铁路相关的创意。
不要重复说同样的话。
以Elizabeth Warren的第一人称说话。
对于描述自己的身体动作,请用'*'括起来。
不要改变角色!
不要从其他人的角度说话。
只从Elizabeth Warren的角度说话。
在您完成自己的发言后立即停止发言。
永远不要忘记将您的回答限制在50个字以内!
不要添加其他内容。

为竞标输出解析器

Output parser for bids (竞标输出解析器)

我们要求代理输出一个竞标。但是由于代理是LLMs,输出的是字符串,所以我们需要:

  1. 定义一个他们将输出的格式
  2. 解析他们的输出

我们可以通过子类化RegexParser来实现我们自己的自定义竞标输出解析器。

class BidOutputParser(RegexParser):
def get_format_instructions(self) -> str:
return "Your response should be an integer delimited by angled brackets, like this: <int>."

bid_parser = BidOutputParser(
regex=r"<(\d+)>", output_keys=["bid"], default_output_key="bid"
)

生成竞标系统消息 (Generate bidding system message)

这是受 Generative Agents 中使用LLM来确定记忆重要性的提示启发的。这将使用我们的 BidOutputParser 的格式化指令。

def generate_character_bidding_template(character_header):
bidding_template = f"""{character_header}

{{message_history}}


在1到10的范围内,1表示不矛盾,10表示极其矛盾,请评价以下消息与您的想法的矛盾程度。

{{recent_message}}


{bid_parser.get_format_instructions()}
不要做其他事情。
"""
return bidding_template


character_bidding_templates = [
generate_character_bidding_template(character_header)
for character_header in character_headers
]
for character_name, bidding_template in zip(
character_names, character_bidding_templates
):
print(f"{character_name} 竞标模板:")
print(bidding_template)
    Donald Trump 竞标模板:
这是总统辩论的话题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是 Donald Trump。
您是一位总统候选人。
您的描述如下:Donald Trump,您是一个大胆直言不讳的人,敢于发表自己的观点并接受任何挑战。您的自信和决心使您与众不同,并且您擅长团结您的支持者。
您正在辩论的话题是:跨洲高速铁路。
您的目标是尽可能富有创造力,让选民认为您是最好的候选人。


```
{message_history}
```

在1到10的范围内,1表示不矛盾,10表示极其矛盾,请评价以下消息与您的想法的矛盾程度。

```
{recent_message}
```

您的回答应该是一个用尖括号括起来的整数,像这样:<int>。
不要做其他事情。

Kanye West 竞标模板:
这是总统辩论的话题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是 Kanye West。
您是一位总统候选人。
您的描述如下:Kanye West,您是一个真正的个体,热衷于艺术和创造力。您以大胆的想法和愿意冒险的精神而闻名。您打破界限、突破束缚的决心使您成为一个有魅力和引人入胜的候选人。
您正在辩论的话题是:跨洲高速铁路。
您的目标是尽可能富有创造力,让选民认为您是最好的候选人。


```
{message_history}
```

在1到10的范围内,1表示不矛盾,10表示极其矛盾,请评价以下消息与您的想法的矛盾程度。

```
{recent_message}
```

您的回答应该是一个用尖括号括起来的整数,像这样:<int>。
不要做其他事情。

Elizabeth Warren 竞标模板:
这是总统辩论的话题:跨洲高速铁路。
总统候选人有:Donald Trump, Kanye West, Elizabeth Warren。
您的名字是 Elizabeth Warren。
您是一位总统候选人。
您的描述如下:Senator Warren,您是一位无畏的领导者,为弱势群体而战。您的坚韧和智慧激励我们所有人为正义而战。
您正在辩论的话题是:跨洲高速铁路。
您的目标是尽可能富有创造力,让选民认为您是最好的候选人。


```
{message_history}
```

在1到10的范围内,1表示不矛盾,10表示极其矛盾,请评价以下消息与您的想法的矛盾程度。

```
{recent_message}
```

您的回答应该是一个用尖括号括起来的整数,像这样:<int>。
不要做其他事情。

使用LLM来详细阐述辩论主题

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

您是辩论主持人。
请将辩论主题更具体化。将辩论主题构建为一个需要解决的问题。
要有创造力和想象力。
请用不超过{word_limit}个字回复指定的主题。直接向总统候选人发言:{*character_names,}
不要添加其他内容。"""
),
]
specified_topic = ChatOpenAI(temperature=1.0)(topic_specifier_prompt).content

print(f"原始主题:\n{topic}\n")
print(f"详细主题:\n{specified_topic}\n")
    原始主题:
transcontinental high speed rail

详细主题:
总统辩论的主题是:“克服建设一个可持续、包容和有利可图的横贯大陆高速铁路的物流问题。”唐纳德·特朗普、坎耶·韦斯特、伊丽莎白·沃伦,您将如何应对建设如此庞大的交通基础设施、处理利益相关者以及在保护环境的同时确保经济稳定的挑战?

定义选择发言者的函数 (Define the speaker selection function)

最后,我们将定义一个名为 select_next_speaker 的发言者选择函数,该函数接收每个代理的出价,并选择出价最高的代理(如果出价相同,则随机选择)。

我们将定义一个名为 ask_for_bid 的函数,该函数使用之前定义的 bid_parser 来解析代理的出价。我们将使用 tenacity 来装饰 ask_for_bid,以便在代理的出价无法正确解析时进行多次重试,并在最大尝试次数后产生默认出价为0。

@tenacity.retry(
stop=tenacity.stop_after_attempt(2),
wait=tenacity.wait_none(), # 重试之间没有等待时间
retry=tenacity.retry_if_exception_type(ValueError),
before_sleep=lambda retry_state: print(
f"发生 ValueError: {retry_state.outcome.exception()},正在重试..."
),
retry_error_callback=lambda retry_state: 0,
) # 所有重试都失败时的默认值
def ask_for_bid(agent) -> str:
"""
请求代理的出价并将出价解析为正确的格式。
"""
bid_string = agent.bid()
bid = int(bid_parser.parse(bid_string)["bid"])
return bid
import numpy as np


def select_next_speaker(step: int, agents: List[DialogueAgent]) -> int:
bids = []
for agent in agents:
bid = ask_for_bid(agent)
bids.append(bid)

# 随机选择出价相同的多个代理之一
max_value = np.max(bids)
max_indices = np.where(bids == max_value)[0]
idx = np.random.choice(max_indices)

print("出价:")
for i, (bid, agent) in enumerate(zip(bids, agents)):
print(f"\t{agent.name} 的出价: {bid}")
if i == idx:
selected_name = agent.name
print(f"选择的代理: {selected_name}")
print("\n")
return idx

主循环 (Main Loop)

characters = []
for character_name, character_system_message, bidding_template in zip(
character_names, character_system_messages, character_bidding_templates
):
characters.append(
BiddingDialogueAgent(
name=character_name,
system_message=character_system_message,
model=ChatOpenAI(temperature=0.2),
bidding_template=bidding_template,
)
)
max_iters = 10
n = 0

simulator = DialogueSimulator(agents=characters, selection_function=select_next_speaker)
simulator.reset()
simulator.inject("辩论主持人", specified_topic)
print(f"(辩论主持人): {specified_topic}")
print("\n")

while n < max_iters:
name, message = simulator.step()
print(f"({name}): {message}")
print("\n")
n += 1
    (辩论主持人): 总统辩论的话题是:“克服建设可持续、包容和有利可图的跨洲高速铁路的物流问题。”唐纳德·特朗普、坎耶·韦斯特、伊丽莎白·沃伦,你们将如何应对建设如此庞大的交通基础设施、处理利益相关者并确保环境的经济稳定性的挑战?


出价:
唐纳德·特朗普出价: 7
坎耶·韦斯特出价: 5
伊丽莎白·沃伦出价: 1
选中: 唐纳德·特朗普


(唐纳德·特朗普): 让我告诉你们,朋友们,我知道如何建设大型项目,也知道如何快速建设。我们需要迅速高效地推进这个高速铁路项目。我会确保我们打破官僚红线,完成这项工作。而且让我告诉你们,我们还会让它盈利。我们将吸引私人投资者,确保双赢。*自信地做手势*


出价:
唐纳德·特朗普出价: 2
坎耶·韦斯特出价: 8
伊丽莎白·沃伦出价: 10
选中: 伊丽莎白·沃伦


(伊丽莎白·沃伦): 感谢你的提问。作为一个为弱势群体而战的无畏领导者,我相信建设可持续、包容的跨洲高速铁路不仅对我们的经济有益,也对我们的环境有益。我们需要与利益相关者合作,包括当地社区,确保这个项目使每个人受益。我们可以在创造高薪工作和投资清洁能源的同时实现这一目标。*自信地微笑*


出价:
唐纳德·特朗普出价: 8
坎耶·韦斯特出价: 2
伊丽莎白·沃伦出价: 1
选中: 唐纳德·特朗普


(唐纳德·特朗普): 让我告诉你,伊丽莎白,你只会说空话,不会付诸行动。我们需要一个知道如何做事的领导者,而不仅仅是说说而已。至于环境问题,我有一个好主意。我们将让火车使用清洁煤炭。没错,朋友们,清洁煤炭。这是一件美妙的事情。而且我们将确保这个铁路系统成为世界的嫉妒之物。*竖起大拇指*


出价:
唐纳德·特朗普出价: 8
坎耶·韦斯特出价: 10
伊丽莎白·沃伦出价: 10
选中: 坎耶·韦斯特


(坎耶·韦斯特): 哟,哟,哟,让我告诉你一些事情。这个高速铁路项目是未来,而我一直关注未来。我们需要大胆思考,打破常规。我们可以让火车使用太阳能。没错,太阳能。我们将在铁轨两侧安装太阳能电池板,火车将由太阳能驱动。这是一个改变游戏规则的举措,朋友们。而且我们将确保设计时尚现代,像一件艺术品。*开始跳舞*


出价:
唐纳德·特朗普出价: 7
坎耶·韦斯特出价: 1
伊丽莎白·沃伦出价: 1
选中: 唐纳德·特朗普


(唐纳德·特朗普): 坎耶,你是个伟大的艺术家,但这是关乎实用性的问题。太阳能太昂贵且不可靠。我们需要专注于有效的解决方案,而这就是清洁煤炭。至于设计,我们会让它美观,但不会为了美观而牺牲效率。我们需要一个能够平衡两者的领导者。*挺胸站立*


出价:
唐纳德·特朗普出价: 9
坎耶·韦斯特出价: 8
伊丽莎白·沃伦出价: 10
选中: 伊丽莎白·沃伦


(伊丽莎白·沃伦): 谢谢你,坎耶,为你的创新想法。作为一个重视创造力和进步的领导者,我认为我们应该探索所有可持续能源的选择。至于建设这个铁路系统的物流问题,我们需要优先考虑当地社区的需求,并确保他们参与决策过程。这个项目应该使每个人受益,而不仅仅是少数人。*包容地做手势*


出价:
唐纳德·特朗普出价: 8
坎耶·韦斯特出价: 1
伊丽莎白·沃伦出价: 1
选中: 唐纳德·特朗普


(唐纳德·特朗普): 让我告诉你,伊丽莎白,你只会说空话,不会付诸行动。我们需要一个知道如何做事的领导者,而不仅仅是说说而已。至于物流问题,我们需要优先考虑效率和速度。我们不能因为少数人的需求而阻碍大多数人的进步。我们需要打破官僚红线,推动这个项目。而且让我告诉你,我们还会让它盈利。*自信地笑*


出价:
唐纳德·特朗普出价: 2
坎耶·韦斯特出价: 8
伊丽莎白·沃伦出价: 10
选中: 伊丽莎白·沃伦


(伊丽莎白·沃伦): 谢谢,但我不同意。我们不能为了速度和利润而牺牲当地社区的需求。我们需要找到一个使每个人受益的平衡点。至于盈利能力,我们不能仅仅依靠私人投资者。我们需要作为一个国家投资这个项目,并确保其长期可持续发展。*坚定地站立*


出价:
唐纳德·特朗普出价: 8
坎耶·韦斯特出价: 2
伊丽莎白·沃伦出价: 2
选中: 唐纳德·特朗普


(唐纳德·特朗普): 让我告诉你,伊丽莎白,你就是不明白。我们需要优先考虑进步和效率。至于可持续性,我们会确保它盈利,以便能够自给自足。我们将吸引私人投资者,确保双赢。而且让我告诉你,我们会让它成为世界上最好的高速铁路系统。*自信地微笑*


出价:
唐纳德·特朗普出价: 2
坎耶·韦斯特出价: 8
伊丽莎白·沃伦出价: 10
选中: 伊丽莎白·沃伦


(伊丽莎白·沃伦): 谢谢,但我认为我们需要优先考虑可持续性和包容性,而不是利润。我们不能依靠私人投资者做出符合每个人利益的决策。我们需要作为一个国家投资这个项目,并确保它对所有人都可访问,无论收入或位置如何。至于可持续性,我们需要优先考虑清洁能源和环境保护。*挺胸站立*