🎧

AI-агент для техподдержки: пошаговое руководство

Создание умного саппорт-бота с RAG, классификацией тикетов и интеграцией в Helpdesk

business ⏱ 25 мин
👤 User Тикет / Запрос 🏷️ Classifier Классификация Срочность · Тема Маршрутизация high confidence 🔍 RAG Pipeline Embedding Search ChromaDB · Re-rank 🤖 Generator LLM Response + шаблоны Автоответ FAQ (быстрый) 🧑‍💻 Human Escalation Сложные кейсы VIP · Critical low confidence / critical 🗄️ ChromaDB 🔌 Zendesk API
Архитектура AI-агента техподдержки: маршрутизация запросов, RAG-поиск и эскалация

1. Архитектура саппорт-агента

AI-агент для техподдержки — это конвейер обработки обращений клиентов, состоящий из четырёх ключевых компонентов. Каждый компонент решает свою задачу и передаёт результат следующему звену. Разберём архитектуру подробно.

Классификатор (Classifier) — первый рубеж обработки. Он анализирует входящий запрос и определяет: тему обращения (billing, technical, account), срочность (low, medium, high, critical), язык и тональность. На основе этих метрик принимается решение о маршрутизации: отправить в RAG-пайплайн, дать быстрый FAQ-ответ или эскалировать на живого оператора.

RAG Pipeline (Retrieval-Augmented Generation) — ядро системы. Здесь происходит семантический поиск по базе знаний (документация, статьи Help Center, база тикетов). Запрос клиента векторизуется, ищутся топ-K релевантных чанков, затем применяется реранкинг для повышения точности. Найденные документы передаются в генератор как контекст.

Генератор ответов (Generator) — LLM-компонент, который на основе запроса клиента и найденных документов формирует финальный ответ. Используются тщательно спроектированные промпты с шаблонами для разных категорий обращений, что гарантирует консистентность стиля и tone-of-voice бренда.

Эскалация (Human Escalation) — важнейший компонент безопасности. Если confidence классификатора ниже порога, запрос помечен как critical или клиент явно просит оператора — тикет автоматически передаётся в Helpdesk с полным контекстом диалога и предложенным AI-драфтом ответа.

💡 Совет: Начинайте с MVP — классификатор + RAG + генератор. Эскалацию и интеграцию с Helpdesk подключайте во второй фазе, когда базовая точность ответов превысит 80%.

2. Подготовка базы знаний

Качество ответов саппорт-агента напрямую зависит от базы знаний. Подготовка включает три этапа: сбор и парсинг документации, разбиение на чанки, векторизация и загрузка в векторную БД. Используем ChromaDB — легковесную и быструю векторную базу, идеальную для прототипирования.

# 1. Парсинг документации из Markdown-файлов
import os, glob
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

docs = []
for path in glob.glob("docs/*.md"):
    loader = TextLoader(path, encoding="utf-8")
    docs.extend(loader.load())

print(f"Загружено документов: {len(docs)}")

# 2. Разбиение на чанки с перекрытием для сохранения контекста
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(docs)
print(f"Чанков создано: {len(chunks)}")

Ключевой параметр здесь — chunk_size. Для техподдержки оптимален размер 256–512 токенов: достаточно мал для точного поиска, но достаточно велик для сохранения контекста ответа. Перекрытие в 64 токена предотвращает разрыв предложений на границе чанков.

# 3. Эмбеддинги и загрузка в ChromaDB
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    openai_api_key=os.environ["OPENAI_API_KEY"]
)

vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_support_db",
    collection_name="support_docs"
)
print("✅ База знаний готова к поиску")
💡 Совет: Для production рекомендуем добавить метаданные к каждому чанку: источник (URL статьи), категорию (billing/technical), дату последнего обновления. Это позволит фильтровать поиск и отслеживать актуальность документации.

3. RAG-поиск по документации

После загрузки базы знаний реализуем непосредственно retrieval — поиск релевантных документов по запросу клиента. Здесь критичны два аспекта: качество первичного поиска и реранкинг результатов для отсеивания шума. Базовый similarity search возвращает топ-K по косинусной близости, но часто требует доработки.

# Базовый retrieval с фильтрацией по категории
def retrieve_context(query: str, category: str | None = None, k: int = 5):
    """Поиск релевантных чанков по запросу"""
    search_kwargs = {"k": k}
    if category:
        search_kwargs["filter"] = {"category": category}

    retriever = vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs=search_kwargs
    )
    docs = retriever.invoke(query)
    return docs

# MMR-ретривер — баланс релевантности и разнообразия
retriever_mmr = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.7}
)

MMR (Maximal Marginal Relevance) особенно полезен для техподдержки: он предотвращает ситуацию, когда все 5 возвращённых чанков — это разные абзацы одной и той же статьи. Параметр lambda_mult=0.7 означает 70% веса на релевантность и 30% на разнообразие.

