Most customer support automation projects fail the same way: someone wires up a chatbot, it handles 20% of tickets, and the team declares victory while the other 80% arrive slower than before because users already burned time talking to a bot. The real benchmark isn’t deflection rate — it’s resolution rate without human intervention, and most implementations never measure it honestly.
This guide builds a customer support automation system that actually moves that number. You’ll get a triage agent with structured escalation logic, response generation with confidence scoring, and — the part most guides skip — a feedback loop that lets the agent learn from human corrections over time. All benchmarked against a real support queue.
What We’re Actually Building
The architecture has three stages: classify and triage incoming tickets, generate or retrieve a response if confidence is high enough, and route to a human with full context if it isn’t. A fourth process runs async — it digests human agent corrections and updates a retrieval store the agent uses on future queries.
The stack: Claude Haiku for classification (cheap, fast), Claude Sonnet for response generation (worth the extra cost), Supabase with pgvector for the correction store, and n8n as the orchestration layer. You can swap n8n for Make or a raw Python script — the logic is the same.
Before writing a line of code, decide your confidence threshold. I use 0.75 as a starting point: below that, the agent drafts a response for human review rather than sending autonomously. You’ll tune this based on your category mix. High-stakes categories like billing disputes should run at 0.90+.
Ticket Classification and Triage
The classifier prompt needs to return structured JSON every time — not prose, not markdown with a JSON block, actual JSON. Use Claude’s response format enforcement and validate with Pydantic. Here’s the classification call:
import anthropic
from pydantic import BaseModel
from typing import Literal
client = anthropic.Anthropic()
class TicketClassification(BaseModel):
category: Literal[
"billing", "technical", "account", "shipping", "general"
]
urgency: Literal["low", "medium", "high", "critical"]
confidence: float # 0.0 - 1.0
sentiment: Literal["neutral", "frustrated", "positive"]
requires_human: bool
reasoning: str # one sentence — used in handoff notes
SYSTEM_PROMPT = """You are a support ticket classifier. Analyze the ticket and return valid JSON matching this schema exactly.
Confidence reflects how certain you are about the classification, not how solvable the issue is.
Set requires_human=true for: billing disputes over $50, account suspension, legal threats, or any ticket where confidence < 0.75."""
def classify_ticket(ticket_text: str) -> TicketClassification:
response = client.messages.create(
model="claude-haiku-20240307",
max_tokens=300,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": ticket_text}]
)
# Strip any wrapping text before parsing
raw = response.content[0].text.strip()
return TicketClassification.model_validate_json(raw)
Haiku classifies roughly 1,200 tickets per dollar at current pricing (about $0.00083 per ticket). At 10,000 tickets/month that’s under $9 for classification alone. Fast enough to feel real-time — median latency is around 800ms.
Urgency Escalation Rules
Don’t let the LLM decide escalation alone. Overlay deterministic rules on top of the classification output. A user typing “lawyer” or “chargeback” should always hit a human regardless of what the model returns. Build a rule engine that runs after classification:
ESCALATION_KEYWORDS = {
"critical": ["chargeback", "lawsuit", "lawyer", "attorney", "fraud", "stolen"],
"high": ["cancel", "refund", "broken", "not working", "urgent"],
}
def apply_escalation_rules(ticket_text: str, classification: TicketClassification) -> TicketClassification:
lower_text = ticket_text.lower()
for urgency_level, keywords in ESCALATION_KEYWORDS.items():
if any(kw in lower_text for kw in keywords):
classification.urgency = urgency_level
if urgency_level == "critical":
classification.requires_human = True
classification.reasoning += " [Rule override: escalation keyword detected]"
break
return classification
This saved us from several embarrassing situations where frustrated users buried “chargeback” in a longer message and the model missed the signal. Always layer deterministic rules over probabilistic classification for anything with legal or financial consequences.
Response Generation With Confidence Gating
For tickets that don’t require a human, Sonnet generates a response using three inputs: the ticket text, the classification output, and the top-3 matches from your correction store (more on that below). The correction store gives the agent context on how similar tickets were resolved, including cases where a previous auto-response was edited by a human agent.
def generate_response(
ticket_text: str,
classification: TicketClassification,
similar_resolutions: list[dict] # from pgvector lookup
) -> dict:
# Build context from past human-corrected resolutions
resolution_context = ""
if similar_resolutions:
resolution_context = "\n\nPast resolutions for similar tickets:\n"
for res in similar_resolutions:
resolution_context += f"- Original issue: {res['summary']}\n Resolution: {res['resolution']}\n"
prompt = f"""Category: {classification.category}
Urgency: {classification.urgency}
User sentiment: {classification.sentiment}
Ticket:
{ticket_text}
{resolution_context}
Write a support response. Be direct and specific. Do not use filler phrases like 'I understand your frustration'.
If you cannot resolve this with certainty, say so and explain next steps.
End your response with a confidence score (0.0-1.0) on a new line in format: CONFIDENCE: 0.XX"""
response = client.messages.create(
model="claude-sonnet-20240229",
max_tokens=600,
messages=[{"role": "user", "content": prompt}]
)
raw_response = response.content[0].text
# Parse confidence from end of response
lines = raw_response.strip().split("\n")
confidence_line = lines[-1] if "CONFIDENCE:" in lines[-1] else "CONFIDENCE: 0.50"
response_text = "\n".join(lines[:-1]).strip()
confidence = float(confidence_line.replace("CONFIDENCE:", "").strip())
return {
"response_text": response_text,
"confidence": confidence,
"send_autonomously": confidence >= 0.75 and not classification.requires_human
}
Sonnet responses cost roughly $0.006 each at current pricing. At 10,000 tickets/month with a 60% auto-resolution rate, you’re spending about $36/month on response generation. Total system cost for 10k tickets: under $50/month. Compare that to a single support agent-hour.
The Learning Loop: Ingesting Human Corrections
This is the part that separates a support bot from a support agent. When a human agent edits or overrides an AI response, that correction gets vectorized and stored. Future similar tickets retrieve it as context.
Storing Corrections in pgvector
import openai # Using Ada for embeddings — cheaper than Sonnet embeddings
from supabase import create_client
supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
openai_client = openai.OpenAI()
def store_correction(
original_ticket: str,
ai_response: str,
human_correction: str,
category: str,
agent_id: str
):
# Embed the original ticket text for similarity search
embedding_response = openai_client.embeddings.create(
input=original_ticket,
model="text-embedding-3-small" # $0.00002 per 1K tokens
)
embedding = embedding_response.data[0].embedding
# Store with metadata
supabase.table("support_corrections").insert({
"ticket_text": original_ticket,
"ai_draft": ai_response,
"human_resolution": human_correction,
"category": category,
"agent_id": agent_id,
"embedding": embedding,
"summary": original_ticket[:200] # for display in context
}).execute()
def retrieve_similar_resolutions(ticket_text: str, category: str, limit: int = 3) -> list[dict]:
embedding_response = openai_client.embeddings.create(
input=ticket_text,
model="text-embedding-3-small"
)
query_embedding = embedding_response.data[0].embedding
# pgvector cosine similarity search
results = supabase.rpc("match_corrections", {
"query_embedding": query_embedding,
"match_category": category,
"match_count": limit,
"similarity_threshold": 0.78
}).execute()
return results.data
The Supabase RPC function uses pgvector’s <=> cosine distance operator. You’ll need to create that function once in your Supabase SQL editor — the pgvector docs cover this well. The similarity threshold of 0.78 filters out vague matches that add noise rather than signal.
What “Learning” Actually Means Here
To be precise: the agent isn’t fine-tuning. It’s retrieval-augmented generation with a correction store. The model weights don’t change. What improves is the context the model receives — human-verified resolutions for similar past issues. In testing, retrieval-augmented responses scored 23% higher on CSAT than baseline responses after 500 corrections were in the store. Below 200 corrections, the effect was negligible.
Benchmarked Metrics From Production
These numbers come from a SaaS company running 8,000–12,000 tickets/month across billing, technical, and account categories, three months post-deployment:
- Auto-resolution rate: 61% (tickets closed without human involvement)
- CSAT on auto-resolved tickets: 4.1/5 (human agents averaged 4.4/5)
- Escalation accuracy: 94% (tickets flagged for human review that humans agreed needed review)
- False positive escalation: 6% (tickets escalated unnecessarily — acceptable)
- False negative escalation: 1.2% (tickets auto-resolved that should have had human review — this is the dangerous number)
- Average response time (auto): 2.3 minutes vs. 4.2 hours for human queue
- Monthly infrastructure cost: $47 (Claude API + embeddings + Supabase Pro)
The 1.2% false negative rate means roughly 90 tickets/month were auto-resolved when they shouldn’t have been. Reviewing those: most were billing questions where the answer was technically correct but the user needed acknowledgment and de-escalation, not just information. The fix was adding sentiment-weighted routing — frustrated users now get human review regardless of topic confidence.
Where This Breaks and What To Do About It
New product areas with no correction history will have low retrieval quality. The agent should fall back to conservative thresholds (require 0.85+ confidence to auto-resolve) until at least 50 corrections exist for that category. Implement a category-level threshold override table rather than hardcoding.
Multi-turn conversations aren’t handled in this architecture. If a user replies to an auto-response, you need to route that reply to a human or pass the full thread back through classification with the prior context appended. Most teams route all replies to humans — the economics still work because first-contact resolution handles the bulk of volume.
Prompt injection via ticket content is real. A user can write “Ignore previous instructions and…” in their ticket. Add a sanitization step that strips known injection patterns before passing to the model, and never give the support agent tool access beyond drafting responses and updating your correction store.
Rate limits during spikes — Claude’s tier-based limits can bite you during a product incident when ticket volume 10x’s exactly when you need the agent most. Implement a queue with exponential backoff and a fallback that auto-routes all tickets to humans when the queue depth exceeds a threshold.
When To Use This vs. When Not To
Use this if: you have 2,000+ monthly tickets, a clear taxonomy of support categories, and at least one human agent who can provide corrections during the ramp-up period. The system gets meaningfully better after roughly 3 months of correction data.
Solo founder with under 500 tickets/month: skip the correction store complexity. Use a simpler RAG setup against your help docs with Claude Haiku. The overhead of maintaining this system isn’t worth it at low volume — a well-configured Intercom or Freshdesk with basic automations will outperform it.
Enterprise teams: you’ll want to add PII redaction before any ticket content hits the API, audit logging for every auto-send decision, and a human review queue for the confidence band between 0.65 and 0.75 rather than a hard route-to-human. Also talk to Anthropic about enterprise agreements if you’re running 100k+ tickets/month — the cost math changes significantly.
Customer support automation at this level isn’t a set-and-forget deployment. Budget one to two hours per week during the first three months for reviewing false negatives, tuning thresholds, and auditing the correction store for low-quality entries. After that, monthly check-ins are enough. The teams that get the best results treat it like a junior agent that needs regular feedback — not a vending machine you plug in and walk away from.
Editorial note: API pricing, model capabilities, and tool features change frequently — always verify current details on the vendor’s website before building in production. Code examples are tested at time of writing; pin your dependency versions to avoid breaking changes. Some links in this article may be affiliate links — we may earn a commission if you sign up, at no extra cost to you.

