Towering mountain peaks piercing through clouds
Agent Guides

Scaling from Single Agent to Multi-Agent Orchestration

DomAIn Labs Team
January 25, 2025
15 min read

Scaling from Single Agent to Multi-Agent Orchestration

A single AI agent can handle customer questions. A multi-agent system can run your entire customer success operation.

Imagine:

  • Intake Agent captures customer inquiry and enriches with CRM data
  • Routing Agent determines which specialist to involve
  • Product Agent answers product-specific questions
  • Support Agent handles technical issues
  • Billing Agent manages payment questions
  • Escalation Agent brings in humans when needed
  • Follow-up Agent ensures customer satisfaction

Each agent is specialized, and together they handle complexity that would overwhelm a single agent.

This guide shows you how to architect and implement multi-agent systems that actually work.

When You Need Multiple Agents

Single Agent is Enough When:

  • Simple, focused use case (e.g., just FAQ answering)
  • Limited domain knowledge required
  • No handoffs between departments needed
  • Volume is manageable (< 1,000 interactions/month)

Multi-Agent System Makes Sense When:

  • Complex workflows spanning multiple departments
  • Different agents need different knowledge bases
  • Specialists can be more accurate than generalists
  • Need to scale beyond single agent capacity
  • Want to update one specialist without affecting others

Example: Customer support that covers products, billing, shipping, returns, and technical issues → Each domain gets its own specialized agent.

Multi-Agent Architecture Patterns

Pattern 1: Hub-and-Spoke (Orchestrator)

One coordinator agent routes to specialist agents.

interface MultiAgentSystem {
  orchestrator: OrchestratorAgent      // Routes and coordinates
  specialists: {
    product: ProductSpecialistAgent
    billing: BillingSpecialistAgent
    technical: TechnicalSpecialistAgent
    shipping: ShippingSpecialistAgent
  }
}

class OrchestratorAgent {
  async handle(message: string, context: any) {
    // 1. Classify intent
    const intent = await this.classifyIntent(message)

    // 2. Route to appropriate specialist
    const specialist = this.getSpecialist(intent)

    // 3. Delegate to specialist
    const response = await specialist.handle(message, context)

    // 4. Post-process if needed
    return this.finalize(response, context)
  }

  private async classifyIntent(message: string): Promise<string> {
    const classification = await llm.generate(`
      Classify this customer message into one category:
      - product: Questions about features, specifications, compatibility
      - billing: Payment, invoices, refunds, pricing
      - technical: Setup, troubleshooting, bugs
      - shipping: Delivery status, tracking, shipping options

      Message: "${message}"

      Return only the category.
    `)

    return classification.trim().toLowerCase()
  }

  private getSpecialist(intent: string): Agent {
    const mapping = {
      'product': this.specialists.product,
      'billing': this.specialists.billing,
      'technical': this.specialists.technical,
      'shipping': this.specialists.shipping
    }

    return mapping[intent] || this.specialists.product  // Default
  }

  private async finalize(response: any, context: any) {
    // Log interaction
    await db.interactions.create({
      data: {
        customerId: context.customerId,
        specialist: response.handledBy,
        message: response.message,
        resolved: response.resolved
      }
    })

    // Add common elements
    return {
      ...response,
      signature: '\n\nBest,\nCustomer Success Team',
      followUp: response.resolved ? null : 'Would you like to speak with a team member?'
    }
  }
}

Pros:

  • Clear routing logic
  • Easy to add new specialists
  • Centralized orchestration

Cons:

  • Orchestrator is single point of failure
  • Harder to handle multi-turn conversations that span specialists

Pattern 2: Chain of Agents (Sequential)

Agents pass work to each other in sequence.

// Example: Order processing pipeline
class OrderProcessingPipeline {
  private agents = [
    new ValidationAgent(),
    new InventoryCheckAgent(),
    new PaymentAgent(),
    new FulfillmentAgent(),
    new NotificationAgent()
  ]

  async processOrder(order: Order) {
    let state: WorkflowState = {
      order,
      status: 'processing',
      context: {}
    }

    // Each agent processes and passes to next
    for (const agent of this.agents) {
      console.log(`Running: ${agent.constructor.name}`)

      state = await agent.execute(state)

      if (state.status === 'failed') {
        await this.handleFailure(state, agent)
        break
      }

      if (state.status === 'requires_human') {
        await this.escalate(state, agent)
        break
      }
    }

    return state
  }
}