# Реранкинг с CrossEncoder для повышения точности
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")

def rerank_docs(query: str, docs, top_n: int = 3):
    """Реранкинг чанков кросс-энкодером"""
    pairs = [(query, doc.page_content) for doc in docs]
    scores = reranker.predict(pairs)
    ranked = sorted(zip(docs, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, score in ranked[:top_n]]

# Пример использования
query = "Как сбросить пароль администратора?"
candidates = retrieve_context(query, k=10)
top_docs = rerank_docs(query, candidates, top_n=3)

print(f"Отобрано чанков после реранкинга: {len(top_docs)}")
💡 Совет: Схема «широкий поиск (k=10–20) → реранкинг (top 3)» даёт значительно лучшие результаты, чем просто similarity search с k=3. CrossEncoder анализирует пару «запрос-документ» совместно, а не по отдельности, что радикально улучшает точность.

4. Классификация обращений

Классификатор — это «диспетчер» агента. Он принимает текст обращения и возвращает структурированную разметку: категорию, срочность, язык и решение о маршрутизации. Реализуем через вызов LLM с JSON-выходом — это обеспечивает гибкость без необходимости обучать отдельную модель.

import json
from openai import OpenAI

client = OpenAI()

# Системный промпт классификатора
CLASSIFIER_PROMPT = """Ты — классификатор обращений в техподдержку.
Определи по тексту запроса:
- category: billing | technical | account | general | feature_request | bug_report
- urgency: low | medium | high | critical
- language: ru | en
- sentiment: positive | neutral | negative | angry
- needs_human: true | false (true если critical, angry, или клиент просит оператора)
- confidence: 0.0–1.0

Верни ТОЛЬКО JSON, без markdown-обёртки."""

def classify_ticket(text: str) -> dict:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": CLASSIFIER_PROMPT},
            {"role": "user", "content": text}
        ],
        temperature=0.0,
        max_tokens=150
    )
    result = json.loads(response.choices[0].message.content)
    return result

# Пример классификации
ticket = "У меня не проходит платёж уже третий раз! Деньги списались, а подписка не активирована. Срочно решите!"
classification = classify_ticket(ticket)
print(json.dumps(classification, ensure_ascii=False, indent=2))

Обратите внимание на temperature=0.0 — для классификации нужна детерминированность, никакой креативности. Использование gpt-4o-mini оптимально: высокая скорость и низкая стоимость при достаточном качестве классификации.

# Маршрутизация на основе классификации
def route_ticket(classification: dict) -> str:
    if classification["needs_human"]:
        return "human_escalation"
    if classification["confidence"] < 0.7:
        return "human_escalation"
    if classification["category"] in ["general", "account"]:
        return "faq_fast_path"
    return "rag_pipeline"
💡 Совет: Добавьте в классификатор определение intent (намерения): refund_request, password_reset, subscription_change и т.д. Это позволит в будущем строить аналитику по типам обращений и оптимизировать документацию под частые запросы.

5. Генерация ответа с контекстом

Финальный этап — генерация ответа клиенту. Здесь критичен промпт-инжиниринг: модель должна опираться на найденные документы, не выдумывать информацию и соблюдать tone-of-voice. Используем структурированный промпт с чёткими инструкциями и примерами.

# Промпт генератора ответов
SUPPORT_PROMPT = """Ты — агент техподдержки QantCore. Твоя задача — помочь клиенту,
используя ТОЛЬКО информацию из предоставленных документов.

ПРАВИЛА:
1. Отвечай на русском, кратко и по делу.
2. Если ответа нет в документах — честно скажи и предложи эскалацию.
3. НЕ выдумывай информацию. Если не уверен — признай это.
4. Структура ответа: приветствие → решение → доп.инфо → закрытие.
5. Для technical-запросов давай пошаговые инструкции.
6. Для billing-запросов уточняй детали, не давай обещаний по возвратам.

Контекст из базы знаний:
{context}

Вопрос клиента: {query}

Тема: {category} | Срочность: {urgency}

Ответ:"""

def generate_response(query: str, context_docs, classification: dict) -> str:
    context = "\n\n---\n\n".join(
        f"[Источник: {doc.metadata.get('source', 'N/A')}]\n{doc.page_content}"
        for doc in context_docs
    )

    prompt = SUPPORT_PROMPT.format(
        context=context,
        query=query,
        category=classification["category"],
        urgency=classification["urgency"]
    )

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.3,
        max_tokens=600
    )
    return response.choices[0].message.content

Ключевые элементы промпта: явный запрет на галлюцинации («НЕ выдумывай информацию»), привязка к контексту через {context}, дифференциация стиля ответа по категориям. Параметр temperature=0.3 даёт лёгкую вариативность формулировок при сохранении фактологической точности.

