← Back to DevBytes

AutoGen Conversable Agents: Complete Beginner Guide

Introduction to AutoGen Conversable Agents

AutoGen, developed by Microsoft Research, is an open-source framework for building multi-agent conversational systems powered by large language models (LLMs). At the heart of this framework lies the ConversableAgent class — the foundational building block upon which all other agents are constructed. Whether you're building a simple chatbot or a complex multi-agent orchestration system, understanding Conversable Agents is your essential first step.

This guide will walk you through everything you need to know: what Conversable Agents are, why they form the backbone of AutoGen's architecture, how to create and configure them, practical patterns for agent-to-agent communication, and battle-tested best practices drawn from real-world usage.

What Is a Conversable Agent?

A ConversableAgent is a unified agent abstraction that can send messages, receive messages, and maintain conversation context. It combines three critical capabilities in a single class:

Unlike simpler agent implementations that only generate responses, Conversable Agents are designed for bidirectional, multi-turn conversations. They can initiate conversations, reply to peers, hand off control, and even participate in group chats with multiple participants simultaneously. This design mirrors real-world collaborative workflows where participants take turns speaking, ask clarifying questions, and build on each other's contributions.

The Agent Hierarchy in AutoGen

All specialized agent types in AutoGen inherit from ConversableAgent. This includes:

Understanding ConversableAgent means you understand the entire agent ecosystem. Every configuration option, every message-handling pattern, and every conversation primitive lives at this base level.

Why Conversable Agents Matter

The Conversable Agent abstraction solves several fundamental challenges in building LLM-powered applications:

Unified Communication Protocol

Before AutoGen, developers building multi-agent systems had to manually wire up message-passing between disparate components — an LLM call here, a tool execution there, a human override somewhere else. Conversable Agents provide a single, consistent interface for all participants. Whether the "agent" is an LLM, a code executor, a human-in-the-loop, or a custom function, they all speak the same protocol: generate_reply and receive.

Seamless Human-AI Collaboration

One of AutoGen's standout features is the ability to mix AI agents and human proxies in the same conversation. A ConversableAgent configured with human_input_mode="ALWAYS" becomes a human surrogate in the conversation, while one with human_input_mode="NEVER" acts autonomously. This flexibility enables patterns like human-in-the-loop approval, where an AI assistant proposes a solution and a human agent reviews and approves it before execution.

Extensible Message Handling

Conversable Agents allow you to register custom reply functions that intercept incoming messages before the default LLM response is generated. This means you can inject business logic, enforce safety checks, transform messages, or route to specialized handlers — all without modifying the core agent implementation.

Creating Your First Conversable Agent

Let's start with the simplest possible setup: creating two agents and having them talk to each other. First, ensure you have AutoGen installed:

pip install pyautogen

Now create a basic Conversable Agent with an LLM backend:

from autogen import ConversableAgent

# Create an agent powered by an LLM
assistant = ConversableAgent(
    name="assistant",
    system_message="You are a helpful AI assistant. Respond concisely.",
    llm_config={
        "config_list": [
            {
                "model": "gpt-4o",
                "api_key": "your-api-key-here",
                "temperature": 0.7,
            }
        ]
    }
)

# Create a user proxy agent that represents a human
user_proxy = ConversableAgent(
    name="user_proxy",
    human_input_mode="NEVER",
    max_consecutive_auto_reply=3,
    llm_config=False  # No LLM; this agent passes messages through
)

# Initiate a conversation: the user proxy sends a message to the assistant
result = user_proxy.initiate_chat(
    recipient=assistant,
    message="Can you explain what a recursive function is in Python?",
    max_turns=3
)

print(result.summary)

In this example, user_proxy initiates the conversation by sending a message to the assistant. The assistant receives it, generates a reply using GPT-4o, and sends it back. The conversation continues until max_turns is reached or a natural stopping condition is met.

Breaking Down the Configuration

Let's examine each parameter in detail:

Understanding Message Flow

When Agent A sends a message to Agent B, the following sequence occurs:

  1. Agent A calls initiate_chat or send with a recipient and a message
  2. Agent B's receive method stores the incoming message in its conversation history
  3. Agent B calls generate_reply, which:
    • Checks for registered reply functions (custom handlers)
    • If no custom handler matches, falls back to the default LLM generation
    • If no LLM is configured, returns a default acknowledgment or waits for human input
  4. The generated reply is sent back to Agent A (or broadcast to all agents in a group chat)

