n4nAI

AutoGen group chat: coordinating three or more agents

Practical guide to AutoGen group chat for three or more agents: role design, speaker selection, termination, and pitfalls when orchestrating multi-agent workflows.

n4n Team4 min read784 words

Audio narration

Coming soon — every post will get a voice note here.

An AutoGen group chat is the right primitive when a task needs three or more specialized agents arguing, drafting, or reviewing together. Single-agent loops force you to cram contradictory personas into one system prompt, which degrades fast. This guide gives an ordered path to stand up a robust multi-agent orchestration with AutoGen, including the speaker-selection logic and termination guards you’ll need in production.

Define roles with sharp boundaries

Before importing anything, write a one-paragraph spec for each agent. In an AutoGen group chat, every participant sees the full transcript by default, so overlapping responsibilities cause redundant tokens and confused handoffs. A researcher, a critic, and a synthesizer should have non-overlapping mandates and explicit output contracts.

Vague prompts like “you are a helpful assistant that helps with research” will collapse into duplicated work. Instead, constrain each agent’s allowed output shape:

researcher_sys = """You are a retrieval agent. Given a query, return 3 bullet points with sources.
Never write final prose. Output only the bullets."""
critic_sys = """You are a skeptical reviewer. Given research bullets, point out gaps or bad sources.
Do not rewrite the content. Output a list of concerns."""
synthesizer_sys = """You are a writer. Given research and critique, produce a final answer of <=100 words.
If critique is unaddressed, output exactly 'NEEDS MORE' on the first line."""

Create the agents (AutoGen 0.4+ API):

from autogen.agents import AssistantAgent

model_cfg = {"model": "gpt-4o-mini", "api_key": "sk-..."}

researcher = AssistantAgent("researcher", system_message=researcher_sys, model_client_config=model_cfg)
critic = AssistantAgent("critic", system_message=critic_sys, model_client_config=model_cfg)
synthesizer = AssistantAgent("synthesizer", system_message=synthesizer_sys, model_client_config=model_cfg)

If you are on AutoGen <0.4, the class is autogen.AssistantAgent and you pass system_message= the same way. The mental model is identical.

Choose a speaker selection strategy

AutoGen group chat offers two common topologies: round-robin and selector. Round-robin is deterministic but dumb—it forces each agent to speak in fixed order regardless of whether it has anything to add. With three or more agents, use SelectorGroupChat, where an LLM picks the next speaker based on the conversation state.

from autogen.agentchat import SelectorGroupChat

group_chat = SelectorGroupChat(
    participants=[researcher, critic, synthesizer],
    model_client_config=model_cfg,
    max_rounds=6,
    selector_prompt="Pick the next agent to act based on the conversation. Choose from: researcher, critic, synthesizer."
)

The selector is itself an LLM call. Tradeoff: you pay latency and tokens for smarter orchestration. In practice, run the selector on a small model (e.g., gpt-4o-mini) even if the workers use a larger one. I’ve seen selector calls add 200–400 ms per turn; acceptable for batch jobs, annoying for chat UIs.

You can also pass allowed_speakers to constrain who may talk after a given message, but that defeats the emergent routing that makes group chat useful. Reserve it for hard invariants, like “only synthesizer may end.”

Drive the conversation loop

The group chat runs as an async coroutine. Seed it with a user message and await the result.

import asyncio

async def main():
    result = await group_chat.run(
        "Research the impact of vector databases on RAG latency. Then produce a final brief."
    )
    print(result.messages[-1].content)

asyncio.run(main())

If you’re on an old AutoGen version, you’d use GroupChatManager and initiate_chat. The pattern is similar but the manager is a separate object that you must register as a participant. The newer GroupChat.run is cleaner and returns a structured result.

Termination and handoff discipline

Unbounded group chats burn money. Set max_rounds as a hard ceiling. More importantly, encode a stop signal in the agent prompts. In the synthesizer prompt above, we used NEEDS MORE to force another round, and a normal final answer to stop.

Implement a custom termination condition:

def terminate_on_final(msg):
    return msg.source == "synthesizer" and "NEEDS MORE" not in msg.content

