多智能体辩论#
多智能体辩论是一种多智能体设计模式,它模拟了多轮交互过程, 在每一轮中,智能体相互交换响应,并根据其他智能体的响应来完善自己的回答。
本示例展示了使用多智能体辩论模式来解决来自GSM8K基准测试的数学问题。
该模式包含两种类型的智能体:求解器智能体和聚合器智能体。 求解器智能体按照稀疏通信拓扑改进多智能体辩论中描述的技术进行稀疏连接。 求解器智能体负责解决数学问题并相互交换响应。 聚合器智能体负责将数学问题分发给求解器智能体, 等待它们的最终响应,并汇总这些响应以得到最终答案。
该模式的工作流程如下:
用户向聚合器智能体发送数学问题
聚合器智能体将问题分发给求解器智能体
每个求解器智能体处理问题,并向其邻居发布响应
每个求解器智能体使用邻居的响应来完善自己的回答,并发布新响应
重复步骤4固定轮数。在最后一轮,每个求解器智能体发布最终响应
聚合器智能体使用多数表决法汇总所有求解器智能体的最终响应得到最终答案,并发布该答案
我们将使用广播API,即publish_message()
,
并通过主题和订阅来实现通信拓扑。
阅读主题与订阅了解其工作原理。
import re
from dataclasses import dataclass
from typing import Dict, List
from autogen_core import (
DefaultTopicId,
MessageContext,
RoutedAgent,
SingleThreadedAgentRuntime,
TypeSubscription,
default_subscription,
message_handler,
)
from autogen_core.models import (
AssistantMessage,
ChatCompletionClient,
LLMMessage,
SystemMessage,
UserMessage,
)
from autogen_ext.models.openai import OpenAIChatCompletionClient
消息协议#
首先,我们定义智能体使用的消息。
IntermediateSolverResponse
是求解器智能体在每轮中交换的消息,
而FinalSolverResponse
是求解器智能体在最后一轮发布的消息。
@dataclass
class Question:
content: str
@dataclass
class Answer:
content: str
@dataclass
class SolverRequest:
content: str
question: str
@dataclass
class IntermediateSolverResponse:
content: str
question: str
answer: str
round: int
@dataclass
class FinalSolverResponse:
answer: str
求解器智能体#
求解器智能体负责解决数学问题并与其他求解器智能体交换响应。
当收到SolverRequest
时,求解器智能体使用LLM生成答案。
然后根据轮数发布IntermediateSolverResponse
或FinalSolverResponse
。
求解器智能体被赋予一个主题类型,用于指示该智能体应向哪个主题发布中间响应。 其邻居会订阅该主题以接收来自该智能体的响应——我们稍后将展示如何实现这一点。
我们使用default_subscription()
让
求解器智能体订阅默认主题,聚合器智能体通过该主题收集求解器智能体的最终响应。
@default_subscription
class MathSolver(RoutedAgent):
def __init__(self, model_client: ChatCompletionClient, topic_type: str, num_neighbors: int, max_round: int) -> None:
super().__init__("A debator.")
self._topic_type = topic_type
self._model_client = model_client
self._num_neighbors = num_neighbors
self._history: List[LLMMessage] = []
self._buffer: Dict[int, List[IntermediateSolverResponse]] = {}
self._system_messages = [
SystemMessage(
content=(
"You are a helpful assistant with expertise in mathematics and reasoning. "
"Your task is to assist in solving a math reasoning problem by providing "
"a clear and detailed solution. Limit your output within 100 words, "
"and your final answer should be a single numerical number, "
"in the form of {{answer}}, at the end of your response. "
"For example, 'The answer is {{42}}.'"
)
)
]
self._round = 0
self._max_round = max_round
@message_handler
async def handle_request(self, message: SolverRequest, ctx: MessageContext) -> None:
# 将问题添加到记忆库中。
self._history.append(UserMessage(content=message.content, source="user"))
# 使用模型进行推理。
model_result = await self._model_client.create(self._system_messages + self._history)
assert isinstance(model_result.content, str)
# 将响应添加到内存中。
self._history.append(AssistantMessage(content=model_result.content, source=self.metadata["type"]))
print(f"{'-'*80}\nSolver {self.id} round {self._round}:\n{model_result.content}")
# 从响应中提取答案。
match = re.search(r"\{\{(\-?\d+(\.\d+)?)\}\}", model_result.content)
if match is None:
raise ValueError("The model response does not contain the answer.")
answer = match.group(1)
# 增加计数器。
self._round += 1
if self._round == self._max_round:
# 如果计数器达到最大轮次,则发布最终响应。
await self.publish_message(FinalSolverResponse(answer=answer), topic_id=DefaultTopicId())
else:
# 将中间响应发布到与此求解器关联的主题。
await self.publish_message(
IntermediateSolverResponse(
content=model_result.content,
question=message.question,
answer=answer,
round=self._round,
),
topic_id=DefaultTopicId(type=self._topic_type),
)
@message_handler
async def handle_response(self, message: IntermediateSolverResponse, ctx: MessageContext) -> None:
# 将邻居的响应添加到缓冲区。
self._buffer.setdefault(message.round, []).append(message)
# 检查是否所有邻居都已响应。
if len(self._buffer[message.round]) == self._num_neighbors:
print(
f"{'-'*80}\nSolver {self.id} round {message.round}:\nReceived all responses from {self._num_neighbors} neighbors."
)
# 为下一个问题准备提示。
prompt = "These are the solutions to the problem from other agents:\n"
for resp in self._buffer[message.round]:
prompt += f"One agent solution: {resp.content}\n"
prompt += (
"Using the solutions from other agents as additional information, "
"can you provide your answer to the math problem? "
f"The original math problem is {message.question}. "
"Your final answer should be a single numerical number, "
"in the form of {{answer}}, at the end of your response."
)
# 将问题发送给代理自身以解决。
await self.send_message(SolverRequest(content=prompt, question=message.question), self.id)
# 清空缓冲区。
self._buffer.pop(message.round)
聚合器代理#
聚合器代理负责处理用户问题并将数学问题分发给求解器代理。
聚合器通过default_subscription()
订阅默认主题。默认主题用于接收用户问题、接收来自求解器代理的最终响应,并将最终答案发布回用户。
在更复杂的应用中,若需将多代理辩论隔离为子组件,应使用type_subscription()
为聚合器-求解器通信设置特定主题类型,并使求解器和聚合器都发布和订阅该主题类型。
@default_subscription
class MathAggregator(RoutedAgent):
def __init__(self, num_solvers: int) -> None:
super().__init__("Math Aggregator")
self._num_solvers = num_solvers
self._buffer: List[FinalSolverResponse] = []
@message_handler
async def handle_question(self, message: Question, ctx: MessageContext) -> None:
print(f"{'-'*80}\nAggregator {self.id} received question:\n{message.content}")
prompt = (
f"Can you solve the following math problem?\n{message.content}\n"
"Explain your reasoning. Your final answer should be a single numerical number, "
"in the form of {{answer}}, at the end of your response."
)
print(f"{'-'*80}\nAggregator {self.id} publishes initial solver request.")
await self.publish_message(SolverRequest(content=prompt, question=message.content), topic_id=DefaultTopicId())
@message_handler
async def handle_final_solver_response(self, message: FinalSolverResponse, ctx: MessageContext) -> None:
self._buffer.append(message)
if len(self._buffer) == self._num_solvers:
print(f"{'-'*80}\nAggregator {self.id} received all final answers from {self._num_solvers} solvers.")
# 找出多数答案。
answers = [resp.answer for resp in self._buffer]
majority_answer = max(set(answers), key=answers.count)
# 发布聚合后的响应。
await self.publish_message(Answer(content=majority_answer), topic_id=DefaultTopicId())
# 清空响应记录。
self._buffer.clear()
print(f"{'-'*80}\nAggregator {self.id} publishes final answer:\n{majority_answer}")
设置辩论环境#
现在我们将设置一个包含4个求解器代理和1个聚合器代理的多代理辩论环境。 求解器代理将以稀疏连接方式组织,如下图所示:
A --- B
| |
| |
D --- C
每个求解器代理与其他两个求解器代理相连。 例如,代理A与代理B和C相连。
让我们先创建一个运行时并注册代理类型。
runtime = SingleThreadedAgentRuntime()
model_client = OpenAIChatCompletionClient(model="gpt-4o-mini")
await MathSolver.register(
runtime,
"MathSolverA",
lambda: MathSolver(
model_client=model_client,
topic_type="MathSolverA",
num_neighbors=2,
max_round=3,
),
)
await MathSolver.register(
runtime,
"MathSolverB",
lambda: MathSolver(
model_client=model_client,
topic_type="MathSolverB",
num_neighbors=2,
max_round=3,
),
)
await MathSolver.register(
runtime,
"MathSolverC",
lambda: MathSolver(
model_client=model_client,
topic_type="MathSolverC",
num_neighbors=2,
max_round=3,
),
)
await MathSolver.register(
runtime,
"MathSolverD",
lambda: MathSolver(
model_client=model_client,
topic_type="MathSolverD",
num_neighbors=2,
max_round=3,
),
)
await MathAggregator.register(runtime, "MathAggregator", lambda: MathAggregator(num_solvers=4))
AgentType(type='MathAggregator')
现在我们将使用 TypeSubscription
创建求解器代理拓扑结构,
该结构将每个求解器代理的发布主题类型映射到其邻居代理类型。
# MathSolverA 发布主题的订阅关系
await runtime.add_subscription(TypeSubscription("MathSolverA", "MathSolverD"))
await runtime.add_subscription(TypeSubscription("MathSolverA", "MathSolverB"))
# MathSolverB 发布主题的订阅关系
await runtime.add_subscription(TypeSubscription("MathSolverB", "MathSolverA"))
await runtime.add_subscription(TypeSubscription("MathSolverB", "MathSolverC"))
# MathSolverC 发布主题的订阅关系
await runtime.add_subscription(TypeSubscription("MathSolverC", "MathSolverB"))
await runtime.add_subscription(TypeSubscription("MathSolverC", "MathSolverD"))
# MathSolverD 发布主题的订阅关系
await runtime.add_subscription(TypeSubscription("MathSolverD", "MathSolverC"))
await runtime.add_subscription(TypeSubscription("MathSolverD", "MathSolverA"))
# 所有求解器和聚合器都订阅默认主题。
解决数学问题#
现在让我们运行辩论来解决一个数学问题。
我们向默认主题发布一个SolverRequest
,
聚合器代理将开始辩论。
question = "Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?"
runtime.start()
await runtime.publish_message(Question(content=question), DefaultTopicId())
# 等待运行时在空闲时停止。
await runtime.stop_when_idle()
# 关闭与模型客户端的连接。
await model_client.close()
--------------------------------------------------------------------------------
Aggregator MathAggregator:default received question:
Natalia sold clips to 48 of her friends in April, and then she sold half as many clips in May. How many clips did Natalia sell altogether in April and May?
--------------------------------------------------------------------------------
Aggregator MathAggregator:default publishes initial solver request.
--------------------------------------------------------------------------------
Solver MathSolverC:default round 0:
In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. To find the total number of clips sold in April and May, we add the amounts: 48 (April) + 24 (May) = 72 clips.
Thus, the total number of clips sold by Natalia is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverB:default round 0:
In April, Natalia sold 48 clips. In May, she sold half as many clips, which is 48 / 2 = 24 clips. To find the total clips sold in April and May, we add both amounts:
48 (April) + 24 (May) = 72.
Thus, the total number of clips sold altogether is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverD:default round 0:
Natalia sold 48 clips in April. In May, she sold half as many, which is \( \frac{48}{2} = 24 \) clips. To find the total clips sold in both months, we add the clips sold in April and May together:
\[ 48 + 24 = 72 \]
Thus, Natalia sold a total of 72 clips.
The answer is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverC:default round 1:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverA:default round 1:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverA:default round 0:
In April, Natalia sold clips to 48 friends. In May, she sold half as many, which is calculated as follows:
Half of 48 is \( 48 \div 2 = 24 \).
Now, to find the total clips sold in April and May, we add the totals from both months:
\( 48 + 24 = 72 \).
Thus, the total number of clips Natalia sold altogether in April and May is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverD:default round 1:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverB:default round 1:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverC:default round 1:
In April, Natalia sold 48 clips. In May, she sold half as many, which is 48 / 2 = 24 clips. The total number of clips sold in April and May is calculated by adding the two amounts: 48 (April) + 24 (May) = 72 clips.
Therefore, the answer is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverA:default round 1:
In April, Natalia sold 48 clips. In May, she sold half of that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we sum the clips from April and May:
48 (April) + 24 (May) = 72.
Thus, Natalia sold a total of {{72}} clips.
The answer is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverD:default round 2:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverB:default round 2:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverD:default round 1:
Natalia sold 48 clips in April. In May, she sold half of that, which is \( 48 \div 2 = 24 \) clips. To find the total clips sold, we add the clips sold in both months:
\[ 48 + 24 = 72 \]
Therefore, the total number of clips sold by Natalia is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverB:default round 1:
In April, Natalia sold 48 clips. In May, she sold half that amount, which is 48 / 2 = 24 clips. To find the total clips sold in both months, we add the amounts:
48 (April) + 24 (May) = 72.
Therefore, the total number of clips sold altogether by Natalia is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverA:default round 2:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverC:default round 2:
Received all responses from 2 neighbors.
--------------------------------------------------------------------------------
Solver MathSolverA:default round 2:
In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \( 48 \div 2 = 24 \) clips. To find the total clips sold in both months, we add the amounts from April and May:
\( 48 + 24 = 72 \).
Thus, the total number of clips sold by Natalia is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverC:default round 2:
In April, Natalia sold 48 clips. In May, she sold half of that amount, which is \( 48 \div 2 = 24 \) clips. To find the total number of clips sold in both months, we add the clips sold in April and May:
48 (April) + 24 (May) = 72.
Thus, the total number of clips sold altogether by Natalia is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverB:default round 2:
In April, Natalia sold 48 clips. In May, she sold half as many, calculated as \( 48 \div 2 = 24 \) clips. To find the total clips sold over both months, we sum the totals:
\( 48 (April) + 24 (May) = 72 \).
Therefore, the total number of clips Natalia sold is {{72}}.
--------------------------------------------------------------------------------
Solver MathSolverD:default round 2:
To solve the problem, we know that Natalia sold 48 clips in April. In May, she sold half that amount, which is calculated as \( 48 \div 2 = 24 \) clips. To find the total number of clips sold over both months, we add the two amounts together:
\[ 48 + 24 = 72 \]
Thus, the total number of clips sold by Natalia is {{72}}.
--------------------------------------------------------------------------------
Aggregator MathAggregator:default received all final answers from 4 solvers.
--------------------------------------------------------------------------------
Aggregator MathAggregator:default publishes final answer:
72