# Полный пайплайн обработки тикета
def process_support_ticket(ticket_text: str) -> dict:
    # Шаг 1: Классификация
    classification = classify_ticket(ticket_text)
    route = route_ticket(classification)

    # Шаг 2: Маршрутизация
    if route == "human_escalation":
        return {
            "action": "escalate",
            "reason": f"needs_human=True, confidence={classification['confidence']}",
            "classification": classification
        }

    if route == "faq_fast_path":
        return {
            "action": "faq_response",
            "response": generate_response(ticket_text, [], classification),
            "classification": classification
        }

    # Шаг 3: RAG — поиск + реранкинг + генерация
    docs = retrieve_context(ticket_text, category=classification["category"], k=10)
    top_docs = rerank_docs(ticket_text, docs, top_n=3)

    return {
        "action": "ai_response",
        "response": generate_response(ticket_text, top_docs, classification),
        "sources": [doc.metadata for doc in top_docs],
        "classification": classification
    }

6. Интеграция с Helpdesk API

Последний шаг — интеграция AI-агента с реальной Helpdesk-системой. Рассмотрим два сценария: приём вебхуков от Zendesk/Intercom и отправка ответов обратно через API. Агент должен работать асинхронно, не блокируя интерфейс оператора.

# FastAPI-сервер для приёма вебхуков
from fastapi import FastAPI, Request, BackgroundTasks
from pydantic import BaseModel
import httpx

app = FastAPI()

class WebhookPayload(BaseModel):
    ticket_id: str
    subject: str
    body: str
    customer_email: str
    source: str  # zendesk | intercom | api

async def process_and_reply(payload: WebhookPayload):
    """Фоновая обработка тикета и отправка ответа в Helpdesk"""
    result = process_support_ticket(payload.body)

    if result["action"] == "ai_response":
        # Отправка AI-ответа как внутренней заметки
        await send_internal_note(payload.ticket_id, result["response"], result["sources"])
    elif result["action"] == "escalate":
        # Эскалация — назначаем на дежурного оператора
        await escalate_to_human(payload.ticket_id, result["reason"])

async def send_internal_note(ticket_id: str, ai_response: str, sources: list):
    """Отправка внутренней заметки в Zendesk"""
    async with httpx.AsyncClient() as client:
        await client.put(
            f"https://{ZENDESK_DOMAIN}.zendesk.com/api/v2/tickets/{ticket_id}",
            auth=(ZENDESK_EMAIL, ZENDESK_API_KEY),
            json={"ticket": {"comment": {
                "body": f"🤖 AI-драфт ответа:\n\n{ai_response}\n\n---\nИсточники: {sources}",
                "public": False
            }}}
        )

Ключевой архитектурный паттерн — AI не отправляет ответ клиенту напрямую. Вместо этого он создаёт внутреннюю заметку с драфтом ответа, которую оператор проверяет и отправляет. Это стратегия «human-in-the-loop»: она исключает риск неверных ответов и постепенно выстраивает доверие к агенту.

# Эндпоинт вебхука
@app.post("/webhook/support")
async def support_webhook(payload: WebhookPayload, background: BackgroundTasks):
    # Мгновенный ответ — тикет принят в обработку
    background.add_task(process_and_reply, payload)
    return {"status": "processing", "ticket_id": payload.ticket_id}

# Автосоздание тикета через Intercom API
async def create_intercom_ticket(customer_id: str, subject: str, body: str):
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "https://api.intercom.io/tickets",
            headers={"Authorization": f"Bearer {INTERCOM_TOKEN}"},
            json={
                "contacts": [{"id": customer_id}],
                "ticket_type_id": 12345,
                "ticket_attributes": {
                    "subject": subject,
                    "ai_processed": True
                }
            }
        )
    return resp.json()
💡 Совет: Для high-load сценариев используйте очередь сообщений (RabbitMQ, Redis Streams) между вебхуком и обработчиком. Это гарантирует, что пиковые нагрузки не приведут к потере тикетов и таймаутам. Также настройте мониторинг latency обработки через Prometheus.

📋 Итог: что мы построили

Мы прошли полный цикл создания AI-агента для техподдержки — от архитектуры до интеграции с реальными Helpdesk-системами. Агент умеет классифицировать обращения, искать ответы в документации через RAG, генерировать драфты ответов и эскалировать сложные кейсы операторам.

🔍 RAG Pipeline

ChromaDB + OpenAI embeddings + CrossEncoder реранкинг

🏷️ Classifier

gpt-4o-mini, JSON-вывод, 6 категорий + срочность

🤖 Generator

Структурированные промпты, anti-hallucination правила

🔌 Интеграция

FastAPI вебхуки, Zendesk/Intercom API, human-in-the-loop

Ключевые выводы: