Skip to content
Learn Agentic AI
Learn Agentic AI12 min read8 views

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:

  1. Router Agent — Classifies incoming requests and routes to specialists
  2. FAQ Agent — Answers common questions from a knowledge base
  3. Billing Agent — Handles invoices, payments, refunds, and subscription changes
  4. 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:

flowchart TD
    INPUT(["Task input"])
    SUPER["Supervisor agent<br/>plans plus monitors"]
    W1["Worker 1<br/>research"]
    W2["Worker 2<br/>code"]
    W3["Worker 3<br/>writing"]
    CRITIC{"Output meets<br/>rubric?"}
    REWORK["Rework or<br/>retry path"]
    SHARED[("Shared scratchpad<br/>and memory")]
    OUT(["Final result"])
    INPUT --> SUPER
    SUPER --> W1 --> CRITIC
    SUPER --> W2 --> CRITIC
    SUPER --> W3 --> CRITIC
    W1 --> SHARED
    W2 --> SHARED
    W3 --> SHARED
    SHARED --> SUPER
    CRITIC -->|Pass| OUT
    CRITIC -->|Fail| REWORK --> SUPER
    style SUPER fill:#4f46e5,stroke:#4338ca,color:#fff
    style CRITIC fill:#f59e0b,stroke:#d97706,color:#1f2937
    style OUT fill:#059669,stroke:#047857,color:#fff
    style SHARED fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
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:

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
@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:

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 [email protected]. 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.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

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

Share

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.

Related Articles You May Like

Agentic AI

Streaming Agent Responses with OpenAI Agents SDK and LangChain in 2026

How to stream tokens, tool-call deltas, and intermediate steps from an agent — with code for both the OpenAI Agents SDK and LangChain — and the gotchas that bite in production.

Agentic AI

Tool Selection Accuracy: The Eval Most Teams Skip — and Should Not (2026)

Your agent picked the wrong tool 12% of the time and the final answer was still right. That's a latent bug. Here's the eval pipeline that surfaces it.

Agentic AI

OpenAI Agents SDK vs Assistants API in 2026: Migration Guide with Eval Parity

Honest principal-engineer comparison of the OpenAI Agents SDK and the legacy Assistants API, with a migration checklist and eval-parity strategy so you don't ship regressions.

Agentic AI

Building Your First Agent with the OpenAI Agents SDK in 2026: A Hands-On Walkthrough

Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.

Agentic AI

Parallel Tool Calling in the OpenAI Agents SDK: When It Helps, When It Hurts (2026)

OpenAI's parallel function calling can cut latency in half — or burn money on dependent calls. The architecture, code, and an eval that proves the win.

Agentic AI

Token-Level Evaluation of Streaming Agents: TTFT, Stream Smoothness, and Mid-Stream Hallucination Detection

Streaming changes the eval game — final-answer correctness isn't enough when users perceive the answer one token at a time. Here's the metric set that matters.