// Each agent in the chain
class ValidationAgent implements Agent {
  async execute(state: WorkflowState): Promise<WorkflowState> {
    const { order } = state

    // Validate order data
    const valid = this.validateOrderData(order)

    if (!valid) {
      return {
        ...state,
        status: 'failed',
        error: 'Invalid order data'
      }
    }

    // Check fraud risk
    const fraudRisk = await this.checkFraudRisk(order)

    if (fraudRisk > 0.8) {
      return {
        ...state,
        status: 'requires_human',
        escalationReason: `High fraud risk: ${fraudRisk}`
      }
    }

    // Pass to next agent
    return {
      ...state,
      context: {
        ...state.context,
        validated: true,
        fraudRisk
      }
    }
  }
}

class InventoryCheckAgent implements Agent {
  async execute(state: WorkflowState): Promise<WorkflowState> {
    const { order } = state

    // Check if items are in stock
    const inStock = await this.checkInventory(order.items)

    if (!inStock) {
      return {
        ...state,
        status: 'requires_human',
        escalationReason: 'Items out of stock'
      }
    }

    // Reserve inventory
    await this.reserveItems(order.items)

    return {
      ...state,
      context: {
        ...state.context,
        inventoryReserved: true
      }
    }
  }
}

// Additional agents follow same pattern...

Pros:

  • Clear sequence, easy to understand
  • Each agent has single responsibility
  • Easy to test individual agents

Cons:

  • Rigid flow, hard to handle branching
  • Failure in one agent blocks entire pipeline

Pattern 3: Collaborative (Hierarchical)

Agents can spawn sub-agents and collaborate.

class CustomerInquiryAgent {
  private subAgents = {
    research: new ResearchAgent(),
    analysis: new AnalysisAgent(),
    response: new ResponseAgent()
  }

  async handle(inquiry: string, customerId: string) {
    // Main agent coordinates sub-agents

    // 1. Research: Gather relevant information
    const research = await this.subAgents.research.gather({
      inquiry,
      sources: ['crm', 'knowledge_base', 'order_history']
    })

    // 2. Analysis: Determine best response approach
    const analysis = await this.subAgents.analysis.analyze({
      inquiry,
      research,
      customerContext: research.customer
    })

    // 3. Response: Generate and send response
    const response = await this.subAgents.response.generate({
      inquiry,
      analysis,
      tone: this.determineTone(research.customer)
    })

    // Main agent makes final decision
    if (analysis.confidence < 0.7) {
      return this.escalateToHuman(inquiry, research, analysis)
    }

    return response
  }
}

class ResearchAgent {
  async gather({ inquiry, sources }: any) {
    // Parallel data gathering
    const [customer, orders, knowledgeBase] = await Promise.all([
      crm.getCustomer(inquiry.customerId),
      db.orders.findMany({ where: { customerId: inquiry.customerId } }),
      vectorDB.search(inquiry.message, { topK: 5 })
    ])

    return {
      customer,
      orders,
      relevantDocs: knowledgeBase,
      context: this.summarize({ customer, orders, knowledgeBase })
    }
  }
}

Pros:

  • Flexible, adaptive behavior
  • Can parallelize work
  • Sub-agents can be reused

Cons:

  • More complex to coordinate
  • Harder to debug

Building a Multi-Agent System

Let's build a complete customer service multi-agent system.

Step 1: Define Agent Roles

interface AgentRole {
  name: string
  responsibility: string
  knowledgeBase: string[]
  capabilities: string[]
  escalationCriteria: string[]
}

const agentRoles: AgentRole[] = [
  {
    name: 'Intake',
    responsibility: 'Receive inquiry, enrich with customer data, route to specialist',
    knowledgeBase: ['customer_database', 'routing_rules'],
    capabilities: ['crm_lookup', 'intent_classification', 'sentiment_analysis'],
    escalationCriteria: ['VIP customer', 'angry sentiment']
  },
  {
    name: 'Product Specialist',
    responsibility: 'Answer product questions, provide recommendations',
    knowledgeBase: ['product_catalog', 'specs', 'compatibility'],
    capabilities: ['product_search', 'comparison', 'recommendations'],
    escalationCriteria: ['custom_request', 'out_of_stock']
  },
  {
    name: 'Technical Support',
    responsibility: 'Troubleshoot issues, provide solutions',
    knowledgeBase: ['troubleshooting_guides', 'known_issues', 'FAQs'],
    capabilities: ['issue_diagnosis', 'solution_retrieval', 'step_by_step'],
    escalationCriteria: ['unsolved after 3 attempts', 'hardware_failure']
  },
  {
    name: 'Billing',
    responsibility: 'Handle payment, refund, invoice questions',
    knowledgeBase: ['billing_policies', 'payment_systems', 'refund_rules'],
    capabilities: ['payment_lookup', 'refund_processing', 'invoice_generation'],
    escalationCriteria: ['refund > $1000', 'payment_failure']
  },
  {
    name: 'Follow-up',
    responsibility: 'Ensure customer satisfaction, gather feedback',
    knowledgeBase: ['satisfaction_surveys', 'follow_up_templates'],
    capabilities: ['survey_send', 'feedback_collection', 'issue_tracking'],
    escalationCriteria: ['negative_feedback', 'unresolved_after_24h']
  }
]