Here's a diagrammatic representation in code — tracing the full message lifecycle:

from autogen import ConversableAgent
import traceback

# Create two simple agents
agent_a = ConversableAgent(
    name="AgentA",
    system_message="You are Agent A. Keep responses brief.",
    llm_config={
        "config_list": [{"model": "gpt-4o-mini", "api_key": "demo-key"}]
    }
)

agent_b = ConversableAgent(
    name="AgentB",
    system_message="You are Agent B. You love asking follow-up questions.",
    llm_config={
        "config_list": [{"model": "gpt-4o-mini", "api_key": "demo-key"}]
    }
)

# Agent A initiates the chat with Agent B
# Behind the scenes, this triggers the full message flow
chat_result = agent_a.initiate_chat(
    recipient=agent_b,
    message="Hello, Agent B! I have a question about machine learning.",
    max_turns=2
)

# You can inspect the complete conversation history
for message in agent_a.chat_messages:
    print(f"From {message['name']}: {message['content'][:100]}...")

Configuring LLM Backends

The llm_config dictionary is where you specify which language model powers your agent. AutoGen supports a wide range of providers through a unified configuration format:

Single Model Configuration

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o",
            "api_key": "sk-your-openai-key",
            "temperature": 0.3,
            "max_tokens": 500,
            "top_p": 0.9,
        }
    ],
    "timeout": 120,           # Maximum seconds to wait for a response
    "cache_seed": 42,         # Seed for response caching (enables deterministic replays)
    "temperature": 0.3,       # Fallback temperature if not set per-model
}

Multiple Models with Fallback

One of AutoGen's most powerful features is the ability to specify a list of models with automatic fallback. If the primary model fails (rate limit, timeout, content filter), the next model in the list is tried:

llm_config = {
    "config_list": [
        {
            "model": "gpt-4o",
            "api_key": "sk-primary-key",
            "temperature": 0.2,
        },
        {
            "model": "gpt-4o-mini",
            "api_key": "sk-fallback-key",
            "temperature": 0.2,
        },
        {
            "model": "claude-3-5-sonnet",
            "api_key": "sk-anthropic-key",
            "api_type": "anthropic",
        }
    ],
    "timeout": 90,
    "cache_seed": None,  # Disable caching when using fallbacks
}

This pattern is invaluable for production systems where reliability matters. You can mix different providers (OpenAI, Anthropic, Azure, local models) in the same config list.

Using Local Models (Ollama / LM Studio)

llm_config = {
    "config_list": [
        {
            "model": "llama3.1",
            "base_url": "http://localhost:11434/v1",
            "api_key": "ollama",  # Ollama accepts any non-empty string
            "api_type": "openai",  # Compatible API format
        }
    ]
}

Custom Reply Functions: The Extension Mechanism

Custom reply functions are the primary way to extend agent behavior without subclassing. They are registered with an agent and called whenever a message matches specific criteria. This is the mechanism that powers tool use, code execution, retrieval, and human approval flows.

Registering a Simple Custom Reply

from autogen import ConversableAgent, register_reply

def count_words_in_message(recipient, messages, sender, config):
    """
    A custom reply function that counts words in the last message.
    If the message has more than 50 words, it appends a word count note.
    """
    last_message = messages[-1]["content"]
    word_count = len(last_message.split())

    if word_count > 50:
        return True, f"[Note: The previous message contained {word_count} words.]"
    return False, None  # False means "I didn't handle it, continue with default behavior"

# Register the custom reply function
assistant = ConversableAgent(
    name="analyst",
    system_message="You analyze text and provide insights.",
    llm_config={"config_list": [{"model": "gpt-4o-mini", "api_key": "key"}]}
)

register_reply(
    agent=assistant,
    reply_func=count_words_in_message,
    position=0,  # Priority: 0 means check this before the default LLM reply
    config=None  # Optional configuration passed to the function
)

# Now when the assistant receives a long message, the custom reply fires first
user_proxy = ConversableAgent(name="user", human_input_mode="NEVER", llm_config=False)
user_proxy.initiate_chat(
    recipient=assistant,
    message="Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * 20,
    max_turns=1
)

The reply function signature is (recipient, messages, sender, config) -> (bool, Union[str, dict, None]). The boolean return value indicates whether the function handled the message. If True, the returned content is used as the reply; if False, the agent proceeds to the next reply function or the default LLM generator.

Chaining Multiple Reply Functions