group_chat = SelectorGroupChat(
    participants=[researcher, critic, synthesizer],
    model_client_config=model_cfg,
    max_rounds=8,
    termination_condition=terminate_on_final,
    selector_prompt="..."
)

Pitfall: if the selector keeps picking the same agent because its output looks like a question, you’ll spin. Log the selector’s choices during dev by wrapping the selector call or inspecting group_chat.message_history. Another pitfall is deadlock: if no agent satisfies the termination condition before max_rounds, you get a truncated transcript. Always inspect the last message in production and flag incomplete runs.

Point agents at a resilient model backend

Each agent needs a model client. If you’re self-hosting or using multiple providers, the OpenAI-compatible endpoint from n4n.ai lets you address 240+ models behind one URL and gets automatic fallback when a provider is rate-limited. You just set the base_url and keep per-token metering without writing retry code.

model_cfg = {
    "model": "openai/gpt-4o-mini",
    "base_url": "https://api.n4n.ai/v1",
    "api_key": "your-key",
    "headers": {"X-Route": "auto"}  # honors client routing directives
}

This is especially useful in group chats because a single stuck provider stalls the whole conversation; fallback keeps the loop alive. The gateway also forwards provider cache-control hints, so prompt prefixes shared across agents can hit cache.

Common pitfalls and tradeoffs

Context bloat. Every agent sees everything. With three agents and 6 rounds, you can blow past 32k tokens on overhead. Use BufferedChatCompletionContext with a window size to trim old messages for some agents.

from autogen.model_context import BufferedChatCompletionContext
ctx = BufferedChatCompletionContext(buffer_size=4)
researcher = AssistantAgent("researcher", system_message=researcher_sys, model_client_config=model_cfg, model_context=ctx)

Selector bias. The LLM selector favors verbose agents. If the synthesizer writes a lot, the selector may think it’s always the right next speaker. Counter by making the selector prompt explicit: “If the last message is a final answer, terminate.” You can also penalize by giving the selector a smaller context.

Debugging opacity. Print group_chat.message_history after runs. In production, ship traces to your logging system with agent name tags. Without this you will not know why the critic stayed silent.

Latency. Three agents in sequence with a selector call per turn means 4+ model calls per round. For interactive apps, set max_rounds=3 and pre-compute where possible. For offline pipelines, bump rounds and let the critic iterate.

Mixed model capabilities. Don’t put a weak model in the synthesizer if the task needs reasoning. But the researcher can often be a cheaper model. Per-agent model_client_config overrides are trivial:

researcher.model_client_config = {"model": "gpt-4o-mini", "api_key": "sk-..."}
synthesizer.model_client_config = {"model": "gpt-4o", "api_key": "sk-..."}

A minimal end-to-end sketch

from autogen.agents import AssistantAgent
from autogen.agentchat import SelectorGroupChat
from autogen.model_context import BufferedChatCompletionContext

model_cfg = {"model": "gpt-4o-mini", "api_key": "sk-..."}

researcher = AssistantAgent("researcher", system_message="Return 3 sourced bullets.", model_client_config=model_cfg)
critic = AssistantAgent("critic", system_message="List gaps only.", model_client_config=model_cfg)
synthesizer = AssistantAgent(
    "synthesizer",
    system_message="Write final <=100 words or 'NEEDS MORE'.",
    model_client_config=model_cfg,
    model_context=BufferedChatCompletionContext(4)
)

def stop(msg):
    return msg.source == "synthesizer" and "NEEDS MORE" not in msg.content

chat = SelectorGroupChat(
    participants=[researcher, critic, synthesizer],
    model_client_config=model_cfg,
    max_rounds=6,
    termination_condition=stop,
    selector_prompt="Pick next: researcher, critic, synthesizer. Stop if synthesizer final."
)

async def run():
    res = await chat.run("Explain speculative decoding.")
    print(res.messages[-1].content)

import asyncio
asyncio.run(run())

That’s the core. An AutoGen group chat is not magic—it’s a state machine with LLM-selected transitions. Get the roles right, cap the rounds, watch the token count, and the orchestration will hold up under real load.

Tagsautogenmulti-agent-orchestrationai-agents

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All autogen & microsoft agent framework posts →