n4nAI

How AutoGen enables multi-agent conversations

A practical breakdown of AutoGen's multi-agent conversation framework, covering agent types, conversation patterns, and a working code example for engineers building agent systems.

n4n Team6 min read1,409 words

Audio narration

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

AutoGen multi-agent conversations are structured interactions between multiple specialized LLM agents that collaborate to solve tasks through defined conversation patterns. Each agent has a distinct role, system prompt, and capability set, and they exchange messages in rounds until a termination condition is met. This framework, developed by Microsoft Research, abstracts away the orchestration logic so developers can focus on agent design rather than message passing plumbing.

How AutoGen structures agent conversations

AutoGen models conversations as a sequence of messages passed between agents. The core abstraction is the Agent class, which wraps an LLM endpoint and maintains conversation history. Agents don’t call each other directly — instead, a GroupChat manager orchestrates the turn-taking based on a speaker selection strategy.

Agent types and their responsibilities

AutoGen provides several built-in agent classes, each designed for a specific role:

AssistantAgent — The general-purpose reasoning agent. It receives messages, calls the LLM, and returns responses. You configure it with a system prompt that defines its expertise, tone, and tool access.

UserProxyAgent — Represents a human or external system. It can execute code, call functions, and request human input. This agent bridges the conversation to the outside world.

GroupChatManager — Controls the flow in multi-agent scenarios. It decides which agent speaks next based on the selection method: round-robin, random, or a custom function that inspects the conversation history.

from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager

# Specialized agents with distinct system prompts
planner = AssistantAgent(
    name="planner",
    system_message="You break down complex tasks into step-by-step plans. "
                   "Output only the plan as a numbered list.",
    llm_config={"model": "gpt-4o"}
)

coder = AssistantAgent(
    name="coder",
    system_message="You write clean, production-ready Python code. "
                   "Include type hints and docstrings. No explanations.",
    llm_config={"model": "gpt-4o"}
)

reviewer = AssistantAgent(
    name="reviewer",
    system_message="You review code for bugs, security issues, and style. "
                   "Output a prioritized list of findings.",
    llm_config={"model": "gpt-4o"}
)

# Human-in-the-loop proxy
user_proxy = UserProxyAgent(
    name="user_proxy",
    human_input_mode="TERMINATE",
    code_execution_config={"work_dir": "coding", "use_docker": False}
)

Conversation patterns

AutoGen supports three primary conversation patterns, each suited to different problem structures:

Sequential (pipeline) — Agents speak in a fixed order. Useful for assembly-line tasks: planner → coder → reviewer → tester. The GroupChat with speaker_selection_method="round_robin" implements this.

Hierarchical (manager-worker) — A manager agent delegates to workers and synthesizes results. The manager decides which worker speaks next based on task state. This mirrors how a tech lead coordinates a sprint.

Free-form (group chat) — Any agent can speak at any turn. The selection function examines the last message and conversation context to pick the most relevant agent. This handles open-ended collaboration where the optimal speaker isn’t predetermined.

def select_next_speaker(last_speaker, agents):
    """Custom selection: route based on message content."""
    last_msg = agents[last_speaker].chat_messages[agents[last_speaker]][-1]["content"]
    
    if "plan" in last_msg.lower():
        return "coder"
    elif "code" in last_msg.lower() or "```python" in last_msg:
        return "reviewer"
    elif "issue" in last_msg.lower() or "bug" in last_msg.lower():
        return "coder"
    return "planner"

groupchat = GroupChat(
    agents=[planner, coder, reviewer, user_proxy],
    messages=[],
    max_round=15,
    speaker_selection_method=select_next_speaker
)

manager = GroupChatManager(groupchat=groupchat, llm_config={"model": "gpt-4o"})

Termination conditions

Conversations need explicit stopping criteria. AutoGen provides several:

  • Max rounds — Hard limit on total turns (prevents infinite loops)
  • Keyword termination — Stop when a message contains “TERMINATE” or a custom string
  • Function-based — A callable that returns True when the conversation should end
def is_task_complete(messages):
    """Stop when reviewer approves and no new issues found."""
    if len(messages) < 3:
        return False
    last_review = messages[-1]["content"].lower()
    return "approved" in last_review and "no issues" in last_review

groupchat = GroupChat(
    agents=[planner, coder, reviewer, user_proxy],
    messages=[],
    max_round=20,
    speaker_selection_method=select_next_speaker,
    termination_condition=is_task_complete
)

Why this architecture matters for production systems

Multi-agent conversations solve a fundamental problem: single prompts cannot reliably handle complex, multi-stage reasoning. By decomposing a task into specialized agents, you get several operational advantages.