You can register multiple reply functions on a single agent. They are executed in order of their position parameter (lower numbers run first). This enables powerful processing pipelines:

def safety_check(recipient, messages, sender, config):
    """Block messages containing forbidden content."""
    forbidden_words = ["password", "secret_key", "confidential"]
    last_content = messages[-1]["content"].lower()
    for word in forbidden_words:
        if word in last_content:
            return True, "I cannot process messages containing sensitive information."
    return False, None

def add_timestamp(recipient, messages, sender, config):
    """Append a timestamp to every reply."""
    from datetime import datetime
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    return False, None  # Let the LLM handle it, but we'll modify later
    # Note: for appending, you'd typically use a different pattern with send()

# Register in priority order
register_reply(assistant, safety_check, position=0)   # Safety first
register_reply(assistant, add_timestamp, position=1)  # Then formatting
# Default LLM reply runs last (position effectively infinity)

Human-in-the-Loop Patterns

One of the most compelling use cases for Conversable Agents is incorporating human oversight into automated workflows. The human_input_mode parameter controls this behavior:

Human Approval Before Code Execution

from autogen import ConversableAgent, UserProxyAgent

# The assistant proposes code solutions
coder = ConversableAgent(
    name="coder",
    system_message="You are a Python developer. When asked to solve a problem, 
    write the complete code solution wrapped in pythonblocks.",
    llm_config={
        "config_list": [{"model": "gpt-4o", "api_key": "your-key"}],
        "temperature": 0.2,
    }
)

# The user proxy executes code but requires human approval first
executor = UserProxyAgent(
    name="executor",
    human_input_mode="ALWAYS",   # Human must approve before each code execution
    code_execution_config={
        "work_dir": "./coding_workspace",
        "use_docker": False,     # Set to True for sandboxed execution
    },
    max_consecutive_auto_reply=5,
)

# Start the collaborative coding session
executor.initiate_chat(
    recipient=coder,
    message="Please write a Python script that fetches the current weather 
    for Tokyo using a public API and displays it nicely formatted.",
    max_turns=5
)

In this workflow, the coder proposes a solution, and before the executor runs any code, it pauses and asks the human: "Press enter to execute the code, or type 'exit' to stop." This gives you a safety gate without sacrificing the automation benefits.

Conditional Human Input

Sometimes you want human input only under specific conditions. You can achieve this with a custom reply function that dynamically adjusts human_input_mode:

def conditional_human_override(recipient, messages, sender, config):
    """
    Only request human input if the message mentions 'production' or 'deploy'.
    """
    last_message = messages[-1]["content"].lower()
    if "production" in last_message or "deploy" in last_message:
        recipient.human_input_mode = "ALWAYS"
        return False, None  # Let the default handler run, now with human input mode
    return False, None

register_reply(executor, conditional_human_override, position=0)

Building a Multi-Agent Team

The true power of Conversable Agents emerges when you compose them into teams. Here's a complete example of a three-agent system for code review:

from autogen import ConversableAgent, GroupChat, GroupChatManager

# Agent 1: The Code Writer
code_writer = ConversableAgent(
    name="writer",
    system_message="""You are an expert Python developer. 
    Write clean, well-documented code. Include unit tests.
    Output code in pythonblocks.""",
    llm_config={
        "config_list": [{"model": "gpt-4o", "api_key": "key"}],
        "temperature": 0.4,
    }
)

# Agent 2: The Code Reviewer
code_reviewer = ConversableAgent(
    name="reviewer",
    system_message="""You are a senior code reviewer. 
    Analyze the provided code for:
    1. Bugs and edge cases
    2. Performance issues
    3. Security vulnerabilities
    4. Adherence to PEP 8
    Provide specific, actionable feedback.""",
    llm_config={
        "config_list": [{"model": "gpt-4o", "api_key": "key"}],
        "temperature": 0.2,
    }
)

# Agent 3: The Human Approver
human_approver = ConversableAgent(
    name="approver",
    human_input_mode="ALWAYS",
    llm_config=False,  # No LLM — purely human-driven
    system_message="You review the final code and approve or reject it."
)

# Set up the group chat
group_chat = GroupChat(
    agents=[code_writer, code_reviewer, human_approver],
    messages=[],
    max_round=6,
    speaker_selection_method="auto",  # LLM decides who speaks next
    allow_repeat_speaker=True,
)

