- Published on
AI Customer Support System — From Intent Detection to Resolution Automation
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
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
- Entity Extraction
- Resolution Flow Design
- Knowledge Base Retrieval for Answers
- Escalation Triggers
- Response Quality Scoring
- CSAT Prediction
- Handoff to Human Agent With Context
- Ticket Auto-Categorization
- Deflection Rate Measurement
- Checklist
- Conclusion
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 > 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 > 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 > 50 && response.length < 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 > 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 >= 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 < 3) {
baseScore -= 0.5;
riskFactors.push('Customer historically dissatisfied');
}
if (customerContext.daysSinceSignup < 30) {
baseScore -= 0.5;
riskFactors.push('New customer, onboarding issues likely');
}
if (response.length < 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.