title: "AI-агенты для поддержки клиентов: автоматизация, эскалация и контроль качества" slug: agents-customer-service-support-2026-ru date: 2026-02-22 lang: ru
AI-агенты для поддержки клиентов: автоматизация, эскалация и контроль качества
Ключевые факты
- Версии пакетов:
anthropic==0.84.0,openai==2.24.0,langchain==1.2.10,langgraph==1.0.10,requests==2.31.0,simple-salesforce==1.12.6 - Категории классификации намерений: спорные платежи, техподдержка, статус заказа, управление аккаунтом, отмена/удержание, вопросы доставки, вопросы о товарах, возвраты, общие вопросы
- Типичные пороги уверенности: высокий риск (отмена, платежи) ≥0.90, средний риск (техподдержка, доставка) ≥0.78, низкий риск (вопросы) ≥0.70
- Триггеры эскалации: оценка настроения >0.75 (разочарование/гнев), уверенность <порог, обнаружены юридические ключевые слова, повторный контакт (≥3 за 30 дней), пробел в знаниях (поиск не удался), клиент явно запросил человека, VIP-клиент (ARR >$100k)
- Endpoints Zendesk API:
/api/v2/tickets.json(создание),/api/v2/search.json(запрос),/api/v2/tickets/{id}.json(обновление),/api/v2/users/{id}/tickets/requested.json(история клиента) - Endpoints Freshdesk API:
/api/v2/tickets(CRUD),/api/v2/contacts(поиск клиента),/api/v2/conversations(ветки тикетов),/api/v2/canned_responses(получение шаблонов) - Целевые SLA: первый ответ <60мин (критичные: <15мин), разрешение <24ч (критичные: <4ч), окно эскалации 2-4ч для средней срочности, <30мин для критичного
- Передача человеку: передать всю историю разговора, контекст клиента (уровень, ARR, количество контактов), причины эскалации, полученные KB-статьи, траекторию настроения, рекомендуемое распределение в очередь
- Теплая vs холодная передача: теплая = человек присоединяется к чату с AI-сгенерированным брифингом; холодная = тикет создан, человек отвечает асинхронно с полной контекстной примечанием
- Размеры качества: точность, релевантность, тон, полнота, безопасность — взвешенная оценка объединяет все в единый score гейта
- Порог гейта качества: взвешенная оценка ≥7.0/10 для отправки без рецензии; <6.0 требует проверку человеком перед доставкой
- Целевой уровень отклонения: >70% полностью разрешено без эскалации (индустриальный бенчмарк: 50-75%)
- Двухэтапная классификация: сходство embeddings для быстрого пути (высокоуверенные запросы), LLM-рассуждение для неоднозначных случаев только
- Уровень попадания KB: целевая >85% запросов возвращают релевантный документ с оценкой >0.80
- Очереди эскалации:
billing_specialists,tier2_technical,legal_and_compliance,customer_success(удержание),executive_support(VIP) - Жесткие ключевые слова эскалации: "lawyer", "sue", "legal action", "GDPR", "data breach", "fraud", "discrimination", "regulatory complaint"
- Обнаружение мультиинтента: значительная доля сообщений клиентов содержит несколько отдельных запросов, требующих раздельной маршрутизации
- Управление сессией: отслеживать состояние разговора, количество поворотов, предыдущие вызовы инструментов, полученные документы, траекторию настроения через повороты
- Требования к журналу аудита: логировать каждую классификацию намерения, запрос поиска, вызов инструмента, решение по эскалации, оценку качества для соответствия и улучшения
- Production-конкурентность: типичное развертывание обрабатывает 50-200 параллельных разговоров с rate-limiting на основе семафора
Что такое агент поддержки клиентов
Агент поддержки клиентов — это многоэтапная AI-система, которая выполняет цикл классификация → разрешение → эскалация:
- Классификация: входящее сообщение классифицируется в категории намерений (платежи, техподдержка, статус заказа и т.д.), используя сходство embeddings (быстрый путь) или LLM-рассуждения (неоднозначные случаи)
- Разрешение: на основе намерения агент либо извлекает статьи KB (RAG), либо выполняет вызовы инструментов CRM (Zendesk, Salesforce), либо маршрутизирует к специализированным обработчикам
- Гейт эскалации: многофакторный механизм принятия решений оценивает настроение, ценность клиента, уверенность, пробелы в знаниях и явные запросы для определения необходимости передачи человеку
- Контроль качества: каждый ответ оценивается по размерам точности, релевантности, тона, полноты и безопасности перед доставкой
В отличие от scripted-чатботов, эти агенты рассуждают над контекстом разговора, консультируются с live-источниками данных и принимают детальные решения по маршрутизации. В отличие от простых RAG-систем, они интегрируют действия CRM и сложную логику эскалации.
Ключевой архитектурный принцип: каждое решение по маршрутизации — это проблема классификации. Надежность системы зависит от производительности классификатора на каждом этапе, не только от финального качества ответа.
Фреймворк принятия решений
| Подход | Когда использовать | Когда НЕ использовать | Возможность отклонения |
|---|---|---|---|
| Scripted IVR | Высокообъемные, низкодисперсионные запросы (поиск статуса заказа, сброс пароля) | Сложная диагностика, спорные платежи, многошаговые задачи | Высокая для узкого scope |
| RAG Chatbot | Вопросы, политика, получение документации по продукту | Действия, специфичные для аккаунта, обработка платежей, создание тикетов | Средняя (без выполнения действий) |
| LLM Agent | Разговоры с смешанным намерением, требуется интеграция CRM, нужна логика эскалации | Простые lookup-только задачи (overkill), сценарии с чувствительностью к стоимости | Высокая (полная способность разрешения) |
Критерии выбора:
- Если >80% запросов отвечены static KB lookup → RAG chatbot
- Если запросы требуют чтения/записи CRM данных → LLM agent
- Если решения по эскалации включают настроение, ценность клиента, контекст многих поворотов → LLM agent
- Если стоимость за разговор должна быть <$0.01 и запросы предсказуемы → scripted IVR
- Если compliance требует полной аудируемости логики решений → LLM agent с структурированным логированием
Таблица ссылки параметров
| Параметр | Значение | Примечания |
|---|---|---|
intent_confidence_threshold |
0.70-0.90 | Ниже для FAQ (0.70), выше для необратимых действий типа отмены (0.90) |
sentiment_escalation_threshold |
0.75 | Оценка настроения 0.0 (положительное) к 1.0 (очень отрицательное); >0.75 вызывает проверку эскалации |
customer_value_score |
0.0-1.0 | Комбинированная: уровень (enterprise=1.0), ARR ($100k+=0.5), повторный контакт (3+=0.4) |
escalation_score_threshold |
0.50 | Агрегированная оценка; ≥0.50 эскалирует, ≥0.70 высокая срочность, ≥0.90 критичное |
max_retrieval_iterations |
3 | Агент может искать KB до 3 раз; превышение вызывает эскалацию пробела знаний |
retrieval_top_k |
5 | Количество документов, возвращаемых за поиск; увеличить для широких тем, уменьшить для точности |
retrieval_score_cutoff |
0.80 | Минимальная оценка сходства для рассмотрения документа релевантным |
quality_score_pass |
7.0/10 | Взвешенная оценка качества, требуемая для отправки ответа без проверки человеком |
quality_score_review |
6.0/10 | Ниже этого, ответ задерживается для проверки человеком перед доставкой |
response_max_tokens |
1500 | Лимит длины LLM-ответа для сообщений, обращенных к клиентам |
handoff_summary_max_tokens |
600 | Внутренний брифинг для человека, берущего на себя |
contact_count_threshold |
3/30d | Клиент с ≥3 контактами за 30 дней отмечается как повторная проблема, увеличивает приоритет эскалации |
sla_first_response_target |
60min | Стандартный уровень; критичные = 15мин, enterprise = 30мин |
sla_resolution_target |
24h | Стандартный уровень; критичные = 4ч, enterprise = 8ч |
max_concurrent_conversations |
50-200 | Лимит семафора для production-оркестратора; настроить на основе лимитов API |
hard_keyword_escalation |
immediate | Юридические/compliance-ключевые слова вызывают мгновенную эскалацию независимо от других сигналов |
kb_freshness_alert |
90 days | Статьи, не обновленные за 90+ дней, отмечены для проверки |
Общие ошибки
Ошибка 1: LLM классификация для каждого сообщения (Cost Explosion)
Воздействие: стоимость классификации $0.002-0.005 за сообщение; при 50k сообщений/день = $100-250/день только для маршрутизации намерения.
❌ Плохо:
def classify_intent(message: str) -> str:
response = client.messages.create(
model="claude-opus-4-6", # Дорогая модель для каждого сообщения
max_tokens=100,
messages=[{"role": "user", "content": f"Classify: {message}"}]
)
return extract_intent(response.content[0].text)
✅ Хорошо:
def classify_intent(message: str) -> dict:
# Быстрый путь: сходство embeddings (60-80% запросов)
intent, confidence = self.classify_fast(message) # <1ms, $0.00001
if confidence >= self.intent_taxonomy[intent]["threshold"]:
return {"intent": intent, "method": "embedding"}
# Медленный путь: LLM только для неоднозначных случаев (20-40% запросов)
return self.classify_with_llm(message) # 500ms, $0.003
Ошибка 2: Отсутствие гейта уверенности при поиске
Воздействие: агент галлюцинирует ответы, когда KB-поиск возвращает низкорелевантные документы; клиент получает неверную информацию по политике.
❌ Плохо:
def answer_question(query: str) -> str:
docs = vector_store.similarity_search(query, k=3) # Проверка оценки отсутствует
context = "\n".join(d.page_content for d in docs)
# LLM попытается ответить даже если docs неважны
return generate_response(query, context)
✅ Хорошо:
def answer_question(query: str) -> dict:
docs = vector_store.similarity_search_with_score(query, k=5)
# Фильтровать по оценке релевантности
relevant_docs = [d for d, score in docs if score >= 0.80]
if not relevant_docs:
return {
"response": "I couldn't find specific information on that. Let me connect you with a specialist.",
"needs_escalation": True,
"escalation_reason": "knowledge_gap"
}
return generate_response(query, relevant_docs)
Ошибка 3: Эскалация только на основе суждения LLM
Воздействие: недетерминированные решения по эскалации; невозможно аудировать, почему клиент был/не был эскалирован; риск соответствия.
❌ Плохо:
def should_escalate(message: str) -> bool:
response = client.messages.create(
model="claude-opus-4-6",
messages=[{"role": "user", "content": f"Should this escalate? {message}"}]
)
# Решение непрозрачно, варьируется между вызовами, не может быть аудировано
return "yes" in response.content[0].text.lower()
✅ Хорошо:
def decide_escalation(message: str, context: dict, history: list) -> EscalationDecision:
reasons = []
score = 0.0
# 1. Жесткие правила (детерминированные)
if any(kw in message.lower() for kw in LEGAL_KEYWORDS):
return EscalationDecision(should_escalate=True, urgency="critical",
reasons=[EscalationReason.LEGAL_THREAT])
# 2. Анализ настроения (структурированный LLM-вызов)
sentiment = self.analyze_sentiment(history)
if sentiment["score"] > 0.75:
reasons.append(EscalationReason.SENTIMENT_CRITICAL)
score += 0.4
# 3. Ценность клиента (детерминированная)
if context.get("annual_revenue", 0) > 100000:
reasons.append(EscalationReason.HIGH_VALUE_CUSTOMER)
score += 0.3
# 4. Явный запрос (детерминированный)
if any(kw in message.lower() for kw in ["human", "agent", "manager"]):
score = 1.0 # Всегда уважать
# Решение по пороговому значению (аудируемое)
should_escalate = score >= 0.5
return EscalationDecision(
should_escalate=should_escalate,
urgency=self._score_to_urgency(score),
reasons=reasons,
context_summary=f"Score: {score:.2f}, Reasons: {[r.value for r in reasons]}"
)
Ошибка 4: Плохой контекст передачи человеку
Воздействие: клиент повторяет всю историю человеческому агенту; агент не знает, что уже пытался AI; разочарование клиента растет.
❌ Плохо:
def escalate_to_human(customer_email: str, message: str):
zendesk.create_ticket(
customer_email=customer_email,
subject="Customer needs help",
description=message # Только текущее сообщение, нет контекста
)
return "An agent will contact you soon."
✅ Хорошо:
def execute_handoff(customer_email: str, conversation: list, context: dict,
escalation: EscalationDecision, retrieved_docs: list) -> dict:
# Сгенерировать структурированный брифинг
summary = self.generate_handoff_summary(
conversation, context, escalation, retrieved_docs
)
ticket = zendesk.create_ticket(
customer_email=customer_email,
subject=f"[AI Escalation] {escalation.urgency.upper()} - {escalation.reasons[0].value}",
description=f"""AI Agent handoff.
ISSUE SUMMARY: {summary["one_sentence_summary"]}
AI ATTEMPTS:
{summary["ai_attempted"]}
STILL NEEDS RESOLUTION:
{summary["outstanding_issues"]}
CUSTOMER STATE: {summary["sentiment"]} | Churn risk: {summary["churn_risk"]}
RECOMMENDED ACTION: {summary["next_action"]}
ACCOUNT FLAGS: {summary["account_flags"]}
""",
priority=escalation.urgency,
tags=["ai_escalated", escalation.recommended_queue]
)
# Внутренняя примечание с полным разговором
zendesk.add_internal_note(ticket["ticket_id"],
f"Full conversation:\n{format_conversation(conversation)}\n\n"
f"KB articles reviewed: {[d['title'] for d in retrieved_docs]}"
)
return {"ticket_id": ticket["ticket_id"], "summary": summary}
Ошибка 5: Отсутствие оценки качества перед доставкой
Воздействие: фактически неверные или off-topic ответы достигают клиентов; доверие к бренду разрушается; нарушения соответствия остаются необнаруженными.
❌ Плохо:
def respond_to_customer(message: str) -> str:
response = agent.generate_response(message)
send_to_customer(response) # Никакой проверки
return response
✅ Хорошо:
def respond_to_customer(message: str, context: dict) -> dict:
response = agent.generate_response(message, context)
# Оценить перед доставкой
quality = self.quality_scorer.score_interaction(
customer_message=message,
ai_response=response["text"],
retrieved_docs=response.get("sources", []),
customer_context=context
)
if quality["weighted_score"] < 6.0 or quality["requires_human_review"]:
# Задержать для проверки
review_queue.add({
"message": message,
"response": response,
"quality_score": quality,
"customer": context["email"]
})
return {"status": "held_for_review", "ticket_created": True}
if quality["weighted_score"] >= 7.0:
send_to_customer(response["text"])
return {"status": "delivered", "quality_score": quality["weighted_score"]}
# 6.0-7.0: отправить но отметить для асинхронной проверки
send_to_customer(response["text"])
async_review_queue.add({"response": response, "quality": quality})
return {"status": "delivered_flagged"}
Классификация намерения и маршрутизация
Классификация намерения — это первая точка принятия решения. Неправильно классифицированное намерение каскадирует в неправильный выбор инструмента, неправильную стратегию поиска и в итоге неправильный ответ.
Двухэтапный подход:
- Быстрый путь: предварительно вычислить embeddings для кластеров примеров намерения; использовать косинусное сходство для классификации (60-80% запросов)
- Медленный путь: LLM-рассуждение для неоднозначных случаев или когда уверенность быстрого пути < порога (20-40% запросов)
Реализация:
from anthropic import Anthropic
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class IntentClassifier:
def __init__(self, embedding_model, llm_client: Anthropic):
self.embedder = embedding_model
self.client = llm_client
self.intent_taxonomy = {
"order_status": {
"description": "Customer wants to know status of their order",
"examples": ["where is my order", "track my package", "delivery status"],
"action": "crm_lookup",
"confidence_threshold": 0.85
},
"billing_dispute": {
"description": "Customer disputes a charge or invoice",
"examples": ["wrong charge", "duplicate billing", "refund request"],
"action": "escalate_billing",
"confidence_threshold": 0.80
},
"technical_support": {
"description": "Customer has a technical problem with the product",
"examples": ["not working", "error message", "how to configure"],
"action": "knowledge_retrieval",
"confidence_threshold": 0.78
}
}
self._build_intent_embeddings()
def _build_intent_embeddings(self):
"""Pre-compute embeddings for intent examples."""
self.intent_embeddings = {}
for intent, config in self.intent_taxonomy.items():
example_embeddings = [
self.embedder.embed(ex) for ex in config["examples"]
]
self.intent_embeddings[intent] = np.mean(example_embeddings, axis=0)
def classify_fast(self, message: str) -> tuple[str, float]:
"""Embedding-based fast path classification."""
message_embedding = self.embedder.embed(message)
similarities = {}
for intent, centroid in self.intent_embeddings.items():
sim = cosine_similarity(
message_embedding.reshape(1, -1),
centroid.reshape(1, -1)
)[0][0]
similarities[intent] = sim
best_intent = max(similarities, key=similarities.get)
best_score = similarities[best_intent]
return best_intent, best_score
def classify_with_llm(self, message: str, context: dict) -> dict:
"""LLM-based classification for ambiguous cases."""
intents_description = "\n".join([
f"- {intent}: {config['description']}"
for intent, config in self.intent_taxonomy.items()
])
prompt = f"""Classify the customer message into one of these intents:
{intents_description}
Customer message: {message}
Recent context: {context.get('recent_messages', 'None')}
Account status: {context.get('account_status', 'Unknown')}
Respond with JSON:
{{
"intent": "<intent_name>",
"confidence": <0.0-1.0>,
"reasoning": "<brief explanation>",
"secondary_intent": "<optional secondary intent if mixed>"
}}"""
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=200,
messages=[{"role": "user", "content": prompt}]
)
import json
return json.loads(response.content[0].text)
def classify(self, message: str, context: dict = None) -> dict:
"""Two-stage classification with fallback."""
context = context or {}
# Fast path: embedding similarity
fast_intent, fast_confidence = self.classify_fast(message)
threshold = self.intent_taxonomy[fast_intent]["confidence_threshold"]
if fast_confidence >= threshold:
return {
"intent": fast_intent,
"confidence": fast_confidence,
"method": "embedding",
"action": self.intent_taxonomy[fast_intent]["action"]
}
# Slow path: LLM classification for ambiguous cases
llm_result = self.classify_with_llm(message, context)
llm_result["method"] = "llm"
llm_result["action"] = self.intent_taxonomy.get(
llm_result["intent"], {}
).get("action", "human_review")
return llm_result
Обнаружение мультиинтента:
Значительная доля сообщений клиентов содержит несколько отдельных намерений (напр., "Где мой заказ И я хочу возврат"). Production-системы должны обнаруживать и маршрутизировать эти отдельно:
def detect_multi_intent(self, message: str) -> list[dict]:
"""Detect when a message contains multiple distinct intents."""
prompt = f"""Analyze this customer message for multiple intents.
Message: {message}
Does this message contain MULTIPLE distinct requests that require different actions?
If yes, list each intent separately with priority (1=highest).
If no, return a single intent.
Return JSON:
{{
"is_multi_intent": true/false,
"intents": [
{{"intent": "<name>", "excerpt": "<relevant part>", "priority": 1}}
]
}}"""
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
import json
result = json.loads(response.content[0].text)
if result["is_multi_intent"]:
return sorted(result["intents"], key=lambda x: x["priority"])
return [{"intent": self.classify(message)["intent"], "excerpt": message}]
Гейт уверенности: если сходство embeddings < порог И уверенность LLM < 0.70, маршрутизировать к циклу уточнения или проверке человеком, а не угадывать намерение.
Агент разрешения
Для намерений, требующих получения информации (техподдержка, вопросы о товарах, запросы политики), агент выполняет RAG-цикл: получить релевантные KB-документы, сгенерировать ответ, используя полученный контекст, повторить если нужно.
Ключевые требования:
- Поиск должен фильтровать по оценке релевантности (≥0.80), чтобы избежать галлюцинаций от неважных документов
- Агент может искать несколько раз (макс 3 итерации) для сбора достаточного контекста
- Если высокорелевантные документы не найдены после 3 попыток → эскалировать с причиной
knowledge_gap
Реализация:
import anthropic
from typing import Optional
class KnowledgeRetrievalAgent:
def __init__(self, vector_store, client: anthropic.Anthropic):
self.vector_store = vector_store
self.client = client
self.tools = [
{
"name": "search_knowledge_base",
"description": "Search the product knowledge base for relevant articles, FAQs, and documentation",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to find relevant knowledge"
},
"filters": {
"type": "object",
"description": "Optional filters: product_category, document_type",
"properties": {
"product_category": {"type": "string"},
"document_type": {
"type": "string",
"enum": ["faq", "guide", "policy", "troubleshooting"]
}
}
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return (default: 5)"
}
},
"required": ["query"]
}
}
]
def search_knowledge_base(self, query: str, filters: dict = None, max_results: int = 5) -> list[dict]:
"""Execute knowledge base search with relevance filtering."""
results = self.vector_store.similarity_search_with_score(
query=query,
k=max_results,
filter=filters or {}
)
# Filter by relevance threshold
relevant_results = [
{
"id": r.metadata["doc_id"],
"title": r.metadata["title"],
"content": r.page_content,
"relevance_score": score,
"source_type": r.metadata.get("doc_type", "unknown"),
"last_updated": r.metadata.get("updated_at")
}
for r, score in results if score >= 0.80
]
return relevant_results
def answer_with_retrieval(
self,
customer_message: str,
customer_context: dict,
max_iterations: int = 3
) -> dict:
"""Agentic retrieval loop that searches until it has enough context."""
system_prompt = """You are a customer support specialist. Your job is to help customers
by searching the knowledge base and providing accurate, helpful responses.
Always cite the specific articles you used. If you cannot find a definitive answer,
say so clearly and offer to escalate to a human agent.
Important: Only use information from the knowledge base. Never make up product specifications,
policies, or procedures."""
messages = [
{
"role": "user",
"content": f"""Customer inquiry: {customer_message}
Customer context:
- Account tier: {customer_context.get('tier', 'Standard')}
- Product: {customer_context.get('product', 'Unknown')}
- Account age: {customer_context.get('account_age_days', 0)} days"""
}
]
retrieved_docs = []
for iteration in range(max_iterations):
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=1500,
system=system_prompt,
tools=self.tools,
messages=messages
)
if response.stop_reason == "end_turn":
# Final response
final_text = next(
b.text for b in response.content
if hasattr(b, 'text')
)
return {
"response": final_text,
"sources": retrieved_docs,
"iterations": iteration + 1,
"needs_escalation": False
}
# Process tool calls
tool_results = []
for block in response.content:
if block.type == "tool_use" and block.name == "search_knowledge_base":
results = self.search_knowledge_base(**block.input)
retrieved_docs.extend(results)
if not results:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": "No relevant articles found with high confidence score."
})
else:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": str(results)
})
# Continue conversation with tool results
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
# Exceeded max iterations
return {
"response": "I need additional assistance to answer your question accurately.",
"sources": retrieved_docs,
"iterations": max_iterations,
"needs_escalation": True,
"escalation_reason": "max_retrieval_iterations_exceeded"
}
Интеграция инструментов CRM:
Для намерений, требующих действий над аккаунтом (поиск заказа, создание тикета, инициирование возврата), агент должен вызывать API CRM:
import requests
class ZendeskTools:
def __init__(self, subdomain: str, email: str, api_token: str):
self.base_url = f"https://{subdomain}.zendesk.com/api/v2"
self.auth = (f"{email}/token", api_token)
self.session = requests.Session()
self.session.auth = self.auth
def get_customer_tickets(self, customer_email: str, status: str = None) -> list[dict]:
"""Retrieve tickets for a customer."""
params = {"query": f"requester:{customer_email}"}
if status:
params["query"] += f" status:{status}"
response = self.session.get(f"{self.base_url}/search.json", params=params)
response.raise_for_status()
tickets = response.json().get("results", [])
return [
{
"id": t["id"],
"subject": t["subject"],
"status": t["status"],
"created_at": t["created_at"],
"priority": t["priority"]
}
for t in tickets
]
def create_ticket(
self,
customer_email: str,
subject: str,
description: str,
priority: str = "normal",
tags: list = None
) -> dict:
"""Create a new support ticket."""
payload = {
"ticket": {
"subject": subject,
"description": description,
"requester": {"email": customer_email},
"priority": priority,
"tags": tags or []
}
}
response = self.session.post(f"{self.base_url}/tickets.json", json=payload)
response.raise_for_status()
ticket = response.json()["ticket"]
return {"ticket_id": ticket["id"], "status": ticket["status"]}
Регистрация инструментов: определить инструменты в формате Anthropic tool и передать в messages.create() с параметром tools=. Агент автономно решает, когда вызвать каждый инструмент.
Логика эскалации
Эскалация — многофакторное решение, комбинирующее анализ настроения, ценность клиента, пробелы в знаниях и явные запросы. Решение должно быть детерминированным и аудируемым для соответствия.
Триггеры эскалации:
- Жесткие ключевые слова (мгновенная эскалация): юридические угрозы, GDPR, утечка данных, мошенничество → маршрут в очередь
legal_and_compliance - Настроение (sentiment_score >0.75): разочарованный/злой клиент → добавить 0.4 к оценке эскалации
- Ценность клиента (ARR >$100k или enterprise-уровень): VIP → добавить 0.3 к оценке эскалации
- Повторный контакт (≥3 за 30 дней): persistent проблема → добавить 0.25 к оценке эскалации
- Пробел знаний (поиск не удался после 3 итераций): AI не имеет ответа → добавить 0.35 к оценке эскалации
- Явный запрос (клиент говорит "human", "agent", "manager"): всегда эскалировать (score=1.0)
Порог: агрегированная оценка ≥0.50 вызывает эскалацию. Оценка ≥0.70 = высокая срочность, ≥0.90 = критичная срочность.
Реализация:
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class EscalationReason(Enum):
SENTIMENT_CRITICAL = "sentiment_critical"
HIGH_VALUE_CUSTOMER = "high_value_customer"
LEGAL_THREAT = "legal_threat"
REPEATED_CONTACT = "repeated_contact"
KNOWLEDGE_GAP = "knowledge_gap"
CUSTOMER_REQUESTED = "customer_requested"
@dataclass
class EscalationDecision:
should_escalate: bool
urgency: str # "low", "medium", "high", "critical"
reasons: list[EscalationReason]
recommended_queue: str
context_summary: str
class EscalationEngine:
def __init__(self, client: anthropic.Anthropic, crm_tools):
self.client = client
self.crm = crm_tools
self.hard_escalation_keywords = [
"lawyer", "lawsuit", "sue", "legal action", "attorney",
"regulatory complaint", "GDPR", "data breach", "fraud"
]
self.escalation_queues = {
"billing": "billing_specialists",
"legal": "legal_and_compliance",
"technical": "tier2_technical",
"retention": "customer_success"
}
def check_hard_triggers(self, message: str) -> Optional[EscalationDecision]:
"""Check for keywords that always trigger escalation."""
message_lower = message.lower()
for keyword in self.hard_escalation_keywords:
if keyword in message_lower:
return EscalationDecision(
should_escalate=True,
urgency="critical",
reasons=[EscalationReason.LEGAL_THREAT],
recommended_queue=self.escalation_queues["legal"],
context_summary=f"Legal keyword detected: '{keyword}'"
)
return None
def evaluate_customer_value(self, customer_context: dict) -> float:
"""Score customer value for escalation prioritization."""
score = 0.0
tier_scores = {"enterprise": 1.0, "business": 0.7, "pro": 0.4, "free": 0.1}
score += tier_scores.get(customer_context.get("tier", "free"), 0.1)
arr = customer_context.get("annual_revenue", 0)
if arr > 100000:
score += 0.5
elif arr > 10000:
score += 0.3
contact_count = customer_context.get("contact_count_30d", 0)
if contact_count >= 3:
score += 0.4
return min(score, 1.0)
def analyze_sentiment(self, messages: list[str]) -> dict:
"""Analyze sentiment trajectory across conversation."""
if not messages:
return {"current": "neutral", "score": 0.5, "churn_risk": "low"}
conversation = "\n".join([f"Turn {i+1}: {m}" for i, m in enumerate(messages)])
prompt = f"""Analyze the emotional trajectory of this customer service conversation.
{conversation}
Return JSON:
{{
"current_sentiment": "positive/neutral/frustrated/angry/distressed",
"sentiment_score": <0.0 very positive to 1.0 very negative>,
"trajectory": "improving/stable/deteriorating",
"churn_risk": "low/medium/high/critical"
}}"""
response = self.client.messages.create(
model="claude-opus-4-6",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
import json
return json.loads(response.content[0].text)
def decide_escalation(
self,
customer_message: str,
customer_context: dict,
conversation_history: list[str],
retrieval_failed: bool = False
) -> EscalationDecision:
"""Multi-factor escalation decision."""
# 1. Check hard triggers first
hard_trigger = self.check_hard_triggers(customer_message)
if hard_trigger:
return hard_trigger
reasons = []
escalation_score = 0.0
# 2. Customer value
customer_value = self.evaluate_customer_value(customer_context)
if customer_value > 0.8:
reasons.append(EscalationReason.HIGH_VALUE_CUSTOMER)
escalation_score += 0.3
# 3. Sentiment
sentiment = self.analyze_sentiment(conversation_history)
if sentiment["sentiment_score"] > 0.75:
reasons.append(EscalationReason.SENTIMENT_CRITICAL)
escalation_score += 0.4
# 4. Repeated contact
if customer_context.get("contact_count_30d", 0) >= 3:
reasons.append(EscalationReason.REPEATED_CONTACT)
escalation_score += 0.25
# 5. Knowledge gap
if retrieval_failed:
reasons.append(EscalationReason.KNOWLEDGE_GAP)
escalation_score += 0.35
# 6. Explicit request
request_keywords = ["human", "agent", "representative", "manager"]
if any(kw in customer_message.lower() for kw in request_keywords):
reasons.append(EscalationReason.CUSTOMER_REQUESTED)
escalation_score = 1.0
should_escalate = escalation_score >= 0.5
if escalation_score >= 0.9:
urgency = "critical"
elif escalation_score >= 0.7:
urgency = "high"
elif escalation_score >= 0.5:
urgency = "medium"
else:
urgency = "low"
# Determine queue
if EscalationReason.LEGAL_THREAT in reasons:
queue = self.escalation_queues["legal"]
elif sentiment.get("churn_risk") in ["high", "critical"]:
queue = self.escalation_queues["retention"]
elif retrieval_failed:
queue = self.escalation_queues["technical"]
else:
queue = self.escalation_queues["billing"]
return EscalationDecision(
should_escalate=should_escalate,
urgency=urgency,
reasons=reasons,
recommended_queue=queue,
context_summary=f"Score: {escalation_score:.2f}, Sentiment: {sentiment['current_sentiment']}"
)
Маршрутизация очереди: каждая причина эскалации отображается на специализированную очередь. Юридические угрозы → legal_and_compliance, риск churn → customer_success (специалисты по удержанию), технические проблемы → tier2_technical.
Production-интеграция
Production-развертывание требует webhook-интеграции с системами тикетов, управления состоянием сессии и комплексного логирования аудита.
Webhook-интеграция Zendesk:
Zendesk может срабатывать webhooks при создании тикета, обновлении тикета или входящих сообщениях. Настроить webhook для POST на endpoint вашего агента:
{
"ticket_id": 12345,
"customer_email": "customer@example.com",
"message": "Where is my order?",
"customer_context": {
"tier": "pro",
"account_age_days": 180,
"contact_count_30d": 1
}
}
Управление сессией:
Отслеживать состояние разговора через несколько поворотов. Хранить в Redis или базе данных:
import asyncio
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class ConversationSession:
session_id: str
customer_email: str
ticket_id: Optional[int]
conversation_history: list[dict] = field(default_factory=list)
customer_context: dict = field(default_factory=dict)
retrieved_docs: list[dict] = field(default_factory=list)
turn_count: int = 0
escalated: bool = False
def add_turn(self, role: str, content: str):
self.conversation_history.append({"role": role, "content": content})
self.turn_count += 1
class SessionManager:
def __init__(self, redis_client):
self.redis = redis_client
def get_session(self, session_id: str) -> Optional[ConversationSession]:
data = self.redis.get(f"session:{session_id}")
if data:
import json
return ConversationSession(**json.loads(data))
return None
def save_session(self, session: ConversationSession):
import json
from dataclasses import asdict
self.redis.setex(
f"session:{session.session_id}",
3600, # 1 hour TTL
json.dumps(asdict(session))
)
Логирование аудита:
Логировать каждое решение для соответствия и улучшения:
import logging
from datetime import datetime
class AuditLogger:
def __init__(self, log_file: str):
self.logger = logging.getLogger("customer_service_agent")
handler = logging.FileHandler(log_file)
handler.setFormatter(logging.Formatter(
'%(asctime)s | %(levelname)s | %(message)s'
))
self.logger.addHandler(handler)
self.logger.setLevel(logging.INFO)
def log_intent_classification(self, session_id: str, message: str,
intent: str, confidence: float, method: str):
self.logger.info(f"INTENT | {session_id} | {intent} | conf={confidence:.3f} | method={method} | msg={message[:100]}")
def log_retrieval(self, session_id: str, query: str, num_results: int,
top_score: float):
self.logger.info(f"RETRIEVAL | {session_id} | query={query[:100]} | results={num_results} | top_score={top_score:.3f}")
def log_escalation(self, session_id: str, decision: EscalationDecision):
self.logger.info(f"ESCALATION | {session_id} | escalate={decision.should_escalate} | urgency={decision.urgency} | reasons={[r.value for r in decision.reasons]}")
def log_quality_score(self, session_id: str, score: float,
requires_review: bool):
self.logger.info(f"QUALITY | {session_id} | score={score:.2f} | review={requires_review}")
Оркестратор с rate-limiting:
import asyncio
from asyncio import Semaphore
class CustomerServiceOrchestrator:
def __init__(self, max_concurrent: int = 50):
self.semaphore = Semaphore(max_concurrent)
self.intent_classifier = IntentClassifier(...)
self.knowledge_agent = KnowledgeRetrievalAgent(...)
self.escalation_engine = EscalationEngine(...)
self.audit_logger = AuditLogger("audit.log")
async def process_message(self, message: dict) -> dict:
"""Process a single customer message with concurrency control."""
async with self.semaphore:
session_id = message["session_id"]
try:
# Intent classification
intent = await asyncio.to_thread(
self.intent_classifier.classify,
message["text"],
message.get("context", {})
)
self.audit_logger.log_intent_classification(
session_id, message["text"],
intent["intent"], intent["confidence"], intent["method"]
)
# Route based on intent
if intent["action"] == "knowledge_retrieval":
result = await asyncio.to_thread(
self.knowledge_agent.answer_with_retrieval,
message["text"],
message.get("context", {})
)
elif intent["action"].startswith("escalate_"):
result = {"needs_escalation": True, "escalation_reason": intent["action"]}
# Escalation check
if result.get("needs_escalation"):
escalation = self.escalation_engine.decide_escalation(
message["text"],
message.get("context", {}),
message.get("history", []),
retrieval_failed=result.get("iterations", 0) >= 3
)
self.audit_logger.log_escalation(session_id, escalation)
if escalation.should_escalate:
result["escalated"] = True
result["escalation_decision"] = escalation
return result
except Exception as e:
self.audit_logger.logger.error(f"ERROR | {session_id} | {str(e)}")
return {
"error": str(e),
"escalated": True,
"escalation_reason": "pipeline_error"
}
Обработка ошибок: любое исключение в pipeline вызывает автоматическую эскалацию с созданием тикета. Никогда не оставляйте клиента без ответа.
Производительность и бенчмарки
Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.
Целевые задержки:
- Классификация намерения (быстрый путь): <50ms
- Классификация намерения (LLM путь): 300-800ms
- Получение знаний (одиночный поиск): 200-500ms
- Полный pipeline разрешения: 1-3 секунды (median), <8 секунд (p99)
- Решение по эскалации: 500-1500ms (включая анализ настроения)
Пропускная способность:
- Один экземпляр оркестратора: 50-200 параллельных разговоров (ограничены лимитами API, не вычислениями)
- Масштабировать горизонтально, добавляя экземпляры за load balancer
- Типичное production: 3-5 экземпляров обрабатывают 10k-50k разговоров/день
Оценки стоимости:
- Классификация embeddings: $0.00001 за сообщение
- Классификация LLM: $0.002-0.005 за сообщение
- Получение знаний (RAG): $0.01-0.03 за разрешение (2-3 LLM вызова)
- Анализ эскалации: $0.003-0.007 за решение
- Полный разговор (4 поворота, разрешен без эскалации): $0.05-0.15
- Полный разговор (эскалирован после 3 поворотов): $0.10-0.25
Уровни отклонения:
- Хорошо настроенная система с комплексным KB: 70-80% полностью разрешено без человека
- Новое развертывание с разреженным KB: 40-60% отклонение
- Высоковариативная база клиентов (сложные продукты): 55-70% отклонение
Метрики качества:
- Точность (фактическая корректность): цель 8.5-9.0/10
- Релевантность (адресует вопрос): цель 8.0-9.0/10
- Тон (профессиональная эмпатия): цель 7.5-8.5/10
- Общее взвешенное качество: цель 7.5-8.5/10
Производительность базы знаний:
- Уровень попадания (запрос возвращает релевантный doc с оценкой ≥0.80): цель >85%
- Точность поиска@5: цель >0.90 (90% результатов top-5 релевантны)
- Охват (% намерений клиентов, ответимых из KB): цель >75%
Достижение SLA:
- Первый ответ <1ч: обычно 95-98% (AI отвечает за <10 секунд)
- Разрешение <24ч: 70-85% (включает эскалированные случаи, обработанные людьми)
- Передача эскалации человеку: <15мин для критичного, <2ч для средней срочности
Ключевые показатели производительности:
- Уровень отклонения: первичная метрика; оптимизировать KB и поиск для максимизации
- Уровень эскалации: 20-30% здорово; <15% предполагает переуверенную систему, >45% предполагает недообученный KB
- CSAT (удовлетворение клиента): AI-взаимодействия должны достичь 4.0-4.3/5.0 (немного ниже, чем человеческие агенты на 4.3-4.5/5.0)
- Стоимость за разрешение: AI-разрешение $0.05-0.20, человеческое разрешение $5-15
- ROI: высокообъемные развертывания (>10k разговоров/месяц) достигают положительного ROI в течение 2-3 месяцев