Separation of concerns mirrors software engineering

Each agent encapsulates a single responsibility. The planner doesn’t write code; the coder doesn’t review. This makes prompts shorter, more focused, and easier to version control. When the coder starts hallucinating imports, you fix the coder’s system prompt — not a 2,000-token monolithic prompt that does everything poorly.

Observable intermediate states

In a single-shot prompt, you see only the final output. With AutoGen multi-agent conversations, every agent turn is a checkpoint. You can log, inspect, and intervene at each stage. This is critical for debugging: when the reviewer catches a SQL injection vulnerability, you know exactly which agent introduced it and what context they had.

Human-in-the-loop at natural boundaries

The UserProxyAgent lets you insert human approval at any point — after planning, after code generation, after review. You don’t need to build a custom UI; the agent pauses and waits for input. This maps directly to code review workflows, deployment gates, and compliance checkpoints.

Parallelization potential

Independent agents can run concurrently. If your planner produces three independent subtasks, you can spin up three coder agents in parallel. The GroupChatManager doesn’t enforce this today, but the message-passing architecture makes it straightforward to implement a custom manager that fans out to worker pools.

Concrete example: Automated feature implementation

Here’s a complete, runnable example that implements a feature from a natural language request. The system plans, writes code, reviews it, and produces a pull request-ready diff.

import os
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager

# Configuration shared across agents
llm_config = {
    "model": "gpt-4o",
    "temperature": 0.1,
    "timeout": 120
}

# Agent definitions
architect = AssistantAgent(
    name="architect",
    system_message=(
        "You are a senior software architect. Given a feature request, "
        "produce a technical specification with: "
        "1. Data model changes (SQLAlchemy models) "
        "2. API endpoint definitions (FastAPI routes) "
        "3. Database migration steps (Alembic) "
        "4. Test cases (pytest) "
        "Output as structured markdown. No implementation code."
    ),
    llm_config=llm_config
)

backend_engineer = AssistantAgent(
    name="backend_engineer",
    system_message=(
        "You implement the specification exactly as written. "
        "Write production-ready Python code with: "
        "- Type hints on all functions "
        "- Comprehensive docstrings "
        "- Proper error handling "
        "- SQLAlchemy 2.0 style "
        "- FastAPI dependency injection "
        "Output only code files with clear path headers."
    ),
    llm_config=llm_config
)

qa_engineer = AssistantAgent(
    name="qa_engineer",
    system_message=(
        "You review the implementation against the spec. Check: "
        "1. All endpoints implemented with correct signatures "
        "2. Data models match specification "
        "3. Error handling covers edge cases "
        "4. Tests cover happy path and error cases "
        "5. No security vulnerabilities (SQL injection, XSS, etc.) "
        "Output a prioritized checklist: CRITICAL, MAJOR, MINOR, NIT."
    ),
    llm_config=llm_config
)

devops = AssistantAgent(
    name="devops",
    system_message=(
        "You generate the deployment artifacts: "
        "1. Dockerfile for the service "
        "2. docker-compose.yml for local dev "
        "3. GitHub Actions CI pipeline "
        "4. Alembic migration script "
        "Output each file with its path."
    ),
    llm_config=llm_config
)

# Human proxy for approval gates
product_owner = UserProxyAgent(
    name="product_owner",
    human_input_mode="ALWAYS",
    code_execution_config=False
)

# Custom speaker selection: enforce workflow order
def workflow_selector(last_speaker, agents):
    last_msg = agents[last_speaker].chat_messages[agents[last_speaker]][-1]["content"].lower()
    
    if last_speaker == "product_owner":
        return "architect"
    elif last_speaker == "architect" and "specification" in last_msg:
        return "backend_engineer"
    elif last_speaker == "backend_engineer" and "```python" in last_msg:
        return "qa_engineer"
    elif last_speaker == "qa_engineer" and "approved" in last_msg:
        return "devops"
    elif last_speaker == "devops":
        return "product_owner"
    return last_speaker  # Stay with current speaker if unclear

# Termination: product owner gives final approval
def final_approval(messages):
    if not messages:
        return False
    last = messages[-1]["content"].lower()
    return last_speaker == "product_owner" and "approve" in last

groupchat = GroupChat(
    agents=[architect, backend_engineer, qa_engineer, devops, product_owner],
    messages=[],
    max_round=25,
    speaker_selection_method=workflow_selector,
    termination_condition=final_approval
)

manager = GroupChatManager(groupchat=groupchat, llm_config=llm_config)

