Published on

AI Customer Support System — From Intent Detection to Resolution Automation

Authors

Introduction

Customer support at scale requires speed, consistency, and deep product knowledge. AI can handle 40-60% of inbound support requests automatically, freeing your team for complex issues. However, the key is knowing when to escalate. This guide covers building systems that detect intent, extract context, route intelligently, and hand off to humans seamlessly.

Intent Classification Pipeline

Classify support requests into actionable categories:

type SupportIntent =
  | 'account_access'
  | 'billing_issue'
  | 'feature_request'
  | 'technical_problem'
  | 'documentation_needed'
  | 'account_upgrade'
  | 'cancellation';

interface ClassifiedTicket {
  id: string;
  customerId: string;
  message: string;
  intent: SupportIntent;
  subIntent?: string;
  confidence: number;
  complexity: 'simple' | 'moderate' | 'complex';
}

async function classifyTicket(
  ticketId: string,
  message: string,
  customerContext: any
): Promise<ClassifiedTicket> {
  const classificationPrompt = `
    Classify this support request:

    Customer message: ${message}
    Customer plan: ${customerContext.plan}
    Account age: ${customerContext.daysSinceSignup} days
    Previous tickets: ${customerContext.ticketCount}

    Possible intents:
    - account_access: Login, password, 2FA issues
    - billing_issue: Payment failures, invoices, subscriptions
    - feature_request: Asking for new capabilities
    - technical_problem: System errors, bugs, outages
    - documentation_needed: How-to questions
    - account_upgrade: Upgrade plans or features
    - cancellation: Account downgrade or deletion

    Return JSON with: { intent, subIntent, confidence (0-1), complexity }
  `;

  const result = JSON.parse(await llm.generate(classificationPrompt));

  return {
    id: ticketId,
    customerId: customerContext.id,
    message,
    intent: result.intent,
    subIntent: result.subIntent,
    confidence: result.confidence,
    complexity: result.complexity
  };
}

Entity Extraction

Pull structured data from unstructured requests:

interface ExtractedEntities {
  orderId?: string;
  productId?: string;
  issueSeverity?: 'critical' | 'high' | 'medium' | 'low';
  affectedFeatures?: string[];
  timeframeReported?: string;
  errorMessages?: string[];
}

async function extractEntities(
  message: string,
  intent: SupportIntent
): Promise<ExtractedEntities> {
  const entityPrompt = `
    Extract structured entities from this support message:

    Message: ${message}
    Intent: ${intent}

    Extract:
    - Order ID or product mentioned
    - Error messages or codes
    - Severity/urgency signals
    - Feature names mentioned
    - Timeline of when issue started

    Return JSON with these fields. Set to null if not mentioned.
  `;

  return JSON.parse(await llm.generate(entityPrompt));
}

Resolution Flow Design

Determine if AI can resolve the issue:

interface ResolutionPath {
  canResolveDirectly: boolean;
  suggestionType: 'self_service' | 'knowledge_article' | 'escalate';
  suggestedResolution?: string;
  escalationReason?: string;
}

async function planResolution(
  ticket: ClassifiedTicket,
  entities: ExtractedEntities,
  customerContext: any
): Promise<ResolutionPath> {
  // Define resolution rules
  const resolutionRules = {
    account_access: {
      canResolve: true,
      suggestionType: 'self_service' as const,
      resolution: 'Send password reset link and 2FA troubleshooting guide'
    },
    billing_issue: {
      canResolve: customerContext.accountStatus === 'active',
      escalateReason: 'Payment disputes require human review'
    },
    feature_request: {
      canResolve: false,
      suggestionType: 'escalate' as const,
      reason: 'Route to product team'
    },
    technical_problem: {
      canResolve: entities.errorMessages ? true : false,
      suggestionType: 'knowledge_article' as const
    },
    documentation_needed: {
      canResolve: true,
      suggestionType: 'knowledge_article' as const
    },
    account_upgrade: {
      canResolve: customerContext.plan === 'free',
      suggestionType: 'self_service' as const,
      resolution: 'Show upgrade comparison and links'
    },
    cancellation: {
      canResolve: false,
      suggestionType: 'escalate' as const,
      reason: 'Cancellations require retention specialist review'
    }
  };

  const rule = resolutionRules[ticket.intent];

  return {
    canResolveDirectly: rule.canResolve,
    suggestionType: rule.suggestionType,
    suggestedResolution: rule.resolution,
    escalationReason: rule.reason
  };
}

Knowledge Base Retrieval for Answers

Look up answers in your support database:

interface KnowledgeArticle {
  id: string;
  title: string;
  content: string;
  intent: SupportIntent;
  tags: string[];
  helpfulCount: number;
  unhelpfulCount: number;
}