# Create the manager
manager = GroupChatManager(
    name="manager",
    group_chat=group_chat,
    llm_config={
        "config_list": [{"model": "gpt-4o-mini", "api_key": "key"}],
    },
    system_message="You orchestrate the code review workflow: 
    writer writes, reviewer reviews, approver gives final approval.",
)

# Kick off the workflow
human_approver.initiate_chat(
    recipient=manager,
    message="Please organize a code review session. We need a Python function 
    that validates email addresses using regex and returns detailed error messages.",
    max_turns=10
)

This example demonstrates a complete collaborative workflow. The GroupChatManager uses an LLM to dynamically select which agent should speak next based on the conversation context. The agents take turns contributing — the writer produces code, the reviewer critiques it, and the human approver has the final say.

Custom Speaker Selection

You can replace the automatic speaker selection with your own logic:

def custom_speaker_selection(last_speaker, group_chat):
    """
    Custom logic: after the writer speaks, the reviewer must speak next.
    After the reviewer, the approver speaks.
    """
    if last_speaker.name == "writer":
        return group_chat.agents_by_name["reviewer"]
    elif last_speaker.name == "reviewer":
        return group_chat.agents_by_name["approver"]
    elif last_speaker.name == "approver":
        if group_chat.round < group_chat.max_round:
            return group_chat.agents_by_name["writer"]
    return None  # End the conversation

group_chat = GroupChat(
    agents=[code_writer, code_reviewer, human_approver],
    messages=[],
    max_round=9,
    speaker_selection_method=custom_speaker_selection,
    allow_repeat_speaker=True,
)

Advanced: Nested Conversations and Agent Handoffs

Conversable Agents support sophisticated conversation topologies. One agent can spawn a sub-conversation with another agent, wait for the result, and then incorporate that result into its reply. This pattern is called nested chat and is implemented via the register_nested_chat mechanism:

from autogen import register_nested_chat

# A research agent that consults a specialist before answering
researcher = ConversableAgent(
    name="researcher",
    system_message="You answer user questions by first consulting a data specialist.",
    llm_config={"config_list": [{"model": "gpt-4o", "api_key": "key"}]},
)

data_specialist = ConversableAgent(
    name="data_specialist",
    system_message="You provide accurate data and statistics. Be precise.",
    llm_config={"config_list": [{"model": "gpt-4o-mini", "api_key": "key"}]},
)

# Define a nested chat: when researcher receives a message, it first talks to data_specialist
register_nested_chat(
    recipient=researcher,
    reply_func=register_nested_chat,  # This is a built-in helper
    position=0,
    nested_chat_config={
        "sender": researcher,
        "recipient": data_specialist,
        "message": "Please provide relevant data for this query: {message}",
        "max_turns": 2,
        "summary_method": "last_msg",  # How to summarize the sub-conversation
    }
)

# Now when the researcher gets a question, it automatically consults the specialist
user = ConversableAgent(name="user", human_input_mode="NEVER", llm_config=False)
user.initiate_chat(
    recipient=researcher,
    message="What were the global renewable energy adoption rates in 2023?",
    max_turns=3
)

Nested chats enable complex patterns like retrieval-augmented generation, fact-checking pipelines, and hierarchical decision-making — all within the Conversable Agent framework.

Best Practices for Production

1. Design Clear System Messages

The system message is the single most important factor in agent behavior. Be specific about the agent's role, output format, constraints, and stopping criteria. A well-crafted system message can dramatically improve reliability:

# Good: specific, constrained, with output format
system_message = """You are a SQL query generator.
Rules:
- Always use parameterized queries to prevent injection
- Include comments explaining each clause
- Output ONLY the SQL query wrapped in sqlblocks
- If the request is ambiguous, ask ONE clarifying question before proceeding
- Never assume table names; ask if not provided"""

# Bad: vague and unbounded
system_message = "You help with databases."

2. Set Reasonable Turn Limits

Always set max_turns or max_consecutive_auto_reply to prevent infinite conversation loops. LLMs can sometimes get stuck in repetitive patterns, and turn limits are your circuit breaker:

# Protective configuration
agent = ConversableAgent(
    name="safe_agent",
    max_consecutive_auto_reply=5,  # Hard limit on auto-replies
    llm_config={...}
)

# In group chats
group_chat = GroupChat(
    max_round=10,  # Maximum total messages in the group conversation
    ...
)

3. Use Response Caching During Development

