Building a Customer Support Multi-Agent System: Router, FAQ, Billing, and Escalation
Build a complete customer support multi-agent system with four specialized agents — a router, FAQ handler, billing specialist, and escalation agent — using the OpenAI Agents SDK with shared context and graceful fallbacks.
Architecture Overview
We are building a customer support system with four agents:
- Router Agent — Classifies incoming requests and routes to specialists
- FAQ Agent — Answers common questions from a knowledge base
- Billing Agent — Handles invoices, payments, refunds, and subscription changes
- Escalation Agent — Takes over when automated agents cannot resolve an issue
This is a practical, production-oriented architecture. Each agent has a clear responsibility boundary, its own tools, and explicit handoff rules.
Step 1: Define the Shared Context
All agents share a context object that tracks the customer, the conversation state, and escalation metadata:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class SupportContext:
# Customer info (populated by router)
customer_id: str = ""
customer_name: str = ""
customer_email: str = ""
subscription_plan: str = ""
# Interaction tracking
issue_category: str = ""
resolution_status: str = "open" # open, resolved, escalated
interaction_log: list[str] = field(default_factory=list)
# Escalation data
escalation_reason: str = ""
escalation_priority: str = "normal" # low, normal, high, urgent
def log(self, agent_name: str, action: str):
timestamp = datetime.now().strftime("%H:%M:%S")
self.interaction_log.append(f"[{timestamp}] {agent_name}: {action}")
Step 2: Build the FAQ Agent
The FAQ agent searches a knowledge base and returns answers. It is the simplest specialist:
from agents import Agent, RunContextWrapper, function_tool
FAQ_DATABASE = {
"password_reset": "To reset your password, go to Settings > Security > Reset Password. You will receive an email with a reset link.",
"supported_browsers": "We support Chrome 90+, Firefox 88+, Safari 15+, and Edge 90+.",
"data_export": "Go to Settings > Data > Export. Select your date range and format (CSV or JSON). Exports are ready within 5 minutes.",
"api_rate_limits": "Free plans: 100 requests/hour. Pro plans: 10,000 requests/hour. Enterprise: custom limits.",
"two_factor_auth": "Go to Settings > Security > Two-Factor Authentication. We support authenticator apps and SMS codes.",
}
@function_tool
def search_faq(
ctx: RunContextWrapper[SupportContext],
query: str,
) -> str:
"""Search the FAQ knowledge base for answers."""
ctx.context.log("FAQ Agent", f"Searched FAQ: {query}")
query_lower = query.lower()
results = []
for key, answer in FAQ_DATABASE.items():
if any(word in query_lower for word in key.split("_")):
results.append(f"**{key.replace('_', ' ').title()}**: {answer}")
if results:
return "\n\n".join(results)
return "No FAQ articles found matching your query."
@function_tool
def mark_resolved(
ctx: RunContextWrapper[SupportContext],
) -> str:
"""Mark the current issue as resolved."""
ctx.context.resolution_status = "resolved"
ctx.context.log("FAQ Agent", "Marked issue as resolved")
return "Issue marked as resolved."
faq_agent = Agent(
name="FAQ Agent",
model="gpt-4o-mini",
instructions="""You answer common customer questions using the FAQ
knowledge base. Search for relevant articles and provide clear,
helpful answers.
Rules:
- Always search the FAQ before answering
- If the FAQ has the answer, provide it and mark the issue resolved
- If the FAQ does not have the answer, say so clearly and suggest
the user might need billing help or escalation
- Never make up information not in the FAQ""",
tools=[search_faq, mark_resolved],
)
Step 3: Build the Billing Agent
The billing agent handles financial operations. It has access to tools the FAQ agent does not:
@function_tool
def get_invoices(
ctx: RunContextWrapper[SupportContext],
customer_id: str,
) -> str:
"""Retrieve recent invoices for a customer."""
ctx.context.log("Billing Agent", f"Retrieved invoices for {customer_id}")
return f"""Invoices for {customer_id}:
- INV-2024-001: $49.00 (Pro Plan - March 2026) - PAID
- INV-2024-002: $49.00 (Pro Plan - February 2026) - PAID
- INV-2024-003: $49.00 (Pro Plan - January 2026) - PAID"""
@function_tool
def process_refund(
ctx: RunContextWrapper[SupportContext],
invoice_id: str,
reason: str,
) -> str:
"""Process a refund for a specific invoice."""
ctx.context.log("Billing Agent", f"Processed refund for {invoice_id}: {reason}")
ctx.context.resolution_status = "resolved"
return f"Refund of $49.00 initiated for {invoice_id}. Will appear in 5-10 business days."
@function_tool
def change_subscription(
ctx: RunContextWrapper[SupportContext],
new_plan: str,
) -> str:
"""Change the customer's subscription plan."""
ctx.context.log("Billing Agent", f"Changed plan to {new_plan}")
ctx.context.subscription_plan = new_plan
return f"Subscription changed to {new_plan}. New billing starts next cycle."
billing_agent = Agent(
name="Billing Agent",
instructions="""You handle billing, payments, invoices, refunds,
and subscription changes.
Rules:
- Always verify the customer ID from context before making changes
- For refunds, confirm the invoice ID and reason with the user
- For plan changes, explain the pricing difference before proceeding
- Maximum refund without escalation: $200. For larger amounts,
hand off to Escalation Agent
- Never discuss other customers' billing information""",
tools=[get_invoices, process_refund, change_subscription],
)
Step 4: Build the Escalation Agent
The escalation agent handles cases that automated agents cannot resolve. It collects information and creates a support ticket:
@function_tool
def create_support_ticket(
ctx: RunContextWrapper[SupportContext],
summary: str,
priority: str,
details: str,
) -> str:
"""Create a support ticket for human review."""
ctx.context.escalation_reason = summary
ctx.context.escalation_priority = priority
ctx.context.resolution_status = "escalated"
ctx.context.log("Escalation Agent", f"Created ticket: {summary} [{priority}]")
ticket_id = f"TKT-{datetime.now().strftime('%Y%m%d%H%M%S')}"
return f"Support ticket {ticket_id} created (Priority: {priority}). A human agent will respond within {'1 hour' if priority == 'urgent' else '24 hours'}."
@function_tool
def get_interaction_history(
ctx: RunContextWrapper[SupportContext],
) -> str:
"""Get the full interaction log for this session."""
return "\n".join(ctx.context.interaction_log) or "No interactions logged."
escalation_agent = Agent(
name="Escalation Agent",
instructions="""You handle cases that automated agents cannot
resolve. Your job is to:
1. Acknowledge the customer's frustration
2. Summarize what has been tried so far (check interaction history)
3. Collect any additional details needed
4. Create a support ticket with appropriate priority
Priority guidelines:
- urgent: service outage, security issue, payment processing failure
- high: significant feature broken, large refund request
- normal: general issues requiring human judgment
- low: feature requests, minor cosmetic issues
Always give the customer the ticket ID and expected response time.""",
tools=[create_support_ticket, get_interaction_history],
)
Step 5: Build the Router
The router ties everything together. It identifies the customer and routes to the right specialist:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from agents import handoff
@function_tool
def identify_customer(
ctx: RunContextWrapper[SupportContext],
email: str,
) -> str:
"""Look up a customer by email address."""
# Simulated lookup
ctx.context.customer_id = "cust_88421"
ctx.context.customer_name = "Sarah Chen"
ctx.context.customer_email = email
ctx.context.subscription_plan = "Pro"
ctx.context.log("Router", f"Identified customer: Sarah Chen ({email})")
return "Customer found: Sarah Chen (Pro plan)"
router = Agent(
name="Support Router",
model="gpt-4o-mini",
instructions="""You are the first point of contact for customer
support. For every conversation:
1. If you know the customer's email, identify them first
2. Classify the request:
- FAQ: general how-to questions, feature questions, documentation
- Billing: payments, invoices, refunds, plan changes
- Escalation: complaints, complex issues, anything you are unsure about
3. Hand off to the appropriate specialist
If a customer seems frustrated or mentions wanting to cancel, route
to Escalation regardless of the topic.
Never try to resolve issues yourself. Route immediately.""",
tools=[identify_customer],
handoffs=[
handoff(faq_agent, tool_description_override="Route to FAQ for general questions and how-to help"),
handoff(billing_agent, tool_description_override="Route to Billing for payments, invoices, and subscriptions"),
handoff(escalation_agent, tool_description_override="Route to Escalation for complaints, complex issues, or frustrated customers"),
],
)
Step 6: Add Cross-Agent Handoffs
Specialists need to escalate or redirect when a request does not match their expertise:
# FAQ agent can escalate or redirect to billing
faq_agent.handoffs = [
handoff(billing_agent, tool_description_override="Transfer to Billing if the user has a payment or subscription question"),
handoff(escalation_agent, tool_description_override="Escalate if you cannot find the answer in the FAQ"),
]
# Billing agent can escalate
billing_agent.handoffs = [
handoff(escalation_agent, tool_description_override="Escalate for refunds over $200 or complex billing disputes"),
]
Running the System
from agents import Runner
context = SupportContext()
result = Runner.run_sync(
router,
"Hi, my email is sarah@example.com. I was charged twice this month and I want a refund.",
context=context,
)
print(result.final_output)
print("\n--- Interaction Log ---")
for entry in context.interaction_log:
print(entry)
print(f"Status: {context.resolution_status}")
This produces a natural conversation flow: Router identifies the customer, recognizes a billing issue, hands off to the Billing Agent, which retrieves invoices, confirms the duplicate charge, and processes the refund.
Production Considerations
Timeout protection. Set max_turns=15 on the Runner to prevent infinite agent loops.
Logging. The interaction_log in the context captures every agent action, creating an audit trail for compliance and quality review.
Graceful degradation. If any specialist fails, the escalation agent is always available as a backstop. Every agent has an escalation handoff.
FAQ
How do I add a new specialist agent to this system?
Create the new agent with its tools and instructions. Add a handoff to it from the router. Add the routing criteria to the router's instructions. No other agents need to change unless they should be able to redirect to the new specialist.
How do I handle returning customers with ongoing issues?
Persist the SupportContext to your session store (Redis or database) keyed by customer ID. When the customer returns, load the context and pass it to the Runner. The agents will see the interaction history and previous resolution status.
What happens if the router misroutes a request?
Each specialist agent has handoffs to other agents and the escalation path. If the FAQ agent receives a billing question, it can hand off to the Billing Agent directly. The system self-corrects through inter-specialist handoffs.
#CustomerSupport #MultiAgentSystems #OpenAIAgentsSDK #ProductionArchitecture #AgentDesign #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.