Step 2: Implement Specialized Agents

abstract class SpecializedAgent {
  protected knowledgeBase: VectorStore
  protected llm: ChatModel

  constructor(
    protected role: AgentRole,
    knowledgeSources: string[]
  ) {
    this.knowledgeBase = this.loadKnowledgeBase(knowledgeSources)
    this.llm = new ChatOpenAI({
      modelName: 'gpt-4-turbo-preview',
      temperature: 0.1  // Lower for consistency
    })
  }

  async handle(message: string, context: any): Promise<AgentResponse> {
    // 1. Check if this agent can handle it
    if (!this.canHandle(message, context)) {
      return this.passToOrchestrator(message, context)
    }

    // 2. Retrieve relevant knowledge
    const knowledge = await this.knowledgeBase.similaritySearch(message, 5)

    // 3. Generate response
    const response = await this.generate(message, knowledge, context)

    // 4. Check if escalation needed
    if (this.shouldEscalate(response, context)) {
      return this.escalate(response, context)
    }

    return response
  }

  abstract canHandle(message: string, context: any): boolean
  abstract generate(message: string, knowledge: any[], context: any): Promise<AgentResponse>

  protected shouldEscalate(response: AgentResponse, context: any): boolean {
    return this.role.escalationCriteria.some(criteria =>
      this.checkCriteria(criteria, response, context)
    )
  }
}

class ProductSpecialistAgent extends SpecializedAgent {
  canHandle(message: string, context: any): boolean {
    // Use LLM to determine if product-related
    const classification = this.classifyIntent(message)
    return classification === 'product'
  }

  async generate(message: string, knowledge: any[], context: any) {
    const prompt = `
You are a product specialist for our company.

Customer question: ${message}

Relevant product information:
${knowledge.map(k => k.pageContent).join('\n\n')}

Customer context:
- Name: ${context.customer?.name}
- Previous purchases: ${context.orders?.length || 0}
- Tier: ${context.customer?.tier}

Provide a helpful, accurate answer. If recommending products, explain why they're a good fit.
If information is not in the knowledge base, say so and offer to connect with a specialist.

Response:
    `

    const response = await this.llm.invoke(prompt)

    return {
      message: response.content,
      handledBy: 'product_specialist',
      confidence: this.calculateConfidence(knowledge),
      resolved: true,
      actions: []
    }
  }
}

class TechnicalSupportAgent extends SpecializedAgent {
  canHandle(message: string, context: any): boolean {
    const keywords = ['not working', 'error', 'issue', 'problem', 'broken', 'fix']
    return keywords.some(kw => message.toLowerCase().includes(kw))
  }

  async generate(message: string, knowledge: any[], context: any) {
    // Technical support uses step-by-step troubleshooting

    const prompt = `
You are a technical support agent. Guide the customer through solving their issue.

Issue reported: ${message}

Troubleshooting guides:
${knowledge.map(k => k.pageContent).join('\n\n')}

Provide clear, step-by-step instructions. Use numbered lists.
After each step, ask the customer to confirm if it worked before proceeding.

Response:
    `

    const response = await this.llm.invoke(prompt)

    return {
      message: response.content,
      handledBy: 'technical_support',
      confidence: this.calculateConfidence(knowledge),
      resolved: false,  // Requires customer confirmation
      requiresFollowUp: true,
      actions: [
        {
          type: 'log_issue',
          data: { description: message, troubleshootingSteps: response.content }
        }
      ]
    }
  }
}

// Additional specialist agents...

Step 3: Build the Orchestrator

