Building an autogen customer support agent team requires more than a single prompt loop. This tutorial walks through a multi-agent group chat where a triage agent, a technical agent, and a billing agent collaborate to resolve tickets, using Microsoft AutoGen’s GroupChat primitive.
Prerequisites
- Python 3.10 or newer
pyautogen0.2.32+ (pip install pyautogen)- An OpenAI-compatible API key (OpenAI, Azure, or a gateway)
- Basic comfort with Python and async control flow
If you plan to run the agents against multiple model providers, set OPENAI_BASE_URL to your gateway. An OpenAI-compatible endpoint that addresses 240+ models lets you swap backends without touching agent code.
Install and configure
pip install pyautogen python-dotenv
Create a .env file:
OPENAI_API_KEY=sk-...
# Optional: point to any OpenAI-compatible base
OPENAI_BASE_URL=https://api.openai.com/v1
Load it in Python:
import os
from dotenv import load_dotenv
load_dotenv()
llm_config = {
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
"base_url": os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
"temperature": 0.1,
}
Define the agent roles
A support team needs clear boundaries. We give each agent a system message that constrains its scope and instructs it to hand off when out of depth.
Triage agent
from autogen import AssistantAgent
triage = AssistantAgent(
name="triage_agent",
llm_config=llm_config,
system_message=(
"You are the first line of support. Read the customer message, "
"classify it as 'technical', 'billing', or 'unknown', and then "
"respond with exactly one line: 'ROUTE: technical' or 'ROUTE: billing'. "
"Do not answer the ticket yourself."
),
)
Technical agent
tech = AssistantAgent(
name="tech_agent",
llm_config=llm_config,
system_message=(
"You handle technical issues: API errors, integration bugs, latency. "
"Provide concise, step-by-step fixes. If the issue is billing, "
"say 'HANDOFF: billing' and stop."
),
)
Billing agent
billing = AssistantAgent(
name="billing_agent",
llm_config=llm_config,
system_message=(
"You handle invoices, charges, refunds, and plan limits. "
"Quote policy precisely. If the issue is technical, "
"say 'HANDOFF: technical' and stop."
),
)
Wire up the group chat
AutoGen’s GroupChat broadcasts messages to all agents; the GroupChatManager decides who speaks next. We include a UserProxyAgent to inject the initial ticket and stay silent afterward.
from autogen import UserProxyAgent, GroupChat, GroupChatManager
user = UserProxyAgent(
name="customer_proxy",
human_input_mode="NEVER",
code_execution_config=False,
default_auto_reply="",
)
groupchat = GroupChat(
agents=[triage, tech, billing, user],
messages=[],
max_round=8,
speaker_selection_method="auto",
)
manager = GroupChatManager(
groupchat=groupchat,
llm_config=llm_config,
)
Run a ticket
ticket = "My account was charged twice for the Pro plan and the API returns 500 on /v1/chat."
user.initiate_chat(manager, message=f"TICKET: {ticket}")
Expected output
The first round should show triage classifying the ticket, then routing to both specialists. A typical transcript excerpt:
triage_agent: ROUTE: billing
billing_agent: You were charged twice. Our policy allows a refund for duplicate charges within 72h. I'll issue a refund for the second charge. HANDOFF: technical
tech_agent: The 500 on /v1/chat indicates an auth mismatch after refund. Rotate your API key in the dashboard and retry. If it persists, send the request ID.
customer_proxy: (silent)
The chat ends after max_round or when an agent sends "TERMINATE". In production you’d detect resolution explicitly rather than relying on round limits.
How the group chat decides who speaks
With speaker_selection_method="auto", the GroupChatManager prompts the LLM to pick the next agent from the participant list. That works, but it adds a call per turn. For a predictable autogen customer support agent team, a manual function is cheaper:
def custom_speaker(last_speaker, gc):
if last_speaker is triage:
last_msg = gc.messages[-1]["content"].lower()
return tech if "technical" in last_msg else billing
if last_speaker in (tech, billing):
return user
return triage
groupchat = GroupChat(
agents=[triage, tech, billing, user],
messages=[],
max_round=8,
speaker_selection_method=custom_speaker,
)
This removes ambiguity and prevents the manager from looping between two chatty agents.
Routing across providers
The llm_config above points at a single endpoint. If you want resilience, swap base_url to an OpenAI-compatible gateway. For example, n4n.ai exposes one OpenAI-compatible endpoint that addresses 240+ models and applies automatic fallback when a provider is rate-limited or degraded. Because AutoGen just uses the OpenAI client, no agent code changes.
You can also pin a model per agent by giving each its own llm_config—e.g., use a cheaper model for triage and a stronger one for tech.
triage_llm = {**llm_config, "model": "gpt-4o-mini"}
tech_llm = {**llm_config, "model": "gpt-4o"}
triage.llm_config = triage_llm
tech.llm_config = tech_llm
Debugging the autogen customer support agent team
The most common failure is an agent ignoring its handoff instruction and trying to solve the whole ticket. Enforce with regex in post-processing:
import re
def enforce_scope(recipient, messages, sender, config):
if recipient is tech and "HANDOFF: billing" in messages[-1]["content"]:
return True, "Escalating to billing."
return False, None
groupchat.register_reply([tech, billing], reply=enforce_scope, position=0)
Also watch for silent infinite loops when max_round is too high and no agent says TERMINATE. Log every message:
def log_msg(recipient, messages, sender, config):
print(f"[LOG] {sender.name} -> {recipient.name}: {messages[-1]['content'][:80]}")
return False, None
groupchat.register_reply([triage, tech, billing], reply=log_msg, position=1)
Production hardening
Group chat without guards burns tokens. Set max_round conservatively and add a timeout wrapper around initiate_chat. Never let agents execute code in a support context—keep code_execution_config=False on all AssistantAgents.
If you need per-token cost tracking, the gateway’s metering (as with n4n.ai’s per-token usage metering) gives you line-item bills without instrumenting each agent. Honor client routing directives by setting base_url per request if you need to isolate high-risk tenants.
Full end-to-end script
import os
from dotenv import load_dotenv
from autogen import (AssistantAgent, UserProxyAgent,
GroupChat, GroupChatManager)
load_dotenv()
llm_config = {
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
"base_url": os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
"temperature": 0.1,
}
triage = AssistantAgent("triage_agent", llm_config,
system_message="Classify as technical/billing. Reply 'ROUTE: x' only.")
tech = AssistantAgent("tech_agent", llm_config,
system_message="Fix technical issues. If billing, 'HANDOFF: billing'.")
billing = AssistantAgent("billing_agent", llm_config,
system_message="Handle billing. If technical, 'HANDOFF: technical'.")
user = UserProxyAgent("customer_proxy", human_input_mode="NEVER",
code_execution_config=False, default_auto_reply="")
def pick(last_speaker, gc):
if last_speaker is triage:
return tech if "technical" in gc.messages[-1]["content"] else billing
return user
gc = GroupChat([triage, tech, billing, user], [], max_round=8,
speaker_selection_method=pick)
mgr = GroupChatManager(gc, llm_config)
user.initiate_chat(mgr, message="TICKET: Double charged and API 500s.")
The autogen customer support agent team pattern scales to more roles—fraud, compliance, onboarding. Add them as AssistantAgents and let the manager schedule. Test speaker selection rigorously; poor prompts cause loops that cost real money.