Multi-Agent Debate#
Multi-Agent Debate ist ein Multi-Agent-Designmuster, das eine Interaktion mit mehreren Runden simuliert, bei der Agenten in jeder Runde ihre Antworten miteinander austauschen und ihre Antworten basierend auf den Antworten anderer Agenten verfeinern.
Dieses Beispiel zeigt eine Implementierung des Multi-Agent-Debattenmusters zur Lösung von Mathematikproblemen aus dem GSM8K-Benchmark.
In diesem Muster gibt es zwei Arten von Agenten: Solver-Agenten und einen Aggregator-Agenten. Die Solver-Agenten sind auf spärliche Weise verbunden, wie in Improving Multi-Agent Debate with Sparse Communication Topology beschrieben. Die Solver-Agenten sind für die Lösung von Mathematikproblemen und den Austausch von Antworten miteinander zuständig. Der Aggregator-Agent ist für die Verteilung von Mathematikproblemen an die Solver-Agenten, das Warten auf deren endgültige Antworten und die Aggregation der Antworten zur Ermittlung der endgültigen Lösung zuständig.
Das Muster funktioniert wie folgt:
Benutzer sendet ein Mathematikproblem an den Aggregator-Agenten.
Der Aggregator-Agent verteilt das Problem an die Solver-Agenten.
Jeder Solver-Agent verarbeitet das Problem und veröffentlicht eine Antwort an seine Nachbarn.
Jeder Solver-Agent nutzt die Antworten seiner Nachbarn, um seine Antwort zu verfeinern und veröffentlicht eine neue Antwort.
Wiederholen Sie Schritt 4 für eine feste Anzahl von Runden. In der letzten Runde veröffentlicht jeder Solver-Agent eine endgültige Antwort.
Der Aggregator-Agent verwendet Mehrheitsentscheidungen, um die endgültigen Antworten aller Solver-Agenten zu aggregieren, um eine endgültige Lösung zu erhalten, und veröffentlicht die Antwort.
Wir werden die Broadcast-API verwenden, d. h. publish_message(), und wir werden Topic und Subscription verwenden, um die Kommunikationsstruktur zu implementieren. Lesen Sie Topics und Subscriptions, um zu verstehen, wie sie funktionieren.
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
Nachrichtenprotokoll#
Zuerst definieren wir die von den Agenten verwendeten Nachrichten. IntermediateSolverResponse ist die Nachricht, die in jeder Runde zwischen den Solver-Agenten ausgetauscht wird, und FinalSolverResponse ist die Nachricht, die von den Solver-Agenten in der letzten Runde veröffentlicht wird.
@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
Solver-Agent#
Der Solver-Agent ist dafür verantwortlich, Mathematikprobleme zu lösen und Antworten mit anderen Solver-Agenten auszutauschen. Nach Erhalt einer SolverRequest verwendet der Solver-Agent ein LLM, um eine Antwort zu generieren. Anschließend veröffentlicht er eine IntermediateSolverResponse oder eine FinalSolverResponse, abhängig von der Rundennummer.
Dem Solver-Agenten wird ein Topic-Typ zugewiesen, der angibt, zu welchem Topic der Agent Zwischenantworten veröffentlichen soll. Dieses Topic wird von seinen Nachbarn abonniert, um Antworten von diesem Agenten zu erhalten – wie dies geschieht, zeigen wir später.
Wir verwenden default_subscription(), damit Solver-Agenten das Standard-Topic abonnieren können, das vom Aggregator-Agenten verwendet wird, um die endgültigen Antworten von den Solver-Agenten zu sammeln.
@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:
# Add the question to the memory.
self._history.append(UserMessage(content=message.content, source="user"))
# Make an inference using the model.
model_result = await self._model_client.create(self._system_messages + self._history)
assert isinstance(model_result.content, str)
# Add the response to the memory.
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}")
# Extract the answer from the response.
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)
# Increment the counter.
self._round += 1
if self._round == self._max_round:
# If the counter reaches the maximum round, publishes a final response.
await self.publish_message(FinalSolverResponse(answer=answer), topic_id=DefaultTopicId())
else:
# Publish intermediate response to the topic associated with this solver.
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:
# Add neighbor's response to the buffer.
self._buffer.setdefault(message.round, []).append(message)
# Check if all neighbors have responded.
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."
)
# Prepare the prompt for the next question.
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."
)
# Send the question to the agent itself to solve.
await self.send_message(SolverRequest(content=prompt, question=message.question), self.id)
# Clear the buffer.
self._buffer.pop(message.round)
Aggregator-Agent#
Der Aggregator-Agent ist dafür verantwortlich, Benutzerfragen zu bearbeiten und Mathematikprobleme an die Solver-Agenten zu verteilen.
Der Aggregator abonniert das Standard-Topic über default_subscription(). Das Standard-Topic wird verwendet, um Benutzerfragen zu empfangen, die endgültigen Antworten von den Solver-Agenten zu empfangen und die endgültige Antwort zurück an den Benutzer zu veröffentlichen.
In einer komplexeren Anwendung, wenn Sie das Multi-Agent-Debatten in eine Unterkomponente auslagern möchten, sollten Sie type_subscription() verwenden, um einen spezifischen Topic-Typ für die Kommunikation zwischen Aggregator und Solver festzulegen, und sowohl den Solver als auch den Aggregator dazu bringen, sich zu diesem Topic-Typ zu veröffentlichen und von ihm zu abonnieren.
@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.")
# Find the majority answer.
answers = [resp.answer for resp in self._buffer]
majority_answer = max(set(answers), key=answers.count)
# Publish the aggregated response.
await self.publish_message(Answer(content=majority_answer), topic_id=DefaultTopicId())
# Clear the responses.
self._buffer.clear()
print(f"{'-'*80}\nAggregator {self.id} publishes final answer:\n{majority_answer}")
Einrichten einer Debatte#
Wir werden nun eine Multi-Agenten-Debatte mit 4 Solver-Agenten und 1 Aggregator-Agenten einrichten. Die Solver-Agenten werden auf spärliche Weise verbunden, wie in der folgenden Abbildung dargestellt.
A --- B
| |
| |
D --- C
Jeder Solver-Agent ist mit zwei anderen Solver-Agenten verbunden. Zum Beispiel ist Agent A mit den Agenten B und C verbunden.
Lassen Sie uns zuerst eine Laufzeitumgebung erstellen und die Agententypen registrieren.
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')
Nun erstellen wir die Solver-Agenten-Topologie mithilfe von TypeSubscription, die den Veröffentlichungstopic-Typ jedes Solver-Agenten den Agententypen seiner Nachbarn zuordnet.
# Subscriptions for topic published to by MathSolverA.
await runtime.add_subscription(TypeSubscription("MathSolverA", "MathSolverD"))
await runtime.add_subscription(TypeSubscription("MathSolverA", "MathSolverB"))
# Subscriptions for topic published to by MathSolverB.
await runtime.add_subscription(TypeSubscription("MathSolverB", "MathSolverA"))
await runtime.add_subscription(TypeSubscription("MathSolverB", "MathSolverC"))
# Subscriptions for topic published to by MathSolverC.
await runtime.add_subscription(TypeSubscription("MathSolverC", "MathSolverB"))
await runtime.add_subscription(TypeSubscription("MathSolverC", "MathSolverD"))
# Subscriptions for topic published to by MathSolverD.
await runtime.add_subscription(TypeSubscription("MathSolverD", "MathSolverC"))
await runtime.add_subscription(TypeSubscription("MathSolverD", "MathSolverA"))
# All solvers and the aggregator subscribe to the default topic.
Lösen von Mathematikproblemen#
Lassen Sie uns nun die Debatte zur Lösung eines Mathematikproblems starten. Wir veröffentlichen eine SolverRequest an das Standard-Topic, und der Aggregator-Agent wird die Debatte starten.
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())
# Wait for the runtime to stop when idle.
await runtime.stop_when_idle()
# Close the connection to the model client.
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