AI-агенты для поддержки клиентов: автоматизация, эскалация и контроль качества


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-система, которая выполняет цикл классификация → разрешение → эскалация:

  1. Классификация: входящее сообщение классифицируется в категории намерений (платежи, техподдержка, статус заказа и т.д.), используя сходство embeddings (быстрый путь) или LLM-рассуждения (неоднозначные случаи)
  2. Разрешение: на основе намерения агент либо извлекает статьи KB (RAG), либо выполняет вызовы инструментов CRM (Zendesk, Salesforce), либо маршрутизирует к специализированным обработчикам
  3. Гейт эскалации: многофакторный механизм принятия решений оценивает настроение, ценность клиента, уверенность, пробелы в знаниях и явные запросы для определения необходимости передачи человеку
  4. Контроль качества: каждый ответ оценивается по размерам точности, релевантности, тона, полноты и безопасности перед доставкой

В отличие от 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"}

Классификация намерения и маршрутизация

Классификация намерения — это первая точка принятия решения. Неправильно классифицированное намерение каскадирует в неправильный выбор инструмента, неправильную стратегию поиска и в итоге неправильный ответ.

Двухэтапный подход:

  1. Быстрый путь: предварительно вычислить embeddings для кластеров примеров намерения; использовать косинусное сходство для классификации (60-80% запросов)
  2. Медленный путь: 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=. Агент автономно решает, когда вызвать каждый инструмент.

Логика эскалации

Эскалация — многофакторное решение, комбинирующее анализ настроения, ценность клиента, пробелы в знаниях и явные запросы. Решение должно быть детерминированным и аудируемым для соответствия.

Триггеры эскалации:

  1. Жесткие ключевые слова (мгновенная эскалация): юридические угрозы, GDPR, утечка данных, мошенничество → маршрут в очередь legal_and_compliance
  2. Настроение (sentiment_score >0.75): разочарованный/злой клиент → добавить 0.4 к оценке эскалации
  3. Ценность клиента (ARR >$100k или enterprise-уровень): VIP → добавить 0.3 к оценке эскалации
  4. Повторный контакт (≥3 за 30 дней): persistent проблема → добавить 0.25 к оценке эскалации
  5. Пробел знаний (поиск не удался после 3 итераций): AI не имеет ответа → добавить 0.35 к оценке эскалации
  6. Явный запрос (клиент говорит "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 месяцев