
Scaling from Single Agent to Multi-Agent Orchestration
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
- Start with one agent: Prove the concept
- Identify specializations: Where would specialists help?
- Design coordination: How will agents work together?
- Build incrementally: Add one specialist at a time
- 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.
About the Author
DomAIn Labs Team
The DomAIn Labs team consists of AI engineers, strategists, and educators passionate about demystifying AI for small businesses.