async function retrieveAnswers(
  ticket: ClassifiedTicket,
  entities: ExtractedEntities,
  limit: number = 3
): Promise<KnowledgeArticle[]> {
  const searchQuery = `${ticket.message} ${entities.affectedFeatures?.join(' ') || ''}`;

  const candidates = await vectorDb.search(
    searchQuery,
    limit * 2,
    { intent: ticket.intent }
  );

  // Score by helpfulness
  const scored = candidates.map(article => ({
    ...article,
    helpfulnessScore: article.helpfulCount / (article.helpfulCount + article.unhelpfulCount + 1)
  }));

  return scored
    .sort((a, b) => b.helpfulnessScore - a.helpfulnessScore)
    .slice(0, limit);
}

async function generateAnswerFromArticles(
  ticket: ClassifiedTicket,
  articles: KnowledgeArticle[]
): Promise<string> {
  const answerPrompt = `
    Customer asked: "${ticket.message}"

    Relevant help articles:
    ${articles.map(a => `**${a.title}**:\n${a.content}`).join('\n\n')}

    Provide a concise answer pulling from these articles.
    Keep response under 300 words.
    Include links to the original articles.
    If articles don''t fully address the issue, say so.
  `;

  return llm.generate(answerPrompt);
}

Escalation Triggers

Determine when to involve a human:

interface EscalationSignal {
  trigger: string;
  severity: number; // 0-10
  shouldEscalate: boolean;
}

async function evaluateEscalation(
  ticket: ClassifiedTicket,
  entities: ExtractedEntities,
  customerContext: any,
  resolutionPath: ResolutionPath
): Promise<EscalationSignal[]> {
  const signals: EscalationSignal[] = [];

  // Sentiment analysis for frustration
  const sentiment = await analyzeSentiment(ticket.message);
  if (sentiment.negative &gt; 0.7) {
    signals.push({
      trigger: 'High frustration detected',
      severity: 8,
      shouldEscalate: true
    });
  }

  // VIP customers
  if (customerContext.tier === 'enterprise') {
    signals.push({
      trigger: 'Enterprise customer',
      severity: 7,
      shouldEscalate: true
    });
  }

  // Complexity assessment
  if (ticket.complexity === 'complex') {
    signals.push({
      trigger: 'Complex issue requiring specialist',
      severity: 6,
      shouldEscalate: true
    });
  }

  // Repeated issues
  if (customerContext.recentTicketCount &gt; 2) {
    signals.push({
      trigger: 'Customer has multiple recent tickets',
      severity: 6,
      shouldEscalate: true
    });
  }

  // Resolution confidence
  if (resolutionPath.canResolveDirectly === false) {
    signals.push({
      trigger: 'AI cannot resolve',
      severity: 5,
      shouldEscalate: true
    });
  }

  return signals;
}

Response Quality Scoring

Ensure suggested responses meet quality standards:

interface ResponseQuality {
  score: number; // 0-100
  issues: string[];
  isPublishable: boolean;
}