The cache_seed parameter enables deterministic replay of LLM responses. During development and testing, set a fixed seed to get consistent results and avoid burning through API credits:

llm_config = {
    "config_list": [...],
    "cache_seed": 42,  # Same seed = same responses for identical prompts
}

Set cache_seed to None in production to ensure fresh responses each time.

4. Implement Graceful Error Handling

LLM calls can fail for many reasons — rate limits, timeouts, content filters, service outages. Use the fallback config list and wrap critical conversations in try-except blocks:

try:
    result = agent.initiate_chat(
        recipient=other_agent,
        message="Process this batch of data",
        max_turns=5
    )
except Exception as e:
    # Log the error and potentially retry with a simpler agent
    fallback_agent = ConversableAgent(
        name="fallback",
        system_message="Provide a brief error response",
        llm_config={"config_list": [fallback_model_config]}
    )
    result = fallback_agent.generate_reply(messages=[{"content": "Error occurred"}])
    print(f"Conversation failed: {e}. Using fallback response.")

5. Structure Multi-Agent Workflows as Directed Graphs

For complex workflows, avoid free-form group chats where agents can speak in any order. Instead, use directed speaker selection or nested chats to enforce a clear workflow structure:

# Linear pipeline: Research → Analyze → Summarize
def pipeline_speaker_selection(last_speaker, group_chat):
    pipeline_order = ["researcher", "analyst", "summarizer"]
    current_index = pipeline_order.index(last_speaker.name)
    next_index = current_index + 1
    if next_index < len(pipeline_order):
        return group_chat.agents_by_name[pipeline_order[next_index]]
    return None  # Pipeline complete

# This ensures A → B → C without deviation

6. Monitor and Log Conversations

Conversable Agents provide rich logging. Enable detailed logs during development:

import autogen

# Configure logging
autogen.logging.basicConfig(
    level=autogen.logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# All agent interactions will now be logged with timestamps
# This is invaluable for debugging complex multi-agent scenarios

7. Keep Agents Focused (Single Responsibility)

Resist the temptation to build one "god agent" that does everything. Instead, decompose responsibilities across multiple agents, each with a narrow, well-defined purpose. This mirrors microservice architecture principles:

Each agent can be independently tested, configured with different models (cheaper models for simple tasks, powerful models for complex reasoning), and evolved separately.

Common Pitfalls and How to Avoid Them

Pitfall 1: Forgetting to Configure an LLM

If you set llm_config=False and human_input_mode="NEVER", the agent has no way to generate responses. It will return empty replies, causing conversations to stall. Always ensure at least one response mechanism is active:

# This agent is mute — it can't generate replies
mute_agent = ConversableAgent(
    name="mute",
    human_input_mode="NEVER",
    llm_config=False
)

# Fix: either provide an LLM or set human_input_mode to "ALWAYS"

Pitfall 2: Infinite Conversation Loops

Two agents with max_consecutive_auto_reply set too high can ping-pong endlessly. Always set conservative limits and implement termination conditions:

# Termination condition via system message
system_message = """... 
When the task is complete, respond with: TERMINATE"""

# Termination condition via custom reply function
def detect_completion(recipient, messages, sender, config):
    if "TERMINATE" in messages[-1]["content"]:
        return True, "Goodbye."  # This ends the conversation
    return False, None

Pitfall 3: Over-Reliance on a Single Model

Using a single model without fallbacks creates a single point of failure. Always configure at least one fallback model in config_list, even if it's a cheaper or slower alternative — reliability trumps performance in production.

Conclusion

Conversable Agents are the universal abstraction that makes AutoGen both powerful and approachable. By unifying message generation, reception, and state management in a single class, they enable developers to compose sophisticated multi-agent systems from simple, reusable building blocks. Whether you're building a straightforward chatbot, a human-in-the-loop approval workflow, or a complex team of specialized agents collaborating on a task, the patterns you've learned here — custom reply functions, nested chats, group conversations, and careful configuration — will serve as your foundation.

The key takeaways are: start simple with two-agent conversations, use custom reply functions to inject domain-specific logic without subclassing, leverage nested chats for complex multi-step reasoning, and always implement guardrails through turn limits, fallback configurations, and clear termination conditions. As you grow more comfortable, you'll discover that Conversable Agents can model almost any collaborative workflow you can imagine — from code review pipelines to research synthesis to automated customer support triage — all while maintaining the clarity and control that production systems demand.

— Ad —

Google AdSense will appear here after approval

← Back to all articles