class MultiAgentOrchestrator {
  private specialists: Map<string, SpecializedAgent>

  constructor(agents: SpecializedAgent[]) {
    this.specialists = new Map(
      agents.map(agent => [agent.role.name, agent])
    )
  }

  async handle(message: string, customerId: string) {
    // 1. Intake: Enrich with customer context
    const context = await this.enrichContext(customerId)

    // 2. Routing: Determine which specialist
    const specialistName = await this.route(message, context)

    const specialist = this.specialists.get(specialistName)

    if (!specialist) {
      return this.handleUnknownIntent(message, context)
    }

    // 3. Delegation: Let specialist handle
    const response = await specialist.handle(message, context)

    // 4. Post-processing
    const final = await this.postProcess(response, context)

    // 5. Logging
    await this.logInteraction(message, final, customerId)

    return final
  }

  private async enrichContext(customerId: string) {
    // Parallel data gathering
    const [customer, orders, interactions] = await Promise.all([
      crm.getCustomer(customerId),
      db.orders.findMany({
        where: { customerId },
        orderBy: { createdAt: 'desc' },
        take: 5
      }),
      db.interactions.findMany({
        where: { customerId },
        orderBy: { createdAt: 'desc' },
        take: 10
      })
    ])

    return { customer, orders, interactions }
  }

  private async route(message: string, context: any): Promise<string> {
    // Use LLM to classify
    const classification = await llm.generate(`
      Classify this customer message into one specialist area:

      Specialists:
      - Product Specialist: Product features, specs, compatibility, recommendations
      - Technical Support: Troubleshooting, errors, issues, bugs
      - Billing: Payments, refunds, invoices, pricing
      - Shipping: Delivery status, tracking, shipping options

      Message: "${message}"

      Context: Customer has ${context.orders?.length || 0} previous orders.
      ${context.interactions?.length > 0 ? `Recent interaction about: ${context.interactions[0].message}` : ''}

      Return only the specialist name.
    `)

    return this.mapToSpecialistName(classification)
  }

  private async postProcess(response: AgentResponse, context: any) {
    // Add personalization
    const greeting = context.customer?.name
      ? `Hi ${context.customer.name},\n\n`
      : ''

    const signature = '\n\nBest,\nCustomer Success Team'

    // Execute any actions
    for (const action of response.actions || []) {
      await this.executeAction(action)
    }

    // Schedule follow-up if needed
    if (response.requiresFollowUp) {
      await this.scheduleFollowUp(context.customer.id, response)
    }

    return {
      ...response,
      message: greeting + response.message + signature
    }
  }

  private async logInteraction(
    message: string,
    response: AgentResponse,
    customerId: string
  ) {
    await db.interactions.create({
      data: {
        customerId,
        message,
        response: response.message,
        handledBy: response.handledBy,
        confidence: response.confidence,
        resolved: response.resolved,
        timestamp: new Date()
      }
    })
  }
}

Step 4: Usage

// Initialize multi-agent system
const orchestrator = new MultiAgentOrchestrator([
  new ProductSpecialistAgent(agentRoles[1], ['product_catalog']),
  new TechnicalSupportAgent(agentRoles[2], ['troubleshooting']),
  new BillingAgent(agentRoles[3], ['billing_policies']),
  new ShippingAgent(agentRoles[4], ['shipping_info'])
])

// Handle customer inquiry
const response = await orchestrator.handle(
  "My shipment hasn't arrived yet, tracking says delivered but I didn't receive it",
  'cust-12345'
)

console.log(response.message)
console.log('Handled by:', response.handledBy)
console.log('Resolved:', response.resolved)

Agent Communication Patterns

Pattern 1: Message Passing

Agents communicate via structured messages:

interface AgentMessage {
  from: string
  to: string
  type: 'request' | 'response' | 'notification'
  payload: any
  correlationId: string
}

class MessageBus {
  async send(message: AgentMessage) {
    const recipient = this.agents.get(message.to)

    if (!recipient) {
      throw new Error(`Agent ${message.to} not found`)
    }

    await recipient.receive(message)
  }
}

// Agents subscribe to messages
class Agent {
  async receive(message: AgentMessage) {
    if (message.type === 'request') {
      const response = await this.process(message.payload)

      await messageBus.send({
        from: this.name,
        to: message.from,
        type: 'response',
        payload: response,
        correlationId: message.correlationId
      })
    }
  }
}

Pattern 2: Shared State

Agents read/write to common state:

class SharedState {
  private state: Map<string, any> = new Map()

  async get(key: string): Promise<any> {
    return this.state.get(key)
  }

  async set(key: string, value: any): Promise<void> {
    this.state.set(key, value)

    // Notify subscribers
    await this.notifySubscribers(key, value)
  }

  subscribe(key: string, callback: (value: any) => void) {
    // Subscribe to state changes
  }
}

// Agents interact via shared state
const sharedState = new SharedState()

// Agent 1 writes
await sharedState.set('customer_sentiment', 'negative')

// Agent 2 reads and reacts
const sentiment = await sharedState.get('customer_sentiment')
if (sentiment === 'negative') {
  await escalationAgent.handle(context)
}

Coordination Strategies

1. Supervisor Pattern

One agent oversees and coordinates others:

class SupervisorAgent {
  async coordinate(task: Task) {
    // Decompose task into subtasks
    const subtasks = await this.decompose(task)

    // Assign to appropriate agents
    const assignments = this.assign(subtasks)

    // Execute in parallel where possible
    const results = await Promise.all(
      assignments.map(({ agent, subtask }) =>
        agent.execute(subtask)
      )
    )

    // Synthesize results
    return this.synthesize(results)
  }
}

2. Consensus Pattern

Multiple agents vote on the best response:

async function consensusResponse(question: string) {
  // Get responses from multiple agents
  const responses = await Promise.all([
    agent1.generate(question),
    agent2.generate(question),
    agent3.generate(question)
  ])

  // Voting or averaging
  const best = responses.reduce((best, current) =>
    current.confidence > best.confidence ? current : best
  )

  return best
}

3. Auction Pattern

Agents bid on tasks based on capability:

async function auctionTask(task: Task) {
  // Agents bid based on how well they can handle it
  const bids = await Promise.all(
    agents.map(async agent => ({
      agent,
      bid: await agent.evaluateTask(task)
    }))
  )

  // Highest bidder wins
  const winner = bids.reduce((max, bid) =>
    bid.bid > max.bid ? bid : max
  )

  return winner.agent.execute(task)
}

Challenges & Solutions

Challenge 1: Coordination Overhead

Problem: Too much time spent coordinating agents

Solution:

  • Use async communication
  • Minimize inter-agent dependencies
  • Cache routing decisions

Challenge 2: Context Sharing

Problem: Agents lose context when handing off

Solution:

  • Use shared state store
  • Pass comprehensive context object
  • Log conversation history

Challenge 3: Conflicting Actions

Problem: Two agents try to do contradictory things

Solution:

  • Centralized action queue with conflict detection
  • Supervisor approval for critical actions
  • Agent priority levels

Challenge 4: Debugging

Problem: Hard to trace which agent did what

Solution:

class AuditLogger {
  async log(event: {
    agent: string
    action: string
    input: any
    output: any
    timestamp: Date
  }) {
    await db.auditLog.create({ data: event })
  }
}

// Every agent action is logged
await auditLogger.log({
  agent: 'product_specialist',
  action: 'generate_response',
  input: { message: question },
  output: { response: answer },
  timestamp: new Date()
})

The Bottom Line

Multi-agent systems are more powerful but more complex:

When to use:

  • Complex domain requiring specialization
  • Need to scale beyond single agent capacity
  • Different knowledge bases for different areas
  • Want to update specialists independently

Architecture patterns:

  • Hub-and-spoke: Good for routing
  • Chain: Good for pipelines
  • Collaborative: Good for complex problem-solving

Coordination strategies:

  • Supervisor: Central control
  • Consensus: Vote on best approach
  • Auction: Agents self-select tasks

Investment: 4-8 weeks to architect and implement

Returns: Handle 10x the complexity, specialist accuracy, scalability

Next Steps

  1. Start with one agent: Prove the concept
  2. Identify specializations: Where would specialists help?
  3. Design coordination: How will agents work together?
  4. Build incrementally: Add one specialist at a time
  5. Monitor and optimize: Track agent performance individually

Need help architecting a multi-agent system? Schedule a consultation to discuss your specific use case.

Remember: Start simple. A well-designed single agent beats a poorly-coordinated multi-agent system every time. Only add complexity when you have clear benefit.

Tags:multi-agentorchestrationarchitecturescalabilitycoordination

About the Author

DomAIn Labs Team

The DomAIn Labs team consists of AI engineers, strategists, and educators passionate about demystifying AI for small businesses.