async function scoreResponse(
  response: string,
  ticket: ClassifiedTicket
): Promise<ResponseQuality> {
  const qualityChecks = {
    addressesQuestion: response.toLowerCase().includes(
      extractMainQuestion(ticket.message).toLowerCase()
    ) ? 20 : 0,
    lengthAppropriate: response.length &gt; 50 && response.length &lt; 1500 ? 15 : 0,
    professionalism: (await analyzeTone(response)).professional ? 15 : 0,
    actionable: response.match(/next step|click|try|visit/i) ? 15 : 0,
    noPlaceholders: !response.includes('{{') ? 15 : 0,
    sources: response.match(/https?:\/\//g)?.length || 0 &gt; 0 ? 10 : 0,
    personalization: response.includes('you') || response.includes('your') ? 10 : 0
  };

  const score = Object.values(qualityChecks).reduce((a, b) => a + b, 0);
  const issues = Object.entries(qualityChecks)
    .filter(([, value]) => value === 0)
    .map(([key]) => key);

  return {
    score,
    issues,
    isPublishable: score &gt;= 75
  };
}

CSAT Prediction

Estimate if customer will be satisfied:

interface CSATRisk {
  predictedScore: number; // 1-5
  riskFactors: string[];
  likelihoodToChurn: number; // 0-1
}

async function predictCSAT(
  ticket: ClassifiedTicket,
  response: string,
  customerContext: any,
  entities: ExtractedEntities
): Promise<CSATRisk> {
  const riskFactors: string[] = [];
  let baseScore = 4;

  if (ticket.complexity === 'complex') {
    baseScore -= 1;
    riskFactors.push('Complex issue may not be fully resolved');
  }

  if (customerContext.previousCsatAverage &lt; 3) {
    baseScore -= 0.5;
    riskFactors.push('Customer historically dissatisfied');
  }

  if (customerContext.daysSinceSignup &lt; 30) {
    baseScore -= 0.5;
    riskFactors.push('New customer, onboarding issues likely');
  }

  if (response.length &lt; 100) {
    baseScore -= 0.5;
    riskFactors.push('Response may seem too brief');
  }

  const churnProbability = ticket.intent === 'cancellation' ? 0.8 : 0.1;

  return {
    predictedScore: Math.max(1, Math.min(5, baseScore)),
    riskFactors,
    likelihoodToChurn: churnProbability
  };
}

Handoff to Human Agent With Context

Pass all information to the agent:

interface HandoffPacket {
  ticketId: string;
  customerId: string;
  customerName: string;
  accountStatus: string;
  originalMessage: string;
  intent: SupportIntent;
  entities: ExtractedEntities;
  aiAnalysis: {
    suggestedApproach: string;
    escalationReasons: string[];
    predictedCSAT: number;
    complexity: string;
  };
  previousTickets: Array<{ date: Date; intent: string; resolved: boolean }>;
  suggestedFirstMessage: string;
}

async function prepareHandoff(
  ticket: ClassifiedTicket,
  entities: ExtractedEntities,
  customerContext: any,
  escalationSignals: EscalationSignal[],
  suggestedResponse: string,
  csatPrediction: CSATRisk
): Promise<HandoffPacket> {
  return {
    ticketId: ticket.id,
    customerId: ticket.customerId,
    customerName: customerContext.name,
    accountStatus: customerContext.status,
    originalMessage: ticket.message,
    intent: ticket.intent,
    entities,
    aiAnalysis: {
      suggestedApproach: suggestedResponse,
      escalationReasons: escalationSignals.map(s => s.trigger),
      predictedCSAT: csatPrediction.predictedScore,
      complexity: ticket.complexity
    },
    previousTickets: customerContext.recentTickets,
    suggestedFirstMessage: `Hi ${customerContext.name}, I''ve reviewed your request. ${suggestedResponse.substring(0, 100)}...`
  };
}

Ticket Auto-Categorization

Auto-tag tickets for analytics:

interface TicketCategory {
  primary: string;
  secondary: string[];
  product?: string;
  urgency: 'critical' | 'high' | 'normal' | 'low';
}

async function categorizeTicket(
  ticket: ClassifiedTicket,
  entities: ExtractedEntities
): Promise<TicketCategory> {
  const categoryPrompt = `
    Categorize this support ticket:

    Message: ${ticket.message}
    Intent: ${ticket.intent}
    Entities: ${JSON.stringify(entities)}

    Return JSON:
    {
      primary: main category,
      secondary: array of related categories,
      product: affected product/feature,
      urgency: critical/high/normal/low
    }
  `;

  return JSON.parse(await llm.generate(categoryPrompt));
}

Deflection Rate Measurement

Track how many tickets AI resolved without escalation:

interface DeflectionMetrics {
  totalTickets: number;
  deflectedTickets: number;
  deflectionRate: number;
  averageResolutionTime: number;
  customerSatisfactionByIntent: Record<SupportIntent, number>;
}

async function computeDeflectionMetrics(
  periodDays: number = 30
): Promise<DeflectionMetrics> {
  const tickets = await ticketDb.query(`
    SELECT id, intent, escalated, resolution_time_ms, csat_score
    FROM tickets
    WHERE created_at > NOW() - INTERVAL '${periodDays} days'
  `);

  const deflected = tickets.filter(t => !t.escalated);
  const intentCsat = new Map<SupportIntent, number[]>();

  for (const ticket of tickets) {
    if (!intentCsat.has(ticket.intent)) {
      intentCsat.set(ticket.intent, []);
    }
    intentCsat.get(ticket.intent)!.push(ticket.csat_score);
  }

  return {
    totalTickets: tickets.length,
    deflectedTickets: deflected.length,
    deflectionRate: deflected.length / tickets.length,
    averageResolutionTime: tickets.reduce((a, t) => a + t.resolution_time_ms, 0) / tickets.length,
    customerSatisfactionByIntent: Object.fromEntries(
      Array.from(intentCsat.entries()).map(([intent, scores]) => [
        intent,
        scores.reduce((a, b) => a + b) / scores.length
      ])
    )
  };
}

Checklist

  • Classify tickets into 6-8 intent categories
  • Extract order IDs, product names, error messages, and timeline
  • Design resolution flows: self-service, knowledge articles, escalate
  • Build knowledge base retrieval with relevance scoring
  • Detect frustration, VIP status, issue complexity, and repeated issues
  • Implement escalation triggers for safety and quality
  • Score response quality: completeness, professionalism, actionability
  • Predict CSAT and churn probability before sending response
  • Prepare rich handoff packets with full context for agents
  • Auto-categorize tickets for analytics and tracking
  • Measure deflection rate and CSAT by intent monthly
  • Set targets: 50%+ deflection, 4+ CSAT, <5 min resolution time
  • Track and improve resolution suggestions with CSAT feedback loop

Conclusion

AI customer support systems excel at deflecting routine requests while maintaining quality through intelligent escalation. By combining intent classification, entity extraction, knowledge base retrieval, and careful handoff, you create a system that speeds up response time and improves CSAT. Start with 30-40% deflection rate on your highest-volume intent categories, then gradually expand coverage as you refine the knowledge base and improve prediction accuracy.