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:
- Message generation — the ability to produce responses using an LLM, a custom function, or a human input
- Message reception — the ability to accept incoming messages and decide how to handle them
- State management — maintaining conversation history, context, and agent-specific memory across turns
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:
AssistantAgent— an agent pre-configured to act as an AI assistantUserProxyAgent— an agent that represents a human user, capable of executing codeGroupChatManager— an agent that orchestrates multi-agent group conversationsRetrieveAssistantAgent— an agent with retrieval-augmented generation capabilities
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:
name: A unique identifier for the agent. Used in conversation logs and for routing messages.system_message: The system prompt that guides the LLM's behavior. This is prepended to every generation request.llm_config: The LLM configuration dictionary. Setting it toFalsemeans the agent won't use an LLM for generation — it will simply relay messages or wait for human input.human_input_mode: Controls how human input is solicited. Options are"ALWAYS","NEVER", or"TERMINATE".max_consecutive_auto_reply: Limits how many times the agent can automatically reply without human intervention. Prevents infinite loops.
Understanding Message Flow
When Agent A sends a message to Agent B, the following sequence occurs:
- Agent A calls
initiate_chatorsendwith a recipient and a message - Agent B's
receivemethod stores the incoming message in its conversation history - 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
- 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:
"NEVER"— The agent never asks for human input; it operates fully autonomously"ALWAYS"— The agent always pauses and waits for human input before generating a reply"TERMINATE"— Human input is only requested when the conversation would otherwise end
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:
- A retriever agent fetches relevant documents
- A coder agent writes code based on retrieved context
- A reviewer agent checks the code for issues
- A summarizer agent produces the final output for the user
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.