AutoGen takes a fundamentally different approach to multi-agent orchestration than LangGraph: instead of a directed graph of nodes and edges, AutoGen models coordination as a conversation between agents. Each agent has a persona, a set of capabilities, and the ability to send messages to other agents or a group. This conversational framing maps naturally onto knowledge-worker workflows, code review, research synthesis, compliance checking, where the right answer emerges through structured dialogue rather than a predetermined execution path.
AutoGen 0.4+ Architecture
AutoGen 0.4 reorganized the library around cleaner abstractions. The base class is ConversableAgent, which provides the messaging protocol. AssistantAgent and UserProxyAgent are specializations: the assistant reasons and generates responses using an LLM, while the user proxy executes code and relays results back to the conversation.
import autogen
from autogen import AssistantAgent, UserProxyAgent, GroupChat, GroupChatManager
# WHY azure_deployment instead of model: Azure OpenAI uses deployment
# names that may differ from canonical model names. Specifying both
# ensures the client routes to the correct Azure endpoint, which is
# required for enterprise tenants that isolate deployments by team.
llm_config = {
"config_list": [
{
"model": "gpt-5.6-sol",
"azure_deployment": "gpt-5.6-sol-enterprise",
"api_type": "azure",
"api_key": AZURE_OPENAI_API_KEY,
"base_url": f"https://{AZURE_RESOURCE}.openai.azure.com/",
"api_version": "2024-05-01-preview",
}
],
# WHY cache_seed=42: deterministic seed enables response caching.
# Identical prompts return cached responses, reducing cost by 60-80%
# in workflows with repetitive patterns (e.g., per-document analysis).
"cache_seed": 42,
"max_tokens": 2048,
}
analyst = AssistantAgent(
name="FinancialAnalyst",
system_message=(
"You are a senior financial analyst. You analyze data, identify trends, "
"and produce structured findings. Always cite the specific data points "
"supporting your conclusions. Never fabricate numbers."
),
llm_config=llm_config,
)
# WHY human_input_mode='NEVER' in production: NEVER means the proxy
# executes code automatically without prompting a human. ALWAYS would
# block waiting for terminal input, which is impossible in async pipelines.
executor = UserProxyAgent(
name="CodeExecutor",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
code_execution_config={
"executor": autogen.coding.DockerCommandLineCodeExecutor(
image="python:3.12-slim",
timeout=60,
# WHY work_dir in a temp location: prevents generated code from
# modifying the host filesystem. Combined with Docker isolation,
# this gives defense-in-depth against runaway code execution.
work_dir="/tmp/autogen_sandbox",
)
},
)
GroupChat Orchestration and Speaker Selection
GroupChat manages turn-taking across multiple agents. The default speaker selection uses an LLM call, adequate for demos but too unpredictable for enterprise workflows where the wrong agent answering a compliance question could introduce liability.
flowchart TD
A["GroupChatManager\n(orchestrator)"] --> B{"speaker_selection_func"}
B -->|"'legal' in message"| C["LegalReviewAgent"]
B -->|"'code' in message"| D["SoftwareEngineerAgent"]
B -->|"'finance' in message"| E["FinancialAnalystAgent"]
B -->|"TERMINATE signal"| F["End Conversation"]
C -->|"response"| A
D -->|"response"| A
E -->|"response"| A
style A fill:#EEF0F7,stroke:#6366F1,color:#0F172A
style B fill:#fff7ed,stroke:#f59e0b,color:#0F172A
style C fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style D fill:#f0fdf9,stroke:#0D9488,color:#0F172A
style E fill:#f0fdf9,stroke:#0D9488,color:#0F172A
def deterministic_speaker_selector(last_speaker, groupchat):
"""
WHY deterministic routing over LLM-based selection:
in regulated industries, the routing decision itself is auditable.
A rules-based selector produces the same routing for the same input
every time, which satisfies audit requirements and eliminates a
hidden LLM call from the critical path.
"""
last_message = groupchat.messages[-1]["content"].lower()
agents_by_name = {a.name: a for a in groupchat.agents}
if "legal" in last_message or "compliance" in last_message:
return agents_by_name["LegalReviewAgent"]
if "```python" in last_message or "execute" in last_message:
return agents_by_name["CodeExecutor"]
if "forecast" in last_message or "revenue" in last_message:
return agents_by_name["FinancialAnalyst"]
# Fall back to LLM-based selection for ambiguous cases
return "auto"
group_chat = GroupChat(
agents=[analyst, executor, legal_agent],
messages=[],
max_round=20, # hard cap on conversation turns = cost ceiling
speaker_selection_func=deterministic_speaker_selector,
allow_repeat_speaker=False, # prevents one agent from monopolizing turns
)
manager = GroupChatManager(groupchat=group_chat, llm_config=llm_config)
Function Registration and Nested Chats
AutoGen’s function registration API lets agents call typed, validated Python functions as tools, the same pattern as OpenAI function calling, but integrated into the conversational loop. Nested chats allow one agent pair to spawn an entire sub-conversation to solve a subtask, returning only the final result to the parent chat.
sequenceDiagram
participant M as GroupChatManager
participant A as AnalystAgent
participant E as ExecutorAgent
participant API as External API
M->>A: "Analyze Q3 revenue by region"
A->>E: request: fetch_revenue_data(quarter="Q3", breakdown="region")
E->>API: HTTP GET /api/revenue?quarter=Q3&breakdown=region
API-->>E: JSON response
E-->>A: structured DataFrame summary
A->>M: "Q3 revenue analysis: APAC +18%, EMEA -3%, Americas +7%"
M->>M: evaluate: TERMINATE condition met?
from autogen import register_function
def fetch_revenue_data(quarter: str, breakdown: str) -> dict:
"""
WHY a typed wrapper instead of letting the agent write raw requests:
the wrapper validates inputs before hitting the API, masks credentials
(the agent never sees the API key), and normalizes the response format.
This is the security boundary between the LLM and your data layer.
"""
validated_quarter = quarter.upper()
if validated_quarter not in ["Q1", "Q2", "Q3", "Q4"]:
raise ValueError(f"Invalid quarter: {quarter}")
response = revenue_client.get(quarter=validated_quarter, breakdown=breakdown)
return response.to_dict()
# WHY register on both caller and executor: the caller (analyst) decides
# WHEN to invoke the function; the executor runs it. Registering on both
# ensures the analyst's LLM knows the function exists (via its tools list)
# while the executor has the actual implementation to run.
register_function(
fetch_revenue_data,
caller=analyst,
executor=executor,
description="Fetch revenue data for a given fiscal quarter, broken down by a dimension such as region or product.",
)
Enterprise Integration: Logging and Cost Controls
Production AutoGen deployments need structured logging of every message exchanged, both for debugging and for compliance audit trails. Hook into AutoGen’s message flow by subclassing ConversableAgent and overriding _process_received_message.
Cost caps are enforced through two levers: max_round on GroupChat (caps LLM calls) and max_tokens on llm_config (caps tokens per call). For hard budget enforcement, instrument your Azure OpenAI deployment with a spending limit, and use AutoGen’s token_count utility after each run to track cumulative cost against a per-workflow budget.
The conversational multi-agent model AutoGen provides excels at knowledge-intensive workflows where the right orchestration path isn’t known in advance. In contrast, when you need guaranteed message delivery and replay between agents running in separate processes, you need an event streaming layer, which is exactly what the next lesson covers with Kafka and Redis Streams.