# Kick off the conversation
feature_request = """
Add a 'team collaboration' feature to the project management API:
- Users can create teams and invite members by email
- Team members have roles: admin, member, viewer
- Projects can be assigned to teams (not just individuals)
- Team-based permissions: admins manage team, members edit projects, viewers read only
- Email notifications for invitations and role changes
"""

product_owner.initiate_chat(manager, message=feature_request)

Running this produces a complete feature implementation with spec, code, tests, review feedback, and deployment configs — each stage visible and auditable. The product owner approves the spec before coding starts, reviews the QA findings, and gives final sign-off on the deployment artifacts.

Common misconceptions

“AutoGen replaces LangChain or CrewAI”

False. AutoGen operates at a different layer. LangChain and CrewAI are orchestration frameworks that can use AutoGen as their multi-agent backend. AutoGen provides the conversation runtime; the others provide higher-level abstractions (chains, tools, memory, RAG pipelines). You’ll often see them composed: LangChain for document retrieval, AutoGen for the agent conversation that reasons over retrieved context.

“More agents always means better results”

Adding agents increases latency, token costs, and failure surface. A three-agent system (planner, coder, reviewer) often outperforms a seven-agent system because each additional handoff loses context and compounds hallucination risk. Start with the minimal viable agent set. Add specialization only when you measure a quality gap.

“The LLM chooses the next speaker intelligently”

The default GroupChatManager uses an LLM to pick the next speaker, but this is expensive and non-deterministic. Production systems almost always replace this with a deterministic selection function (like the workflow_selector above) or a lightweight classifier. Reserve LLM-based routing for genuinely ambiguous cases where the optimal speaker depends on semantic content.

“AutoGen handles memory and context automatically”

It doesn’t. Each agent maintains its own conversation history, but there’s no built-in long-term memory, vector store integration, or context window management. You must implement:

  • Context truncation strategies (summarize old turns, keep recent)
  • Shared scratchpad for cross-agent state
  • Persistence layer for conversation replay
# Example: manual context window management
def truncate_history(agent, max_tokens=8000):
    """Keep system prompt + last N turns within token budget."""
    history = agent.chat_messages[agent]
    # Implementation depends on your tokenizer
    # This is your responsibility, not AutoGen's

“It works out of the box with any OpenAI-compatible endpoint”

Mostly true, but with caveats. AutoGen expects the OpenAI Chat Completions format. Providers that deviate (some Anthropic-compatible endpoints, certain local model servers) require a wrapper. Function calling support varies — if your agents need tools, verify the endpoint implements tools and tool_choice parameters correctly. When routing across multiple providers, you’ll need a gateway that normalizes these differences — this is where a layer like n4n.ai helps by presenting a consistent interface across 240+ models while preserving provider-specific features like cache-control hints.

Operational considerations

Cost attribution

Each agent turn generates a separate API call. Tag your requests with agent names and conversation IDs to break down costs per agent type. The reviewer might cost 3x the coder if it processes large diffs. This visibility lets you optimize: maybe the reviewer only needs a smaller model, or you batch reviews.

Latency budgets

A 5-agent conversation with 3 rounds each at 2s per call = 30s minimum. Add network overhead, retries, and human gates. Set expectations: this isn’t a chatbot. It’s a batch job with streaming intermediate results. Expose progress via WebSocket or Server-Sent Events so the frontend can show “Architect working…” → “Coder implementing…” → “QA reviewing…”

Evaluation strategy

You can’t unit test non-deterministic agent conversations. Instead:

  • Golden conversations — Record successful runs as regression fixtures
  • Critic agents — Add an evaluator agent that scores outputs against rubrics
  • A/B prompt versions — Run parallel conversations with different system prompts, compare outcomes
  • Human evaluation — Sample 5% of production conversations for manual review

When to reach for AutoGen

Use AutoGen multi-agent conversations when:

  • The task decomposes cleanly into distinct roles with different expertise
  • You need audit trails and human approval gates
  • Intermediate outputs have value (specs, code, reviews, docs)
  • The problem benefits from adversarial or collaborative reasoning

Skip it when:

  • A single well-crafted prompt with few-shot examples solves it
  • Latency must stay under 5 seconds
  • You don’t have observability infrastructure to debug multi-turn failures
  • The team lacks capacity to maintain prompt engineering across 4+ agents

AutoGen gives you a runtime for agent conversations. The architecture decisions — how many agents, what they do, how they hand off, when humans intervene — remain yours. That’s where the engineering leverage lives.

Tagsautogenmulti-agent-systemstutorial

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 multi-agent systems & agent orchestration posts →